diff --git a/Makefile b/Makefile index 01f94485..801c16e3 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,14 @@ .PHONY: clean clean: - rm -rf build common/build fabric/build forge/build + rm -rf build common/build fabric/build neoforge/build #forge/build .PHONY: jar jar: ./gradlew remapJar ls -1 fabric/build/libs + ls -1 neoforge/build/libs # ls -1 forge/build/libs test: @@ -39,14 +40,18 @@ deps: .PHONY: inst inst: # rm -f ~/minecraft/instances/1.20.1-forge-dev/.minecraft/mods/fastback* + rm -f ~/minecraft/instances/1.20.1-neoforge-dev/.minecraft/mods/fastback* rm -f ~/minecraft/instances/1.20.1-fabric-dev/.minecraft/mods/fasback* cp fabric/build/libs/fastback*-fabric.jar ~/minecraft/instances/1.20.1-fabric-dev/.minecraft/mods/ + cp neoforge/build/libs/fastback*-neoforge.jar ~/minecraft/instances/1.20.1-neoforge-dev/.minecraft/mods/ # cp forge/build/libs/fastback*-forge.jar ~/minecraft/instances/1.20.1-forge-dev/.minecraft/mods/ .PHONY: tvf tvf: + jar -tvf neoforge/build/libs/fastback*-neoforge.jar # jar -tvf forge/build/libs/fastback*-forge.jar .PHONY: tvfs tvfs: + jar -tvf neoforge/build/libs/fastback*-shadow.jar # jar -tvf forge/build/libs/fastback*-shadow.jar diff --git a/README.md b/README.md index c2f1a73d..45ba2934 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ of disk space. ## Features +* **Now with NeoForge support!** * Incrementally backup just the changed files * Faster, smaller backups than zipping * Back up locally @@ -31,7 +32,6 @@ of disk space. * Support for restoring remote snapshots * Better management of remote snapshots * UI for managing backups from the title screen -* ~~Forge support (maybe)~~ ## Acknowledgements diff --git a/build.gradle b/build.gradle index fbde8332..ec7ac4b1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "architectury-plugin" version "3.4-SNAPSHOT" - id "dev.architectury.loom" version "1.6-SNAPSHOT" apply false + id "dev.architectury.loom" version "1.7-SNAPSHOT" apply false } @@ -19,6 +19,7 @@ subprojects { maven { url 'https://maven.fabricmc.net/' } maven { url 'https://maven.nucleoid.xyz' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } + maven { url 'https://maven.neoforged.net/releases/' } } java { diff --git a/common/build.gradle b/common/build.gradle index 79f8757a..637840e5 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,6 +1,7 @@ architectury { common("fabric") + common("neoforge") // NOTE: Forge is no longer supported and the build has been disabled // See: https://github.com/pcal43/fastback/issues/333 //common("forge") diff --git a/common/src/main/java/net/pcal/fastback/logging/Log4jLogger.java b/common/src/main/java/net/pcal/fastback/logging/Log4jLogger.java index 68636615..4abcd1b4 100644 --- a/common/src/main/java/net/pcal/fastback/logging/Log4jLogger.java +++ b/common/src/main/java/net/pcal/fastback/logging/Log4jLogger.java @@ -54,6 +54,11 @@ public void warn(String message) { this.log4j.warn(message); } + @Override + public void warn(String message, Throwable t) { + this.log4j.warn(message, t); + } + @Override public void info(String message) { this.log4j.info(message); diff --git a/common/src/main/java/net/pcal/fastback/logging/SystemLogger.java b/common/src/main/java/net/pcal/fastback/logging/SystemLogger.java index 8fcc60c8..4d7266f5 100644 --- a/common/src/main/java/net/pcal/fastback/logging/SystemLogger.java +++ b/common/src/main/java/net/pcal/fastback/logging/SystemLogger.java @@ -44,6 +44,8 @@ default void error(Throwable e) { void warn(String message); + void warn(String message, Throwable t); + void info(String message); void debug(String message); diff --git a/common/src/main/java/net/pcal/fastback/mod/ModImpl.java b/common/src/main/java/net/pcal/fastback/mod/ModImpl.java index 69b6df30..0082ca53 100644 --- a/common/src/main/java/net/pcal/fastback/mod/ModImpl.java +++ b/common/src/main/java/net/pcal/fastback/mod/ModImpl.java @@ -30,6 +30,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Map; +import java.util.ServiceConfigurationError; import static java.nio.file.Files.createTempDirectory; import static java.util.Objects.requireNonNull; @@ -162,10 +163,14 @@ public void onInitialize() { syslog().info("git-lfs is installed: " + gitLfsVersion); } } - if (SshSessionFactory.getInstance() == null) { - syslog().warn("An ssh provider was not initialized for jgit. Operations on a remote repo over ssh will fail."); - } else { - syslog().info("SshSessionFactory: " + SshSessionFactory.getInstance().toString()); + try { + if (SshSessionFactory.getInstance() == null) { + syslog().warn("An ssh provider was not initialized for jgit. Operations on a remote repo over ssh will fail."); + } else { + syslog().info("SshSessionFactory: " + SshSessionFactory.getInstance().toString()); + } + } catch (Exception | ServiceConfigurationError e) { + syslog().warn("An ssh provider was not initialized for jgit. Operations on a remote repo over ssh will fail.", e); } syslog().debug("onInitialize complete"); } diff --git a/etc/release.sh b/etc/release.sh index a66e8781..01efb8b8 100755 --- a/etc/release.sh +++ b/etc/release.sh @@ -50,6 +50,7 @@ fi # FABRIC_LIBS_DIR='fabric/build/libs' +NEOFORGE_LIBS_DIR='neoforge/build/libs' # NOTE: Forge is no longer supported and the build has been disabled # See: https://github.com/pcal43/fastback/issues/333 @@ -69,6 +70,7 @@ rm gradle.properties mv gradle.properties.temp gradle.properties rm -rf "${FABRIC_LIBS_DIR}" +rm -rf "${NEOFORGE_LIBS_DIR}" # NOTE: Forge is no longer supported and the build has been disabled # See: https://github.com/pcal43/fastback/issues/333 @@ -84,7 +86,7 @@ git push # Do github release # set -x -gh release create --generate-notes --title "${RELEASE_VERSION}" --notes "release ${RELEASE_VERSION}" ${RELEASE_VERSION} "${FABRIC_LIBS_DIR}"/* # "${FORGE_LIBS_DIR}"/* +gh release create --generate-notes --title "${RELEASE_VERSION}" --notes "release ${RELEASE_VERSION}" ${RELEASE_VERSION} "${FABRIC_LIBS_DIR}"/* "${NEOFORGE_LIBS_DIR}"/* # "${FORGE_LIBS_DIR}"/* set +x diff --git a/fabric/src/main/java/net/pcal/fastback/mod/fabric/BaseFabricProvider.java b/fabric/src/main/java/net/pcal/fastback/mod/fabric/BaseFabricProvider.java index 7e40ca30..dfbb700d 100644 --- a/fabric/src/main/java/net/pcal/fastback/mod/fabric/BaseFabricProvider.java +++ b/fabric/src/main/java/net/pcal/fastback/mod/fabric/BaseFabricProvider.java @@ -27,7 +27,6 @@ import net.minecraft.commands.CommandSourceStack; import net.minecraft.server.MinecraftServer; import net.minecraft.world.level.storage.LevelStorageSource; -import net.minecraft.world.level.storage.LevelSummary; import net.pcal.fastback.logging.Log4jLogger; import net.pcal.fastback.logging.SystemLogger; import net.pcal.fastback.logging.UserMessage; diff --git a/fabric/src/main/java/net/pcal/fastback/mod/fabric/mixins/MinecraftServerMixin.java b/fabric/src/main/java/net/pcal/fastback/mod/fabric/mixins/MinecraftServerMixin.java index 6978bd16..87964a5d 100644 --- a/fabric/src/main/java/net/pcal/fastback/mod/fabric/mixins/MinecraftServerMixin.java +++ b/fabric/src/main/java/net/pcal/fastback/mod/fabric/mixins/MinecraftServerMixin.java @@ -41,7 +41,7 @@ public class MinecraftServerMixin { * Intercept the call to saveAll that triggers on autosave, pass it through and then send out notification that * the autosave is done. */ - @Redirect(method = "tickServer(Ljava/util/function/BooleanSupplier;)V", + @Redirect(method = "autoSave()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;saveEverything(ZZZ)Z")) public boolean fastback_saveAll(MinecraftServer instance, boolean suppressLogs, boolean flush, boolean force) { boolean result = instance.saveEverything(suppressLogs, flush, force); diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 0a3f151c..37c2e70b 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -25,7 +25,7 @@ "fastback.mixins.json" ], "depends": { - "fabricloader": ">=0.15.11", + "fabricloader": ">=0.16.4", "fabric": "*", "minecraft": "1.21.x", "java": ">=21" diff --git a/gradle.properties b/gradle.properties index 8b32086b..c40eb9ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ # # Fastback # -mod_version = 0.19.1+1.21-prerelease +mod_version = 0.19.2+1.21.3-prerelease maven_group = net.pcal maven_name = fastback archives_base_name = fastback @@ -10,9 +10,9 @@ archives_base_name = fastback # # Fabric & Minecraft - https://fabricmc.net/develop # -minecraft_version=1.21 -loader_version=0.15.11 -fabric_version=0.100.6+1.21 +minecraft_version=1.21.3 +loader_version=0.16.4 +fabric_version=0.106.1+1.21.3 # # Forge @@ -22,15 +22,20 @@ fabric_version=0.100.6+1.21 # NOTE: Forge is no longer supported and the build has been disabled # See: https://github.com/pcal43/fastback/issues/333 +# NeoForge +# +# https://projects.neoforged.net/neoforged/neoforge +neo_version = 21.3.31-beta + # # common dependencies # # https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -jgit_version = 6.8.0.202311291450-r +jgit_version = 6.10.0.202406032230-r # https://mvnrepository.com/artifact/org.apache.sshd/sshd-core -apache_sshd_version = 2.12.1 +apache_sshd_version = 2.14.0 # https://mvnrepository.com/artifact/com.googlecode.javaewah/JavaEWAH JavaEWAH_version = 1.2.3 @@ -39,10 +44,10 @@ JavaEWAH_version = 1.2.3 eddsa_version = 0.3.0 # https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -test_log4j_version = 2.23.1 +test_log4j_version = 2.24.1 # https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -junit_jupiter_version = 5.10.2 +junit_jupiter_version = 5.11.2 # @@ -54,13 +59,13 @@ fabric_permissions_version = 0.3.1 # https://github.com/NucleoidMC/Server-Translations/releases # https://maven.nucleoid.xyz/xyz/nucleoid/server-translations-api/ -server_translations_version = 2.3.1+1.21-pre2 +server_translations_version = 2.4.0+1.21.2-rc1 # # Build settings # -architectury_version = 12.1.2 +architectury_version = 13.0.8 org.gradle.daemon = true org.gradle.parallel = true org.gradle.jvmargs = -Xmx4G diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136..a4b76b95 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a..9355b415 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a42..f5feea6d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 6689b85b..9b42019c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/neoforge/build.gradle b/neoforge/build.gradle new file mode 100644 index 00000000..e4196efd --- /dev/null +++ b/neoforge/build.gradle @@ -0,0 +1,148 @@ +plugins { + id "com.github.johnrengelman.shadow" version "8.1.1" + id "com.modrinth.minotaur" version "2.8.7" + id "com.matthewprenger.cursegradle" version "1.4.0" +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +shadowJar { + //https://stackoverflow.com/questions/73286776/grpc-unsupportedaddresstypeexception-but-only-when-packaged-with-shadowjar + // This does in fact make service discovery work when packaged; still doesn't work in the IDE. see SshHacks. + mergeServiceFiles() +} + +configurations { + common + shadowCommon + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentNeoForge.extendsFrom common +} + +archivesBaseName = "${project.archives_base_name}" +version = "${project.mod_version}-neoforge" +group = project.maven_group + +dependencies { + neoForge("net.neoforged:neoforge:${project.neo_version}") { transitive false } + + // note to self: implementation, NOT include. include does implicit jarjar + + common(project(path: ":common", configuration: "namedElements")) { transitive false } + shadowCommon(project(path: ":common", configuration: "transformProductionNeoForge")) { transitive = false } + + // FIXME? I still don't understand if I need to declare all of these things as forgeRuntimeLibrary. It sort + // of seems like I do. + + forgeRuntimeLibrary implementation("org.eclipse.jgit:org.eclipse.jgit:${project.jgit_version}") { transitive = false } + shadowCommon("org.eclipse.jgit:org.eclipse.jgit:${project.jgit_version}") { transitive = false } + + forgeRuntimeLibrary runtimeOnly("com.googlecode.javaewah:JavaEWAH:${project.JavaEWAH_version}") { transitive = false } + shadowCommon("com.googlecode.javaewah:JavaEWAH:${project.JavaEWAH_version}") { transitive = false } + + // https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit.ssh.apache + forgeRuntimeLibrary runtimeOnly("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:${project.jgit_version}") { transitive = false } + shadowCommon("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:${project.jgit_version}") { transitive = false } + + // https://mvnrepository.com/artifact/org.apache.sshd/sshd-core + forgeRuntimeLibrary runtimeOnly("org.apache.sshd:sshd-core:${project.apache_sshd_version}") { transitive = false } + shadowCommon("org.apache.sshd:sshd-core:${project.apache_sshd_version}") { transitive = false } + + // https://mvnrepository.com/artifact/org.apache.sshd/sshd-common + forgeRuntimeLibrary runtimeOnly("org.apache.sshd:sshd-common:${project.apache_sshd_version}") { transitive = false } + shadowCommon("org.apache.sshd:sshd-common:${project.apache_sshd_version}") { transitive = false } + + // this enables ed25519 support in apache_sshd + // https://github.com/apache/mina-sshd/blob/dfa109b7b535d64e8ee395ddd0419e7696fb24ee/docs/dependencies.md + forgeRuntimeLibrary runtimeOnly("net.i2p.crypto:eddsa:${project.eddsa_version}") { transitive = false } + shadowCommon("net.i2p.crypto:eddsa:${project.eddsa_version}") { transitive = false } +} + +processResources { + inputs.property "version", project.version + + filesMatching("META-INF/mods.toml") { + expand "version": project.version + } +} + +shadowJar { + configurations = [project.configurations.shadowCommon] + exclude('META-INF/maven/**') + + // https://stackoverflow.com/questions/36659980/java-jar-classnotfoundexception-even-though-dependent-library-exists + // Forge has full control over loading the classes of a mod and it specifically checks the package information + // of every class it loads against a set of restricted package paths to protect its own dependencies from + // accidentally being overwritten by loading a different version of a similar dependency. In this case, Forge + // uses a few Apache libs, so it prevents the loading of classes from the org.apache package namespace. + + relocate 'org/eclipse', 'net/pcal/fastback/shaded/org/eclipse' + relocate 'com/jcraft', 'net/pcal/fastback/shaded/com/jcraft' + archiveClassifier = 'dev-shadow' +} + +remapJar { + inputFile.set shadowJar.archiveFile + dependsOn shadowJar + archiveClassifier = null +} + +jar { + + archiveClassifier = 'dev' +} + +sourcesJar { + def commonSources = project(":common").sourcesJar + dependsOn commonSources + from commonSources.archiveFile.map { zipTree(it) } +} + +processResources { + inputs.property "version", project.version + filesMatching("META-INF/neoforge.mods.toml") { + expand "version": project.version + } +} + + +// https://github.com/modrinth/minotaur +modrinth { + token = System.getenv("MODRINTH_TOKEN") ?: 'MODRINTH_TOKEN_NOT_SET' + projectId = "fastback" + versionNumber = "${project.version}" + versionType = "alpha" + uploadFile = remapJar + changelog = "

https://github.com/pcal43/fastback/releases/tag/${project.mod_version}

" + gameVersions = ["${project.minecraft_version}"] + loaders = ["neoforge"] + dependencies {} +} + + +// https://github.com/matthewprenger/CurseGradle +curseforge { + apiKey = System.getenv("CURSEFORGE_TOKEN") ?: 'CURSEFORGE_TOKEN_NOT_SET' + + project { + id = "667417" + releaseType = "alpha" + changelog = "https://github.com/pcal43/fastback/releases/tag/${project.mod_version}" + changelogType = "markdown" + mod_version = "${project.version}" + addGameVersion((String) project.minecraft_version) + addGameVersion "NeoForge" + mainArtifact(remapJar) + afterEvaluate { + uploadTask.dependsOn("remapJar") + } + } + + options { + forgeGradleIntegration = false + } +} diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 00000000..7da18ea6 --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeClientProvider.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeClientProvider.java new file mode 100644 index 00000000..3057e4e0 --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeClientProvider.java @@ -0,0 +1,113 @@ +package net.pcal.fastback.mod.neoforge; + +import net.neoforged.fml.ModContainer; +import net.neoforged.neoforge.client.event.CustomizeGuiOverlayEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.fml.ModLoadingContext; +import net.pcal.fastback.logging.UserMessage; + +import static java.util.Objects.requireNonNull; +import static net.pcal.fastback.logging.SystemLogger.syslog; +import static net.pcal.fastback.mod.MinecraftProvider.messageToText; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +/** + * Handles client-specific tasks. + * + * @author pcal + * @since 0.16.0 + */ +final class ForgeClientProvider extends ForgeCommonProvider { + + // ====================================================================== + // Constants + + private static final long TEXT_TIMEOUT = 10 * 1000; + + // ====================================================================== + // Fields + + //private MinecraftClient client = null; + private Component hudText; + private long hudTextTime; + private final Minecraft client; + + public ForgeClientProvider(ModContainer container) { + super(container); + container.getEventBus().addListener(this::onClientStartupEvent); + NeoForge.EVENT_BUS.addListener(this::onGuiOverlayEvent); + NeoForge.EVENT_BUS.addListener(this::onScreenRenderEvent); + this.client = requireNonNull(Minecraft.getInstance(), "MinecraftClient.getInstance() returned null"); + } + + // ====================================================================== + // Forge Event handlers + + private void onClientStartupEvent(FMLClientSetupEvent event) { + this.onInitialize(); + } + + private void onGuiOverlayEvent(CustomizeGuiOverlayEvent.Chat event) { + this.renderOverlayText(event.getGuiGraphics()); + } + + private void onScreenRenderEvent(ScreenEvent.Render.Post event) { + this.renderOverlayText(event.getGuiGraphics()); + } + + // ====================================================================== + // MinecraftProvider implementation + + @Override + public boolean isClient() { + return true; + } + + @Override + public void setHudText(UserMessage userMessage) { + if (userMessage == null) { + clearHudText(); + } else { + this.hudText = messageToText(userMessage); // so the hud renderer can find it + this.hudTextTime = System.currentTimeMillis(); + } + } + + @Override + public void clearHudText() { + this.hudText = null; + // TODO someday it might be nice to bring back the fading text effect. But getting to it properly + // clean up 100% of the time is more than I want to deal with right now. + } + + @Override + public void setMessageScreenText(UserMessage userMessage) { + final Component text = messageToText(userMessage); + this.hudText = text; + final Screen screen = client.screen; + // TODO; fix this + //if (screen != null) screen.title = text; + } + + @Override + void renderOverlayText(final GuiGraphics drawContext) { + if (this.hudText == null) return; + // if (!this.client.options.getShowAutosaveIndicator().getValue()) return; FIXME + if (System.currentTimeMillis() - this.hudTextTime > TEXT_TIMEOUT) { + // Don't leave it sitting up there forever if we fail to call clearHudText() + this.hudText = null; + syslog().debug("hud text timed out. somebody forgot to clean up"); + return; + } + if (client != null) { + drawContext.drawString(this.client.font, this.hudText, 2, 2, 1); + } + } +} \ No newline at end of file diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeCommonProvider.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeCommonProvider.java new file mode 100644 index 00000000..24c18e16 --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeCommonProvider.java @@ -0,0 +1,217 @@ +package net.pcal.fastback.mod.neoforge; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.LevelResource; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.event.lifecycle.FMLDedicatedServerSetupEvent; +import net.neoforged.fml.ModLoadingContext; +import net.pcal.fastback.logging.SystemLogger; +import net.pcal.fastback.logging.UserMessage; +import net.pcal.fastback.mod.LifecycleListener; +import net.pcal.fastback.mod.MinecraftProvider; +import org.slf4j.LoggerFactory; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static java.util.Objects.requireNonNull; +import static net.pcal.fastback.commands.Commands.createBackupCommand; +import static net.pcal.fastback.logging.SystemLogger.syslog; +import static net.pcal.fastback.mod.MinecraftProvider.messageToText; + +/** + * @author pcal + * @since 0.16.0 + */ +class ForgeCommonProvider implements MinecraftProvider, MixinGateway { + + static final String MOD_ID = "fastback"; + private MinecraftServer logicalServer; + private ModContainer container; + private LifecycleListener lifecycleListener = null; + private Runnable autoSaveListener; + private boolean isWorldSaveEnabled = true; + + ForgeCommonProvider(ModContainer container) { + this.container = container; + + container.getEventBus().addListener(this::onDedicatedServerStartupEvent); + NeoForge.EVENT_BUS.addListener(this::onServerStartupEvent); + NeoForge.EVENT_BUS.addListener(this::onServerStoppingEvent); + NeoForge.EVENT_BUS.addListener(this::onRegisterCommandEvent); + } + + + // ====================================================================== + // Forge Event handlers + + private void onDedicatedServerStartupEvent(FMLDedicatedServerSetupEvent event) { + this.onInitialize(); + } + + private void onServerStartupEvent(ServerStartedEvent event) { + this.logicalServer = event.getServer(); + requireNonNull(this.lifecycleListener).onWorldStart(); + } + + private void onServerStoppingEvent(ServerStoppingEvent event) { + requireNonNull(this.lifecycleListener).onWorldStop(); + this.logicalServer = null; + } + + private void onRegisterCommandEvent(RegisterCommandsEvent event) { + final CommandDispatcher commandDispatcher = event.getDispatcher(); + final LiteralArgumentBuilder backupCommand = + createBackupCommand(permName -> x -> true); + commandDispatcher.register(backupCommand); + } + + // ====================================================================== + // Protected + + /** + * This is the key initialization routine. Registers the logger, the frameworkprovider and the commands + * where the rest of the mod can get at them. + */ + void onInitialize() { + SystemLogger.Singleton.register(new Slf4jSystemLogger(LoggerFactory.getLogger(MOD_ID))); + this.lifecycleListener = MinecraftProvider.register(this); + syslog().debug("registered backup command"); + this.lifecycleListener.onInitialize(); + syslog().info("Fastback " + getModVersion() + " initialized"); + syslog().warn("------------------------------------------------------------------------------------"); + syslog().warn("Thanks for trying the new NeoForge version of Fastback. For help, go to:"); + syslog().warn("https://pcal43.github.io/fastback/"); + syslog().warn("Please note that this is an alpha release. A list of known issues is available here:"); + syslog().warn("https://github.com/pcal43/fastback/issues?q=is%3Aissue+is%3Aopen+label%3Aforge"); + syslog().warn("------------------------------------------------------------------------------------"); + MixinGateway.Singleton.register(this); + } + + + // ====================================================================== + // Fastback MinecraftProvider implementation + + @Override + public boolean isClient() { + return false; + } + + @Override + public void setHudText(UserMessage userMessage) { + } + + @Override + public void clearHudText() { + } + + @Override + public void setMessageScreenText(UserMessage userMessage) { + } + + void renderOverlayText(GuiGraphics drawContext) { + } + + @Override + public String getModVersion() { + return container.getModInfo().getVersion().toString(); + } + + @Override + public void autoSaveCompleted() { + syslog().debug("autoSaveCompleted"); + this.autoSaveListener.run(); + } + + @Override + public Path getWorldDirectory() { + if (logicalServer == null) throw new IllegalStateException("minecraftServer is null"); + return logicalServer.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize(); + } + + @Override + public void setWorldSaveEnabled(boolean enabled) { + isWorldSaveEnabled = enabled; + if (logicalServer == null) throw new IllegalStateException("minecraftServer is null"); + for (ServerLevel world : logicalServer.getAllLevels()) { + world.noSave = !enabled; + } + } + + @Override + public boolean isWorldSaveEnabled() { + return isWorldSaveEnabled; + } + + @Override + public void saveWorld() { + if (this.logicalServer == null) throw new IllegalStateException(); + this.logicalServer.saveEverything(false, true, true); // suppressLogs, flush, force + } + + @Override + public void sendBroadcast(UserMessage userMessage) { + if (this.logicalServer != null && this.logicalServer.isDedicatedServer()) { + logicalServer.getPlayerList().broadcastSystemMessage(messageToText(userMessage), false); + } + } + + @Override + public void setAutoSaveListener(Runnable runnable) { + this.autoSaveListener = requireNonNull(runnable); + } + + @Override + public Path getSavesDir() { + if (this.isClient()) { + return logicalServer.getServerDirectory().resolve("saves"); + } else { + return null; + } + } + + @Override + public String getWorldName() { + return this.logicalServer.getWorldData().getLevelName(); + } + + /** + * Add extra properties that will be stored in .fastback/backup.properties. + */ + @Override + public void addBackupProperties(Map props) { + props.put("fastback-version", this.getModVersion()); + if (this.logicalServer != null) { + props.put("minecraft-version", logicalServer.getServerVersion()); + props.put("minecraft-game-mode", String.valueOf(logicalServer.getWorldData().getGameType())); + props.put("minecraft-level-name", logicalServer.getWorldData().getLevelName()); + } + } + + /** + * @return paths to the files and directories that should be backed up when config-backup is enabled. + */ + @Override + public Collection getModsBackupPaths() { + final List out = new ArrayList<>(); + out.add(FMLPaths.GAMEDIR.get().resolve("options.txt")); + out.add(FMLPaths.MODSDIR.get()); + out.add(FMLPaths.CONFIGDIR.get()); + out.add(FMLPaths.GAMEDIR.get().resolve("resourcepacks")); + return out; + } +} diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeInitializer.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeInitializer.java new file mode 100644 index 00000000..34903d06 --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/ForgeInitializer.java @@ -0,0 +1,32 @@ +package net.pcal.fastback.mod.neoforge; + +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.ModLoadingContext; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLEnvironment; + +import java.lang.reflect.InvocationTargetException; + +/** + * @author pcal + * @since 0.16.0 + */ +@Mod("fastback") +final public class ForgeInitializer { + + public ForgeInitializer(ModContainer container) { + try { + if (FMLEnvironment.dist.isDedicatedServer()) { + new ForgeCommonProvider(container); + } else if (FMLEnvironment.dist.isClient()) { + // Forge yells at us if we touch any client classes in a server. So, + Class.forName("net.pcal.fastback.mod.neoforge.ForgeClientProvider").getConstructor(ModContainer.class).newInstance(container); + } else { + throw new IllegalStateException("where am i? server or client?"); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/MixinGateway.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/MixinGateway.java new file mode 100644 index 00000000..2929e0f7 --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/MixinGateway.java @@ -0,0 +1,44 @@ +/* + * FastBack - Fast, incremental Minecraft backups powered by Git. + * Copyright (C) 2022 pcal.net + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ + +package net.pcal.fastback.mod.neoforge; + +/** + * Singleton 'gateway' that mixin code goes through to call back into the mod. + * + * @author pcal + * @since 0.13.1 + */ +public interface MixinGateway { + + static MixinGateway get() { + return Singleton.INSTANCE; + } + + boolean isWorldSaveEnabled(); + + void autoSaveCompleted(); + + class Singleton { + private static MixinGateway INSTANCE = null; + + public static void register(MixinGateway gateway) { + Singleton.INSTANCE = gateway; + } + } +} diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/Slf4jSystemLogger.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/Slf4jSystemLogger.java new file mode 100644 index 00000000..080d05a7 --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/Slf4jSystemLogger.java @@ -0,0 +1,66 @@ +package net.pcal.fastback.mod.neoforge; + +import net.pcal.fastback.logging.SystemLogger; +import org.slf4j.Logger; + +import static java.util.Objects.requireNonNull; + +/** + * @author pcal + * @since 0.16.0 + */ +class Slf4jSystemLogger implements SystemLogger { + + private final Logger slf4j; + private boolean forceDebugEnabled = false; + + Slf4jSystemLogger(Logger slf4j) { + this.slf4j = requireNonNull(slf4j); + } + + @Override + public void setForceDebugEnabled(boolean forceDebugEnabled) { + this.forceDebugEnabled = forceDebugEnabled; + } + + @Override + public void error(String message) { + this.slf4j.error(message); + } + + @Override + public void error(String message, Throwable t) { + this.slf4j.error(message, t); + } + + @Override + public void warn(String message) { + this.slf4j.warn(message); + } + + @Override + public void warn(String message, Throwable t) {this.slf4j.warn(message, t);} + + @Override + public void info(String message) { + this.slf4j.info(message); + } + + @Override + public void debug(String message) { + if (this.forceDebugEnabled) { + this.slf4j.info("[DEBUG] " + message); + } else { + this.slf4j.debug(message); + } + } + + @Override + public void debug(String message, Throwable t) { + if (this.forceDebugEnabled) { + this.slf4j.info("[DEBUG] " + message, t); + } else { + this.slf4j.debug(message, t); + } + } +} diff --git a/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/mixins/MinecraftServerMixin.java b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/mixins/MinecraftServerMixin.java new file mode 100644 index 00000000..e1d6d83a --- /dev/null +++ b/neoforge/src/main/java/net/pcal/fastback/mod/neoforge/mixins/MinecraftServerMixin.java @@ -0,0 +1,84 @@ +/* + * FastBack - Fast, incremental Minecraft backups powered by Git. + * Copyright (C) 2022 pcal.net + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see . + */ +package net.pcal.fastback.mod.neoforge.mixins; + +import net.minecraft.server.MinecraftServer; +import net.pcal.fastback.mod.neoforge.MixinGateway; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import static net.pcal.fastback.logging.SystemLogger.syslog; + +/** + * Allows us to disable vanilla saving during 'git add' to avoid coherency problems in the backup snapshots. Also + * sends notifications when autosaving completes so we can follow them with automated backups. + * + * @author pcal + * @since 0.0.1 + */ +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + + /** + * Intercept the call to saveAll that triggers on autosave, pass it through and then send out notification that + * the autosave is done. + */ + @Redirect(method = "autoSave()V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;saveEverything(ZZZ)Z")) + public boolean fastback_saveAll(MinecraftServer instance, boolean suppressLogs, boolean flush, boolean force) { + boolean result = instance.saveEverything(suppressLogs, flush, force); + MixinGateway.get().autoSaveCompleted(); + return result; + } + + /** + * Intercept save so we can hard-disable saving during critical parts of the backup. + */ + @Inject(at = @At("HEAD"), method = "saveAllChunks(ZZZ)Z", cancellable = true) + public void fastback_save(boolean suppressLogs, boolean flush, boolean force, CallbackInfoReturnable ci) { + synchronized (this) { + if (MixinGateway.get().isWorldSaveEnabled()) { + syslog().debug("world saves are enabled, doing requested save"); + } else { + syslog().warn("Skipping requested save because a backup is in progress."); + ci.setReturnValue(false); + ci.cancel(); + } + } + } + + /** + * Intercept saveAll so we can hard-disable saving during critical parts of the backup. + */ + @Inject(at = @At("HEAD"), method = "saveEverything(ZZZ)Z", cancellable = true) + public void fastback_saveAll(boolean suppressLogs, boolean flush, boolean force, CallbackInfoReturnable ci) { + synchronized (this) { + if (MixinGateway.get().isWorldSaveEnabled()) { + syslog().debug("world saves are enabled, doing requested saveAll"); + //TODO should call save here to ensure all synced? + } else { + syslog().warn("Skipping requested saveAll because a backup is in progress."); + ci.setReturnValue(false); + ci.cancel(); + } + } + } +} diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 00000000..21873836 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,7 @@ +public net.minecraft.server.MinecraftServer f_129744_ # storageSource +public net.minecraft.server.MinecraftServer session +public net.minecraft.world.level.storage.LevelStorage$Session directory + +# Note it's 'gui.screen' in my dev but 'gui.screens' at runtimein a launcher. +# Mapping difference I guess, not sure which is best to use for Forge. +public-f net.minecraft.client.gui.screens.Screen f_96539_ # title \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 00000000..24070ba1 --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,32 @@ +modLoader = "javafml" +loaderVersion = "[5,)" +issueTrackerURL = "https://github.com/pcal43/fastback" +license = "GPL2" + +[[mods]] +modId = "fastback" +version = "0.19.2+1.21.3-prerelease" +displayName = "Fast Backups" +authors = "pcal" +description = ''' +Fast, incremental world backups powered by Git. +https://pcal43.github.io/fastback/ +''' +logoFile = "fastback-icon.png" + +[[dependencies.fastback]] +modId = "neoforge" +mandatory = true +versionRange = "[21.3.29-beta,)" +ordering = "NONE" +side = "CLIENT" + +[[dependencies.fastback]] +modId = "minecraft" +mandatory = true +versionRange = "1.21.3" +ordering = "NONE" +side = "CLIENT" + +[[mixins]] +config="fastback.mixins.json" diff --git a/neoforge/src/main/resources/fastback-icon.png b/neoforge/src/main/resources/fastback-icon.png new file mode 100644 index 00000000..0bbcbfa5 Binary files /dev/null and b/neoforge/src/main/resources/fastback-icon.png differ diff --git a/neoforge/src/main/resources/fastback.mixins.json b/neoforge/src/main/resources/fastback.mixins.json new file mode 100644 index 00000000..cc2278b9 --- /dev/null +++ b/neoforge/src/main/resources/fastback.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.pcal.fastback.mod.neoforge.mixins", + "compatibilityLevel": "JAVA_16", + "mixins": [ + "MinecraftServerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/neoforge/src/main/resources/pack.mcmeta b/neoforge/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..90073a5e --- /dev/null +++ b/neoforge/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "Fast Backups", + "pack_format": 13 + } +} diff --git a/settings.gradle b/settings.gradle index d103e060..689bdd52 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,12 +11,12 @@ pluginManagement { } //mavenCentral() gradlePluginPortal() + maven { + name = 'NeoForged' + url = 'https://maven.neoforged.net/releases' + } // NOTE: Forge is no longer supported and the build has been disabled // See: https://github.com/pcal43/fastback/issues/333 - // maven { - // name = 'NeoForged' - // url = 'https://maven.neoforged.net/releases' - //} // architectury-loom still demands this for some reason maven { name = "Forge" @@ -29,6 +29,7 @@ rootProject.name = "fastback" include(":common") include(":fabric") +include(":neoforge") // NOTE: Forge is no longer supported and the build has been disabled // See: https://github.com/pcal43/fastback/issues/333