From 70a0506acc4e39f93f80ca50f7343f4a38ef15b7 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sat, 6 Jun 2026 23:03:54 +0800 Subject: [PATCH 01/11] Port HotBaaaar to 1.19.2 Forge (client-only) and add multi-target CI Converts the NeoForge mod to a client-only Forge 1.19.2 build using a row-flip mechanic (scroll past the edge swaps a whole inventory row into the real hotbar via container clicks, accepted by vanilla servers). Removes all networking and server-side mixin overrides. Adds GitHub Actions CI/release workflows and the .github/ci/java-version JDK hint for the branch matrix. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/ci/java-version | 1 + .github/workflows/ci.yml | 51 ++++ .github/workflows/release.yml | 91 +++++++ BUILDING.md | 47 ++++ build.gradle | 250 ++++++------------ gradle.properties | 48 ++-- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 10 +- .../cn/ussshenzhou/hotbaaaar/HotBaaaar.java | 20 +- .../hotbaaaar/client/HotbaaaarClient.java | 171 ++++++++++++ .../ussshenzhou/hotbaaaar/mixin/GuiMixin.java | 172 ++++++------ .../hotbaaaar/mixin/InventoryMixin.java | 85 ++---- .../hotbaaaar/mixin/MinecraftMixin.java | 30 +-- .../network/DisplayResolutionPacket.java | 37 --- .../hotbaaaar/network/ModNetworkRegistry.java | 20 -- .../hotbaaaar/util/HotBaaaarHelper.java | 39 --- .../util/HotBaaaarServerManager.java | 12 - .../util/SendDisplayResolutionListener.java | 21 -- .../cn/ussshenzhou/hotbaaaar/util/Util.java | 15 +- .../resources/META-INF/accesstransformer.cfg | 1 - src/main/resources/META-INF/mods.toml | 27 ++ src/main/resources/hotbaaaar.mixins.json | 8 +- src/main/resources/pack.mcmeta | 4 +- .../templates/META-INF/neoforge.mods.toml | 80 ------ 24 files changed, 638 insertions(+), 604 deletions(-) create mode 100644 .github/ci/java-version create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 BUILDING.md create mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/network/DisplayResolutionPacket.java delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/network/ModNetworkRegistry.java delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarHelper.java delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/util/HotBaaaarServerManager.java delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/util/SendDisplayResolutionListener.java delete mode 100644 src/main/resources/META-INF/accesstransformer.cfg create mode 100644 src/main/resources/META-INF/mods.toml delete mode 100644 src/main/templates/META-INF/neoforge.mods.toml diff --git a/.github/ci/java-version b/.github/ci/java-version new file mode 100644 index 0000000..98d9bcb --- /dev/null +++ b/.github/ci/java-version @@ -0,0 +1 @@ +17 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8174898 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +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 + ./gradlew build --no-daemon --stacktrace + + - 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..2dfe630 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,91 @@ +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: | + json=$(gh api "repos/${{ github.repository }}/branches?per_page=100" \ + --jq '[.[].name | select(startswith("mc/"))]') + 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 + ./gradlew build --no-daemon --stacktrace + + - 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..41f4614 100644 --- a/build.gradle +++ b/build.gradle @@ -1,159 +1,118 @@ -plugins { - id 'java-library' - id 'maven-publish' - id 'idea' - id 'net.neoforged.moddev' version '2.0.141' +buildscript { + repositories { + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } + mavenCentral() + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '6.0.+', changing: true + classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT' + } } +apply plugin: 'net.minecraftforge.gradle' +apply plugin: 'org.spongepowered.mixin' +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: 'maven-publish' +apply plugin: 'java' + 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 - } +java.toolchain.languageVersion = JavaLanguageVersion.of(17) - parchment { - mappingsVersion = project.parchment_mappings_version - minecraftVersion = project.parchment_minecraft_version - } +minecraft { + mappings channel: 'official', version: minecraft_version - // This line is optional. Access Transformers are automatically detected - // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + // Access transformer is optional; this mod uses @Shadow instead, so none is declared. - // 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 + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + mods { + "${mod_id}" { + source sourceSets.main + } + } } 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 + workingDirectory project.file('run') + property 'forge.logging.markers', 'REGISTRIES' + property 'forge.logging.console.level', 'debug' + property 'mixin.env.remapRefMap', 'true' + property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" + mods { + "${mod_id}" { + source sourceSets.main + } + } } } +} - 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) - } - } +mixin { + add sourceSets.main, "${mod_id}.refmap.json" + config "${mod_id}.mixins.json" } -// Include resources generated by data generators. -sourceSets.main.resources { - srcDir 'src/generated/resources' +repositories { + mavenLocal() + maven { url = 'https://jitpack.io' } } 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 "net.minecraftforge:forge:${minecraft_version}-${forge_version}" + annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' } -// 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] +tasks.named('processResources', ProcessResources).configure { + var replaceProperties = [ + minecraft_version : minecraft_version, + forge_version : forge_version, + loader_version_range : loader_version_range, + minecraft_version_range: minecraft_version_range, + forge_version_range : forge_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" + filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { + expand replaceProperties + } } -// 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 +tasks.named('jar', Jar).configure { + manifest { + attributes([ + 'Specification-Title' : mod_id, + 'Specification-Vendor' : mod_authors, + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : mod_authors, + 'MixinConfigs' : "${mod_id}.mixins.json" + ]) + } + finalizedBy 'reobfJar' +} -// Example configuration to allow publishing using the maven-publish plugin publishing { publications { register('mavenJava', MavenPublication) { - from components.java + artifact jar } } repositories { @@ -163,47 +122,6 @@ publishing { } } -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..058679f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,45 +1,33 @@ # 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 must agree with the Forge version to get a valid artifact +minecraft_version=1.19.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,) +minecraft_version_range=[1.19.2,1.20) +# The Forge version must agree with the Minecraft version to get a valid artifact +forge_version=43.5.0 +# The Forge version range can use any version of Forge as bounds. +forge_version_range=[43,) # The loader version range can only use the major version of FML as bounds -loader_version_range=[4,) +loader_version_range=[43,) ## 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. +mod_version=1.19.2-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.properties b/gradle/wrapper/gradle-wrapper.properties index eca8efd..43212be 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v9.2.1/gradle-9.2.1-bin.zip +distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v8.1.1/gradle-8.1.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle b/settings.gradle index 84e68b0..18d16d0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,17 +1,13 @@ pluginManagement { repositories { - mavenLocal() gradlePluginPortal() - maven { url = 'https://maven.neoforged.net/releases' } - maven { - name = 'Fabric' - url = 'https://maven.fabricmc.net/' - } + maven { url = 'https://maven.minecraftforge.net/' } + maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } } } 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 index 7771c7f..e629996 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java @@ -1,22 +1,20 @@ package cn.ussshenzhou.hotbaaaar; -import net.neoforged.bus.api.IEventBus; -import net.neoforged.fml.common.Mod; +import net.minecraftforge.fml.common.Mod; /** + * Client-only "super long hotbar" mod for 1.19.2 Forge. + *

+ * All behaviour lives in client-side mixins ({@code GuiMixin}, {@code InventoryMixin}, + * {@code MinecraftMixin}) and {@code cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient}. + * The mod does nothing on a dedicated server and is safe to connect to vanilla servers. + * * @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 class HotBaaaar { public static final String MOD_ID = "hotbaaaar"; - //TODO 加入T88,支持强制指定多少个 - //TODO 配置 数字键选择当前快捷栏 - //TODO 创造模式物品栏显示全部4格 - public HotBaaaar(IEventBus modEventBus) - { - + public HotBaaaar() { } } 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..10b4ad2 --- /dev/null +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java @@ -0,0 +1,171 @@ +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.ClickType; + +/** + * 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. We never auto-restore + * the inventory order; we only reset this internal map (see {@link #resetMapping()}) when a container + * screen opens or the player instance changes, so external rearrangement can't desync the view. + * + * @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; + + 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; + resetMapping(); + } + if (activeLogicalRow >= getRows()) { + resetMapping(); + } + } + + /** + * 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. + */ + 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.selected - 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.selected = 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 = activeLogicalRow + delta; + if (target < 0 || target >= getRows()) { + return false; + } + 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. + * + * @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.handleInventoryMouseClick(0, menuSlot, col, ClickType.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..2de17cf 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -1,113 +1,131 @@ package cn.ussshenzhou.hotbaaaar.mixin; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; +import cn.ussshenzhou.hotbaaaar.util.Util; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.PoseStack; 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.renderer.RenderPipelines; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.renderer.GameRenderer; 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: up to four 9-slot rows laid out as one wide strip, with items drawn at + * fixed logical positions (see {@link HotbaaaarClient}). Faithfully reproduces the vanilla 1.19.2 + * hotbar (background, selection frame, offhand slot, attack indicator) generalised to N rows. + * * @author USS_Shenzhou */ @Mixin(Gui.class) -public abstract class GuiMixin { +public abstract class GuiMixin extends GuiComponent { @Shadow @Final private Minecraft minecraft; @Shadow - @Nullable + private int screenWidth; + + @Shadow + private int screenHeight; + + @Shadow protected abstract Player getCameraPlayer(); @Shadow - protected abstract void extractSlot(GuiGraphicsExtractor graphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack itemStack, int seed); - - /** - * @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) { + protected abstract void renderSlot(int x, int y, float partialTick, Player player, ItemStack stack, int seed); + + @Inject(method = "renderHotbar", at = @At("HEAD"), cancellable = true) + private void hotbaaaar$renderHotbar(float partialTick, PoseStack poseStack, 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(); + + ItemStack offhand = player.getOffhandItem(); + HumanoidArm offhandArm = player.getMainArm().getOpposite(); - //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++); + final int oneHotbar = Util.HOTBAR_UNIT_LENGTH; + final int half = 91; + final int height = Util.HOTBAR_UNIT_HEIGHT; + int center = this.screenWidth / 2; + int x0 = center - rows * half; + int x1 = x0 + rows * oneHotbar; + + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + RenderSystem.setShaderTexture(0, Util.WIDGETS_LOCATION); + RenderSystem.enableBlend(); + + // backgrounds + for (int i = 0; i < rows; i++) { + this.blit(poseStack, x0 + i * oneHotbar, this.screenHeight - height, 0, 0, oneHotbar, height); + } + + // selection frame at the logical selected slot + int logicalSelected = Mth.clamp(HotbaaaarClient.getActiveLogicalRow(), 0, rows - 1) * 9 + inv.selected; + this.blit(poseStack, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), this.screenHeight - height - 1, 0, 22, 24, 22); + + // offhand frame + if (!offhand.isEmpty()) { + if (offhandArm == HumanoidArm.LEFT) { + this.blit(poseStack, x0 - 29, this.screenHeight - 23, 24, 22, 29, 24); + } else { + this.blit(poseStack, x1, this.screenHeight - 23, 53, 22, 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 = this.screenHeight - 16 - 3; + this.renderSlot(x, y, partialTick, player, inv.items.get(physicalSlot), seed++); + } - //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++); - } + // offhand item + if (!offhand.isEmpty()) { + int y = this.screenHeight - 16 - 3; + if (offhandArm == HumanoidArm.LEFT) { + this.renderSlot(x0 - 26, y, partialTick, player, offhand, seed++); + } else { + this.renderSlot(x1 + 10, y, partialTick, player, offhand, 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; - } - - 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 = this.screenHeight - 20; + int x = (offhandArm == HumanoidArm.RIGHT) ? x0 - 22 : x1 + 6; + RenderSystem.setShaderTexture(0, Util.GUI_ICONS_LOCATION); + int progress = (int) (scale * 19.0F); + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + this.blit(poseStack, x, y, 36, 94, 18, 18); + this.blit(poseStack, x, y + 18 - progress, 52, 94 + 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..ba45aa9 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -1,83 +1,28 @@ package cn.ussshenzhou.hotbaaaar.mixin; -import cn.ussshenzhou.hotbaaaar.util.HotBaaaarHelper; -import cn.ussshenzhou.hotbaaaar.util.Util; -import com.mojang.blaze3d.platform.InputConstants; -import net.minecraft.client.Minecraft; -import net.minecraft.core.NonNullList; -import net.minecraft.util.Mth; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; 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 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; /** + * Replaces the client-side mouse-wheel hotbar selection with the row-flipping logic. Vanilla + * {@code swapPaint} only ever runs on the client (from {@code MouseHandler#onScroll}); the guard + * keeps us off any logical server just in case. + * * @author USS_Shenzhou */ @Mixin(Inventory.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. - */ - @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 = "swapPaint", at = @At("HEAD"), cancellable = true) + private void hotbaaaar$swapPaint(double direction, CallbackInfo ci) { + Inventory self = (Inventory) (Object) this; + if (self.player.level.isClientSide) { + HotbaaaarClient.onScroll(direction); + 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..6fd7d10 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java @@ -1,34 +1,28 @@ package cn.ussshenzhou.hotbaaaar.mixin; -import cn.ussshenzhou.hotbaaaar.network.DisplayResolutionPacket; -import com.llamalad7.mixinextras.sugar.Local; +import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; 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 net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 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; /** + * When any container screen opens (player inventory, chest, ...) the player may rearrange items, so we + * reset the internal logical-to-physical map to match the real inventory order. This does not move any + * items; it just prevents the swap bookkeeping from desyncing the rendered view. + * * @author USS_Shenzhou */ @Mixin(Minecraft.class) -public abstract class MinecraftMixin { +public class MinecraftMixin { - @Shadow - @Nullable - public abstract ClientPacketListener getConnection(); - - @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 screen, CallbackInfo ci) { + if (screen instanceof AbstractContainerScreen) { + HotbaaaarClient.resetMapping(); } } } 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..3cdc998 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java @@ -1,18 +1,19 @@ package cn.ussshenzhou.hotbaaaar.util; -import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceLocation; /** + * 1.19.2 GUI texture sheets and hotbar geometry constants. + * * @author USS_Shenzhou */ public class Util { - public static final Identifier HOTBAR_SPRITE = Identifier.withDefaultNamespace("hud/hotbar"); - public static final Identifier HOTBAR_SELECTION_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_selection"); - public static final Identifier HOTBAR_OFFHAND_LEFT_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_offhand_left"); - 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"); + public static final ResourceLocation WIDGETS_LOCATION = new ResourceLocation("textures/gui/widgets.png"); + public static final ResourceLocation GUI_ICONS_LOCATION = new ResourceLocation("textures/gui/icons.png"); + + /** 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/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..d84d51f --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -0,0 +1,27 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +# IGNORE_ALL_VERSION: this is a client-only mod, never flag a version mismatch when connecting to a server. +displayTest="IGNORE_ALL_VERSION" +logoFile="hotbaaaar.png" +authors="${mod_authors}" +description='''${mod_description}''' + +[[dependencies.${mod_id}]] +modId="forge" +mandatory=true +versionRange="${forge_version_range}" +ordering="NONE" +side="CLIENT" + +[[dependencies.${mod_id}]] +modId="minecraft" +mandatory=true +versionRange="${minecraft_version_range}" +ordering="NONE" +side="CLIENT" diff --git a/src/main/resources/hotbaaaar.mixins.json b/src/main/resources/hotbaaaar.mixins.json index 6aef85c..4d7d090 100644 --- a/src/main/resources/hotbaaaar.mixins.json +++ b/src/main/resources/hotbaaaar.mixins.json @@ -2,13 +2,11 @@ "required": true, "minVersion": "0.8", "package": "cn.ussshenzhou.hotbaaaar.mixin", - "compatibilityLevel": "JAVA_8", - "refmap": "hooootbar.refmap.json", - "mixins": [ - "InventoryMixin" - ], + "compatibilityLevel": "JAVA_17", + "refmap": "hotbaaaar.refmap.json", "client": [ "GuiMixin", + "InventoryMixin", "MinecraftMixin" ], "injectors": { diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 0b6f605..6641830 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": "${mod_name} resources", + "pack_format": 9 } } 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 From 985ebd9fb7e36e2c5bfce647ff4a63974dee4849 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sat, 6 Jun 2026 23:05:52 +0800 Subject: [PATCH 02/11] Sync release.yml discover fix from master Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2dfe630..8704d65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,13 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - json=$(gh api "repos/${{ github.repository }}/branches?per_page=100" \ - --jq '[.[].name | select(startswith("mc/"))]') + 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" From a501dd7b2eb9f604c26e0eb381ca9dab23a3a59c Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sun, 7 Jun 2026 00:06:02 +0800 Subject: [PATCH 03/11] Add mc/1.20.1-forge target (GuiGraphics rendering) Forge 1.20.1 (Java 17). Rewrites GuiMixin onto the 1.20.x GuiGraphics API (blit via guiGraphics, no RenderSystem), still using the widgets/icons atlases. Simplifies InventoryMixin to drop the level-side guard (swapPaint is client-only; onScroll guards on Minecraft.player) to stay version-agnostic. compileJava + mixin refmap validated. Co-Authored-By: Claude Opus 4.8 (1M context) --- gradle.properties | 12 ++-- .../ussshenzhou/hotbaaaar/mixin/GuiMixin.java | 60 +++++++------------ .../hotbaaaar/mixin/InventoryMixin.java | 12 ++-- src/main/resources/pack.mcmeta | 2 +- 4 files changed, 35 insertions(+), 51 deletions(-) diff --git a/gradle.properties b/gradle.properties index 058679f..39c5bf6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,15 @@ org.gradle.daemon=false ## Environment Properties # The Minecraft version must agree with the Forge version to get a valid artifact -minecraft_version=1.19.2 +minecraft_version=1.20.1 # The Minecraft version range can use any release version of Minecraft as bounds. -minecraft_version_range=[1.19.2,1.20) +minecraft_version_range=[1.20.1,1.21) # The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=43.5.0 +forge_version=47.3.0 # The Forge version range can use any version of Forge as bounds. -forge_version_range=[43,) +forge_version_range=[47,) # The loader version range can only use the major version of FML as bounds -loader_version_range=[43,) +loader_version_range=[47,) ## Mod Properties @@ -24,7 +24,7 @@ mod_name=HotBaaaar # The license of the mod. mod_license=GNU GPL 3.0 # The mod version. -mod_version=1.19.2-1.0 +mod_version=1.20.1-1.0 # The group ID for the mod. mod_group_id=cn.ussshenzhou # The authors of the mod. diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java index 2de17cf..d5109f3 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -2,13 +2,10 @@ import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; import cn.ussshenzhou.hotbaaaar.util.Util; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.blaze3d.vertex.PoseStack; import net.minecraft.client.AttackIndicatorStatus; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.util.Mth; import net.minecraft.world.entity.HumanoidArm; import net.minecraft.world.entity.player.Inventory; @@ -22,33 +19,28 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * Renders the extended hotbar: up to four 9-slot rows laid out as one wide strip, with items drawn at - * fixed logical positions (see {@link HotbaaaarClient}). Faithfully reproduces the vanilla 1.19.2 - * hotbar (background, selection frame, offhand slot, attack indicator) generalised to N rows. + * Renders the extended hotbar on the 1.20.x {@link GuiGraphics} API: up to four 9-slot rows laid out + * 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. Still uses the {@code widgets.png}/{@code icons.png} atlases (1.20.1). * * @author USS_Shenzhou */ @Mixin(Gui.class) -public abstract class GuiMixin extends GuiComponent { +public abstract class GuiMixin { @Shadow @Final private Minecraft minecraft; - @Shadow - private int screenWidth; - - @Shadow - private int screenHeight; - @Shadow protected abstract Player getCameraPlayer(); @Shadow - protected abstract void renderSlot(int x, int y, float partialTick, Player player, ItemStack stack, int seed); + protected abstract void renderSlot(GuiGraphics guiGraphics, int x, int y, float partialTick, Player player, ItemStack stack, int seed); @Inject(method = "renderHotbar", at = @At("HEAD"), cancellable = true) - private void hotbaaaar$renderHotbar(float partialTick, PoseStack poseStack, CallbackInfo ci) { + private void hotbaaaar$renderHotbar(float partialTick, GuiGraphics guiGraphics, CallbackInfo ci) { Player player = this.getCameraPlayer(); if (player == null) { return; @@ -57,36 +49,33 @@ public abstract class GuiMixin extends GuiComponent { Inventory inv = player.getInventory(); int rows = HotbaaaarClient.getRows(); + int screenWidth = this.minecraft.getWindow().getGuiScaledWidth(); + int screenHeight = this.minecraft.getWindow().getGuiScaledHeight(); ItemStack offhand = player.getOffhandItem(); HumanoidArm offhandArm = player.getMainArm().getOpposite(); final int oneHotbar = Util.HOTBAR_UNIT_LENGTH; final int half = 91; final int height = Util.HOTBAR_UNIT_HEIGHT; - int center = this.screenWidth / 2; + int center = screenWidth / 2; int x0 = center - rows * half; int x1 = x0 + rows * oneHotbar; - RenderSystem.setShader(GameRenderer::getPositionTexShader); - RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); - RenderSystem.setShaderTexture(0, Util.WIDGETS_LOCATION); - RenderSystem.enableBlend(); - // backgrounds for (int i = 0; i < rows; i++) { - this.blit(poseStack, x0 + i * oneHotbar, this.screenHeight - height, 0, 0, oneHotbar, height); + guiGraphics.blit(Util.WIDGETS_LOCATION, x0 + i * oneHotbar, screenHeight - height, 0, 0, oneHotbar, height); } // selection frame at the logical selected slot int logicalSelected = Mth.clamp(HotbaaaarClient.getActiveLogicalRow(), 0, rows - 1) * 9 + inv.selected; - this.blit(poseStack, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), this.screenHeight - height - 1, 0, 22, 24, 22); + guiGraphics.blit(Util.WIDGETS_LOCATION, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), screenHeight - height - 1, 0, 22, 24, 22); // offhand frame if (!offhand.isEmpty()) { if (offhandArm == HumanoidArm.LEFT) { - this.blit(poseStack, x0 - 29, this.screenHeight - 23, 24, 22, 29, 24); + guiGraphics.blit(Util.WIDGETS_LOCATION, x0 - 29, screenHeight - 23, 24, 22, 29, 24); } else { - this.blit(poseStack, x1, this.screenHeight - 23, 53, 22, 29, 24); + guiGraphics.blit(Util.WIDGETS_LOCATION, x1, screenHeight - 23, 53, 22, 29, 24); } } @@ -97,17 +86,17 @@ public abstract class GuiMixin extends GuiComponent { int col = i % 9; int physicalSlot = HotbaaaarClient.physicalRowOfLogical(logicalRow) * 9 + col; int x = x0 + i * 20 + 3 + (i / 9 * 2); - int y = this.screenHeight - 16 - 3; - this.renderSlot(x, y, partialTick, player, inv.items.get(physicalSlot), seed++); + int y = screenHeight - 16 - 3; + this.renderSlot(guiGraphics, x, y, partialTick, player, inv.items.get(physicalSlot), seed++); } // offhand item if (!offhand.isEmpty()) { - int y = this.screenHeight - 16 - 3; + int y = screenHeight - 16 - 3; if (offhandArm == HumanoidArm.LEFT) { - this.renderSlot(x0 - 26, y, partialTick, player, offhand, seed++); + this.renderSlot(guiGraphics, x0 - 26, y, partialTick, player, offhand, seed++); } else { - this.renderSlot(x1 + 10, y, partialTick, player, offhand, seed++); + this.renderSlot(guiGraphics, x1 + 10, y, partialTick, player, offhand, seed++); } } @@ -115,14 +104,11 @@ public abstract class GuiMixin extends GuiComponent { if (this.minecraft.options.attackIndicator().get() == AttackIndicatorStatus.HOTBAR) { float scale = this.minecraft.player.getAttackStrengthScale(0.0F); if (scale < 1.0F) { - int y = this.screenHeight - 20; + int y = screenHeight - 20; int x = (offhandArm == HumanoidArm.RIGHT) ? x0 - 22 : x1 + 6; - RenderSystem.setShaderTexture(0, Util.GUI_ICONS_LOCATION); int progress = (int) (scale * 19.0F); - RenderSystem.enableBlend(); - RenderSystem.defaultBlendFunc(); - this.blit(poseStack, x, y, 36, 94, 18, 18); - this.blit(poseStack, x, y + 18 - progress, 52, 94 + 18 - progress, 18, progress); + guiGraphics.blit(Util.GUI_ICONS_LOCATION, x, y, 36, 94, 18, 18); + guiGraphics.blit(Util.GUI_ICONS_LOCATION, x, y + 18 - progress, 52, 94 + 18 - progress, 18, progress); } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index ba45aa9..a91fd09 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -9,8 +9,9 @@ /** * Replaces the client-side mouse-wheel hotbar selection with the row-flipping logic. Vanilla - * {@code swapPaint} only ever runs on the client (from {@code MouseHandler#onScroll}); the guard - * keeps us off any logical server just in case. + * {@code swapPaint} is only ever called on the client (from {@code MouseHandler#onScroll}) against the + * local player's inventory, and {@link HotbaaaarClient#onScroll} guards on {@code Minecraft.player}, + * so no extra side check is needed here. * * @author USS_Shenzhou */ @@ -19,10 +20,7 @@ public class InventoryMixin { @Inject(method = "swapPaint", at = @At("HEAD"), cancellable = true) private void hotbaaaar$swapPaint(double direction, CallbackInfo ci) { - Inventory self = (Inventory) (Object) this; - if (self.player.level.isClientSide) { - HotbaaaarClient.onScroll(direction); - ci.cancel(); - } + HotbaaaarClient.onScroll(direction); + ci.cancel(); } } diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 6641830..ae03c9b 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "description": "${mod_name} resources", - "pack_format": 9 + "pack_format": 15 } } From 550bbac5d26b5bb0f379a8f0bb232098dae0436c Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sun, 7 Jun 2026 00:20:34 +0800 Subject: [PATCH 04/11] Add mc/1.21.1-forge target (sprite rendering, Java 21) Forge 1.21.1 (52.1.14, Java 21). Rewrites GuiMixin onto the 1.21.x sprite API (guiGraphics.blitSprite + DeltaTracker); Util now holds hud sprite ids. Bumps the Gradle wrapper to 8.8 (Tencent mirror; aliyun lacks 8.8) for Java 21 toolchain support, build toolchain + .github/ci/java-version to 21, mixin compatLevel JAVA_21, pack_format 34. compileJava + mixin refmap validated. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/ci/java-version | 2 +- build.gradle | 2 +- gradle.properties | 12 +++---- gradle/wrapper/gradle-wrapper.properties | 2 +- .../ussshenzhou/hotbaaaar/mixin/GuiMixin.java | 34 +++++++++---------- .../cn/ussshenzhou/hotbaaaar/util/Util.java | 11 ++++-- src/main/resources/hotbaaaar.mixins.json | 2 +- src/main/resources/pack.mcmeta | 2 +- 8 files changed, 36 insertions(+), 31 deletions(-) diff --git a/.github/ci/java-version b/.github/ci/java-version index 98d9bcb..aabe6ec 100644 --- a/.github/ci/java-version +++ b/.github/ci/java-version @@ -1 +1 @@ -17 +21 diff --git a/build.gradle b/build.gradle index 41f4614..9ebb59d 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ base { archivesName = mod_id } -java.toolchain.languageVersion = JavaLanguageVersion.of(17) +java.toolchain.languageVersion = JavaLanguageVersion.of(21) minecraft { mappings channel: 'official', version: minecraft_version diff --git a/gradle.properties b/gradle.properties index 39c5bf6..e222951 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,15 +5,15 @@ org.gradle.daemon=false ## Environment Properties # The Minecraft version must agree with the Forge version to get a valid artifact -minecraft_version=1.20.1 +minecraft_version=1.21.1 # The Minecraft version range can use any release version of Minecraft as bounds. -minecraft_version_range=[1.20.1,1.21) +minecraft_version_range=[1.21.1,1.22) # The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=47.3.0 +forge_version=52.1.14 # The Forge version range can use any version of Forge as bounds. -forge_version_range=[47,) +forge_version_range=[52,) # The loader version range can only use the major version of FML as bounds -loader_version_range=[47,) +loader_version_range=[52,) ## Mod Properties @@ -24,7 +24,7 @@ mod_name=HotBaaaar # The license of the mod. mod_license=GNU GPL 3.0 # The mod version. -mod_version=1.20.1-1.0 +mod_version=1.21.1-1.0 # The group ID for the mod. mod_group_id=cn.ussshenzhou # The authors of the mod. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 43212be..3fdd479 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.aliyun.com/gradle/distributions/v8.1.1/gradle-8.1.1-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java index d5109f3..d62168f 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -3,6 +3,7 @@ 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.GuiGraphics; @@ -19,10 +20,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * Renders the extended hotbar on the 1.20.x {@link GuiGraphics} API: up to four 9-slot rows laid out - * 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. Still uses the {@code widgets.png}/{@code icons.png} atlases (1.20.1). + * Renders the extended hotbar on the 1.21.x 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 */ @@ -37,10 +37,10 @@ public abstract class GuiMixin { protected abstract Player getCameraPlayer(); @Shadow - protected abstract void renderSlot(GuiGraphics guiGraphics, int x, int y, float partialTick, Player player, ItemStack stack, int seed); + protected abstract void renderSlot(GuiGraphics guiGraphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack stack, int seed); @Inject(method = "renderHotbar", at = @At("HEAD"), cancellable = true) - private void hotbaaaar$renderHotbar(float partialTick, GuiGraphics guiGraphics, CallbackInfo ci) { + private void hotbaaaar$renderHotbar(GuiGraphics guiGraphics, DeltaTracker deltaTracker, CallbackInfo ci) { Player player = this.getCameraPlayer(); if (player == null) { return; @@ -49,8 +49,8 @@ public abstract class GuiMixin { Inventory inv = player.getInventory(); int rows = HotbaaaarClient.getRows(); - int screenWidth = this.minecraft.getWindow().getGuiScaledWidth(); - int screenHeight = this.minecraft.getWindow().getGuiScaledHeight(); + int screenWidth = guiGraphics.guiWidth(); + int screenHeight = guiGraphics.guiHeight(); ItemStack offhand = player.getOffhandItem(); HumanoidArm offhandArm = player.getMainArm().getOpposite(); @@ -63,19 +63,19 @@ public abstract class GuiMixin { // backgrounds for (int i = 0; i < rows; i++) { - guiGraphics.blit(Util.WIDGETS_LOCATION, x0 + i * oneHotbar, screenHeight - height, 0, 0, oneHotbar, height); + guiGraphics.blitSprite(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.selected; - guiGraphics.blit(Util.WIDGETS_LOCATION, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), screenHeight - height - 1, 0, 22, 24, 22); + guiGraphics.blitSprite(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.blit(Util.WIDGETS_LOCATION, x0 - 29, screenHeight - 23, 24, 22, 29, 24); + guiGraphics.blitSprite(Util.HOTBAR_OFFHAND_LEFT_SPRITE, x0 - 29, screenHeight - 23, 29, 24); } else { - guiGraphics.blit(Util.WIDGETS_LOCATION, x1, screenHeight - 23, 53, 22, 29, 24); + guiGraphics.blitSprite(Util.HOTBAR_OFFHAND_RIGHT_SPRITE, x1, screenHeight - 23, 29, 24); } } @@ -87,16 +87,16 @@ public abstract class GuiMixin { int physicalSlot = HotbaaaarClient.physicalRowOfLogical(logicalRow) * 9 + col; int x = x0 + i * 20 + 3 + (i / 9 * 2); int y = screenHeight - 16 - 3; - this.renderSlot(guiGraphics, x, y, partialTick, player, inv.items.get(physicalSlot), seed++); + this.renderSlot(guiGraphics, x, y, deltaTracker, player, inv.items.get(physicalSlot), seed++); } // offhand item if (!offhand.isEmpty()) { int y = screenHeight - 16 - 3; if (offhandArm == HumanoidArm.LEFT) { - this.renderSlot(guiGraphics, x0 - 26, y, partialTick, player, offhand, seed++); + this.renderSlot(guiGraphics, x0 - 26, y, deltaTracker, player, offhand, seed++); } else { - this.renderSlot(guiGraphics, x1 + 10, y, partialTick, player, offhand, seed++); + this.renderSlot(guiGraphics, x1 + 10, y, deltaTracker, player, offhand, seed++); } } @@ -107,8 +107,8 @@ public abstract class GuiMixin { int y = screenHeight - 20; int x = (offhandArm == HumanoidArm.RIGHT) ? x0 - 22 : x1 + 6; int progress = (int) (scale * 19.0F); - guiGraphics.blit(Util.GUI_ICONS_LOCATION, x, y, 36, 94, 18, 18); - guiGraphics.blit(Util.GUI_ICONS_LOCATION, x, y + 18 - progress, 52, 94 + 18 - progress, 18, progress); + guiGraphics.blitSprite(Util.HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE, x, y, 18, 18); + guiGraphics.blitSprite(Util.HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE, 18, 18, 0, 18 - progress, x, y + 18 - progress, 18, progress); } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java index 3cdc998..443d679 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java @@ -3,14 +3,19 @@ import net.minecraft.resources.ResourceLocation; /** - * 1.19.2 GUI texture sheets and hotbar geometry constants. + * 1.21.x GUI hotbar sprites and geometry constants. 1.20.5+ renders the HUD from the sprite atlas, + * so these are sprite ids (under {@code minecraft:gui/sprites/...}), not raw texture sheets. * * @author USS_Shenzhou */ public class Util { - public static final ResourceLocation WIDGETS_LOCATION = new ResourceLocation("textures/gui/widgets.png"); - public static final ResourceLocation GUI_ICONS_LOCATION = new ResourceLocation("textures/gui/icons.png"); + public static final ResourceLocation HOTBAR_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar"); + public static final ResourceLocation HOTBAR_SELECTION_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_selection"); + public static final ResourceLocation HOTBAR_OFFHAND_LEFT_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_offhand_left"); + public static final ResourceLocation HOTBAR_OFFHAND_RIGHT_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_offhand_right"); + public static final ResourceLocation HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_attack_indicator_background"); + public static final ResourceLocation HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_attack_indicator_progress"); /** Width of one vanilla hotbar in GUI pixels. */ public static final int HOTBAR_UNIT_LENGTH = 182; diff --git a/src/main/resources/hotbaaaar.mixins.json b/src/main/resources/hotbaaaar.mixins.json index 4d7d090..2b30ff6 100644 --- a/src/main/resources/hotbaaaar.mixins.json +++ b/src/main/resources/hotbaaaar.mixins.json @@ -2,7 +2,7 @@ "required": true, "minVersion": "0.8", "package": "cn.ussshenzhou.hotbaaaar.mixin", - "compatibilityLevel": "JAVA_17", + "compatibilityLevel": "JAVA_21", "refmap": "hotbaaaar.refmap.json", "client": [ "GuiMixin", diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index ae03c9b..77bed85 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { "description": "${mod_name} resources", - "pack_format": 15 + "pack_format": 34 } } From f37218e8f2d8baa112c672fde919caa6476cc062 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sun, 7 Jun 2026 00:53:00 +0800 Subject: [PATCH 05/11] Add mc/1.21.1-fabric target (Fabric Loom, Java 21) Fabric 1.21.1 (Java 21), sprite-era GuiMixin. Loom requires the Gradle JVM itself to be Java 21 for 1.21 (not just the toolchain), so .github/ci/java-version=21 is required for this branch. Build validated locally with a JDK 21. Co-Authored-By: Claude Opus 4.8 (1M context) --- build.gradle | 121 +++--------------- gradle.properties | 15 +-- settings.gradle | 3 +- .../cn/ussshenzhou/hotbaaaar/HotBaaaar.java | 20 --- src/main/resources/META-INF/mods.toml | 27 ---- src/main/resources/fabric.mod.json | 22 ++++ 6 files changed, 46 insertions(+), 162 deletions(-) delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java delete mode 100644 src/main/resources/META-INF/mods.toml create mode 100644 src/main/resources/fabric.mod.json diff --git a/build.gradle b/build.gradle index 9ebb59d..e9b8f06 100644 --- a/build.gradle +++ b/build.gradle @@ -1,124 +1,41 @@ -buildscript { - repositories { - maven { url = 'https://maven.minecraftforge.net/' } - maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } - mavenCentral() - } - dependencies { - classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '6.0.+', changing: true - classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT' - } +plugins { + id 'fabric-loom' version '1.7.4' + id 'maven-publish' } -apply plugin: 'net.minecraftforge.gradle' -apply plugin: 'org.spongepowered.mixin' -apply plugin: 'eclipse' -apply plugin: 'idea' -apply plugin: 'maven-publish' -apply plugin: 'java' - version = mod_version group = mod_group_id + base { archivesName = mod_id } java.toolchain.languageVersion = JavaLanguageVersion.of(21) -minecraft { - mappings channel: 'official', version: minecraft_version - - // Access transformer is optional; this mod uses @Shadow instead, so none is declared. - - runs { - client { - workingDirectory project.file('run') - property 'forge.logging.markers', 'REGISTRIES' - property 'forge.logging.console.level', 'debug' - property 'mixin.env.remapRefMap', 'true' - property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" - mods { - "${mod_id}" { - source sourceSets.main - } - } - } - - server { - workingDirectory project.file('run') - property 'forge.logging.markers', 'REGISTRIES' - property 'forge.logging.console.level', 'debug' - property 'mixin.env.remapRefMap', 'true' - property 'mixin.env.refMapRemappingFile', "${projectDir}/build/createSrgToMcp/output.srg" - mods { - "${mod_id}" { - source sourceSets.main - } - } - } +loom { + // Generate the mixin refmap under the name our mixins.json references (keeps it identical to the + // Forge/NeoForge branches), instead of Loom's default "-refmap.json". + mixin { + defaultRefmapName = "${mod_id}.refmap.json" } } -mixin { - add sourceSets.main, "${mod_id}.refmap.json" - config "${mod_id}.mixins.json" -} - repositories { - mavenLocal() - maven { url = 'https://jitpack.io' } + mavenCentral() + maven { url = 'https://maven.fabricmc.net/' } } dependencies { - minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}" - annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' -} - -tasks.named('processResources', ProcessResources).configure { - var replaceProperties = [ - minecraft_version : minecraft_version, - forge_version : forge_version, - loader_version_range : loader_version_range, - minecraft_version_range: minecraft_version_range, - forge_version_range : forge_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 - filesMatching(['META-INF/mods.toml', 'pack.mcmeta']) { - expand replaceProperties - } + minecraft "com.mojang:minecraft:${minecraft_version}" + // Use Mojang's official mappings so the shared client/mixin code compiles unchanged across loaders. + mappings loom.officialMojangMappings() + modImplementation "net.fabricmc:fabric-loader:${loader_version}" } -tasks.named('jar', Jar).configure { - manifest { - attributes([ - 'Specification-Title' : mod_id, - 'Specification-Vendor' : mod_authors, - 'Specification-Version' : '1', - 'Implementation-Title' : project.name, - 'Implementation-Version' : project.jar.archiveVersion, - 'Implementation-Vendor' : mod_authors, - 'MixinConfigs' : "${mod_id}.mixins.json" - ]) - } - finalizedBy 'reobfJar' -} - -publishing { - publications { - register('mavenJava', MavenPublication) { - artifact jar - } - } - repositories { - maven { - url "file://${project.projectDir}/repo" - } +processResources { + inputs.property 'version', project.version + filesMatching('fabric.mod.json') { + expand 'version': project.version } } diff --git a/gradle.properties b/gradle.properties index e222951..79432f3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,16 +4,9 @@ org.gradle.daemon=false ## Environment Properties -# The Minecraft version must agree with the Forge version to get a valid artifact minecraft_version=1.21.1 -# The Minecraft version range can use any release version of Minecraft as bounds. -minecraft_version_range=[1.21.1,1.22) -# The Forge version must agree with the Minecraft version to get a valid artifact -forge_version=52.1.14 -# The Forge version range can use any version of Forge as bounds. -forge_version_range=[52,) -# The loader version range can only use the major version of FML as bounds -loader_version_range=[52,) +# Fabric loader version - https://fabricmc.net/develop/ +loader_version=0.16.14 ## Mod Properties @@ -23,8 +16,8 @@ mod_id=hotbaaaar mod_name=HotBaaaar # The license of the mod. mod_license=GNU GPL 3.0 -# The mod version. -mod_version=1.21.1-1.0 +# The mod version. Suffixed -fabric so the jar does not collide with the Forge jar of the same MC version. +mod_version=1.21.1-fabric-1.0 # The group ID for the mod. mod_group_id=cn.ussshenzhou # The authors of the mod. diff --git a/settings.gradle b/settings.gradle index 18d16d0..8cbbc55 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,8 +1,7 @@ pluginManagement { repositories { gradlePluginPortal() - maven { url = 'https://maven.minecraftforge.net/' } - maven { url = 'https://repo.spongepowered.org/repository/maven-public/' } + maven { url = 'https://maven.fabricmc.net/' } } } 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 e629996..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/HotBaaaar.java +++ /dev/null @@ -1,20 +0,0 @@ -package cn.ussshenzhou.hotbaaaar; - -import net.minecraftforge.fml.common.Mod; - -/** - * Client-only "super long hotbar" mod for 1.19.2 Forge. - *

- * All behaviour lives in client-side mixins ({@code GuiMixin}, {@code InventoryMixin}, - * {@code MinecraftMixin}) and {@code cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient}. - * The mod does nothing on a dedicated server and is safe to connect to vanilla servers. - * - * @author USS_Shenzhou - */ -@Mod(HotBaaaar.MOD_ID) -public class HotBaaaar { - public static final String MOD_ID = "hotbaaaar"; - - public HotBaaaar() { - } -} diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml deleted file mode 100644 index d84d51f..0000000 --- a/src/main/resources/META-INF/mods.toml +++ /dev/null @@ -1,27 +0,0 @@ -modLoader="javafml" -loaderVersion="${loader_version_range}" -license="${mod_license}" - -[[mods]] -modId="${mod_id}" -version="${mod_version}" -displayName="${mod_name}" -# IGNORE_ALL_VERSION: this is a client-only mod, never flag a version mismatch when connecting to a server. -displayTest="IGNORE_ALL_VERSION" -logoFile="hotbaaaar.png" -authors="${mod_authors}" -description='''${mod_description}''' - -[[dependencies.${mod_id}]] -modId="forge" -mandatory=true -versionRange="${forge_version_range}" -ordering="NONE" -side="CLIENT" - -[[dependencies.${mod_id}]] -modId="minecraft" -mandatory=true -versionRange="${minecraft_version_range}" -ordering="NONE" -side="CLIENT" diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..e3ded09 --- /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.16.0", + "minecraft": "~1.21.1", + "java": ">=21" + } +} From d46539e04a678dac948d49eb6eae1a8986a96480 Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sun, 7 Jun 2026 02:48:56 +0800 Subject: [PATCH 06/11] Sync workflows from master (CI artifact naming + tag-versioned release) Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 6 +++++- .github/workflows/release.yml | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8174898..4f5d2d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,11 @@ jobs: - name: Build run: | chmod +x ./gradlew - ./gradlew build --no-daemon --stacktrace + # 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8704d65..443fee7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,13 @@ jobs: - name: Build run: | chmod +x ./gradlew - ./gradlew build --no-daemon --stacktrace + # 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 From e64315a4ccbb9a30c39b0706615290c12f09191e Mon Sep 17 00:00:00 2001 From: Wen_He Date: Sun, 7 Jun 2026 05:12:12 +0800 Subject: [PATCH 07/11] Restore inventory order on open, re-apply active row on close Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hotbaaaar/client/HotbaaaarClient.java | 94 ++++++++++++++++++- .../hotbaaaar/mixin/MinecraftMixin.java | 26 +++-- 2 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java index 10b4ad2..543f770 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java @@ -16,9 +16,13 @@ * 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. We never auto-restore - * the inventory order; we only reset this internal map (see {@link #resetMapping()}) when a container - * screen opens or the player instance changes, so external rearrangement can't desync the view. + * 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 */ @@ -34,6 +38,10 @@ public class HotbaaaarClient { 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() { } @@ -73,6 +81,7 @@ public static void tickSanity() { Player p = Minecraft.getInstance().player; if (p != lastPlayer) { lastPlayer = p; + restoredForInventory = false; resetMapping(); } if (activeLogicalRow >= getRows()) { @@ -80,6 +89,82 @@ public static void tickSanity() { } } + // --- 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. @@ -144,7 +229,8 @@ private static boolean activateLogicalRow(int target) { /** * 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. + * 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. */ diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java index 6fd7d10..1dbd6e8 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java @@ -4,25 +4,39 @@ import net.minecraft.client.Minecraft; 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.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * When any container screen opens (player inventory, chest, ...) the player may rearrange items, so we - * reset the internal logical-to-physical map to match the real inventory order. This does not move any - * items; it just prevents the swap bookkeeping from desyncing the rendered view. + * 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 class MinecraftMixin { + @Shadow + public Screen screen; + @Inject(method = "setScreen", at = @At("HEAD")) - private void hotbaaaar$onSetScreen(Screen screen, CallbackInfo ci) { - if (screen instanceof AbstractContainerScreen) { - HotbaaaarClient.resetMapping(); + 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(); } } } From 439677397f9ed78b04b954d5d4d2f892ecb7808e Mon Sep 17 00:00:00 2001 From: FFDVDGD Date: Sat, 20 Jun 2026 00:00:15 +0800 Subject: [PATCH 08/11] Port to Minecraft 1.21.11 Fabric --- build.gradle | 2 +- gradle.properties | 6 ++--- gradle/wrapper/gradle-wrapper.properties | 2 +- .../hotbaaaar/client/HotbaaaarClient.java | 4 ++-- .../ussshenzhou/hotbaaaar/mixin/GuiMixin.java | 19 ++++++++------- .../hotbaaaar/mixin/InventoryMixin.java | 24 ++++++++----------- .../cn/ussshenzhou/hotbaaaar/util/Util.java | 14 +++++------ src/main/resources/fabric.mod.json | 4 ++-- 8 files changed, 36 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index e9b8f06..a1c4366 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '1.7.4' + id 'fabric-loom' version '1.10.5' id 'maven-publish' } diff --git a/gradle.properties b/gradle.properties index 79432f3..2bc4380 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,9 +4,9 @@ org.gradle.daemon=false ## Environment Properties -minecraft_version=1.21.1 +minecraft_version=1.21.11 # Fabric loader version - https://fabricmc.net/develop/ -loader_version=0.16.14 +loader_version=0.19.3 ## Mod Properties @@ -17,7 +17,7 @@ mod_name=HotBaaaar # The license of the mod. mod_license=GNU GPL 3.0 # The mod version. Suffixed -fabric so the jar does not collide with the Forge jar of the same MC version. -mod_version=1.21.1-fabric-1.0 +mod_version=1.21.11-fabric-1.0 # The group ID for the mod. mod_group_id=cn.ussshenzhou # The authors of the mod. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fdd479..ff23a68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java index 543f770..bb77ebe 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java @@ -181,7 +181,7 @@ public static void onScroll(double direction) { } tickSanity(); Inventory inv = player.getInventory(); - int newSelected = inv.selected - dir; + int newSelected = inv.getSelectedSlot() - dir; if (newSelected < 0) { // past the left edge -> previous row setSelected(inv, flipRow(-1) ? ROW - 1 : 0); @@ -194,7 +194,7 @@ public static void onScroll(double direction) { } private static void setSelected(Inventory inv, int slot) { - inv.selected = slot; + inv.setSelectedSlot(slot); Minecraft mc = Minecraft.getInstance(); if (mc.getConnection() != null) { mc.getConnection().send(new ServerboundSetCarriedItemPacket(slot)); diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java index d62168f..80aaec7 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -7,6 +7,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderPipelines; import net.minecraft.util.Mth; import net.minecraft.world.entity.HumanoidArm; import net.minecraft.world.entity.player.Inventory; @@ -39,7 +40,7 @@ public abstract class GuiMixin { @Shadow protected abstract void renderSlot(GuiGraphics guiGraphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack stack, int seed); - @Inject(method = "renderHotbar", at = @At("HEAD"), cancellable = true) + @Inject(method = "renderItemHotbar", at = @At("HEAD"), cancellable = true) private void hotbaaaar$renderHotbar(GuiGraphics guiGraphics, DeltaTracker deltaTracker, CallbackInfo ci) { Player player = this.getCameraPlayer(); if (player == null) { @@ -63,19 +64,19 @@ public abstract class GuiMixin { // backgrounds for (int i = 0; i < rows; i++) { - guiGraphics.blitSprite(Util.HOTBAR_SPRITE, x0 + i * oneHotbar, screenHeight - height, oneHotbar, height); + 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.selected; - guiGraphics.blitSprite(Util.HOTBAR_SELECTION_SPRITE, x0 - 1 + logicalSelected * 20 + (logicalSelected / 9 * 2), screenHeight - height - 1, 24, 23); + 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(Util.HOTBAR_OFFHAND_LEFT_SPRITE, x0 - 29, screenHeight - 23, 29, 24); + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_OFFHAND_LEFT_SPRITE, x0 - 29, screenHeight - 23, 29, 24); } else { - guiGraphics.blitSprite(Util.HOTBAR_OFFHAND_RIGHT_SPRITE, x1, screenHeight - 23, 29, 24); + guiGraphics.blitSprite(RenderPipelines.GUI_TEXTURED, Util.HOTBAR_OFFHAND_RIGHT_SPRITE, x1, screenHeight - 23, 29, 24); } } @@ -87,7 +88,7 @@ public abstract class GuiMixin { int physicalSlot = HotbaaaarClient.physicalRowOfLogical(logicalRow) * 9 + col; int x = x0 + i * 20 + 3 + (i / 9 * 2); int y = screenHeight - 16 - 3; - this.renderSlot(guiGraphics, x, y, deltaTracker, player, inv.items.get(physicalSlot), seed++); + this.renderSlot(guiGraphics, x, y, deltaTracker, player, inv.getItem(physicalSlot), seed++); } // offhand item @@ -107,8 +108,8 @@ public abstract class GuiMixin { int y = screenHeight - 20; int x = (offhandArm == HumanoidArm.RIGHT) ? x0 - 22 : x1 + 6; int progress = (int) (scale * 19.0F); - guiGraphics.blitSprite(Util.HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE, x, y, 18, 18); - guiGraphics.blitSprite(Util.HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE, 18, 18, 0, 18 - progress, x, y + 18 - progress, 18, progress); + 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); } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index a91fd09..6a76926 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -1,26 +1,22 @@ package cn.ussshenzhou.hotbaaaar.mixin; import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; -import net.minecraft.world.entity.player.Inventory; +import net.minecraft.client.Minecraft; +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; -/** - * Replaces the client-side mouse-wheel hotbar selection with the row-flipping logic. Vanilla - * {@code swapPaint} is only ever called on the client (from {@code MouseHandler#onScroll}) against the - * local player's inventory, and {@link HotbaaaarClient#onScroll} guards on {@code Minecraft.player}, - * so no extra side check is needed here. - * - * @author USS_Shenzhou - */ -@Mixin(Inventory.class) +@Mixin(MouseHandler.class) public class InventoryMixin { - @Inject(method = "swapPaint", at = @At("HEAD"), cancellable = true) - private void hotbaaaar$swapPaint(double direction, CallbackInfo ci) { - HotbaaaarClient.onScroll(direction); - ci.cancel(); + @Inject(method = "onScroll", at = @At("HEAD"), 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.screen == null && yOffset != 0) { + HotbaaaarClient.onScroll(yOffset); + ci.cancel(); + } } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java index 443d679..7a542ab 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java @@ -1,6 +1,6 @@ package cn.ussshenzhou.hotbaaaar.util; -import net.minecraft.resources.ResourceLocation; +import net.minecraft.resources.Identifier; /** * 1.21.x GUI hotbar sprites and geometry constants. 1.20.5+ renders the HUD from the sprite atlas, @@ -10,12 +10,12 @@ */ public class Util { - public static final ResourceLocation HOTBAR_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar"); - public static final ResourceLocation HOTBAR_SELECTION_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_selection"); - public static final ResourceLocation HOTBAR_OFFHAND_LEFT_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_offhand_left"); - public static final ResourceLocation HOTBAR_OFFHAND_RIGHT_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_offhand_right"); - public static final ResourceLocation HOTBAR_ATTACK_INDICATOR_BACKGROUND_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_attack_indicator_background"); - public static final ResourceLocation HOTBAR_ATTACK_INDICATOR_PROGRESS_SPRITE = ResourceLocation.withDefaultNamespace("hud/hotbar_attack_indicator_progress"); + public static final Identifier HOTBAR_SPRITE = Identifier.withDefaultNamespace("hud/hotbar"); + public static final Identifier HOTBAR_SELECTION_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_selection"); + public static final Identifier HOTBAR_OFFHAND_LEFT_SPRITE = Identifier.withDefaultNamespace("hud/hotbar_offhand_left"); + 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; diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index e3ded09..c465615 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -15,8 +15,8 @@ } ], "depends": { - "fabricloader": ">=0.16.0", - "minecraft": "~1.21.1", + "fabricloader": ">=0.19.0", + "minecraft": "~1.21.11", "java": ">=21" } } From 43383db4fbc5099f3fa3844a2b2df2250e224195 Mon Sep 17 00:00:00 2001 From: FFDVDGD Date: Mon, 22 Jun 2026 19:52:01 +0800 Subject: [PATCH 09/11] Add tweakeroo zoom scroll compatibility When tweakeroo's zoom is active (toggle keybind held, zoom activate held, or spyglass zoom), skip hotbar scroll interception and let the scroll event pass through to tweakeroo/malilib for FOV adjustment. Uses FabricLoader mod detection + cached reflection to avoid a hard dependency on tweakeroo. Co-Authored-By: Claude Opus 4.6 --- .../hotbaaaar/compat/TweakerooCompat.java | 62 +++++++++++++++++++ .../hotbaaaar/mixin/InventoryMixin.java | 4 ++ 2 files changed, 66 insertions(+) create mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java b/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java new file mode 100644 index 0000000..35463ca --- /dev/null +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java @@ -0,0 +1,62 @@ +package cn.ussshenzhou.hotbaaaar.compat; + +import net.fabricmc.loader.api.FabricLoader; + +import java.lang.reflect.Method; + +public class TweakerooCompat { + + private static final boolean LOADED = FabricLoader.getInstance().isModLoaded("tweakeroo"); + private static Method isZoomActive; + private static Method isSpyglassZoomActive; + private static Object tweakZoomEnum; + private static Method getKeybind; + private static Method isKeybindHeld; + + static { + if (LOADED) { + try { + Class miscUtils = Class.forName("fi.dy.masa.tweakeroo.util.MiscUtils"); + isZoomActive = miscUtils.getMethod("isZoomActive"); + isSpyglassZoomActive = miscUtils.getMethod("isSpyglassZoomActive"); + + @SuppressWarnings("unchecked") + Class> featureToggle = (Class>) Class.forName("fi.dy.masa.tweakeroo.config.FeatureToggle"); + for (Enum e : featureToggle.getEnumConstants()) { + if ("TWEAK_ZOOM".equals(e.name())) { + tweakZoomEnum = e; + break; + } + } + if (tweakZoomEnum != null) { + getKeybind = tweakZoomEnum.getClass().getMethod("getKeybind"); + Object kb = getKeybind.invoke(tweakZoomEnum); + isKeybindHeld = kb.getClass().getMethod("isKeybindHeld"); + } + } catch (Exception ignored) { + } + } + } + + public static boolean isZoomScrollActive() { + if (!LOADED) { + return false; + } + try { + if (tweakZoomEnum != null && getKeybind != null && isKeybindHeld != null) { + Object kb = getKeybind.invoke(tweakZoomEnum); + if ((boolean) isKeybindHeld.invoke(kb)) { + return true; + } + } + if (isZoomActive != null && (boolean) isZoomActive.invoke(null)) { + return true; + } + if (isSpyglassZoomActive != null && (boolean) isSpyglassZoomActive.invoke(null)) { + return true; + } + } catch (Exception ignored) { + } + return false; + } +} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index 6a76926..a6cd063 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -1,6 +1,7 @@ package cn.ussshenzhou.hotbaaaar.mixin; import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; +import cn.ussshenzhou.hotbaaaar.compat.TweakerooCompat; import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; import org.spongepowered.asm.mixin.Mixin; @@ -15,6 +16,9 @@ public class InventoryMixin { private void hotbaaaar$onScroll(long window, double xOffset, double yOffset, CallbackInfo ci) { Minecraft mc = Minecraft.getInstance(); if (mc.player != null && !mc.player.isSpectator() && mc.screen == null && yOffset != 0) { + if (TweakerooCompat.isZoomScrollActive()) { + return; + } HotbaaaarClient.onScroll(yOffset); ci.cancel(); } From bcceb66563bbfaadbfde0624084f8a61573c91a7 Mon Sep 17 00:00:00 2001 From: FFDVDGD Date: Tue, 30 Jun 2026 15:52:35 +0800 Subject: [PATCH 10/11] Fix scroll input compatibility --- .../hotbaaaar/compat/TweakerooCompat.java | 62 ------------------- .../hotbaaaar/mixin/InventoryMixin.java | 20 ++++-- 2 files changed, 15 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java b/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java deleted file mode 100644 index 35463ca..0000000 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/compat/TweakerooCompat.java +++ /dev/null @@ -1,62 +0,0 @@ -package cn.ussshenzhou.hotbaaaar.compat; - -import net.fabricmc.loader.api.FabricLoader; - -import java.lang.reflect.Method; - -public class TweakerooCompat { - - private static final boolean LOADED = FabricLoader.getInstance().isModLoaded("tweakeroo"); - private static Method isZoomActive; - private static Method isSpyglassZoomActive; - private static Object tweakZoomEnum; - private static Method getKeybind; - private static Method isKeybindHeld; - - static { - if (LOADED) { - try { - Class miscUtils = Class.forName("fi.dy.masa.tweakeroo.util.MiscUtils"); - isZoomActive = miscUtils.getMethod("isZoomActive"); - isSpyglassZoomActive = miscUtils.getMethod("isSpyglassZoomActive"); - - @SuppressWarnings("unchecked") - Class> featureToggle = (Class>) Class.forName("fi.dy.masa.tweakeroo.config.FeatureToggle"); - for (Enum e : featureToggle.getEnumConstants()) { - if ("TWEAK_ZOOM".equals(e.name())) { - tweakZoomEnum = e; - break; - } - } - if (tweakZoomEnum != null) { - getKeybind = tweakZoomEnum.getClass().getMethod("getKeybind"); - Object kb = getKeybind.invoke(tweakZoomEnum); - isKeybindHeld = kb.getClass().getMethod("isKeybindHeld"); - } - } catch (Exception ignored) { - } - } - } - - public static boolean isZoomScrollActive() { - if (!LOADED) { - return false; - } - try { - if (tweakZoomEnum != null && getKeybind != null && isKeybindHeld != null) { - Object kb = getKeybind.invoke(tweakZoomEnum); - if ((boolean) isKeybindHeld.invoke(kb)) { - return true; - } - } - if (isZoomActive != null && (boolean) isZoomActive.invoke(null)) { - return true; - } - if (isSpyglassZoomActive != null && (boolean) isSpyglassZoomActive.invoke(null)) { - return true; - } - } catch (Exception ignored) { - } - return false; - } -} diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index a6cd063..3699044 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -1,7 +1,6 @@ package cn.ussshenzhou.hotbaaaar.mixin; import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; -import cn.ussshenzhou.hotbaaaar.compat.TweakerooCompat; import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; import org.spongepowered.asm.mixin.Mixin; @@ -12,13 +11,24 @@ @Mixin(MouseHandler.class) public class InventoryMixin { - @Inject(method = "onScroll", at = @At("HEAD"), cancellable = true) + /** + * 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.

+ */ + @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.screen == null && yOffset != 0) { - if (TweakerooCompat.isZoomScrollActive()) { - return; - } HotbaaaarClient.onScroll(yOffset); ci.cancel(); } From 3a16c72820cec8b419fe1c4c209e27d10f40711f Mon Sep 17 00:00:00 2001 From: FFDVDGD Date: Sat, 11 Jul 2026 01:03:41 +0800 Subject: [PATCH 11/11] Port Fabric mod to Minecraft 26.2 --- .github/ci/java-version | 2 +- build.gradle | 22 +++---- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 48462 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 15 +++-- gradlew.bat | 54 +++++++----------- .../hotbaaaar/client/HotbaaaarClient.java | 10 ++-- .../ussshenzhou/hotbaaaar/mixin/GuiMixin.java | 26 +++++---- .../hotbaaaar/mixin/InventoryMixin.java | 2 +- .../hotbaaaar/mixin/MinecraftMixin.java | 4 +- .../cn/ussshenzhou/hotbaaaar/util/Util.java | 2 +- src/main/resources/fabric.mod.json | 4 +- src/main/resources/hotbaaaar.mixins.json | 3 +- src/main/resources/pack.mcmeta | 4 +- 15 files changed, 70 insertions(+), 86 deletions(-) diff --git a/.github/ci/java-version b/.github/ci/java-version index aabe6ec..7273c0f 100644 --- a/.github/ci/java-version +++ b/.github/ci/java-version @@ -1 +1 @@ -21 +25 diff --git a/build.gradle b/build.gradle index a1c4366..4e836f8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'fabric-loom' version '1.10.5' + id 'net.fabricmc.fabric-loom' version '1.17.13' id 'maven-publish' } @@ -10,15 +10,7 @@ base { archivesName = mod_id } -java.toolchain.languageVersion = JavaLanguageVersion.of(21) - -loom { - // Generate the mixin refmap under the name our mixins.json references (keeps it identical to the - // Forge/NeoForge branches), instead of Loom's default "-refmap.json". - mixin { - defaultRefmapName = "${mod_id}.refmap.json" - } -} +java.toolchain.languageVersion = JavaLanguageVersion.of(25) repositories { mavenCentral() @@ -27,15 +19,15 @@ repositories { dependencies { minecraft "com.mojang:minecraft:${minecraft_version}" - // Use Mojang's official mappings so the shared client/mixin code compiles unchanged across loaders. - mappings loom.officialMojangMappings() - modImplementation "net.fabricmc:fabric-loader:${loader_version}" + implementation "net.fabricmc:fabric-loader:${loader_version}" } +def artifactVersion = version + processResources { - inputs.property 'version', project.version + inputs.property 'version', artifactVersion filesMatching('fabric.mod.json') { - expand 'version': project.version + expand 'version': artifactVersion } } diff --git a/gradle.properties b/gradle.properties index 2bc4380..0a2b5bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.daemon=false ## Environment Properties -minecraft_version=1.21.11 +minecraft_version=26.2 # Fabric loader version - https://fabricmc.net/develop/ loader_version=0.19.3 @@ -17,7 +17,7 @@ mod_name=HotBaaaar # The license of the mod. mod_license=GNU GPL 3.0 # The mod version. Suffixed -fabric so the jar does not collide with the Forge jar of the same MC version. -mod_version=1.21.11-fabric-1.0 +mod_version=26.2-fabric-1.0 # The group ID for the mod. mod_group_id=cn.ussshenzhou # The authors of the mod. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..b1b8ef56b44f16b14dc800fa8103a6d89abb526f 100644 GIT binary patch delta 39897 zcmXVXQ(&Fl_jDTDwr$(Clg75~)9A!JQDfVUZQFK(#&-Jk{r$h2=jyrIYwejevu2Mh zg3r8x*V`iln5L)ULs?NF_xwq(6LHq&o-*{y)ge>G0d<>gcBCVTVNZ`$&8w!zS_Tqf>a}*%?yt0T&>_t5 zzJseOz`)wgPU@wCfq}Jyf%c^dLCeVU0AkE8pD8)V)|iz6b*aR{vRu6z;$MrL+~dsS zPH02Ikx@zdLim{9@;V9O4^pTRrc=IvO_buB65=pZ7;mjYIN)p9&t z16ISi9lZ0Qh@j)(KEL_bOVz_mlgiOAGZzGwtATT>_&L|#za5rq)QEi&{C8T1Fz2#l zAYoK3fQG&bh8pS@vg7v+=5*}nJ*-s(N85_6CiMe^TXFRvUGV-Fw>2RL6bJ9DZKiqL zSCTiF04)l1PB!6pfe(zH$X^Jl=teVBKR>N{_;#nh-rg@^etc(S?b_Y+Hwr3pbPJSO z19I1$>FbFt=16PkZKcN>I`9JX+ZY)8g~UkC0b2}zJPWRUYJg?a&aRUfP&KyL1YO@# z#E{5JPpo`?);qQ5^e#6t!uxm{Wy6!U&^eMB*k)mFW7WzSV6v#pD5k{#*Oqsley~y6 zDNimBQ`o`**+#h(N+*#NQalvWuydqCH>FeQYdAx#uq5%ITV9!2zHf0WFl23tKVx}t z0BDsXZ*ZZj$nt+HR6T_GKW5%AAkuqJYfG$s{W+&rE2%8g!e+Yig_Oe|STw|2F_rZu zwF|mI4H;5ewdGnZF}6N6N^E%`K{ag&ZJ-zGP1ZM$v~ahle@l`V70bk(^(--=4Mtp` z_d`vf03sj;mKEE_b7ID_OX~3C3Te8+0mgZm4;rH-_l8}xq;_2oaGU)z+l~;@o%O_n zN9HI(v##3^2iFZ`iWF#WqocFhQiqVXoOJl^4aL%yN=kg_jb8VE8I_NjlSdK4FJ%(} zBQ}}^j>S`H)ZksbXB01Wm7o?i;|QQ7sQra`OnwdbCUQV06ZLn zN9nw0n5zd}BwaDm*B*Vd>c}Ss$pPkfY3{VLhXm0KT)6j`4~YNTv&J_hHx3*OY!l*t zmzf?z9N?(3sEQfHOpxMf{sZ=fSv#>?6)r_zdsn#R~NE>!j`ILoIpEA;%FN74%>C>gxJxW7Mn2U9dr159jb zaM0y4@2>~%J@XTEgRp%IX0kDcQ@K%9t)G!t1XLnap|yO>>DIckU?NB_vIXvNZu3T< zq7XCPvYTqXh8f%Y5<7-@I_)HoEx4ATPNX7VULjzqGi|W2Ff*ko>0c0#MD*i6)Z~{i z-flfYhT_qd9nm`Z7h3o_*)rV(GGD@5`t@tkE?w9$q1*k!LqOr8#KV45=eJ@wNSb3E z0wlI5lOam~HVEO~5(Zg?o1R87Y@gB%8q|si_D&~G)tt0)%q7him{NwC7OUe9(GF(J zqEv<)m6zX>AcEQ_h<8t@5){gsu!e>7$FJFAA@4}GoKOru$dnz!{#^8{CRZC>g_! zOmrQ6*>42AaF3vzg_^NP`6~I_&}af_2E46-OH3y-plKnDXlQ@pUQ3`87SMXo2Fvxe_2QyLt?(qb_@ zXNawF*ctad`US7-Ssf>ZiXapkJ+HF?&fsd~xI#4K;#5WP5;LAvJ3yazWSQkM(^oHI ztaS6SZr?9m^Hh5p+bhyFOknsr1K>U}PscAC6wPQ~Mk0E^@?XnAWS9-yqcqc<8sS^d3>VB!i#54S>`Ju=nAEt zLz~cc*?H!rXz5p{>yc6#3-zz83->(tE8iKu=Zmb?Sz|EXggiWAN5^ftj}q;+9_EWm z{<1&zcG}BCRi+tRJcRncvMWiq09d7ax%tEV&z#-N51w_lzJIpxd4!U*2)L#Xkfvb+ zVA(v$W)X%w5lAjpGfU-u=$vd=h&I>*nZoSXxWswF&gYH7aH#M8At^og5=@Q>qT9G_ zM@O4gMu_Q#t9^UsWL4?dS!t5K2V>0UvgMseR%fnD1kOGmw{r0Sq2kAIRh)+BTQzWF*_aOEfSP{$t^r6J#+XntJ|C@ zCE6#3(AQ!OnI~<;9CUCndtSRfn{owWyvcl#UHaB%VOk_!79uYIaEDB>q90LrMLYFr zDxH<3@jZgr^ICGe?UfYPzLrHi=H>S>C9c2NWqD^=mnP&ION=BboX?U~!Lv4?fz=IJX;e;G=*hg9|7xnRi8ue6={iOfdB_C~5vRLi9f~R#KX(^5GXlCo1Nba%MVM2Zo9s)zem-y*Y>cR| zc-NygrZqbNeRN17<_*jI(H*Z@iJa)>#|8wlrN!Y(5F^0SPVuRP9^S~A9C_vF-^NWM zW$B{5qRSVXI^+ql=^Z5A{r|gT4l;WS>JVUHAejHbdLo`8;M|yS87~ML8#+yn{0Lf( zGiw?t&5#ubE4UO~I*u_B8hksWa`5UK_0*Mvl@*zdl3#Cm_d3JMicw(&mt&D3X_B$N z$fHDW>tE6L2EdORt+(uT!?nKBu%Msr$3oY?ed|s>ejh%6!S3;5V|Dqx-u-Mj2`%yq zP%lRBe=-SR1mGf%)L3k=dal;1Cl#ln7HH0ghi_G|18FEAX!wB2@5^D zl~q@fL9CF7EJ7h%+#IuS#X1^gFO>`7D3FPlO{!i4$~V=x~bHV4vS3R>a>L!lz>m)jfK_@RpxW~rTe*o6T#EF!(&4Jjj&M$ZgJgWnZcpW zN*2l{l4Hd~R^?j;+DOgFS!i_pM2io(oJV*|*~xT8`qezy^7ylSzb(07Q5NqC4oZiB z=k)2V7EpV=KC)$tuf1n4xXf8yfV}yk+F;nff*w?{R*~PuHoCSaBjjb<2GhJ;-bak2 zRi4)Q&iSH7;r$H>pL^U;b4iL$B>UwIBs>|S;<+-Z zBS2q6TAUKxJdJ3~+jyw>O0?D$A_9d!Lch`_nT^kds<D1eH#4Zi5ImkyB z_k=XiD#r=vrtd7ezVe`%4ONXp7FZ~4l7-124`=iGeDrL?fP*WW`C8$w{Np`d;XZ=;`^l{oaf#!8 z{fwJ@cJmbyJlx6TOSH#z)kRT&D1bA-a3S-iFhq%g+(PHJkugcKaMB_kA3p~3Nw-u^ z^X?5K^r93{1-;wX$_FAkd=bm@L=B(zdR)2}3W_p(W~+Oin< z_4Ye|`e62&UpOW;mEz8L9~nMy?>y3rCZj`yA6dG?<$zAQsse-pO4yUKRc__dE)s?5 zNwW1V0w1lDiw2%(%YTO#1MV&j`iV&3x0WZYLt5{W_G$vieHijEiD@VzYd&_4cGcu{ zT!b*Ab3}}Leh|f!Cp;o``?H zm?Xbrj_(ORoW8fol5(U?D@peCh@U!4B8WMSPqKHeyGHTh#AZT40;~wF1YoEz=U=N( z%|OFjFBt*#KeP)kc44nZ{I^#7WgufQW;Ih5Wk_6eMxq33O&2h0VPae__h!5AaWlhP zBKLJ|zBLe#!8xu&hJ)vR5j`mAsruIcJQDiYJdt`OoH=szxKrF%({qq`Gz_i#cx#l_ z3aJg0ylr}uW3;JB1zbmEmo{Rn(|@DVuC}$oy9AMq(CB&h)QL-%8ADfo{1h{W`H6Gu zQN7*z!@4nYbc5k5HWS)dVpwoQCewUh)QcfG&mfq-uv$N~LNWR3n! z@9#(@XO$YFX$tW|v4k&2z>Py!QbdjA!1eWB-SJF)^0E~`H$Mt}!3!veni;|g0uxEO zZSsIdf)mer*vkEyz0S_{znV4P1%T-S$8SuXucZ!ib`DnJ6I*%;^&PuQ^@Ddkg@`+Y zIY0Zbk5IyCd?-)NBLFg`%V;agOZOCkCDYxqOAoc?Z(a5sa1xQjn+jcG8})3F&d-7g zglZ94t>{r626U3A@)2E~m>Uz-FT!@EJ{zWIPk}t`vg28ed z0pT;uMOf3|HPSXUW+}N^ep%AU5*9KHb?&<^m2X%Wc?L-aAdhtRc}6+(Bz2~oGxKBf ziK=-Hm9!EEJ89}%GPC##-QNh*Z*6)tkFJ**BiVxi)LH*#T1oB8L0s91@{c6S3q|o_ zo%hrROp&lH=l>ecj;uyO;h&IC2kl7{ffDhkKz$U{0F85H3^B|vAh?18+Jus*rh%CL zMu|a*L6~L;5K;j`G6XQ@A>e@H)oS4tgGzyN!0{r1lrD)u(ig#=>nJu!b>`H_@t$)X zaKdgK^!N1^p^p+9PdG2Tx=gr|ul5>&rf#P4tu|Bju`tj?Z;K-)-U{bF#D$Kr-%xA> z+?%T#K>85aYSH=Ug1OZoNfiZg`WPnj%pR6an5$o7D7Av&?{=~!+8%Zr_pt`yphs9@ zsX3}2ZLvZFtV*0>Y;jW;ucioBFk2XRG*v~+YAF?NY0QEwe)0)Z(h?I#yJnN))sc=I z-DC;F(cVUIp8Fd_dioe}RHz_7mrQN(HB*KHAo1%fZy>*R2bFg5UW?rwG0v#p14icB zWEcMA*NM{T12*oCr2V3OL_#+LMc6SZzDq)ZaK{0JX;XMUDY`U9NKZv*!SwMcpIEqj z-2yAH_Vf?2$dO!(fVA7-{j4SPQ?6JPa|fFvpizK^s4~;s;QZI}7-HC|%l}$l9_oK0=a^a< zU`*w>c&Enu*m#hb?}Uw>sLdoEA|ENQrjND+(K>!*akz4BxV}@aE(L)78=_G{{POLi zJlYl3&KZ{EW8`{TaCWM0Gb<1520s_>CCKxUuLqGjBO9a~+D-Zsx>HR3s6_MBqF}H~ zHp*8bdJa8?qsf%L=x9s39qFmGobeJGV2ORK3taF=McP4RJ2t@hTa%|P&t__k+jB?9 z56T>q&0!OuF@I$_KK!aDiJ+Sfb*>_XY#NoaM1P~jg2Tn6j$se2D5O2&1kaO3rSYzcJX2T?!*t=l9q?Z9{G}?z^Y!B4R@t zZV&KRf_wreC(jjpBU~-%oPECc(Jhw1j9zR6E9o+3

Igj3NXa_|8<`c7r$X{#UWi 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[sex+d!zoXF=a?l-EdR8AUn8#-&A&o?v{ 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

yb%-OYcg6{P)b3;TV;9l88> zH7}mIwVJI>bGwUSXd1>bE7+mmCfKsNAfs{*q1f=$vJWmoDyVo4@x5=l%f2_Eq_{ z{BA6Js?P81xnJ+Xu|K?JgY)GVk*>Fsr|fExbxKT|W$*%csmL8}?)re2w_jwsE`7CN zdPumP^?|RoN!Cfca|6w7KV${y>(%{QC4POM?n_UHOT0=##V`Jg9k>(0B!2eaU70Mt zR_Bj~Kk^qU%DtC5{3lRH=IEn9ovj}pRK|3*D1BUMf2j5N#|^E=>ixJAf5l`cF5W+t zE&7nVj!oLav-`W<{Ked=|I9Uc#VNmXqrm11&+BKkv2%B~|2=<6BT!*VkaYd(wl9gW@ou&?~8$?-q>F2_DbRq&+0i&=i9=| zdF(fiTw-3{@Wxg4o$^k@66Wr|;<`1Z;`=82S1JzNrmR#H6|rl1Sd0Ao6(8e!b~+jU zH$OY&u!P=SMU}(zzr0t~{`$rC{Gh@7qW+yD1 z>Cus2cGvZ8(shp?dRd2+PwOw3=9{*7@~X_qg7bXZJaR2t55Ktr99pgi)i>(|ZaoMF zUV(X(XY%^>k({|3?Q9)@3!d#K3vO_d@z#A`qXg_^6#%gz1MmbGIM~fMS#XogMhG zY)}+k69p@36H}S&uvtI``QA=j6oqAyK!p)9@Jkkf)}fkoNC9k;tPyT2 zH7bK8`Zu#pR@|&F1HK6j=5)0^cRA1fKfSYEeB?@*l_yQ)N ziRXYO8ljjNrVUoSUq=e85Pl62(DC*t3e$AK3NP!agB2pK_dpMMe*>UGCE)c%{9uKM zv-;5$#+puU-0CO;I$}N;=(aULtb=0ZS4*G*rAgaVW#DISV?^O<>&cGW{AA!qHv>Ir zhhm7B{p9ZLkuu;zgn>550Tt+@DDZWhyl|)JWYHa_GVtS4fz~*pC~|TJD~j96F}Zn% zy$s@zNCpNI6orr7;0k~5Fp>eCUj#Y6h=D;7MPaTNRH3>I{2U#i`%t4&I|Qsq!C4a= zmhfX&fS%ArF-;^4sL(?Ov9W-GK?_AeU<6Qs^kjuy+>sXmHasH+)(LC#1^~+; yR9`KhG /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/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java index bb77ebe..8d4f74d 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/client/HotbaaaarClient.java @@ -5,7 +5,7 @@ import net.minecraft.util.Mth; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.ContainerInput; /** * Client-only state and logic for the "super long hotbar". @@ -168,6 +168,7 @@ private static boolean swapWithHotbarTracked(int p) { /** * 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(); @@ -203,10 +204,7 @@ private static void setSelected(Inventory inv, int slot) { /** @return true if the row was actually flipped. */ private static boolean flipRow(int delta) { - int target = activeLogicalRow + delta; - if (target < 0 || target >= getRows()) { - return false; - } + int target = Math.floorMod(activeLogicalRow + delta, getRows()); return activateLogicalRow(target); } @@ -250,7 +248,7 @@ private static boolean swapPhysicalRowWithHotbar(int physical) { 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.handleInventoryMouseClick(0, menuSlot, col, ClickType.SWAP, player); + 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 80aaec7..5ecdcd0 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/GuiMixin.java @@ -5,8 +5,8 @@ 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.GuiGraphics; +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; @@ -21,13 +21,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** - * Renders the extended hotbar on the 1.21.x sprite API: up to four 9-slot rows as one wide strip, + * 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 @@ -35,13 +35,17 @@ public abstract class GuiMixin { private Minecraft minecraft; @Shadow - protected abstract Player getCameraPlayer(); + private Player getCameraPlayer() { + throw new AssertionError(); + } @Shadow - protected abstract void renderSlot(GuiGraphics guiGraphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack stack, int seed); + private void extractSlot(GuiGraphicsExtractor guiGraphics, int x, int y, DeltaTracker deltaTracker, Player player, ItemStack stack, int seed) { + throw new AssertionError(); + } - @Inject(method = "renderItemHotbar", at = @At("HEAD"), cancellable = true) - private void hotbaaaar$renderHotbar(GuiGraphics guiGraphics, DeltaTracker deltaTracker, CallbackInfo ci) { + @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) { return; @@ -88,16 +92,16 @@ public abstract class GuiMixin { int physicalSlot = HotbaaaarClient.physicalRowOfLogical(logicalRow) * 9 + col; int x = x0 + i * 20 + 3 + (i / 9 * 2); int y = screenHeight - 16 - 3; - this.renderSlot(guiGraphics, x, y, deltaTracker, player, inv.getItem(physicalSlot), seed++); + this.extractSlot(guiGraphics, x, y, deltaTracker, player, inv.getItem(physicalSlot), seed++); } // offhand item if (!offhand.isEmpty()) { int y = screenHeight - 16 - 3; if (offhandArm == HumanoidArm.LEFT) { - this.renderSlot(guiGraphics, x0 - 26, y, deltaTracker, player, offhand, seed++); + this.extractSlot(guiGraphics, x0 - 26, y, deltaTracker, player, offhand, seed++); } else { - this.renderSlot(guiGraphics, x1 + 10, y, deltaTracker, player, offhand, seed++); + this.extractSlot(guiGraphics, x1 + 10, y, deltaTracker, player, offhand, seed++); } } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java index 3699044..1ff8ccb 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/InventoryMixin.java @@ -28,7 +28,7 @@ public class InventoryMixin { ) private void hotbaaaar$onScroll(long window, double xOffset, double yOffset, CallbackInfo ci) { Minecraft mc = Minecraft.getInstance(); - if (mc.player != null && !mc.player.isSpectator() && mc.screen == null && yOffset != 0) { + if (mc.player != null && !mc.player.isSpectator() && mc.gui.screen() == null && yOffset != 0) { HotbaaaarClient.onScroll(yOffset); ci.cancel(); } diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java index 1dbd6e8..f0731ee 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/mixin/MinecraftMixin.java @@ -1,7 +1,7 @@ package cn.ussshenzhou.hotbaaaar.mixin; import cn.ussshenzhou.hotbaaaar.client.HotbaaaarClient; -import net.minecraft.client.Minecraft; +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; @@ -22,7 +22,7 @@ * * @author USS_Shenzhou */ -@Mixin(Minecraft.class) +@Mixin(Gui.class) public class MinecraftMixin { @Shadow diff --git a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java index 7a542ab..f1ee7a5 100644 --- a/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java +++ b/src/main/java/cn/ussshenzhou/hotbaaaar/util/Util.java @@ -3,7 +3,7 @@ import net.minecraft.resources.Identifier; /** - * 1.21.x GUI hotbar sprites and geometry constants. 1.20.5+ renders the HUD from the sprite atlas, + * 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 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index c465615..1a297c5 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -16,7 +16,7 @@ ], "depends": { "fabricloader": ">=0.19.0", - "minecraft": "~1.21.11", - "java": ">=21" + "minecraft": "~26.2", + "java": ">=25" } } diff --git a/src/main/resources/hotbaaaar.mixins.json b/src/main/resources/hotbaaaar.mixins.json index 2b30ff6..eeb7389 100644 --- a/src/main/resources/hotbaaaar.mixins.json +++ b/src/main/resources/hotbaaaar.mixins.json @@ -2,8 +2,7 @@ "required": true, "minVersion": "0.8", "package": "cn.ussshenzhou.hotbaaaar.mixin", - "compatibilityLevel": "JAVA_21", - "refmap": "hotbaaaar.refmap.json", + "compatibilityLevel": "JAVA_25", "client": [ "GuiMixin", "InventoryMixin", diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta index 77bed85..347bbae 100644 --- a/src/main/resources/pack.mcmeta +++ b/src/main/resources/pack.mcmeta @@ -1,6 +1,6 @@ { "pack": { - "description": "${mod_name} resources", - "pack_format": 34 + "description": "HotBaaaar resources", + "pack_format": 88 } }