diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 934baf2..a1f7946 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -716,6 +716,41 @@ jobs: path: fabric/1.21.2/build/libs/AdvancedBackups-fabric-1.21.2-${{ inputs.version }}.jar + fabric-26-1: + runs-on: ubuntu-latest + needs: core + + steps: + + - uses: actions/checkout@v3 + with: + ref: main + + - name: Download corelib + uses: actions/download-artifact@v4 + with: + name: advancedbackups-corelib.jar + path: core/build/libs/ + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: 25 + distribution: 'temurin' + + - name: Build Mod + run: | + cd fabric/26.1 + ./gradlew -Pversion=${{ inputs.version }} build + + + - name: Upload Mod + uses: actions/upload-artifact@v4 + with: + name: AdvancedBackups-fabric-26.1-${{ inputs.version }}.jar + path: fabric/26.1/build/libs/AdvancedBackups-fabric-26.1-${{ inputs.version }}.jar + + fabric-26-2: runs-on: ubuntu-latest needs: core @@ -935,6 +970,41 @@ jobs: + neoforge-26-1: + runs-on: ubuntu-latest + needs: core + + steps: + + - uses: actions/checkout@v3 + with: + ref: main + + - name: Download corelib + uses: actions/download-artifact@v4 + with: + name: advancedbackups-corelib.jar + path: core/build/libs/ + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: 25 + distribution: 'temurin' + + - name: Build Mod + run: | + cd neoforge/26.1 + ./gradlew -Pversion=${{ inputs.version }} build + + + - name: Upload Mod + uses: actions/upload-artifact@v4 + with: + name: AdvancedBackups-neoforge-26.1-${{ inputs.version }}.jar + path: neoforge/26.1/build/libs/AdvancedBackups-neoforge-26.1-${{ inputs.version }}.jar + + neoforge-26-2: runs-on: ubuntu-latest needs: core @@ -1015,7 +1085,7 @@ jobs: runs-on: ubuntu-latest needs: [core, forge-1-7-10, forge-1-12-2, forge-1-16, forge-1-18, fabric-1-18, forge-1-19-2, forge-1-19-3, fabric-1-19-2, fabric-1-19-3, forge-1-20, forge-1-20-2, fabric-1-20, fabric-1-20-2, neoforge-1-20-2, - forge-1-20-4, forge-1-20-6, fabric-1-20-4, fabric-1-21, fabric-1-21-2, fabric-26-2, neoforge-1-20-4, fabric-1-20-6, neoforge-1-20-6, neoforge-1-21, neoforge-1-21-2, neoforge-26-2, + forge-1-20-4, forge-1-20-6, fabric-1-20-4, fabric-1-21, fabric-1-21-2, fabric-26-1, fabric-26-2, neoforge-1-20-4, fabric-1-20-6, neoforge-1-20-6, neoforge-1-21, neoforge-1-21-2, neoforge-26-1, neoforge-26-2, spigot-1-21] steps: @@ -1135,6 +1205,12 @@ jobs: name: AdvancedBackups-fabric-1.21.2-${{ inputs.version }}.jar path: archives/ + - name: Download Fabric-26-1 + uses: actions/download-artifact@v4 + with: + name: AdvancedBackups-fabric-26.1-${{ inputs.version }}.jar + path: archives/ + - name: Download Fabric-26-2 uses: actions/download-artifact@v4 with: @@ -1171,6 +1247,12 @@ jobs: name: AdvancedBackups-neoforge-1.21.2-${{ inputs.version }}.jar path: archives/ + - name: Download Neoforge-26-1 + uses: actions/download-artifact@v4 + with: + name: AdvancedBackups-neoforge-26.1-${{ inputs.version }}.jar + path: archives/ + - name: Download Neoforge-26-2 uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/minecraft-harness.yml b/.github/workflows/minecraft-harness.yml new file mode 100644 index 0000000..f537cdd --- /dev/null +++ b/.github/workflows/minecraft-harness.yml @@ -0,0 +1,55 @@ +# Run Advanced Backups against a live dedicated Minecraft server. +name: Minecraft harness + +on: + workflow_dispatch: + inputs: + loader: + description: Mod loader + type: choice + required: true + default: fabric + options: + - fabric + - neoforge + mc: + description: Minecraft line + type: choice + required: true + default: '26.1' + options: + - '26.1' + - '26.2' + +jobs: + harness: + runs-on: ubuntu-latest + timeout-minutes: 40 + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 (core) + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: temurin + + - name: Set up JDK 25 (Minecraft 26.x) + uses: actions/setup-java@v4 + with: + java-version: 25 + distribution: temurin + + - name: Run dedicated-server harness + run: python3 scripts/minecraft-harness/run_harness.py --loader ${{ inputs.loader }} --mc ${{ inputs.mc }} + + - name: Upload harness logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: harness-${{ inputs.loader }}-${{ inputs.mc }} + path: | + ${{ inputs.loader }}/${{ inputs.mc }}/harness-gradle.log + ${{ inputs.loader }}/${{ inputs.mc }}/run-harness/logs/ + if-no-files-found: ignore diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a70587e..e4b6cce 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,12 +37,14 @@ on: - Fabric 1.20.6 - Fabric 1.21 - Fabric 1.21.2 + - Fabric 26.1 - Fabric 26.2 - Neoforge 1.20.2 - Neoforge 1.20.4 - Neoforge 1.20.6 - Neoforge 1.21 - Neoforge 1.21.2 + - Neoforge 26.1 - Neoforge 26.2 - Spigot 1.21 @@ -631,6 +633,33 @@ jobs: changelog: ${{ vars.CHANGELOG }} + - name: Upload Fabric 26.1 + + if: inputs.version == 'Fabric 26.1' || inputs.version == 'All' + uses: Kir-Antipov/mc-publish@v3.3 + with: + curseforge-id: 876284 + curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} + modrinth-id: Jrmoreqs + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: false + files: | + **/AdvancedBackups-fabric-26.1-${{ steps.version.outputs.VERSION }}.jar + name: Fabric 26.1 - ${{ steps.version.outputs.VERSION }} + version: ${{ steps.version.outputs.VERSION }} + version-type: release + loaders: | + fabric + quilt + game-versions: | + 26.1 + 26.1.1 + 26.1.2 + java: | + Java 25 + changelog: ${{ vars.CHANGELOG }} + + - name: Upload Fabric 26.2 if: inputs.version == 'Fabric 26.2' || inputs.version == 'All' @@ -798,6 +827,32 @@ jobs: changelog: ${{ vars.CHANGELOG }} + - name: Upload Neoforge 26.1 + + if: inputs.version == 'Neoforge 26.1' || inputs.version == 'All' + uses: Kir-Antipov/mc-publish@v3.3 + with: + curseforge-id: 876284 + curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} + modrinth-id: Jrmoreqs + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-featured: false + files: | + **/AdvancedBackups-neoforge-26.1-${{ steps.version.outputs.VERSION }}.jar + name: Neoforge 26.1 - ${{ steps.version.outputs.VERSION }} + version: ${{ steps.version.outputs.VERSION }} + version-type: release + loaders: | + neoforge + game-versions: | + 26.1 + 26.1.1 + 26.1.2 + java: | + Java 25 + changelog: ${{ vars.CHANGELOG }} + + - name: Upload Neoforge 26.2 if: inputs.version == 'Neoforge 26.2' || inputs.version == 'All' diff --git a/.gitignore b/.gitignore index 52146f4..36c2632 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,8 @@ build # other run runs +run-harness +harness-gradle.log +__pycache__/ +*.pyc forge*changelog.txt diff --git a/README.md b/README.md index 2cfe92d..c7f8be3 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,15 @@ A powerful backup mod for Minecraft, supporting Neoforge, Forge and Fabric. Many Minecraft versions are supported - request more if the one you want isn't yet supported. -### Minecraft 26.2 Support -**Version 3.8-26.2** adds Fabric and NeoForge support for Minecraft 26.2 (Chaos Cubed). +### Minecraft 26.1 and 26.2 Support +**Version 3.8-26.1** and **3.8-26.2** add Fabric and NeoForge support for Minecraft 26.1 (including 26.1.2) and 26.2 (Chaos Cubed). -| Requirement | Version | -|-------------|---------| -| Fabric Loader | 0.19.3+ | -| Fabric API | 0.157.0+26.2 | -| NeoForge | 26.2.0.59+ | -| Java | 25+ | +| Requirement | 26.1 | 26.2 | +|-------------|------|------| +| Fabric Loader | 0.19.3+ | 0.19.3+ | +| Fabric API | 0.155.2+26.1.2 | 0.157.0+26.2 | +| NeoForge | 26.1.2.97+ | 26.2.0.59+ | +| Java | 25+ | 25+ | Toast notifications render with the advancement toast background, text, item icon, and progress bar. @@ -40,12 +40,12 @@ The latest Minecraft release supported here is **26.2**. | Loader | Minecraft versions | |--------|--------------------| -| Fabric | 1.18, 1.19.2, 1.19.3, 1.20, 1.20.2, 1.20.6, 1.21, 1.21.2, 1.21.10, **26.2** | -| NeoForge | 1.20.2, 1.20.4, 1.20.6, 1.21, 1.21.2, **26.2** | +| Fabric | 1.18, 1.19.2, 1.19.3, 1.20, 1.20.2, 1.20.6, 1.21, 1.21.2, 1.21.10, **26.1**, **26.2** | +| NeoForge | 1.20.2, 1.20.4, 1.20.6, 1.21, 1.21.2, **26.1**, **26.2** | | Forge | 1.7.10, 1.12, 1.16, 1.18, 1.19.2, 1.19.3, 1.20, 1.20.2, 1.20.6 | | Spigot | 1.21 | -Minecraft 26.2 requires **Java 25**. Older ports keep their original Java requirements. +Minecraft 26.1 and 26.2 require **Java 25**. Older ports keep their original Java requirements. ## Features: - Command-line restoration tool built into the jar, with helper scripts to run it diff --git a/fabric/26.1/README.md b/fabric/26.1/README.md new file mode 100644 index 0000000..c8df509 --- /dev/null +++ b/fabric/26.1/README.md @@ -0,0 +1,14 @@ +# Fabric - 26.1 + +This is the port for Fabric Minecraft 26.1, compiled against 26.1.2. + +## Build Information +- **Version**: 3.8-26.1 +- **Minecraft**: 26.1 / 26.1.1 / 26.1.2 +- **Fabric Loader**: 0.19.3+ +- **Fabric API**: 0.155.2+26.1.2 +- **Java**: 25+ + +Toast background rendering uses `GuiGraphicsExtractor.blitSprite` and `RenderPipelines.GUI_TEXTURED`. + +For full documentation, see the main repository README. diff --git a/fabric/26.1/build.gradle b/fabric/26.1/build.gradle new file mode 100644 index 0000000..8478078 --- /dev/null +++ b/fabric/26.1/build.gradle @@ -0,0 +1,89 @@ +plugins { + id 'net.fabricmc.fabric-loom' version "${loom_version}" + id 'maven-publish' +} + +apply from: '../../global.properties' + +group = project.maven_group +version = project.version + +base { + archivesName = 'AdvancedBackups-' + modloaderName + "-" + minecraftVersion +} + +loom { + runs { + harnessServer { + server() + runDir = "run-harness" + vmArg "-Xmx2G" + programArg "nogui" + } + } +} + +repositories { +} + +configurations { + extraLibs +} + +dependencies { + minecraft "com.mojang:minecraft:${project.minecraft_version}" + implementation "net.fabricmc:fabric-loader:${project.loader_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + + implementation files(abCoreLibPath) + extraLibs files(abCoreLibPath) +} + +processResources { + def version = project.version + inputs.property "version", version + + filesMatching("fabric.mod.json") { + expand "version": version + } +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 25 + it.options.encoding = 'UTF-8' +} + +java { + withSourcesJar() + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } +} + +jar { + manifest { + attributes([ + "Main-Class" : "computer.heather.advancedbackups.cli.AdvancedBackupsCLI", + "Specification-Title" : "advancedbackups", + "Specification-Vendor" : "Heather White", + "Specification-Version" : "1", + "Implementation-Title" : project.name, + "Implementation-Version" : project.jar.archiveVersion, + "Implementation-Vendor" : "Heather White", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } + from { + configurations.extraLibs.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +publishing { + publications { + create("mavenJava", MavenPublication) { + from components.java + } + } + repositories { + } +} diff --git a/fabric/26.1/gradle.properties b/fabric/26.1/gradle.properties new file mode 100644 index 0000000..056fec2 --- /dev/null +++ b/fabric/26.1/gradle.properties @@ -0,0 +1,21 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G +org.gradle.parallel=true +org.gradle.configuration-cache=false + +# Fabric Properties +# check these on https://fabricmc.net/develop +minecraft_version=26.1.2 +loader_version=0.19.3 +loom_version=1.17-SNAPSHOT + +# Mod Properties +version=3.8-26.1 +mod_version=3.8-26.1 +maven_group=computer.heather.advancedbackups +archives_base_name=advancedbackups +modloaderName=fabric +minecraftVersion=26.1 + +# Dependencies +fabric_api_version=0.155.2+26.1.2 diff --git a/fabric/26.1/gradle/wrapper/gradle-wrapper.jar b/fabric/26.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/fabric/26.1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/fabric/26.1/gradle/wrapper/gradle-wrapper.properties b/fabric/26.1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df6a6ad --- /dev/null +++ b/fabric/26.1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/fabric/26.1/gradlew b/fabric/26.1/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/fabric/26.1/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# 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/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# 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 -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 + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/fabric/26.1/gradlew.bat b/fabric/26.1/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/fabric/26.1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +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 + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/fabric/26.1/settings.gradle b/fabric/26.1/settings.gradle new file mode 100644 index 0000000..cba66f2 --- /dev/null +++ b/fabric/26.1/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'fabric-26.1' diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java new file mode 100644 index 0000000..6d8045c --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java @@ -0,0 +1,126 @@ +package computer.heather.advancedbackups; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import computer.heather.advancedbackups.client.ClientContactor; +import computer.heather.advancedbackups.core.ABCore; +import computer.heather.advancedbackups.core.backups.BackupTimer; +import computer.heather.advancedbackups.core.backups.BackupWrapper; +import computer.heather.advancedbackups.core.config.ConfigManager; +import computer.heather.advancedbackups.network.PacketBackupStatus; +import computer.heather.advancedbackups.network.PacketToastSubscribe; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.storage.LevelResource; + +public class AdvancedBackups implements ModInitializer { + + public static final Logger LOGGER = LoggerFactory.getLogger("advanced-backups"); + + public static final Consumer infoLogger = LOGGER::info; + public static final Consumer warningLogger = LOGGER::warn; + public static final Consumer errorLogger = LOGGER::error; + + public static final ArrayList players = new ArrayList<>(); + + public static MinecraftServer server; + + @Override + public void onInitialize() { + + ServerLifecycleEvents.SERVER_STARTING.register((server) -> { + AdvancedBackups.server = server; + ABCore.worldName = server.getWorldData().getLevelName(); + ABCore.worldDir = server.getWorldPath(LevelResource.ROOT); + + ABCore.disableSaving = AdvancedBackups::disableSaving; + ABCore.enableSaving = AdvancedBackups::enableSaving; + ABCore.saveOnce = AdvancedBackups::saveOnce; + + ABCore.infoLogger = infoLogger; + ABCore.warningLogger = warningLogger; + ABCore.errorLogger = errorLogger; + + ABCore.resetActivity = AdvancedBackups::resetActivity; + + ABCore.clientContactor = new ClientContactor(); + ABCore.modJar = FabricLoader.getInstance() + .getModContainer("advancedbackups") + .get() + .getOrigin() + .getPaths() + .get(0) + .toFile(); + + ConfigManager.loadOrCreateConfig(); + LOGGER.info("Config loaded!!"); + }); + + ServerLifecycleEvents.SERVER_STARTED.register((server) -> { + BackupWrapper.checkStartupBackups(); + }); + ServerLifecycleEvents.SERVER_STOPPING.register((server) -> { + BackupWrapper.checkShutdownBackups(); + }); + + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { + ABCore.setActivity(true); + }); + + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + AdvancedBackupsCommand.register(dispatcher); + }); + + ServerTickEvents.END_SERVER_TICK.register((server) -> { + BackupTimer.check(); + }); + + PayloadTypeRegistry.clientboundPlay().register(PacketBackupStatus.TYPE, PacketBackupStatus.CODEC); + PayloadTypeRegistry.serverboundPlay().register(PacketToastSubscribe.TYPE, PacketToastSubscribe.CODEC); + + ServerPlayNetworking.registerGlobalReceiver(PacketToastSubscribe.TYPE, PacketToastSubscribe::handle); + } + + public static void disableSaving() { + MinecraftServer server = AdvancedBackups.server; + for (ServerLevel level : server.getAllLevels()) { + if (level != null && !level.noSave) { + level.noSave = true; + } + } + } + + public static void enableSaving() { + MinecraftServer server = AdvancedBackups.server; + for (ServerLevel level : server.getAllLevels()) { + if (level != null && level.noSave) { + level.noSave = false; + } + } + } + + public static void saveOnce(boolean flush) { + MinecraftServer server = AdvancedBackups.server; + server.saveEverything(true, flush, true); + } + + public static void resetActivity() { + List players = server.getPlayerList().getPlayers(); + ABCore.setActivity(!players.isEmpty()); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java new file mode 100644 index 0000000..28eb0e8 --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java @@ -0,0 +1,64 @@ +package computer.heather.advancedbackups; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; + +import computer.heather.advancedbackups.core.CoreCommandSystem; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; + +public class AdvancedBackupsCommand { + public static void register(CommandDispatcher stack) { + stack.register(Commands.literal("backup").requires((runner) -> { + return !AdvancedBackups.server.isDedicatedServer() || Commands.LEVEL_GAMEMASTERS.check(runner.permissions()); + }).then(Commands.literal("start").executes((runner) -> { + CoreCommandSystem.startBackup((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reload-config").executes((runner) -> { + CoreCommandSystem.reloadConfig((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reset-chain").executes((runner) -> { + CoreCommandSystem.resetChainLength((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("snapshot").executes((runner) -> { + CoreCommandSystem.snapshot((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }, "snapshot"); + return 1; + }) + .then(Commands.argument("name", StringArgumentType.greedyString()).executes((runner) -> { + String snapshotName = StringArgumentType.getString(runner, "name"); + CoreCommandSystem.snapshot((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }, snapshotName); + return 1; + }))) + + .then(Commands.literal("cancel").executes((runner) -> { + CoreCommandSystem.cancelBackup((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reload-client-config").executes((runner) -> { + runner.getSource().sendSuccess(() -> Component.literal("This command can only be ran on the client!"), true); + return 1; + })) + + ); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java new file mode 100644 index 0000000..b6f1d31 --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java @@ -0,0 +1,57 @@ +package computer.heather.advancedbackups.client; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; + +import computer.heather.advancedbackups.core.CoreCommandSystem; +import net.fabricmc.fabric.api.client.command.v2.ClientCommands; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; + +public class AdvancedBackupsClientCommand { + public static void register(CommandDispatcher dispatcher) { + dispatcher.register(ClientCommands.literal("backup").requires((runner) -> { + return true; + }).then(ClientCommands.literal("start").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup start")); + return 1; + })) + + .then(ClientCommands.literal("reload-config").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup reload-config")); + return 1; + })) + + .then(ClientCommands.literal("reset-chain").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup reset-chain")); + return 1; + })) + + .then(ClientCommands.literal("snapshot").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup snapshot")); + return 1; + }) + + .then(ClientCommands.argument("name", StringArgumentType.greedyString()).executes((runner) -> { + String snapshotName = StringArgumentType.getString(runner, "name"); + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup snapshot " + snapshotName)); + return 1; + }))) + + .then(ClientCommands.literal("cancel").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup cancel")); + return 1; + })) + + .then(ClientCommands.literal("reload-client-config").executes((runner) -> { + CoreCommandSystem.reloadClientConfig((response) -> { + runner.getSource().sendFeedback(Component.literal(response)); + }); + return 1; + })) + + ); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java new file mode 100644 index 0000000..c54bcb2 --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java @@ -0,0 +1,138 @@ +package computer.heather.advancedbackups.client; + +import computer.heather.advancedbackups.core.config.ClientConfigManager; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +public class BackupToast implements Toast { + + public static boolean starting; + public static boolean started; + public static boolean failed; + public static boolean finished; + public static boolean cancelled; + + public static int progress; + public static int max; + + public static boolean exists = false; + + private static long time; + private static boolean timeSet = false; + + public static final ItemStack stack = new ItemStack(Items.PAPER); + private static final Identifier TEXTURE = Identifier.withDefaultNamespace("toast/advancement"); + + public static String title = "You shouldn't see this!"; + public static int textColour = 0; + + private Visibility visibility = Visibility.SHOW; + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, Font font, long fullyVisibleForMs) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, ClientConfigManager.darkMode.get() ? 0 : this.height(), this.width(), this.height()); + graphics.fakeItem(stack, 8, 8); + + float percent = finished ? 100 : (float) progress / (float) max; + + graphics.fill(4, 28, 156, 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBackgroundRed.get(), + (int) ClientConfigManager.progressBackgroundGreen.get(), + (int) ClientConfigManager.progressBackgroundBlue.get())); + + float f = Math.min(156, (156 * percent)); + + if (!exists) { + if (title.equals(I18n.get("advancedbackups.backup_finished"))) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + graphics.text(font, I18n.get(title), 25, 11, textColour, false); + graphics.fill(4, 28, 156, 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBarRed.get(), + (int) ClientConfigManager.progressBarGreen.get(), + (int) ClientConfigManager.progressBarBlue.get())); + } else { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + graphics.text(font, I18n.get(title), 25, 11, textColour, false); + } + visibility = Visibility.HIDE; + return; + } + + title = "You shouldn't see this!"; + + if (starting) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.backup_starting"); + } else if (started) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.progress", round(percent * 100)); + } else if (failed) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + title = I18n.get("advancedbackups.backup_failed"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } else if (finished) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.backup_finished"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } else if (cancelled) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + title = I18n.get("advancedbackups.backup_cancelled"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } + + graphics.text(font, title, 25, 11, textColour, false); + + if (timeSet && System.currentTimeMillis() >= time + 5000) { + starting = false; + started = false; + failed = false; + finished = false; + progress = 0; + max = 0; + timeSet = false; + exists = false; + visibility = Visibility.HIDE; + return; + } + + graphics.fill(4, 28, Math.max(4, (int) f), 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBarRed.get(), + (int) ClientConfigManager.progressBarGreen.get(), + (int) ClientConfigManager.progressBarBlue.get())); + + visibility = Visibility.SHOW; + } + + private static String round(float value) { + return String.format("%.1f", value); + } + + @Override + public Visibility getWantedVisibility() { + return visibility; + } + + @Override + public void update(ToastManager manager, long fullyVisibleForMs) { + // Toast lifetime is tracked in extractRenderState. + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java new file mode 100644 index 0000000..e420f4d --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java @@ -0,0 +1,50 @@ +package computer.heather.advancedbackups.client; + +import java.util.List; + +import computer.heather.advancedbackups.AdvancedBackups; +import computer.heather.advancedbackups.interfaces.IClientContactor; +import computer.heather.advancedbackups.network.NetworkHandler; +import computer.heather.advancedbackups.network.PacketBackupStatus; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +public class ClientContactor implements IClientContactor { + + @Override + public void backupComplete(boolean all) { + send(new PacketBackupStatus(false, false, false, true, false, 0, 0), all); + } + + @Override + public void backupFailed(boolean all) { + send(new PacketBackupStatus(false, false, true, false, false, 0, 0), all); + } + + @Override + public void backupProgress(int progress, int max, boolean all) { + send(new PacketBackupStatus(false, true, false, false, false, progress, max), all); + } + + @Override + public void backupStarting(boolean all) { + send(new PacketBackupStatus(true, false, false, false, false, 0, 0), all); + } + + @Override + public void backupCancelled(boolean all) { + send(new PacketBackupStatus(false, false, false, false, true, 0, 0), all); + } + + private void send(PacketBackupStatus packet, boolean all) { + MinecraftServer server = AdvancedBackups.server; + List players = server.getPlayerList().getPlayers(); + for (ServerPlayer player : players) { + if (!AdvancedBackups.players.contains(player.getStringUUID())) continue; + if (!server.isDedicatedServer() || Commands.LEVEL_ADMINS.check(player.permissions()) || all) { + NetworkHandler.sendToClient(player, packet); + } + } + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java new file mode 100644 index 0000000..d193c36 --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java @@ -0,0 +1,61 @@ +package computer.heather.advancedbackups.client; + +import computer.heather.advancedbackups.AdvancedBackups; +import computer.heather.advancedbackups.core.ABCore; +import computer.heather.advancedbackups.core.config.ClientConfigManager; +import computer.heather.advancedbackups.network.PacketBackupStatus; +import computer.heather.advancedbackups.network.PacketToastSubscribe; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.Minecraft; + +public class ClientWrapper implements ClientModInitializer { + + @Override + public void onInitializeClient() { + + ABCore.infoLogger = AdvancedBackups.infoLogger; + ABCore.warningLogger = AdvancedBackups.warningLogger; + ABCore.errorLogger = AdvancedBackups.errorLogger; + + ClientLifecycleEvents.CLIENT_STARTED.register((client) -> { + ClientConfigManager.loadOrCreateConfig(); + }); + + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + PacketToastSubscribe packet = new PacketToastSubscribe(ClientConfigManager.showProgress.get()); + if (ClientPlayNetworking.canSend(PacketToastSubscribe.TYPE)) { + ClientPlayNetworking.send(packet); + } else { + ABCore.warningLogger.accept("Refusing to send packet " + packet + " as the server cannot accept it!"); + } + }); + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { + AdvancedBackupsClientCommand.register(dispatcher); + }); + + ClientPlayNetworking.registerGlobalReceiver(PacketBackupStatus.TYPE, ClientWrapper::handle); + } + + public static void handle(PacketBackupStatus message, ClientPlayNetworking.Context context) { + Minecraft.getInstance().execute(() -> { + BackupToast.starting = message.starting(); + BackupToast.started = message.started(); + BackupToast.failed = message.failed(); + BackupToast.finished = message.finished(); + BackupToast.cancelled = message.cancelled(); + + BackupToast.progress = message.progress(); + BackupToast.max = message.max(); + + if (!BackupToast.exists) { + BackupToast.exists = true; + Minecraft.getInstance().getToastManager().addToast(new BackupToast()); + } + }); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java new file mode 100644 index 0000000..9da12ef --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java @@ -0,0 +1,29 @@ +package computer.heather.advancedbackups.client; + +//A colour helper, loosely based on that of vanilla 1.16. +public class ColourHelper { + + public static int alpha(int in) { + return in >>> 24; + } + + public static int red(int in) { + return in >> 16 & 255; + } + + public static int green(int in) { + return in >> 8 & 255; + } + + public static int blue(int in) { + return in & 255; + } + + public static int colour(int a, int r, int g, int b) { + return a << 24 | r << 16 | g << 8 | b; + } + + public static int colour(int a, long r, long g, long b) { + return colour(a, (int) r, (int) g, (int) b); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java new file mode 100644 index 0000000..b9f70df --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java @@ -0,0 +1,11 @@ +package computer.heather.advancedbackups.network; + +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.server.level.ServerPlayer; + +public class NetworkHandler { + + public static void sendToClient(ServerPlayer player, PacketBackupStatus packet) { + ServerPlayNetworking.send(player, packet); + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java new file mode 100644 index 0000000..280477d --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java @@ -0,0 +1,34 @@ +package computer.heather.advancedbackups.network; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +public record PacketBackupStatus(boolean starting, boolean started, boolean failed, boolean finished, boolean cancelled, int progress, int max) implements CustomPacketPayload { + + public static final Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("advancedbackups", "backup_status")); + + public static final StreamCodec CODEC = StreamCodec.of((buf, packet) -> { + buf.writeBoolean(packet.starting); + buf.writeBoolean(packet.started); + buf.writeBoolean(packet.failed); + buf.writeBoolean(packet.finished); + buf.writeBoolean(packet.cancelled); + buf.writeInt(packet.progress); + buf.writeInt(packet.max); + }, buf -> new PacketBackupStatus( + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readInt(), + buf.readInt() + )); + + @Override + public Type type() { + return TYPE; + } +} diff --git a/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java new file mode 100644 index 0000000..4a90ba9 --- /dev/null +++ b/fabric/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java @@ -0,0 +1,34 @@ +package computer.heather.advancedbackups.network; + +import computer.heather.advancedbackups.AdvancedBackups; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; + +public record PacketToastSubscribe(boolean enable) implements CustomPacketPayload { + + public static final Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("advancedbackups", "toast_subscribe")); + + public static final StreamCodec CODEC = StreamCodec.of( + (buf, packet) -> buf.writeBoolean(packet.enable), + buf -> new PacketToastSubscribe(buf.readBoolean()) + ); + + public static void handle(PacketToastSubscribe message, ServerPlayNetworking.Context context) { + ServerPlayer player = context.player(); + + if (message.enable() && !AdvancedBackups.players.contains(player.getStringUUID())) { + AdvancedBackups.players.add(player.getStringUUID()); + } else if (!message.enable()) { + AdvancedBackups.players.remove(player.getStringUUID()); + } + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/fabric/26.1/src/main/resources/fabric.mod.json b/fabric/26.1/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..bc54a12 --- /dev/null +++ b/fabric/26.1/src/main/resources/fabric.mod.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "id": "advancedbackups", + "version": "${version}", + "name": "Advanced Backups", + "description": "An extremely advanced backup mod.\n\nSupports many backup types.\n\nConfig file and github contain documentation.", + "authors": [ + "Mommy Heather" + ], + "contact": { + "homepage": "https://github.com/HeatherComputer/advancedbackups" + }, + "license": "BSD", + "icon": "assets/advanced-backups/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "computer.heather.advancedbackups.AdvancedBackups" + ], + "client": [ + "computer.heather.advancedbackups.client.ClientWrapper" + ] + }, + "depends": { + "fabricloader": ">=0.19.3", + "minecraft": "~26.1", + "java": ">=25", + "fabric-api": "*" + } +} diff --git a/fabric/26.1/src/main/resources/pack.mcmeta b/fabric/26.1/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..16c8027 --- /dev/null +++ b/fabric/26.1/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "advancedbackups resources", + "pack_format": 81 + } +} diff --git a/fabric/26.2/build.gradle b/fabric/26.2/build.gradle index 8879ec2..8478078 100644 --- a/fabric/26.2/build.gradle +++ b/fabric/26.2/build.gradle @@ -12,6 +12,17 @@ base { archivesName = 'AdvancedBackups-' + modloaderName + "-" + minecraftVersion } +loom { + runs { + harnessServer { + server() + runDir = "run-harness" + vmArg "-Xmx2G" + programArg "nogui" + } + } +} + repositories { } diff --git a/neoforge/26.1/README.md b/neoforge/26.1/README.md new file mode 100644 index 0000000..569f39e --- /dev/null +++ b/neoforge/26.1/README.md @@ -0,0 +1,13 @@ +# NeoForge - 26.1 + +This is the port for NeoForge Minecraft 26.1, compiled against 26.1.2. + +## Build Information +- **Version**: 3.8-26.1 +- **Minecraft**: 26.1 / 26.1.1 / 26.1.2 +- **NeoForge**: 26.1.2.97+ +- **Java**: 25+ + +Toast background rendering uses `GuiGraphicsExtractor.blitSprite` and `RenderPipelines.GUI_TEXTURED`. + +For full documentation, see the main repository README. diff --git a/neoforge/26.1/build.gradle b/neoforge/26.1/build.gradle new file mode 100644 index 0000000..23f5d6d --- /dev/null +++ b/neoforge/26.1/build.gradle @@ -0,0 +1,123 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.neoforged.gradle.userdev' version '7.1.38' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN +} + +apply from: '../../global.properties' + +group = mod_group_id +version = "${project.version == 'unspecified' ? mod_version : project.version}" + +configurations { + extraLibs +} + +repositories { + mavenLocal() +} + +base { + archivesName = 'AdvancedBackups-' + modloaderName + "-" + minecraftVersion +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(25) + +runs { + configureEach { + systemProperty 'forge.logging.markers', 'REGISTRIES' + systemProperty 'forge.logging.console.level', 'debug' + workingDirectory project.layout.projectDirectory.dir('run').dir(name) + modSource project.sourceSets.main + } + + client { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + argument '--nogui' + jvmArgument '-Xmx2G' + } + + gameTestServer { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } +} + +sourceSets.main.resources { srcDir 'src/generated/resources' } + +dependencies { + implementation "net.neoforged:neoforge:${neo_version}" + implementation files(abCoreLibPath) + extraLibs files(abCoreLibPath) +} + +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range : minecraft_version_range, + neo_version : neo_version, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + [project: project] + } +} + +tasks.named('jar', Jar).configure { + manifest { + attributes([ + "Main-Class" : "computer.heather.advancedbackups.cli.AdvancedBackupsCLI", + "Specification-Title" : "advancedbackups", + "Specification-Vendor" : "Heather White", + "Specification-Version" : "1", + "Implementation-Title" : project.name, + "Implementation-Version" : project.jar.archiveVersion, + "Implementation-Vendor" : "Heather White", + "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } + from { + configurations.extraLibs.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + } + } + repositories { + maven { + url "file://${project.projectDir}/repo" + } + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 25 +} + +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} diff --git a/neoforge/26.1/gradle.properties b/neoforge/26.1/gradle.properties new file mode 100644 index 0000000..eda24d3 --- /dev/null +++ b/neoforge/26.1/gradle.properties @@ -0,0 +1,19 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false +org.gradle.parallel=true +org.gradle.configuration-cache=false + +minecraft_version=26.1.2 +minecraft_version_range=[26.1,26.2) +neo_version=26.1.2.97 + +mod_id=advancedbackups +mod_name=Advanced Backups +mod_license=BSD +mod_version=3.8-26.1 +mod_group_id=computer.heather.advancedbackups +mod_authors=Heather White +mod_description=A highly advanced and configurable backup mod. Github contains documentation. + +modloaderName=neoforge +minecraftVersion=26.1 diff --git a/neoforge/26.1/gradle/wrapper/gradle-wrapper.jar b/neoforge/26.1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/neoforge/26.1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/neoforge/26.1/gradle/wrapper/gradle-wrapper.properties b/neoforge/26.1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5dd3c01 --- /dev/null +++ b/neoforge/26.1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/neoforge/26.1/gradlew b/neoforge/26.1/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/neoforge/26.1/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# 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/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# 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 -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 + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/neoforge/26.1/gradlew.bat b/neoforge/26.1/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/neoforge/26.1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@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 ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +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 + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/neoforge/26.1/settings.gradle b/neoforge/26.1/settings.gradle new file mode 100644 index 0000000..d5ec427 --- /dev/null +++ b/neoforge/26.1/settings.gradle @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'neoforge-26.1' diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java new file mode 100644 index 0000000..b5702e4 --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackups.java @@ -0,0 +1,139 @@ +package computer.heather.advancedbackups; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import computer.heather.advancedbackups.client.ClientContactor; +import computer.heather.advancedbackups.client.ClientWrapper; +import computer.heather.advancedbackups.core.ABCore; +import computer.heather.advancedbackups.core.backups.BackupTimer; +import computer.heather.advancedbackups.core.backups.BackupWrapper; +import computer.heather.advancedbackups.core.config.ConfigManager; +import computer.heather.advancedbackups.network.NetworkHandler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.storage.LevelResource; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModList; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStartingEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; + +@Mod("advancedbackups") +public class AdvancedBackups { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final Consumer infoLogger = LOGGER::info; + public static final Consumer warningLogger = LOGGER::warn; + public static final Consumer errorLogger = LOGGER::error; + + public static final ArrayList players = new ArrayList<>(); + + public static MinecraftServer server; + + public AdvancedBackups(IEventBus modEventBus) { + NeoForge.EVENT_BUS.register(this); + modEventBus.addListener(NetworkHandler::onRegisterPayloadHandler); + + if (FMLEnvironment.getDist() == Dist.CLIENT) { + modEventBus.addListener(NetworkHandler::onRegisterClientPayloadHandler); + modEventBus.addListener(this::clientSetup); + } + + ABCore.infoLogger = infoLogger; + ABCore.warningLogger = warningLogger; + ABCore.errorLogger = errorLogger; + } + + @SubscribeEvent + public void onServerStarting(ServerStartingEvent event) { + AdvancedBackups.server = event.getServer(); + ABCore.worldName = event.getServer().getWorldData().getLevelName(); + ABCore.worldDir = event.getServer().getWorldPath(LevelResource.ROOT); + + ABCore.disableSaving = AdvancedBackups::disableSaving; + ABCore.enableSaving = AdvancedBackups::enableSaving; + ABCore.saveOnce = AdvancedBackups::saveOnce; + + ABCore.resetActivity = AdvancedBackups::resetActivity; + + ABCore.clientContactor = new ClientContactor(); + ABCore.modJar = ModList.get().getModFileById("advancedbackups").getFile().getFilePath().toFile(); + + ConfigManager.loadOrCreateConfig(); + LOGGER.info("Config loaded!!"); + } + + public void clientSetup(FMLClientSetupEvent e) { + ClientWrapper.init(e); + } + + @SubscribeEvent + public void onServerStarted(ServerStartedEvent event) { + BackupWrapper.checkStartupBackups(); + } + + @SubscribeEvent + public void onServerStopping(ServerStoppingEvent event) { + BackupWrapper.checkShutdownBackups(); + } + + @SubscribeEvent + public void onPlayerJoined(PlayerEvent.PlayerLoggedInEvent event) { + ABCore.setActivity(true); + } + + @SubscribeEvent + public void registerCommands(RegisterCommandsEvent event) { + AdvancedBackupsCommand.register(event.getDispatcher()); + } + + @SubscribeEvent + public void onPostTick(ServerTickEvent.Post event) { + BackupTimer.check(); + } + + public static void disableSaving() { + MinecraftServer server = AdvancedBackups.server; + for (ServerLevel level : server.getAllLevels()) { + if (level != null && !level.noSave) { + level.noSave = true; + } + } + } + + public static void enableSaving() { + MinecraftServer server = AdvancedBackups.server; + for (ServerLevel level : server.getAllLevels()) { + if (level != null && level.noSave) { + level.noSave = false; + } + } + } + + public static void saveOnce(boolean flush) { + MinecraftServer server = AdvancedBackups.server; + server.saveEverything(true, flush, true); + } + + public static void resetActivity() { + List players = server.getPlayerList().getPlayers(); + ABCore.setActivity(!players.isEmpty()); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java new file mode 100644 index 0000000..aad867d --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/AdvancedBackupsCommand.java @@ -0,0 +1,65 @@ +package computer.heather.advancedbackups; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; + +import computer.heather.advancedbackups.core.CoreCommandSystem; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; + +public class AdvancedBackupsCommand { + public static void register(CommandDispatcher stack) { + stack.register(Commands.literal("backup").requires((runner) -> { + return AdvancedBackups.server == null || !AdvancedBackups.server.isDedicatedServer() || Commands.LEVEL_GAMEMASTERS.check(runner.permissions()); + }).then(Commands.literal("start").executes((runner) -> { + CoreCommandSystem.startBackup((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reload-config").executes((runner) -> { + CoreCommandSystem.reloadConfig((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reset-chain").executes((runner) -> { + CoreCommandSystem.resetChainLength((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("snapshot").executes((runner) -> { + CoreCommandSystem.snapshot((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }, "snapshot"); + return 1; + }) + + .then(Commands.argument("name", StringArgumentType.greedyString()).executes((runner) -> { + String name = StringArgumentType.getString(runner, "name"); + CoreCommandSystem.snapshot((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }, name); + return 1; + }))) + + .then(Commands.literal("cancel").executes((runner) -> { + CoreCommandSystem.cancelBackup((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + .then(Commands.literal("reload-client-config").executes((runner) -> { + runner.getSource().sendSuccess(() -> Component.literal("This command can only be ran on the client!"), true); + return 1; + })) + + ); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java new file mode 100644 index 0000000..7db9f5f --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/AdvancedBackupsClientCommand.java @@ -0,0 +1,62 @@ +package computer.heather.advancedbackups.client; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; + +import computer.heather.advancedbackups.core.CoreCommandSystem; +import net.minecraft.client.Minecraft; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; + +public class AdvancedBackupsClientCommand { + public static void register(CommandDispatcher commandDispatcher) { + commandDispatcher.register(literal("backup").requires((runner) -> { + return true; + }).then(literal("start").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup start")); + return 1; + })) + + .then(literal("reload-config").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup reload-config")); + return 1; + })) + + .then(literal("reset-chain").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup reset-chain")); + return 1; + })) + + .then(literal("snapshot").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup snapshot")); + return 1; + }) + + .then(Commands.argument("name", StringArgumentType.greedyString()).executes((runner) -> { + String name = StringArgumentType.getString(runner, "name"); + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup snapshot " + name)); + return 1; + }))) + + .then(literal("cancel").executes((runner) -> { + Minecraft.getInstance().player.connection.send(new ServerboundChatCommandPacket("backup cancel")); + return 1; + })) + + .then(literal("reload-client-config").executes((runner) -> { + CoreCommandSystem.reloadClientConfig((response) -> { + runner.getSource().sendSuccess(() -> Component.literal(response), true); + }); + return 1; + })) + + ); + } + + public static LiteralArgumentBuilder literal(String literal) { + return LiteralArgumentBuilder.literal(literal); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java new file mode 100644 index 0000000..556b34c --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/BackupToast.java @@ -0,0 +1,136 @@ +package computer.heather.advancedbackups.client; + +import computer.heather.advancedbackups.core.config.ClientConfigManager; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.ToastManager; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.client.resources.language.I18n; +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +public class BackupToast implements Toast { + + public static boolean starting; + public static boolean started; + public static boolean failed; + public static boolean finished; + public static boolean cancelled; + + public static int progress; + public static int max; + + public static boolean exists = false; + + private static long time; + private static boolean timeSet = false; + + public static final ItemStack stack = new ItemStack(Items.PAPER); + private static final Identifier TEXTURE = Identifier.withDefaultNamespace("toast/advancement"); + + private int textColour; + private String title = "You shouldn't see this!"; + + private Visibility visibility = Visibility.SHOW; + + @Override + public void extractRenderState(GuiGraphicsExtractor graphics, Font font, long fullyVisibleForMs) { + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, TEXTURE, 0, ClientConfigManager.darkMode.get() ? 0 : this.height(), this.width(), this.height()); + graphics.fakeItem(stack, 8, 8); + + float percent = finished ? 100 : (float) progress / (float) max; + + graphics.fill(4, 28, 156, 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBackgroundRed.get(), + (int) ClientConfigManager.progressBackgroundGreen.get(), + (int) ClientConfigManager.progressBackgroundBlue.get())); + + float f = Math.min(156, (156 * percent)); + + if (!exists) { + if (title.equals(I18n.get("advancedbackups.backup_finished"))) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + graphics.text(font, I18n.get(title), 25, 11, textColour, false); + graphics.fill(4, 28, 156, 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBarRed.get(), + (int) ClientConfigManager.progressBarGreen.get(), + (int) ClientConfigManager.progressBarBlue.get())); + } else { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + graphics.text(font, I18n.get(title), 25, 11, textColour, false); + } + visibility = Visibility.HIDE; + return; + } + + if (starting) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.backup_starting"); + } else if (started) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.progress", round(percent * 100)); + } else if (failed) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + title = I18n.get("advancedbackups.backup_failed"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } else if (finished) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.progressTextRed.get(), (int) ClientConfigManager.progressTextGreen.get(), (int) ClientConfigManager.progressTextBlue.get()); + title = I18n.get("advancedbackups.backup_finished"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } else if (cancelled) { + textColour = ColourHelper.colour(255, (int) ClientConfigManager.errorTextRed.get(), (int) ClientConfigManager.errorTextGreen.get(), (int) ClientConfigManager.errorTextBlue.get()); + title = I18n.get("advancedbackups.backup_cancelled"); + if (!timeSet) { + time = System.currentTimeMillis(); + timeSet = true; + } + } + + graphics.text(font, title, 25, 11, textColour, false); + + if (timeSet && System.currentTimeMillis() >= time + 5000) { + starting = false; + started = false; + failed = false; + finished = false; + progress = 0; + max = 0; + timeSet = false; + exists = false; + visibility = Visibility.HIDE; + return; + } + + graphics.fill(4, 28, Math.max(4, (int) f), 29, ColourHelper.colour( + 255, + (int) ClientConfigManager.progressBarRed.get(), + (int) ClientConfigManager.progressBarGreen.get(), + (int) ClientConfigManager.progressBarBlue.get())); + + visibility = Visibility.SHOW; + } + + private static String round(float value) { + return String.format("%.1f", value); + } + + @Override + public Visibility getWantedVisibility() { + return visibility; + } + + @Override + public void update(ToastManager manager, long fullyVisibleForMs) { + // Toast lifetime is tracked in extractRenderState. + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java new file mode 100644 index 0000000..e420f4d --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientContactor.java @@ -0,0 +1,50 @@ +package computer.heather.advancedbackups.client; + +import java.util.List; + +import computer.heather.advancedbackups.AdvancedBackups; +import computer.heather.advancedbackups.interfaces.IClientContactor; +import computer.heather.advancedbackups.network.NetworkHandler; +import computer.heather.advancedbackups.network.PacketBackupStatus; +import net.minecraft.commands.Commands; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; + +public class ClientContactor implements IClientContactor { + + @Override + public void backupComplete(boolean all) { + send(new PacketBackupStatus(false, false, false, true, false, 0, 0), all); + } + + @Override + public void backupFailed(boolean all) { + send(new PacketBackupStatus(false, false, true, false, false, 0, 0), all); + } + + @Override + public void backupProgress(int progress, int max, boolean all) { + send(new PacketBackupStatus(false, true, false, false, false, progress, max), all); + } + + @Override + public void backupStarting(boolean all) { + send(new PacketBackupStatus(true, false, false, false, false, 0, 0), all); + } + + @Override + public void backupCancelled(boolean all) { + send(new PacketBackupStatus(false, false, false, false, true, 0, 0), all); + } + + private void send(PacketBackupStatus packet, boolean all) { + MinecraftServer server = AdvancedBackups.server; + List players = server.getPlayerList().getPlayers(); + for (ServerPlayer player : players) { + if (!AdvancedBackups.players.contains(player.getStringUUID())) continue; + if (!server.isDedicatedServer() || Commands.LEVEL_ADMINS.check(player.permissions()) || all) { + NetworkHandler.sendToClient(player, packet); + } + } + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java new file mode 100644 index 0000000..4dc3212 --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ClientWrapper.java @@ -0,0 +1,55 @@ +package computer.heather.advancedbackups.client; + +import computer.heather.advancedbackups.core.ABCore; +import computer.heather.advancedbackups.core.config.ClientConfigManager; +import computer.heather.advancedbackups.network.PacketBackupStatus; +import computer.heather.advancedbackups.network.PacketToastSubscribe; +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.event.RegisterClientCommandsEvent; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public class ClientWrapper { + + public static void handle(PacketBackupStatus packet, IPayloadContext context) { + BackupToast.starting = packet.starting(); + BackupToast.started = packet.started(); + BackupToast.failed = packet.failed(); + BackupToast.finished = packet.finished(); + BackupToast.cancelled = packet.cancelled(); + + BackupToast.progress = packet.progress(); + BackupToast.max = packet.max(); + + if (!BackupToast.exists) { + BackupToast.exists = true; + Minecraft.getInstance().getToastManager().addToast(new BackupToast()); + } + } + + public static void init(FMLClientSetupEvent e) { + NeoForge.EVENT_BUS.addListener(ClientWrapper::registerClientCommands); + NeoForge.EVENT_BUS.addListener(ClientWrapper::onServerConnected); + ClientConfigManager.loadOrCreateConfig(); + } + + public static void registerClientCommands(RegisterClientCommandsEvent event) { + AdvancedBackupsClientCommand.register(event.getDispatcher()); + } + + public static void onServerConnected(ClientPlayerNetworkEvent.LoggingIn event) { + sendToServer(new PacketToastSubscribe(ClientConfigManager.showProgress.get())); + } + + public static void sendToServer(MSG message) { + try { + ClientPacketDistributor.sendToServer(message); + } catch (UnsupportedOperationException e) { + ABCore.warningLogger.accept("Refusing to send packet " + message + " to server as the server cannot receive it."); + } + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java new file mode 100644 index 0000000..bc1e22b --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/client/ColourHelper.java @@ -0,0 +1,28 @@ +package computer.heather.advancedbackups.client; + +public class ColourHelper { + + public static int alpha(int in) { + return in >>> 24; + } + + public static int red(int in) { + return in >> 16 & 255; + } + + public static int green(int in) { + return in >> 8 & 255; + } + + public static int blue(int in) { + return in & 255; + } + + public static int colour(int a, int r, int g, int b) { + return a << 24 | r << 16 | g << 8 | b; + } + + public static int colour(int a, long r, long g, long b) { + return colour(a, (int) r, (int) g, (int) b); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java new file mode 100644 index 0000000..d96c622 --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/NetworkHandler.java @@ -0,0 +1,25 @@ +package computer.heather.advancedbackups.network; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.client.network.event.RegisterClientPayloadHandlersEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +public class NetworkHandler { + + public static void onRegisterPayloadHandler(RegisterPayloadHandlersEvent event) { + final PayloadRegistrar registrar = event.registrar("1").optional(); + registrar.playToClient(PacketBackupStatus.TYPE, PacketBackupStatus.CODEC); + registrar.playToServer(PacketToastSubscribe.TYPE, PacketToastSubscribe.CODEC, PacketToastSubscribe::handle); + } + + public static void onRegisterClientPayloadHandler(RegisterClientPayloadHandlersEvent event) { + event.register(PacketBackupStatus.TYPE, PacketBackupStatus::handle); + } + + public static void sendToClient(ServerPlayer player, MSG message) { + PacketDistributor.sendToPlayer(player, message); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java new file mode 100644 index 0000000..06c8d3e --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketBackupStatus.java @@ -0,0 +1,40 @@ +package computer.heather.advancedbackups.network; + +import computer.heather.advancedbackups.client.ClientWrapper; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public record PacketBackupStatus(boolean starting, boolean started, boolean failed, boolean finished, boolean cancelled, int progress, int max) implements CustomPacketPayload { + + public static final Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("advancedbackups", "backup_status")); + + public static final StreamCodec CODEC = StreamCodec.of((buf, packet) -> { + buf.writeBoolean(packet.starting); + buf.writeBoolean(packet.started); + buf.writeBoolean(packet.failed); + buf.writeBoolean(packet.finished); + buf.writeBoolean(packet.cancelled); + buf.writeInt(packet.progress); + buf.writeInt(packet.max); + }, buf -> new PacketBackupStatus( + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readBoolean(), + buf.readInt(), + buf.readInt() + )); + + @Override + public Type type() { + return TYPE; + } + + public static void handle(PacketBackupStatus packet, IPayloadContext context) { + ClientWrapper.handle(packet, context); + } +} diff --git a/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java new file mode 100644 index 0000000..b00122e --- /dev/null +++ b/neoforge/26.1/src/main/java/computer/heather/advancedbackups/network/PacketToastSubscribe.java @@ -0,0 +1,34 @@ +package computer.heather.advancedbackups.network; + +import computer.heather.advancedbackups.AdvancedBackups; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +public record PacketToastSubscribe(boolean enable) implements CustomPacketPayload { + + public static final Type TYPE = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath("advancedbackups", "toast_subscribe")); + + public static final StreamCodec CODEC = StreamCodec.of( + (buf, packet) -> buf.writeBoolean(packet.enable), + buf -> new PacketToastSubscribe(buf.readBoolean()) + ); + + public static void handle(PacketToastSubscribe message, IPayloadContext context) { + Player player = context.player(); + + if (message.enable() && !AdvancedBackups.players.contains(player.getStringUUID())) { + AdvancedBackups.players.add(player.getStringUUID()); + } else if (!message.enable()) { + AdvancedBackups.players.remove(player.getStringUUID()); + } + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/neoforge/26.1/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/26.1/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..ffb2a21 --- /dev/null +++ b/neoforge/26.1/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,22 @@ +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] + modId="neoforge" + type="required" + versionRange="[${neo_version},)" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + type="required" + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" diff --git a/neoforge/26.1/src/main/resources/pack.mcmeta b/neoforge/26.1/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..0ec6b6a --- /dev/null +++ b/neoforge/26.1/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "advancedbackups resources", + "pack_format": 81 + } +} diff --git a/neoforge/26.2/build.gradle b/neoforge/26.2/build.gradle index 8513472..23f5d6d 100644 --- a/neoforge/26.2/build.gradle +++ b/neoforge/26.2/build.gradle @@ -44,6 +44,7 @@ runs { server { systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id argument '--nogui' + jvmArgument '-Xmx2G' } gameTestServer { diff --git a/scripts/minecraft-harness/README.md b/scripts/minecraft-harness/README.md new file mode 100644 index 0000000..6249dab --- /dev/null +++ b/scripts/minecraft-harness/README.md @@ -0,0 +1,37 @@ +# Minecraft dedicated-server harness + +Boots a headless Fabric or NeoForge dedicated server with Advanced Backups, runs `/backup start` over RCON, and checks that a zip backup is written. + +This is an in-game environment test, not a unit test. It needs Java 25 (via the Gradle toolchain), network access to download Minecraft, and several minutes for first-time world generation. + +## Usage + +From the repository root: + +```bash +python3 scripts/minecraft-harness/run_harness.py --loader fabric --mc 26.1 +python3 scripts/minecraft-harness/run_harness.py --loader neoforge --mc 26.1 +python3 scripts/minecraft-harness/run_harness.py --loader fabric --mc 26.2 +``` + +Or `scripts/minecraft-harness/run.sh --loader fabric --mc 26.1`. + +The script: + +1. Builds `core` (if needed) and compiles the selected loader project +2. Prepares the run directory with `eula=true`, a flat offline world, RCON, and zip-backup config +3. Starts Fabric `runHarnessServer` or NeoForge `runServer` +4. Waits for `Config loaded!!` and `Done (` +5. Sends `backup start`, then `stop` +6. Asserts a non-empty zip exists under the run directory's `backups/` folder + +## Options + +| Flag | Meaning | +|------|---------| +| `--skip-build` | Do not compile; use the current project output | +| `--keep-world` | Reuse the run directory instead of wiping it | +| `--start-timeout` | Seconds to wait for the server to become ready (default 300) | +| `--backup-timeout` | Seconds to wait for backup completion (default 180) | + +Gradle/server output is written to `//harness-gradle.log`. Worlds and backups live in `fabric//run-harness/` or `neoforge//run/server/` (gitignored). diff --git a/scripts/minecraft-harness/run.sh b/scripts/minecraft-harness/run.sh new file mode 100755 index 0000000..fdc0271 --- /dev/null +++ b/scripts/minecraft-harness/run.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +exec python3 "${repo_root}/scripts/minecraft-harness/run_harness.py" "$@" diff --git a/scripts/minecraft-harness/run_harness.py b/scripts/minecraft-harness/run_harness.py new file mode 100755 index 0000000..b781c4b --- /dev/null +++ b/scripts/minecraft-harness/run_harness.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Boot a dedicated Minecraft server with Advanced Backups and assert /backup start works.""" + +from __future__ import annotations + +import argparse +import os +import re +import signal +import socket +import struct +import subprocess +import sys +import time +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_RCON_PASSWORD = "ab-harness" +READY_PATTERNS = ( + re.compile(r"Done \("), + re.compile(r'For help, type ["\']help["\']'), +) +MOD_LOADED_PATTERN = re.compile(r"Config loaded!!") +BACKUP_COMPLETE_PATTERNS = ( + re.compile(r"Backup complete!"), + re.compile(r"SAVING ENABLED - BACKUP COMPLETE!"), +) +BACKUP_FAILED_PATTERN = re.compile(r"ERROR MAKING BACKUP!") + + +class HarnessError(RuntimeError): + pass + + +def find_free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return int(sock.getsockname()[1]) + + +def write_text(path: Path, contents: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(contents, encoding="utf-8") + + +def server_properties(game_port: int, rcon_port: int, password: str) -> str: + return "\n".join( + [ + "accepts-transfers=false", + "allow-flight=false", + "broadcast-console-to-ops=true", + "broadcast-rcon-to-ops=true", + "difficulty=peaceful", + "enable-command-block=false", + "enable-jmx-monitoring=false", + "enable-query=false", + "enable-rcon=true", + "enable-status=false", + "enforce-secure-profile=false", + "enforce-whitelist=false", + "force-gamemode=false", + "function-permission-level=2", + "gamemode=creative", + "generate-structures=false", + "hardcore=false", + "hide-online-players=true", + "level-name=world", + "level-seed=harness", + "level-type=minecraft:flat", + "max-players=1", + "max-tick-time=-1", + "motd=AdvancedBackups harness", + "online-mode=false", + "op-permission-level=4", + "pause-when-empty-seconds=86400", + "player-idle-timeout=0", + "pvp=false", + f"rcon.password={password}", + f"rcon.port={rcon_port}", + f"server-port={game_port}", + "simulation-distance=2", + "spawn-animals=false", + "spawn-monsters=false", + "spawn-npcs=false", + "spawn-protection=0", + "sync-chunk-writes=false", + "view-distance=2", + "white-list=false", + "", + ] + ) + + +def backup_properties() -> str: + return "\n".join( + [ + "config.advancedbackups.enabled=true", + "config.advancedbackups.save=true", + "config.advancedbackups.togglesave=true", + "config.advancedbackups.buffer=1048576", + "config.advancedbackups.flush=false", + "config.advancedbackups.activity=false", + "config.advancedbackups.blacklist=session.lock,*_old", + "config.advancedbackups.type=zip", + "config.advancedbackups.path=./backups", + "config.advancedbackups.frequency.min=0", + "config.advancedbackups.frequency.max=24", + "config.advancedbackups.frequency.uptime=true", + "config.advancedbackups.frequency.schedule=1:00", + "config.advancedbackups.frequency.shutdown=false", + "config.advancedbackups.frequency.startup=false", + "config.advancedbackups.frequency.delay=30", + "config.advancedbackups.logging.clients=none", + "config.advancedbackups.logging.clientfrequency=500", + "config.advancedbackups.logging.console=true", + "config.advancedbackups.logging.consolefrequency=500", + "config.advancedbackups.zips.compression=1", + "config.advancedbackups.chains.length=50", + "config.advancedbackups.chains.compress=true", + "config.advancedbackups.chains.smart=true", + "config.advancedbackups.chains.maxpercent=50", + "config.advancedbackups.purge.size=50", + "config.advancedbackups.purge.days=0", + "config.advancedbackups.purge.count=0", + "config.advancedbackups.purge.incrementals=true", + "config.advancedbackups.purge.incrementalchains=1", + "", + ] + ) + + +class MinecraftRcon: + TYPE_RESPONSE = 0 + TYPE_COMMAND = 2 + TYPE_LOGIN = 3 + + def __init__(self, host: str, port: int, password: str, timeout: float = 10.0) -> None: + self.sock = socket.create_connection((host, port), timeout=timeout) + self.sock.settimeout(timeout) + self.request_id = 0 + self._login(password) + + def close(self) -> None: + try: + self.sock.close() + except OSError: + pass + + def command(self, text: str) -> str: + request_id = self._send(self.TYPE_COMMAND, text) + reply_id, _, payload = self._recv() + if reply_id != request_id: + raise HarnessError(f"Unexpected RCON id {reply_id} for command {text!r}") + return payload + + def _login(self, password: str) -> None: + request_id = self._send(self.TYPE_LOGIN, password) + reply_id, _, _ = self._recv() + if reply_id == -1 or reply_id != request_id: + raise HarnessError("RCON authentication failed") + + def _send(self, packet_type: int, payload: str) -> int: + self.request_id += 1 + request_id = self.request_id + body = struct.pack(" tuple[int, int, str]: + length = struct.unpack(" bytes: + chunks = bytearray() + while len(chunks) < size: + piece = self.sock.recv(size - len(chunks)) + if not piece: + raise HarnessError("RCON connection closed") + chunks.extend(piece) + return bytes(chunks) + + +def wait_for_patterns( + paths: list[Path], + patterns: tuple[re.Pattern[str], ...], + timeout: float, + failed_pattern: re.Pattern[str] | None = None, +) -> str: + deadline = time.time() + timeout + last_text = "" + while time.time() < deadline: + chunks = [ + path.read_text(encoding="utf-8", errors="replace") + for path in paths + if path.exists() + ] + last_text = "\n".join(chunks) + if failed_pattern and failed_pattern.search(last_text): + raise HarnessError(f"Saw failure marker {failed_pattern.pattern}") + if any(pattern.search(last_text) for pattern in patterns): + return last_text + time.sleep(0.5) + raise HarnessError( + f"Timed out after {int(timeout)}s waiting for {', '.join(pattern.pattern for pattern in patterns)}" + ) + + +def tail(text: str, lines: int = 80) -> str: + return "\n".join(text.splitlines()[-lines:]) + + +def connect_rcon(host: str, port: int, password: str, timeout: float) -> MinecraftRcon: + deadline = time.time() + timeout + last_error: Exception | None = None + while time.time() < deadline: + try: + return MinecraftRcon(host, port, password, timeout=8.0) + except (OSError, HarnessError) as exc: + last_error = exc + time.sleep(1.0) + raise HarnessError(f"Could not connect to RCON on {host}:{port}: {last_error}") + + +def stop_process(proc: subprocess.Popen[bytes]) -> None: + if proc.poll() is not None: + return + try: + os.killpg(proc.pid, signal.SIGTERM) + except (ProcessLookupError, PermissionError, OSError): + proc.terminate() + try: + proc.wait(timeout=30) + except subprocess.TimeoutExpired: + try: + os.killpg(proc.pid, signal.SIGKILL) + except (ProcessLookupError, PermissionError, OSError): + proc.kill() + proc.wait(timeout=10) + + +def prepare_run_dir(run_dir: Path, game_port: int, rcon_port: int, password: str) -> None: + write_text(run_dir / "eula.txt", "eula=true\n") + write_text(run_dir / "server.properties", server_properties(game_port, rcon_port, password)) + write_text(run_dir / "config" / "AdvancedBackups.properties", backup_properties()) + + +def build_core_and_mod(project_dir: Path, skip_build: bool) -> None: + if skip_build: + return + core_jar = REPO_ROOT / "core" / "build" / "libs" / "advancedbackups-corelib.jar" + if not core_jar.exists(): + print("Building core library...", flush=True) + subprocess.run(["./gradlew", "shadowJar", "--no-daemon"], cwd=REPO_ROOT / "core", check=True) + print(f"Compiling {project_dir.relative_to(REPO_ROOT)}...", flush=True) + subprocess.run(["./gradlew", "compileJava", "--no-daemon"], cwd=project_dir, check=True) + + +def collect_backup_zips(run_dir: Path) -> list[Path]: + backups = run_dir / "backups" + if not backups.exists(): + return [] + return [ + path + for path in backups.rglob("*.zip") + if path.is_file() and "incomplete" not in path.name.lower() + ] + + +def gradle_task_and_run_dir(loader: str, project_dir: Path) -> tuple[str, Path]: + if loader == "fabric": + return "runHarnessServer", project_dir / "run-harness" + return "runServer", project_dir / "run" / "server" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--loader", choices=("fabric", "neoforge"), default="fabric") + parser.add_argument("--mc", default="26.1", help="Minecraft line directory, e.g. 26.1 or 26.2") + parser.add_argument("--skip-build", action="store_true") + parser.add_argument("--keep-world", action="store_true", help="Do not wipe run-harness before start") + parser.add_argument("--start-timeout", type=int, default=300) + parser.add_argument("--backup-timeout", type=int, default=180) + return parser.parse_args() + + +def dump_logs(log_paths: list[Path]) -> None: + combined = "\n".join( + path.read_text(encoding="utf-8", errors="replace") if path.exists() else "" + for path in log_paths + ) + if combined.strip(): + print("--- server log tail ---", file=sys.stderr) + print(tail(combined), file=sys.stderr) + + +def main() -> int: + args = parse_args() + project_dir = REPO_ROOT / args.loader / args.mc + if not project_dir.is_dir(): + raise HarnessError(f"Missing project directory {project_dir}") + + gradle_task, run_dir = gradle_task_and_run_dir(args.loader, project_dir) + gradle_log = project_dir / "harness-gradle.log" + game_port = find_free_port() + rcon_port = find_free_port() + proc: subprocess.Popen[bytes] | None = None + log_paths = [gradle_log, run_dir / "logs" / "latest.log"] + + build_core_and_mod(project_dir, args.skip_build) + + if run_dir.exists() and not args.keep_world: + subprocess.run(["rm", "-rf", str(run_dir)], check=True) + prepare_run_dir(run_dir, game_port, rcon_port, DEFAULT_RCON_PASSWORD) + + print( + f"Starting {args.loader} {args.mc} dedicated server on :{game_port} (RCON :{rcon_port})", + flush=True, + ) + with gradle_log.open("wb") as log_handle: + proc = subprocess.Popen( + ["./gradlew", gradle_task, "--no-daemon"], + cwd=project_dir, + stdout=log_handle, + stderr=subprocess.STDOUT, + start_new_session=True, + ) + + try: + try: + wait_for_patterns(log_paths, (MOD_LOADED_PATTERN,), args.start_timeout) + print("Advanced Backups loaded.", flush=True) + except HarnessError: + dump_logs(log_paths) + raise HarnessError("Advanced Backups did not log 'Config loaded!!'") from None + + wait_for_patterns(log_paths, READY_PATTERNS, args.start_timeout) + print("Server is ready. Triggering backup...", flush=True) + + rcon = connect_rcon("127.0.0.1", rcon_port, DEFAULT_RCON_PASSWORD, timeout=30) + try: + reply = rcon.command("backup start") + print(f"RCON backup start -> {reply.strip() or '(no reply)'}", flush=True) + wait_for_patterns( + log_paths, + BACKUP_COMPLETE_PATTERNS, + args.backup_timeout, + failed_pattern=BACKUP_FAILED_PATTERN, + ) + finally: + try: + rcon.command("stop") + except Exception: + pass + rcon.close() + + zips = collect_backup_zips(run_dir) + if not zips: + dump_logs(log_paths) + raise HarnessError("Backup finished according to logs, but no zip was found under backups/") + + largest = max(zips, key=lambda path: path.stat().st_size) + size = largest.stat().st_size + if size < 256: + raise HarnessError(f"Backup zip is too small: {largest} ({size} bytes)") + + print(f"PASS: {largest.relative_to(run_dir)} ({size} bytes)", flush=True) + return 0 + except HarnessError as exc: + print(f"FAIL: {exc}", file=sys.stderr) + dump_logs(log_paths) + return 1 + finally: + if proc is not None: + stop_process(proc) + + +if __name__ == "__main__": + try: + sys.exit(main()) + except subprocess.CalledProcessError as exc: + print(f"FAIL: command exited {exc.returncode}: {exc.cmd}", file=sys.stderr) + sys.exit(exc.returncode)