diff --git a/.github/ci/java-version b/.github/ci/java-version new file mode 100644 index 0000000..7273c0f --- /dev/null +++ b/.github/ci/java-version @@ -0,0 +1 @@ +25 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f5d2d2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: + - 'mc/**' + pull_request: + branches: + - 'mc/**' + - master + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Read JDK version + id: jdk + run: echo "version=$(cat .github/ci/java-version 2>/dev/null || echo 21)" >> "$GITHUB_OUTPUT" + + - name: Set up JDK ${{ steps.jdk.outputs.version }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ steps.jdk.outputs.version }} + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: | + chmod +x ./gradlew + # Mark CI jars distinctly (e.g. hotbaaaar-1.21.1-forge-ci-a1b2c3d.jar) so they're never + # mistaken for a release build; -P overrides the in-repo dev mod_version. + base="$(grep '^mod_version=' gradle.properties | cut -d= -f2)" + base="${base%-*}" + ./gradlew build --no-daemon --stacktrace -Pmod_version="${base}-ci-${GITHUB_SHA:0:7}" + + - name: Compute artifact name + id: meta + run: echo "name=$(echo '${{ github.ref_name }}' | tr '/:' '__')" >> "$GITHUB_OUTPUT" + + - name: Upload jars + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.name }}-jars + path: | + build/libs/*.jar + !build/libs/*-sources.jar + if-no-files-found: error diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..443fee7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,102 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + # Discover every target branch (mc/-) so new ports are picked up automatically. + discover: + runs-on: ubuntu-latest + outputs: + branches: ${{ steps.list.outputs.branches }} + steps: + - name: List mc/* branches + id: list + env: + GH_TOKEN: ${{ github.token }} + run: | + names=$(gh api "repos/${{ github.repository }}/branches?per_page=100" \ + --jq '.[].name' | { grep '^mc/' || true; }) + if [ -z "$names" ]; then + json='[]' + else + json=$(printf '%s\n' "$names" | jq -R . | jq -cs .) + fi + echo "Discovered target branches: $json" + echo "branches=$json" >> "$GITHUB_OUTPUT" + + build: + needs: discover + if: needs.discover.outputs.branches != '[]' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + branch: ${{ fromJson(needs.discover.outputs.branches) }} + steps: + - name: Checkout ${{ matrix.branch }} + uses: actions/checkout@v4 + with: + ref: ${{ matrix.branch }} + + - name: Read JDK version + id: jdk + run: echo "version=$(cat .github/ci/java-version 2>/dev/null || echo 21)" >> "$GITHUB_OUTPUT" + + - name: Set up JDK ${{ steps.jdk.outputs.version }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ steps.jdk.outputs.version }} + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build + run: | + chmod +x ./gradlew + # Combine this branch's "-" prefix with the release tag version so the jar is + # named e.g. hotbaaaar-1.21.1-forge-1.0.0.jar. -P overrides gradle.properties mod_version. + base="$(grep '^mod_version=' gradle.properties | cut -d= -f2)" + base="${base%-*}" + version="${GITHUB_REF_NAME#v}" + echo "Building ${base} @ ${version}" + ./gradlew build --no-daemon --stacktrace -Pmod_version="${base}-${version}" + + - name: Compute artifact name + id: meta + run: echo "name=$(echo '${{ matrix.branch }}' | tr '/:' '__')" >> "$GITHUB_OUTPUT" + + - name: Upload jars + uses: actions/upload-artifact@v4 + with: + name: jars-${{ steps.meta.outputs.name }} + path: | + build/libs/*.jar + !build/libs/*-sources.jar + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all jars + uses: actions/download-artifact@v4 + with: + path: artifacts + pattern: jars-* + merge-multiple: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + files: artifacts/**/*.jar + generate_release_notes: true + fail_on_unmatched_files: true diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000..a7d8e4f --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,47 @@ +# Building HotBaaaar (multi-version / multi-loader) + +This repository uses a **branch matrix**. Each Minecraft version + mod loader combination lives on +its own self-contained, buildable branch. The `master` branch is a **hub**: it holds the CI/release +workflows and this document, but no mod project. + +## Branch convention + +``` +mc/- +``` + +Examples: `mc/1.19.2-forge`, `mc/1.20.1-neoforge`, `mc/1.21.1-fabric`. + +Each target branch is a complete project (its own `build.gradle`, sources, and `mods.toml` / +`fabric.mod.json`) and carries: + +- `.github/workflows/*` — inherited from `master`, so push-CI runs on the branch. +- `.github/ci/java-version` — the JDK used to **run Gradle** on that branch (e.g. `17` for the + Forge 1.19.2 branch, which uses ForgeGradle 6 + Gradle 8.1.1). The JDK used to **compile** the mod + is provisioned automatically by Gradle's toolchain via the `foojay-resolver` plugin. + +## CI (`.github/workflows/ci.yml`) + +On every push to `mc/**` and on PRs into `mc/**` or `master`, the branch is built with +`./gradlew build` and the resulting jars are uploaded as a workflow artifact. + +## Release (`.github/workflows/release.yml`) + +Push a tag matching `v*` (e.g. `v1.0.0`) on `master`. The workflow: + +1. **discover** — lists every `mc/*` branch via the GitHub API. +2. **build** — a matrix job (`fail-fast: false`) checks out and builds each branch. +3. **release** — collects all jars and publishes a single GitHub Release for the tag. + +Each jar is named from its own branch's `gradle.properties` (`mod_version`). + +## Adding a new target + +1. Branch from the closest existing target, e.g. `git switch -c mc/1.20.1-forge mc/1.19.2-forge`. +2. Port the code (rendering, mixin targets, loader entry point, metadata) for the new version/loader. +3. Update `.github/ci/java-version` if the new build needs a different JDK to run Gradle. +4. `git push -u origin mc/1.20.1-forge` → CI builds it automatically. +5. Next `v*` tag on `master` includes it in the release automatically. + +> Note: each branch carries its own copy of the workflows. If you change `ci.yml`, merge the change +> into the target branches. `release.yml` only needs to be current on the tagged `master` commit. diff --git a/build.gradle b/build.gradle index ccd62dd..4e836f8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,209 +1,36 @@ plugins { - id 'java-library' + id 'net.fabricmc.fabric-loom' version '1.17.13' id 'maven-publish' - id 'idea' - id 'net.neoforged.moddev' version '2.0.141' } version = mod_version group = mod_group_id -repositories { - mavenLocal() - maven { url = 'https://jitpack.io' } -} - base { archivesName = mod_id } java.toolchain.languageVersion = JavaLanguageVersion.of(25) -neoForge { - // Specify the version of NeoForge to use. - enable { - version = project.neo_version - } - - parchment { - mappingsVersion = project.parchment_mappings_version - minecraftVersion = project.parchment_minecraft_version - } - - // This line is optional. Access Transformers are automatically detected - // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') - - // Default run configurations. - // These can be tweaked, removed, or duplicated as needed. - runs { - client { - client() - - // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - server { - server() - programArgument '--nogui' - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - // This run config launches GameTestServer and runs all registered gametests, then exits. - // By default, the server will crash when no gametests are provided. - // The gametest system is also enabled by default for other run configs under the /test command. - gameTestServer { - type = "gameTestServer" - systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id - } - - data { - clientData() - - // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it - // gameDirectory = project.file('run-data') - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. - programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() - } - - // applies to all the run configs above - configureEach { - // Recommended logging data for a userdev environment - // The markers can be added/remove as needed separated by commas. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. - systemProperty 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels - logLevel = org.slf4j.event.Level.DEBUG - } - } - - mods { - // define mod <-> source bindings - // these are used to tell the game which sources are for which mod - // mostly optional in a single mod project - // but multi mod projects should define one per mod - "${mod_id}" { - sourceSet(sourceSets.main) - } - } -} - -// Include resources generated by data generators. -sourceSets.main.resources { - srcDir 'src/generated/resources' +repositories { + mavenCentral() + maven { url = 'https://maven.fabricmc.net/' } } dependencies { - // Example mod dependency with JEI - // The JEI API is declared for compile time use, while the full JEI artifact is used at runtime - // compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}" - // compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}" - // runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}" - //api("com.github.QiuYe-123:Mekanism:26.1-SNAPSHOT") - - // Example mod dependency using a mod jar from ./libs with a flat dir repository - // This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar - // The group id is ignored when searching -- in this case, it is "blank" - // implementation "blank:coolmod-${mc_version}:${coolmod_version}" - // Example mod dependency using a file as dependency - // implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar") - - // Example project dependency using a sister or child project: - // implementation project(":myproject") - - // For more info: - // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html - // http://www.gradle.org/docs/current/userguide/dependency_management.html + minecraft "com.mojang:minecraft:${minecraft_version}" + implementation "net.fabricmc:fabric-loader:${loader_version}" } -// This block of code expands all declared replace properties in the specified resource targets. -// A missing property will result in an error. Properties are expanded using ${} Groovy notation. -var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) { - from "src/main/templates" - - var replaceProperties = [minecraft_version : minecraft_version, - minecraft_version_range: minecraft_version_range, - neo_version : neo_version, - neo_version_range : neo_version_range, - loader_version_range : loader_version_range, - mod_id : mod_id, - mod_name : mod_name, - mod_license : mod_license, - mod_version : mod_version, - mod_authors : mod_authors, - mod_description : mod_description] - inputs.properties replaceProperties - expand replaceProperties - into "build/generated/sources/modMetadata" -} +def artifactVersion = version -// Include the output of "generateModMetadata" as an input directory for the build -// this works with both building through Gradle and the IDE. -sourceSets.main.resources.srcDir generateModMetadata -// To avoid having to run "generateModMetadata" manually, make it run on every project reload -neoForge.ideSyncTask generateModMetadata - -// Example configuration to allow publishing using the maven-publish plugin -publishing { - publications { - register('mavenJava', MavenPublication) { - from components.java - } - } - repositories { - maven { - url "file://${project.projectDir}/repo" - } +processResources { + inputs.property 'version', artifactVersion + filesMatching('fabric.mod.json') { + expand 'version': artifactVersion } } -java { - withSourcesJar() -} - -// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. -idea { - module { - downloadSources = true - downloadJavadoc = true - } -} - -tasks.register("runWithRenderDoc", Exec) { - def javaExecTask = tasks.withType(JavaExec).named("runClient").get() - def javaHome = javaExecTask.javaLauncher.get().metadata.installationPath.asFile.absolutePath - - commandLine = [ - "C:\\Program Files\\RenderDoc\\renderdoccmd.exe", - "capture", - "--opt-hook-children", - "--wait-for-exit", - "--working-dir", - ".", - "$javaHome\\bin\\java.exe", - "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", - "-Xmx64m", - "-Xms64m", - "-Dorg.gradle.appname=gradlew", - "-Dorg.gradle.java.home=$javaHome", - "-classpath", - "gradle/wrapper/gradle-wrapper.jar", - "org.gradle.wrapper.GradleWrapperMain", - "runClient" - ] -} - tasks.withType(JavaCompile).configureEach { - options.compilerArgs += [ - '--add-exports', 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', - '--add-exports', 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED' - ] -} \ No newline at end of file + options.encoding = 'UTF-8' +} diff --git a/gradle.properties b/gradle.properties index 4cf00f2..0a2b5bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,45 +1,26 @@ # Sets default memory used for gradle commands. Can be overridden by user or command line properties. -org.gradle.jvmargs=-Xmx2G +org.gradle.jvmargs=-Xmx3G org.gradle.daemon=false -org.gradle.debug=false -org.gradle.parallel=true -org.gradle.caching=true -org.gradle.configuration-cache=true -# https://parchmentmc.org/docs/getting-started -parchment_minecraft_version=1.21.11 -parchment_mappings_version=2025.12.20 ## Environment Properties -# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge -# The Minecraft version must agree with the Neo version to get a valid artifact -minecraft_version=26.1.2 -# The Minecraft version range can use any release version of Minecraft as bounds. -# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly -# as they do not follow standard versioning conventions. -minecraft_version_range=[26.1,) -# The Neo version must agree with the Minecraft version to get a valid artifact -neo_version=26.1.2.64-beta -# The Neo version range can use any version of Neo as bounds -neo_version_range=[26,) -# The loader version range can only use the major version of FML as bounds -loader_version_range=[4,) + +minecraft_version=26.2 +# Fabric loader version - https://fabricmc.net/develop/ +loader_version=0.19.3 ## Mod Properties -# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} -# Must match the String constant located in the main mod class annotated with @Mod. +# The unique mod identifier for the mod. Must be lowercase in English locale. mod_id=hotbaaaar # The human-readable display name for the mod. mod_name=HotBaaaar -# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +# The license of the mod. mod_license=GNU GPL 3.0 -# The mod version. See https://semver.org/ -mod_version=26.1.2-1 -# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. -# This should match the base package used for the mod sources. -# See https://maven.apache.org/guides/mini/guide-naming-conventions.html +# The mod version. Suffixed -fabric so the jar does not collide with the Forge jar of the same MC version. +mod_version=26.2-fabric-1.0 +# The group ID for the mod. mod_group_id=cn.ussshenzhou -# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +# The authors of the mod. mod_authors=USS_Shenzhou -# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. -mod_description= \ No newline at end of file +# The description of the mod. +mod_description=Super long hotbar, client-side only. Scroll past the edge to flip the active row into the real hotbar. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..b1b8ef5 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eca8efd..df6a6ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v9.2.1/gradle-9.2.1-bin.zip +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/gradlew b/gradlew index 1aa94a4..b9bb139 100644 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 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. @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -112,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -170,7 +171,6 @@ fi # 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" ) @@ -203,15 +203,14 @@ fi 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, +# * DEFAULT_JVM_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 \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..24c62d5 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -21,8 +23,8 @@ @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -43,13 +45,13 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :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 +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle b/settings.gradle index 84e68b0..8cbbc55 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,17 +1,12 @@ pluginManagement { repositories { - mavenLocal() gradlePluginPortal() - maven { url = 'https://maven.neoforged.net/releases' } - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } + maven { url = 'https://maven.fabricmc.net/' } } } plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' } -rootProject.name = "hotbaaaar" +rootProject.name = "hotbaaaar" diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java b/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java deleted file mode 100644 index 7771c7f..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java +++ /dev/null @@ -1,22 +0,0 @@ -package cn.ussshenzhou.hotbaaaar; - -import net.neoforged.bus.api.IEventBus; -import net.neoforged.fml.common.Mod; - -/** - * @author USS_Shenzhou - */ -// The value here should match an entry in the META-INF/mods.toml file -@Mod(HotBaaaar.MOD_ID) -public class HotBaaaar -{ - public static final String MOD_ID = "hotbaaaar"; - - //TODO 加入T88,支持强制指定多少个 - //TODO 配置 数字键选择当前快捷栏 - //TODO 创造模式物品栏显示全部4格 - public HotBaaaar(IEventBus modEventBus) - { - - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java new file mode 100644 index 0000000..8d4f74d --- /dev/null +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java @@ -0,0 +1,255 @@ +package cn.ussshenzhou.hotbaaaar.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ContainerInput; + +/** + * Client-only state and logic for the "super long hotbar". + *

+ * The real hotbar is always vanilla physical slots 0-8. We present the whole inventory as a wide + * strip of up to four 9-slot rows and let the selection glide across them. To use a row other than + * the one currently in the real hotbar, that row is physically swapped into slots 0-8 via container + * SWAP clicks (which a vanilla server accepts), so no server-side mod is required. + *

+ * Items are rendered at fixed logical positions; {@link #physicalOfLogical} tracks which + * physical row currently holds each logical row so the swap stays invisible on the HUD. + *

+ * When the player opens their own inventory we {@link #restoreCanonical() restore} the real slot order + * so the inventory screen looks normal, and when it closes we re-apply the active row so the held item + * is preserved (see {@link #onInventoryOpen()} / {@link #onInventoryClose()}). Foreign containers + * (chests) and creative keep the previous behaviour ({@link #resetMapping()}), since we cannot safely + * issue player-inventory clicks while another container is open. + * + * @author USS_Shenzhou + */ +public class HotbaaaarClient { + + private static final int ROW = 9; + private static final int MAX_ROWS = 4; + + /** {@code physicalOfLogical[logicalRow]} = physical row (0..3) currently holding that logical row's items. */ + private static final int[] physicalOfLogical = {0, 1, 2, 3}; + /** The logical row whose items currently sit in the real hotbar (physical row 0). */ + private static int activeLogicalRow = 0; + + private static Player lastPlayer = null; + + /** While the player's own inventory screen is open we restore canonical order; remember what to re-apply. */ + private static boolean restoredForInventory = false; + private static int savedActiveRow = 0; + + private HotbaaaarClient() { + } + + public static int getActiveLogicalRow() { + return activeLogicalRow; + } + + public static int physicalRowOfLogical(int logicalRow) { + if (logicalRow < 0 || logicalRow >= MAX_ROWS) { + return logicalRow; + } + return physicalOfLogical[logicalRow]; + } + + /** Number of addressable rows, mirroring the original width-based behaviour. */ + public static int getRows() { + Minecraft mc = Minecraft.getInstance(); + if (mc.getWindow() == null) { + return 1; + } + return Mth.clamp(mc.getWindow().getGuiScaledWidth() / 182, 1, MAX_ROWS); + } + + /** Reset the logical-to-physical map to identity without moving any items. */ + public static void resetMapping() { + for (int i = 0; i < MAX_ROWS; i++) { + physicalOfLogical[i] = i; + } + activeLogicalRow = 0; + } + + /** + * Keep internal state consistent: reset when the player changes (login / respawn / dimension) + * or when the window shrank below the active row. Called from rendering and scrolling. + */ + public static void tickSanity() { + Player p = Minecraft.getInstance().player; + if (p != lastPlayer) { + lastPlayer = p; + restoredForInventory = false; + resetMapping(); + } + if (activeLogicalRow >= getRows()) { + resetMapping(); + } + } + + // --- inventory-screen open/close: restore on open, re-apply on close ------------------------- + + /** Player's own inventory opened: remember the active row and physically restore canonical order. */ + public static void onInventoryOpen() { + if (restoredForInventory) { + return; + } + savedActiveRow = activeLogicalRow; + restoreCanonical(); + restoredForInventory = true; + } + + /** Player's own inventory closed: re-apply the saved active row so the held item is preserved. */ + public static void onInventoryClose() { + if (!restoredForInventory) { + return; + } + restoredForInventory = false; + if (savedActiveRow > 0 && savedActiveRow < getRows()) { + activateLogicalRow(savedActiveRow); + } + } + + /** A foreign container (chest, creative, ...) opened: we can't safely restore, so just resync the map. */ + public static void onForeignContainer() { + restoredForInventory = false; + resetMapping(); + } + + /** Physically swap the inventory rows back to their home positions (identity), via the hotbar row. */ + public static void restoreCanonical() { + // Selection-sort each physical position using the hotbar row (physical 0) as the pivot/buffer. + for (int i = 1; i < MAX_ROWS; i++) { + if (logicalAtPhysical(i) == i) { + continue; + } + if (logicalAtPhysical(0) != i) { + // bring logical row i to the hotbar first + if (!swapWithHotbarTracked(physicalOfLogical[i])) { + break; + } + } + // place logical row i at its home physical row i + if (!swapWithHotbarTracked(i)) { + break; + } + } + activeLogicalRow = logicalAtPhysical(0); + } + + private static int logicalAtPhysical(int physical) { + for (int l = 0; l < MAX_ROWS; l++) { + if (physicalOfLogical[l] == physical) { + return l; + } + } + return physical; + } + + /** Swap physical row {@code p} with the hotbar (physical 0) and update the tracking map. */ + private static boolean swapWithHotbarTracked(int p) { + if (p == 0) { + return true; + } + if (!swapPhysicalRowWithHotbar(p)) { + return false; + } + int a = logicalAtPhysical(0); + int b = logicalAtPhysical(p); + physicalOfLogical[a] = p; + physicalOfLogical[b] = 0; + return true; + } + + // --- scrolling ------------------------------------------------------------------------------ + + /** + * Replacement for vanilla {@code Inventory.swapPaint}: glide the selection within the active row, + * and flip to the adjacent row (swapping it into the real hotbar) when scrolling past an edge. + * The first and last logical slots are connected, so scrolling wraps around the entire strip. + */ + public static void onScroll(double direction) { + Minecraft mc = Minecraft.getInstance(); + Player player = mc.player; + if (player == null) { + return; + } + int dir = (int) Math.signum(direction); + if (dir == 0) { + return; + } + tickSanity(); + Inventory inv = player.getInventory(); + int newSelected = inv.getSelectedSlot() - dir; + if (newSelected < 0) { + // past the left edge -> previous row + setSelected(inv, flipRow(-1) ? ROW - 1 : 0); + } else if (newSelected >= ROW) { + // past the right edge -> next row + setSelected(inv, flipRow(1) ? 0 : ROW - 1); + } else { + setSelected(inv, newSelected); + } + } + + private static void setSelected(Inventory inv, int slot) { + inv.setSelectedSlot(slot); + Minecraft mc = Minecraft.getInstance(); + if (mc.getConnection() != null) { + mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); + } + } + + /** @return true if the row was actually flipped. */ + private static boolean flipRow(int delta) { + int target = Math.floorMod(activeLogicalRow + delta, getRows()); + return activateLogicalRow(target); + } + + /** Bring logical row {@code target} into the real hotbar (physical row 0). */ + private static boolean activateLogicalRow(int target) { + if (target == activeLogicalRow) { + return true; + } + int targetPhysical = physicalOfLogical[target]; + if (!swapPhysicalRowWithHotbar(targetPhysical)) { + return false; + } + // The old active row (was at physical 0) now lives where the target used to be, and vice-versa. + int old = activeLogicalRow; + physicalOfLogical[old] = targetPhysical; + physicalOfLogical[target] = 0; + activeLogicalRow = target; + return true; + } + + /** + * Swap physical row {@code physical} (1..3) with the real hotbar (physical row 0) via nine SWAP + * container clicks against the player inventory menu. Only valid while no other screen is open + * (or while the player's own inventory screen is open, which keeps container id 0 active). + * + * @return true if the swap was performed (state should be updated), false otherwise. + */ + private static boolean swapPhysicalRowWithHotbar(int physical) { + if (physical == 0) { + return true; + } + Minecraft mc = Minecraft.getInstance(); + Player player = mc.player; + if (player == null || mc.gameMode == null) { + return false; + } + // The player inventory menu (id 0) must be the active container, i.e. no chest/etc. open. + if (player.containerMenu != player.inventoryMenu) { + return false; + } + for (int col = 0; col < ROW; col++) { + // InventoryMenu slot index for inventory index (physical*9 + col), physical >= 1, equals the same number. + int menuSlot = physical * ROW + col; + mc.gameMode.handleContainerInput(0, menuSlot, col, ContainerInput.SWAP, player); + } + return true; + } +} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java index 776005f..5ecdcd0 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -1,27 +1,33 @@ package cn.ussshenzhou.hotbaaaar.mixin; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; +import cn.ussshenzhou.hotbaaaar.util.Util; import net.minecraft.client.AttackIndicatorStatus; import net.minecraft.client.DeltaTracker; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.Hud; import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.util.Mth; import net.minecraft.world.entity.HumanoidArm; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; import org.spongepowered.asm.mixin.Shadow; - -import static cn.ussshenzhou.hotbaaaar.util.Util.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** + * Renders the extended hotbar on the sprite API: up to four 9-slot rows as one wide strip, + * items drawn at fixed logical positions (see {@link HotbaaaarClient}). Faithfully reproduces the + * vanilla hotbar (background, selection frame, offhand slot, attack indicator) generalised to N rows. + * * @author USS_Shenzhou */ -@Mixin(Gui.class) +@Mixin(Hud.class) public abstract class GuiMixin { @Shadow @@ -29,85 +35,88 @@ public abstract class GuiMixin { private Minecraft minecraft; @Shadow - @Nullable - protected abstract Player getCameraPlayer(); + private Player getCameraPlayer() { + throw new AssertionError(); + } @Shadow - protected abstract void extractSlot(GuiGraphicsExtractor graphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack itemStack, int seed); + private void extractSlot(GuiGraphicsExtractor guiGraphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack stack, int seed) { + throw new AssertionError(); + } - /** - * @author USS_Shenzhou - * @reason Inject at HEAD would do the same, but overwrite is cheaper and more convenient. - */ - @Overwrite - private void extractItemHotbar(GuiGraphicsExtractor graphics, DeltaTracker deltaTracker) { + @Inject(method = "extractItemHotbar", at = @At("HEAD"), cancellable = true) + private void hotbaaaar$extractHotbar(GuiGraphicsExtractor guiGraphics, DeltaTracker deltaTracker, CallbackInfo ci) { Player player = this.getCameraPlayer(); - if (player != null) { - ItemStack offhand = player.getOffhandItem(); - HumanoidArm offhandArm = player.getMainArm().getOpposite(); - int screenCenter = graphics.guiWidth() / 2; - final int oneHotbarLength = 182; - final int halfHotbar = 91; - final int hotbarHeight = 22; - int hotbarAmount = Mth.clamp(graphics.guiWidth() / oneHotbarLength, 1, 4); - int x0 = screenCenter - hotbarAmount * halfHotbar; - int x1 = x0 + hotbarAmount * oneHotbarLength; - //render background----- - for (int i = 0; i < hotbarAmount; i++) { - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_SPRITE, x0 + i * oneHotbarLength, graphics.guiHeight() - hotbarHeight, oneHotbarLength, hotbarHeight); - } - //render select frame----- - graphics.blitSprite( - RenderPipelines.GUI_TEXTURED, - HOTBAR_SELECTION_SPRITE, - x0 - 1 + player.getInventory().getSelectedSlot() * 20 + (player.getInventory().getSelectedSlot() / 9 * 2), - graphics.guiHeight() - 22 - 1, - 24, - 23 - ); - //render offhand----- - if (!offhand.isEmpty()) { - if (offhandArm == HumanoidArm.LEFT) { - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_OFFHAND_LEFT_SPRITE, x0 - 29, graphics.guiHeight() - 23, 29, 24); - } else { - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_OFFHAND_RIGHT_SPRITE, x1 + halfHotbar, graphics.guiHeight() - 23, 29, 24); - } - } + if (player == null) { + return; + } + HotbaaaarClient.tickSanity(); + Inventory inv = player.getInventory(); + int rows = HotbaaaarClient.getRows(); - //render items----- - int seed = 1; - for (int i = 0; i < hotbarAmount * 9; i++) { - int x = x0 + i * 20 + 3 + (i / 9 * 2); - int y = graphics.guiHeight() - 16 - 3; - this.extractSlot(graphics, x, y, deltaTracker, player, player.getInventory().getItem(i), seed++); - } + int screenWidth = guiGraphics.guiWidth(); + int screenHeight = guiGraphics.guiHeight(); + ItemStack offhand = player.getOffhandItem(); + HumanoidArm offhandArm = player.getMainArm().getOpposite(); - //render offhand item----- - if (!offhand.isEmpty()) { - int y = graphics.guiHeight() - 16 - 3; - if (offhandArm == HumanoidArm.LEFT) { - this.extractSlot(graphics, x0 - 26, y, deltaTracker, player, offhand, seed++); - } else { - this.extractSlot(graphics, x1 + 10, y, deltaTracker, player, offhand, seed++); - } + final int oneHotbar = Util.HOTBAR_UNIT_LENGTH; + final int half = 91; + final int height = Util.HOTBAR_UNIT_HEIGHT; + int center = screenWidth / 2; + int x0 = center - rows * half; + int x1 = x0 + rows * oneHotbar; + + // backgrounds + for (int i = 0; i < rows; i++) { + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_SPRITE, x0 + i * oneHotbar, screenHeight - height, oneHotbar, height); + } + + // selection frame at the logical selected slot + int logicalSelected = Mth.clamp(HotbaaaarClient.getActiveLogicalRow(), 0, rows - 1) * 9 + inv.getSelectedSlot(); + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_SELECTION_SPRITE, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), screenHeight - height - 1, 24, 23); + + // offhand frame + if (!offhand.isEmpty()) { + if (offhandArm == HumanoidArm.LEFT) { + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_OFFHAND_LEFT_SPRITE, x0 - 29, screenHeight - 23, 29, 24); + } else { + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_OFFHAND_RIGHT_SPRITE, x1, screenHeight - 23, 29, 24); } + } + + // items, read from the physical slot that currently holds each logical position + int seed = 1; + for (int i = 0; i < rows * 9; i++) { + int logicalRow = i / 9; + int col = i % 9; + int physicalSlot = HotbaaaarClient.physicalRowOfLogical(logicalRow) * 9 + col; + int x = x0 + i * 20 + 3 + (i / 9 * 2); + int y = screenHeight - 16 - 3; + this.extractSlot(guiGraphics, x, y, deltaTracker, player, inv.getItem(physicalSlot), seed++); + } - if (this.minecraft.options.attackIndicator().get() == AttackIndicatorStatus.HOTBAR) { - float attackStrengthScale = this.minecraft.player.getAttackStrengthScale(0.0F); - if (attackStrengthScale < 1.0F) { - int y = graphics.guiHeight() - 20; - int x = x1 + 6; - if (offhandArm == HumanoidArm.RIGHT) { - x = x0 - 22; - } + // offhand item + if (!offhand.isEmpty()) { + int y = screenHeight - 16 - 3; + if (offhandArm == HumanoidArm.LEFT) { + this.extractSlot(guiGraphics, x0 - 26, y, deltaTracker, player, offhand, seed++); + } else { + this.extractSlot(guiGraphics, x1 + 10, y, deltaTracker, player, offhand, seed++); + } + } - int progress = (int)(attackStrengthScale * 19.0F); - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE, x, y, 18, 18); - graphics.blitSprite( - RenderPipelines.GUI_TEXTURED, HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE, 18, 18, 0, 18 - progress, x, y + 18 - progress, 18, progress - ); - } + // attack indicator + if (this.minecraft.options.attackIndicator().get() == AttackIndicatorStatus.HOTBAR) { + float scale = this.minecraft.player.getAttackStrengthScale(0.0F); + if (scale < 1.0F) { + int y = screenHeight - 20; + int x = (offhandArm == HumanoidArm.RIGHT) ? x0 - 22 : x1 + 6; + int progress = (int) (scale * 19.0F); + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE, x, y, 18, 18); + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE, 18, 18, 0, 18 - progress, x, y + 18 - progress, 18, progress); } } + + ci.cancel(); } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index a9697fe..1ff8ccb 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -1,83 +1,36 @@ package cn.ussshenzhou.hotbaaaar.mixin; -import cn.ussshenzhou.hotbaaaar.util.HotBaaaarHelper; -import cn.ussshenzhou.hotbaaaar.util.Util; -import com.mojang.blaze3d.platform.InputConstants; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; import net.minecraft.client.Minecraft; -import net.minecraft.core.NonNullList; -import net.minecraft.util.Mth; -import net.minecraft.world.entity.player.Inventory; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.item.ItemStack; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.LogicalSide; -import net.neoforged.fml.loading.FMLConfig; -import net.neoforged.fml.loading.FMLEnvironment; -import org.lwjgl.glfw.GLFW; -import org.spongepowered.asm.mixin.*; -import org.spongepowered.asm.mixin.injection.Constant; -import org.spongepowered.asm.mixin.injection.ModifyConstant; +import net.minecraft.client.MouseHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -/** - * @author USS_Shenzhou - */ -@Mixin(Inventory.class) +@Mixin(MouseHandler.class) public class InventoryMixin { - @Shadow - public int selected; - - @Shadow - @Final - public Player player; - - @Shadow - @Final - public NonNullList items; - - @Unique - private static final StackWalker WALKER = StackWalker.getInstance(); - - @ModifyConstant(method = "isHotbarSlot", constant = @Constant(intValue = 9)) - private static int hotBaaaarAllSelectable(int constant) { - return 36; - } - /** - * @author USS_Shenzhou - * @reason Inject at HEAD would do the same, but overwrite is cheaper and more convenient. + * Intercept only when vanilla is about to change the selected hotbar slot. + * + *

Input frameworks such as MaLiLib handle and consume modifier + scroll actions earlier in + * {@code MouseHandler.onScroll}. Injecting at HEAD and cancelling there prevents those handlers + * (including Litematica's tool-mode and selection controls) from ever seeing the event.

*/ - @Overwrite - public static int getSelectionSize() { - if (WALKER.walk(s -> s.anyMatch(f -> f.getClassName().startsWith("mekanism")))) { - return 9; - } - if (FMLEnvironment.getDist() == Dist.CLIENT) { - return 9 * HotBaaaarHelper.getHotBaaaarAmount(null); - } else { - return 36; - } - } - - /** - * @author USS_Shenzhou - * @reason Inject at HEAD would do the same, but overwrite is cheaper and more convenient. - */ - @Overwrite - public int getSuitableHotbarSlot() { - int max = HotBaaaarHelper.getHotBaaaarAmount(this.player.getUUID()) * 9; - for (int i = 0; i < max; ++i) { - int j = (this.selected + i) % max; - if (this.items.get(j).isEmpty()) { - return j; - } - } - for (int k = 0; k < max; ++k) { - int l = (this.selected + k) % max; - if (!this.items.get(l).isNotReplaceableByPickAction(this.player, l)) { - return l; - } + @Inject( + method = "onScroll", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/entity/player/Inventory;setSelectedSlot(I)V" + ), + cancellable = true + ) + private void hotbaaaar$onScroll(long window, double xOffset, double yOffset, CallbackInfo ci) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null && !mc.player.isSpectator() && mc.gui.screen() == null && yOffset != 0) { + HotbaaaarClient.onScroll(yOffset); + ci.cancel(); } - return this.selected; } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java index f7ad0e5..f0731ee 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java @@ -1,34 +1,42 @@ package cn.ussshenzhou.hotbaaaar.mixin; -import cn.ussshenzhou.hotbaaaar.network.DisplayResolutionPacket; -import com.llamalad7.mixinextras.sugar.Local; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.ClientPacketListener; -import net.minecraft.world.entity.player.Inventory; -import net.neoforged.neoforge.client.network.ClientPacketDistributor; -import org.jspecify.annotations.Nullable; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.InventoryScreen; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Constant; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** + * Drives the row restore/re-apply around the player's own inventory screen: + *
    + *
  • opening {@link InventoryScreen} -> restore canonical slot order so the GUI looks normal;
  • + *
  • closing it -> re-apply the active row so the held item is preserved;
  • + *
  • any other container (chest, creative, ...) -> just resync the internal map (we can't safely + * issue player-inventory clicks while a foreign container is open).
  • + *
+ * * @author USS_Shenzhou */ -@Mixin(Minecraft.class) -public abstract class MinecraftMixin { +@Mixin(Gui.class) +public class MinecraftMixin { @Shadow - @Nullable - public abstract ClientPacketListener getConnection(); + public Screen screen; - @Inject(method = "resizeGui", at = @At("RETURN")) - private void hotbaaaarSendNewSizeToServer(CallbackInfo ci) { - if (this.getConnection() != null) { - ClientPacketDistributor.sendToServer(new DisplayResolutionPacket(Minecraft.getInstance().getWindow().getGuiScaledWidth())); + @Inject(method = "setScreen", at = @At("HEAD")) + private void hotbaaaar$onSetScreen(Screen newScreen, CallbackInfo ci) { + Screen old = this.screen; + if (newScreen instanceof InventoryScreen) { + HotbaaaarClient.onInventoryOpen(); + } else if (old instanceof InventoryScreen) { + HotbaaaarClient.onInventoryClose(); + } else if (newScreen instanceof AbstractContainerScreen) { + HotbaaaarClient.onForeignContainer(); } } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/network/DisplayResolutionPacket.java b/src/main/java/cn/ussshenzhou/hotbaaaar/network/DisplayResolutionPacket.java deleted file mode 100644 index 2212134..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/network/DisplayResolutionPacket.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.network; - -import cn.ussshenzhou.hotbaaaar.HotBaaaar; -import cn.ussshenzhou.hotbaaaar.util.HotBaaaarServerManager; -import io.netty.buffer.ByteBuf; -import net.minecraft.network.codec.ByteBufCodecs; -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; - -/** - * @author USS_Shenzhou - */ -public class DisplayResolutionPacket implements CustomPacketPayload { - public static final CustomPacketPayload.Type TYPE = new Type<>(Identifier.fromNamespaceAndPath(HotBaaaar.MOD_ID, "display_resolution")); - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - ByteBufCodecs.VAR_INT, - o -> o.width, - DisplayResolutionPacket::new - ); - - public int width; - - public DisplayResolutionPacket(int width) { - this.width = width; - } - - public void handle(IPayloadContext context) { - HotBaaaarServerManager.HOTBAR_AMOUNT.put(context.player().getUUID(), this.width); - } - - @Override - public Type type() { - return TYPE; - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/network/ModNetworkRegistry.java b/src/main/java/cn/ussshenzhou/hotbaaaar/network/ModNetworkRegistry.java deleted file mode 100644 index e214d09..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/network/ModNetworkRegistry.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.network; - -import cn.ussshenzhou.hotbaaaar.HotBaaaar; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; - -/** - * @author USS_Shenzhou - */ -@EventBusSubscriber -public class ModNetworkRegistry { - - @SubscribeEvent - public static void networkPacketRegistry(RegisterPayloadHandlersEvent event) { - var registrar = event.registrar(HotBaaaar.MOD_ID); - - registrar.playToServer(DisplayResolutionPacket.TYPE, DisplayResolutionPacket.STREAM_CODEC, DisplayResolutionPacket::handle); - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarHelper.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarHelper.java deleted file mode 100644 index 924445b..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarHelper.java +++ /dev/null @@ -1,39 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.util; - -import net.minecraft.client.Minecraft; -import net.minecraft.util.Mth; -import net.neoforged.fml.loading.FMLEnvironment; - -import javax.annotation.Nullable; -import java.security.InvalidParameterException; -import java.util.UUID; -import java.util.function.Supplier; - -/** - * @author USS_Shenzhou - */ -public class HotBaaaarHelper { - - private static final Supplier CLIENT = new Supplier() { - @SuppressWarnings("ConstantValue") - @Override - public Integer get() { - var window = Minecraft.getInstance().getWindow(); - if (window == null) { - return 36; - } - return Mth.clamp(window.getGuiScaledWidth() / Util.HOTBAR_UNIT_LENGTH,1, 4); - } - }; - - public static int getHotBaaaarAmount(@Nullable UUID uuid) { - if (FMLEnvironment.getDist().isClient()) { - return CLIENT.get(); - } else { - if (uuid == null) { - throw new InvalidParameterException("uuid is null on server"); - } - return Mth.clamp(HotBaaaarServerManager.HOTBAR_AMOUNT.get(uuid) / Util.HOTBAR_UNIT_LENGTH, 1, 4); - } - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarServerManager.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarServerManager.java deleted file mode 100644 index e0b4f34..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarServerManager.java +++ /dev/null @@ -1,12 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.util; - -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -/** - * @author USS_Shenzhou - */ -public class HotBaaaarServerManager { - - public static final ConcurrentHashMap HOTBAR_AMOUNT = new ConcurrentHashMap<>(); -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/SendDisplayResolutionListener.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/SendDisplayResolutionListener.java deleted file mode 100644 index c09b18c..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/SendDisplayResolutionListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.util; - -import cn.ussshenzhou.hotbaaaar.network.DisplayResolutionPacket; -import net.minecraft.client.Minecraft; -import net.neoforged.api.distmarker.Dist; -import net.neoforged.bus.api.SubscribeEvent; -import net.neoforged.fml.common.EventBusSubscriber; -import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; -import net.neoforged.neoforge.client.network.ClientPacketDistributor; - -/** - * @author USS_Shenzhou - */ -@EventBusSubscriber(Dist.CLIENT) -public class SendDisplayResolutionListener { - - @SubscribeEvent - public static void onLogin(ClientPlayerNetworkEvent.LoggingIn event) { - ClientPacketDistributor.sendToServer(new DisplayResolutionPacket(Minecraft.getInstance().getWindow().getGuiScaledWidth())); - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java index 94af39f..f1ee7a5 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java @@ -3,6 +3,9 @@ import net.minecraft.resources.Identifier; /** + * GUI hotbar sprites and geometry constants. The HUD renders from the sprite atlas, + * so these are sprite ids (under {@code minecraft:gui/sprites/...}), not raw texture sheets. + * * @author USS_Shenzhou */ public class Util { @@ -13,6 +16,9 @@ public class Util { public static final Identifier HOTBAR_OFFHAND_RIGHT_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_offhand_right"); public static final Identifier HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_attack_indicator_background"); public static final Identifier HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_attack_indicator_progress"); + + /** Width of one vanilla hotbar in GUI pixels. */ public static final int HOTBAR_UNIT_LENGTH = 182; + /** Height of one vanilla hotbar in GUI pixels. */ public static final int HOTBAR_UNIT_HEIGHT = 22; } diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg deleted file mode 100644 index 485f822..0000000 --- a/src/main/resources/META-INF/accesstransformer.cfg +++ /dev/null @@ -1 +0,0 @@ -public net.minecraft.world.entity.player.Inventory * \ No newline at end of file diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..1a297c5 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "hotbaaaar", + "version": "${version}", + "name": "HotBaaaar", + "description": "Super long hotbar, client-side only. Scroll past the edge to flip the active row into the real hotbar.", + "authors": ["USS_Shenzhou"], + "license": "GNU GPL 3.0", + "icon": "hotbaaaar.png", + "environment": "client", + "mixins": [ + { + "config": "hotbaaaar.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": ">=0.19.0", + "minecraft": "~26.2", + "java": ">=25" + } +} diff --git a/src/main/resources/hotbaaaar.mixins.json b/src/main/resources/hotbaaaar.mixins.json index 6aef85c..eeb7389 100644 --- a/src/main/resources/hotbaaaar.mixins.json +++ b/src/main/resources/hotbaaaar.mixins.json @@ -2,13 +2,10 @@ "required": true, "minVersion": "0.8", "package": "cn.ussshenzhou.hotbaaaar.mixin", - "compatibilityLevel": "JAVA_8", - "refmap": "hooootbar.refmap.json", - "mixins": [ - "InventoryMixin" - ], + "compatibilityLevel": "JAVA_25", "client": [ "GuiMixin", + "InventoryMixin", "MinecraftMixin" ], "injectors": { diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 0b6f605..347bbae 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { - "description": "hooootbar resources", - "pack_format": 26 + "description": "HotBaaaar resources", + "pack_format": 88 } } diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml deleted file mode 100644 index d266684..0000000 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,80 +0,0 @@ -# This is an example mods.toml file. It contains the data relating to the loading mods. -# There are several mandatory fields (#mandatory), and many more that are optional (#optional). -# The overall format is standard TOML format, v0.5.0. -# Note that there are a couple of TOML lists in this file. -# Find more information on toml format here: https://github.com/toml-lang/toml -# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml -modLoader="javafml" #mandatory -# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. -loaderVersion="${loader_version_range}" #mandatory -# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. -# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. -license="${mod_license}" -# A URL to refer people to when problems occur with this mod -#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional -# A list of mods - how many allowed here is determined by the individual mod loader -[[mods]] #mandatory -# The modid of the mod -modId="${mod_id}" #mandatory -# The version number of the mod -version="${mod_version}" #mandatory -# A display name for the mod -displayName="${mod_name}" #mandatory -# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ -#updateJSONURL="https://change.me.example.invalid/updates.json" #optional -# A URL for the "homepage" for this mod, displayed in the mod UI -#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional -# A file name (in the root of the mod JAR) containing a logo for display -logoFile="hotbaaaar.png" #optional -# A text field displayed in the mod UI -#credits="" #optional -# A text field displayed in the mod UI -authors="${mod_authors}" #optional -# Display Test controls the display for your mod in the server connection screen -# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. -# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. -# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. -# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. -# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. -#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) - -# The description text for the mod (multi line!) (#mandatory) -description='''${mod_description}''' -# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. -[[dependencies."${mod_id}"]] #optional -# the modid of the dependency -modId="neoforge" #mandatory -# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). -# 'required' requires the mod to exist, 'optional' does not -# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning -type="required" #mandatory -# Optional field describing why the dependency is required or why it is incompatible -# reason="..." -# The version range of the dependency -versionRange="${neo_version_range}" #mandatory -# An ordering relationship for the dependency. -# BEFORE - This mod is loaded BEFORE the dependency -# AFTER - This mod is loaded AFTER the dependency -ordering="NONE" -# Side this dependency is applied on - BOTH, CLIENT, or SERVER -side="BOTH" -# Here's another dependency -[[dependencies."${mod_id}"]] -modId="minecraft" -type="required" -# This version range declares a minimum of the current minecraft version up to but not including the next major version -versionRange="${minecraft_version_range}" -ordering="NONE" -side="BOTH" - -# Features are specific properties of the game environment, that you may want to declare you require. This example declares -# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't -# stop your mod loading on the server for example. -#[features."${mod_id}"] -#openGLVersion="[3.2,)" - -[[mixins]] -config="hotbaaaar.mixins.json" - -[[accessTransformers]] -file="META-INF/accesstransformer.cfg" \ No newline at end of file