feat: NeoForge support#8480
Conversation
|
Heads up: this is still a draft. Some functionality is still being tested, and it depends on CCBlueX/mcef#24 (the |
…code Moves Fabric-specific code (ModMenu integration, FabricCreativeModeTab, FabricLoader access) behind a ServiceLoader-discovered Platform interface implemented in the new src/fabric source set. The loader-agnostic sources in src/main are kept honest by a new checkLoaderPurity verification task.
Adds a :neoforge subproject built with ModDevGradle that compiles the shared loader-agnostic sources in src/main against NeoForge, registers its own Platform implementation and bundles the Kotlin runtime libraries that fabric-language-kotlin provides on Fabric. The access widener remains the single source of truth for member access: ConvertAccessWidenerTask now also propagates widened methods to their overrides, which is required because access transformers are applied to sources, where javac rejects overrides with weaker access than their super method.
Mixins whose vanilla injection points are patched away by NeoForge move into per-loader companion mixin configs (liquidbounce-fabric.mixins.json, liquidbounce-neoforge.mixins.json), with shared handler logic exposed through duck interfaces where needed. Affected hooks: LocalPlayer#aiStep mayfly read, Player#doSweepAttack and Player#getDestroySpeed (patched into added overloads), Gui#extractHotbarAndDecorations (split into GUI layers) and the screen extraction in GameRenderer#extractGui. NeoForge runs the registry bootstrap thread before the Minecraft instance exists, which the initialization path now tolerates: the config system resolves the game directory through the new Platform#gameDirectory instead of the Minecraft instance, the Minecraft coroutine dispatcher resolves the instance on first dispatch instead of class initialization, and the HTTP client no longer requires the MCEF class to be present. Client resource reload listeners register through the Platform; NeoForge collects them via AddClientReloadListenersEvent, falling back to the direct reload when the event has already fired. Creative tabs are built with a free top-row column on NeoForge to pass the position validation, and the Kotlin runtime is available in NeoForge dev runs.
Nests the mcef-neoforge artifact the same way the Fabric jar includes mcef, making the JCEF-based UI fully functional on NeoForge. The MCEF class captures the Minecraft instance when it is loaded, so the HTTP client handoff moves out of the HttpClient class initializer (which NeoForge can run from the registry bootstrap thread, before the instance exists) to the point where the browser backend initializes.
The NeoForge dev runtime now loads the NeoForge variants of the same sodium, lithium, iris and ImmediatelyFast versions the Fabric dev runtime uses. DiscordIPC's transitive Reflect dependency is constrained to 1.6.2: the dev classpath copy wins over the 1.6.2 that ImmediatelyFast bundles, and 1.6.1 does not support Java 25, crashing the client during startup. Resource reload listeners now take part in manual reloads (F3+T): AddClientReloadListenersEvent registers a lazy wrapper per known listener id, and the platform binds the actual listener as the wrapper's delegate once the client registers it during startup. The previous buffering approach could not register anything because the event fires before the client start hook. The access widener conversion handles records: component fields only exist in class files, so their access transformers never applied to the recompiled sources and are dropped, and a widened record class also widens its canonical constructor, which must not keep a weaker access level than the class. Access transformer validation is enabled to catch such entries from future rebases. git.properties is generated for the NeoForge module, so GitInfo reports the real version and commit instead of "unofficial"/"unknown" in the window title, logs and config metadata.
Resource reload listeners register through a single all-or-nothing call, with the listener ids shared between the call site and the NeoForge wrapper registration, so a partially registered state cannot exist and new listeners cannot silently miss the NeoForge list. The unused Platform#loaderName is removed and the service loading rejects multiple implementations. The NeoForge manifest accepts only the exact Minecraft version, like the Fabric manifest: the mixins do not survive game updates. Jar deletion uses Files.deleteIfExists, which actually reports failures, and building the creative tab no longer assumes that a top-row tab exists. The NeoForge module is now covered by detekt and writes git.properties to a dedicated directory: generating into the resources output overlaps with processResources' outputs, which Gradle cleans up as stale when tasks re-execute. The access widener conversion is covered by tests.
…ersion NeoForge releases lag new Minecraft versions, so right after a Minecraft update the shared sources are not compilable for NeoForge. Disabling the module's tasks keeps the build green during that window; NeoForge distributions are built from the last commit where the versions still aligned.
NeoForge pre-releases carry dotted suffixes (e.g. 26.1.0.0-alpha.1), which the previous parse turned into 26.1.0.0-alpha and misread as a version mismatch. Taking the first three components handles both shapes.
…dule Mirrors the root project, which removed -Xexplicit-backing-fields and -Xcontext-parameters with the update to Kotlin 2.4.
Upstream nextgen advanced to Minecraft 26.2, so bump the NeoForge subproject to the matching NeoForge 26.2 beta and the 26.2 dev-runtime compat mods (Sodium, Lithium, ImmediatelyFast). Iris has no NeoForge build for 26.2 yet, so it is dropped from the NeoForge dev runtime until it is published; the Fabric side keeps it. The mcef dependency follows upstream's 3.3.2-26.2 snapshot (mcef-neoforge published from the matching mcef branch).
… changes Minecraft 26.2 reshaped several vanilla targets, and NeoForge patches some of them into different shapes than Fabric, so the loader-specific companions are re-derived against the 26.2 NeoForge-patched jar: - Gui -> Hud split: the HUD hooks (overlay start, exp-level tweak) moved to the new Hud class. The shared mixin is renamed accordingly and the companions (MixinGui -> MixinHud) now target Hud; NeoForge still splits the hotbar and experience extraction, so it keeps its own anchors. - Screen render: upstream now fires ScreenRenderEvent from Gui#extractRenderState. NeoForge routes that screen extraction through ClientHooks#extractScreen, so the hook is split out of the shared client MixinGui into per-loader companions (Fabric anchors the vanilla Screen#extractRenderStateWithTooltipAndSubtitles call, NeoForge the ClientHooks#extractScreen call). This replaces the old GameRenderer#extractGui companions, which upstream removed. - Slime bounce: NeoForge moves Entity#restituteMovementAfterCollisions into an added BlockPos overload, so the Math#max hook is split out of the shared MixinEntity into per-loader companions. Both loaders boot to the LiquidBounce UI on Minecraft 26.2 with no injection errors; an offline descriptor-exact scan of the shared mixins against the 26.2 NeoForge-patched jar reports only the known lambda false positive.
86f8aec to
3cc26a8
Compare
…r change NeoForge inserts a self-position BlockPos into the ModelBlockRenderer shouldRenderFace overload that the block tesselation path calls, so the shared @ModifyReturnValue captured-argument signature no longer matches on NeoForge (it crashes when the first chunk renders). Move injectXRayDrawSide out of the shared mixin into per-loader companions: Fabric keeps the vanilla 4-argument overload, NeoForge targets the 5-argument overload explicitly. Verified in-world on both loaders (chunks render, no InvalidInjectionException).
A per-mixin audit against the NeoForge-patched 26.2 jar (which is stripped of local-variable names, unlike the Fabric mojmap jar) found three shared hooks that misbehave on NeoForge: - MixinFireworkRocketEntity#hookExtendedFirework captured two Vec3 locals by name; with names stripped and multiple Vec3 candidates in scope the @Local is ambiguous on NeoForge (throws when a firework is ticked). Switch to ordinal capture (0 = lookAngle, 1 = movement), which is stable across both loaders. - MixinLivingEntity#hookEquipmentChange and MixinBlock#injectXRay used bare method selectors that also match NeoForge-added overloads (setItemSlot(..., boolean) and shouldRenderFace(BlockGetter,BlockPos,...)), double-firing the equipment event / double-applying the XRay result. Qualify both selectors to the vanilla descriptor so they bind exactly one overload on each loader. Verified: both loaders still boot and enter a world with zero injection errors; the rest of the 156-mixin audit came back clean.
…hed jar Adds a MixinDivergenceCheckTask (buildSrc, ASM) that verifies every shared mixin injector against the NeoForge-patched Minecraft jar at build time, so the class of bug where NeoForge reshapes a vanilla method and an injector silently breaks (only crashing in-world) is caught in CI instead. Wired into :neoforge:check. It reports BLOCKERs (selector resolves to no method; an @at INVOKE/FIELD/NEW ref is absent from every matched overload; or an @Inject/@ModifyReturnValue/ @ModifyExpressionValue handler's captured args are not a valid prefix/suffix of a matched overload — the exact InvalidInjectionException trigger) and advisory SUSPECTs (bare selectors that match a NeoForge-added overload; name-based @Local that cannot bind by name on the LVT-stripped patched jar). BLOCKERs fail the build; SUSPECTs are logged unless -Pstrict. Includes buildSrc unit tests for the prefix/suffix compatibility detection.
@Coerce Addresses defects found in adversarial review of the previous commit: - The @Local(name=...) rule was built on a false premise ("the patched jar strips local-variable names"). The NeoForge-patched 26.2 jar actually retains LocalVariableTables with names, so the rule flagged 44 false suspects. The check now reads the target method's real LVT and only flags a name-only @Local when the name is genuinely absent (or the type fallback is ambiguous). Suspect count drops from 48 to 3 (all legitimate multi-overload advisories). - @Coerce params in the middle of a captured-arg sequence were dropped instead of kept as positional wildcards, which could emit a spurious BLOCKER on a valid injector once someone uses interior @Coerce. They are now treated as wildcards that match any target type at their position. - Added an @OutputFile report so the task is cacheable / UP-TO-DATE instead of re-running every build. Positive control still fails the build on the real ModelBlockRenderer suffix mismatch; buildSrc tests cover the @Coerce case.
…replaces them
On Fabric the status-bar HUD component cancels Hud#extractPlayerHealth (via the
shared mixin) so the vanilla health/hunger/armor/air are hidden and only the
client's styled bars render. On NeoForge that aggregator is never called — the
status bars are registered as individual GUI layers that invoke
extract{Health,Armor,Food,Air}Level directly — so the vanilla hearts and hunger
were still drawn underneath the client's bars. Cancel those four layer methods
in the NeoForge companion under the same DISABLE_STATUS_BAR tweak. Verified
in-world: NeoForge now renders only the styled bars, matching Fabric.
950510b to
468ab3e
Compare
|
Rebased onto lb-sidebyside.mp4Known limitations, all from 26.2 being new — none affect the Fabric build:
|
Trying it locallyThe only piece not yet on # 1. Publish the missing dep (mcef-neoforge 26.2)
git clone -b neoforge-26.2 https://github.com/obus-globus/mcef.git
cd mcef && ./gradlew :neoforge:publishToMavenLocal && cd ..
# 2. Clone this branch and run
git clone -b feat/neoforge-support https://github.com/obus-globus/LiquidBounce.git
cd LiquidBounce
./gradlew :neoforge:runClient # NeoForge
./gradlew :runClient # Fabric (step 1 not required)Needs JDK 21+. Step 1 drops |
Adds a NeoForge distribution as an additive
neoforge/Gradle module. The root project stays the Fabric project with the same build and dev workflow; all client code stays insrc/main, nothing is duplicated per loader../gradlew :neoforge:buildproduces the jar;:neoforge:runClientboots a dev client with the full browser UI.How it works
src/fabric(appended to the root main source set, compiled as before). Theneoforge/module includessrc/maininto its own source set.Platforminterface (ServiceLoader) abstracts the loader;checkLoaderPuritykeeps loader imports out ofsrc/main.liquidbounce.mixins.jsonis used unchanged. A handful of injectors target vanilla methods that NeoForge's patches reshape (renamed fields, added overloads, split or re-signatured methods); those live in per-loader companion configs that delegate to shared handlers.validateAccessTransformerson).mcef-neoforge, plus everything the Fabric jar nests.Fabric impact
The Fabric jar's file listing only gains the companion mixin config and classes, the
Platform/FabricPlatformtypes, two duck interfaces and a lazy dispatcher class (and loses theHideAppearanceKtfacade).ConfigSystem.rootFolderand two class-init points now resolve through the loader instead of theMinecraftinstance, because NeoForge runs the registry bootstrap before the client exists; the resulting paths/behavior are unchanged in production.Notes
FabricCreativeModeTabequivalent); everything else is at parity.mcef-neoforgeneeds a published artifact (feat: NeoForge artifact mcef#24; currently mavenLocal); a NeoForge CI job follows once it is published.Tested on both loaders: clean build, dev client boots, no mixin errors; buildSrc has tests for the access widener conversion.