Changelog

All user-facing changes to Retromod. The format is loosely based on Keep a Changelog. Versions are semver. The 1.0.0 line ran 1.0.0-beta.N1.0.0-rc.N → stable 1.0.0; from 1.1.0 on, minor/major releases use snapshot.Nrc.N → stable (patch releases ship directly).

[1.3.0-snapshot.2] - 2026-07-14

Second snapshot of the 1.3.0 line. Continues the mixin-translation theme into 26.x worldgen: one more real mixin translation (YUNG’s NoiseChunkMixin), a broad Registry value-getter rename fix, a trailing-parameter re-signature, and the new predictive mixin-discovery tooling. Two worldgen limitations found by a headless-server pass are documented rather than shipped.

Added

  • Removed-@Shadow-field demotion, and YUNG’s API NoiseChunkMixin translated with it (worldgen). New MixinShadowFieldDemotion: when a mixin @Shadow @Finals a vanilla field the host deleted, but the field’s value is still reachable as a constructor parameter the mixin’s own @Inject handler captures, the field is demoted to a plain @Unique mixin field and the handler is prepended with this.<field> = <capturedParam>. NoiseChunk.noiseSettings was deleted in the 1.21.5 worldgen refactor (verified absent on 26.x, present on 1.21.1) while the NoiseSettings constructor argument is unchanged, so YUNG’s NoiseChunkMixin (whose aquifer-override reads noiseSettings.height()/minY()) is repaired instead of stripped. Gated to 1.21.5+ hosts; the parameter is located by type-matching the field descriptor, so it survives a mod rebuild. Verified on a NeoForge 26.2 dedicated server: yungsapi loads and 49 force-loaded chunks generate cleanly. Retires the NoiseChunkMixin blocklist strip. Tested (MixinShadowFieldDemotionTest, incl. a byte-for-byte check against the shipped YungsApi 5.1.6 jar).
  • Registry value getter get(Identifier) -> getValue(Identifier) (26.1 rename). The ResourceLocation -> Identifier rename had a companion: the registry VALUE getter Registry.get(Identifier) (returns the value T) became getValue(Identifier), while get(Identifier) now returns Optional<Holder.Reference> (a different method). A 1.21.1 mod that calls the old value getter (e.g. BuiltInRegistries.SOUND_EVENT.get(id)) linked against the vanished get(Identifier)Object and died at construct time with NoSuchMethodError: DefaultedRegistry.get(Identifier) (YUNG’s Better Strongholds, verified) - and the fuzzy resolver couldn’t help because get(Identifier) still “resolves” by name+params. A descriptor-scoped method redirect (registered for Registry/DefaultedRegistry/MappedRegistry/DefaultedMappedRegistry, keyed on the post-remap Identifier value-getter descriptor so the Optional-returning get is untouched) renames just that overload to getValue, on all three loaders. Unblocks YUNG’s structure mods (and any 1.21.x mod using the value getter) past construction on 26.x. Tested (RegistryValueGetterRenameTest).
  • @Inject TRAILING-parameter re-signature (MixinHandlerResignature). The #69 engine inserted a LEADING param (the ServerLevel prepend); it now also handles a param appended at the END of a target’s capture list, right before the CallbackInfo trailer (shifting only the CallbackInfo slot). 26.1 appended a ResourceKey<Level> (the dimension key) to ChunkGenerator.tryGenerateStructure, so a 1.21.1 @Inject that captured the old 9 params was missing the trailing arg and died InvalidInjectionException. Registered for tryGenerateStructure with a StructureSet$StructureSelectionEntry first-param guard (YUNG’s Better Strongholds DisableVanillaStrongholdsMixin, verified applied on 26.2). Tested (MixinHandlerResignatureTest).
  • Mixin discovery tooling (mixin-scan CLI + mixin-rank/mixin-crossjoin/mixin-refmap-harvest scripts). Enumerate every @Mixin injector across a mod corpus, rank targets by corpus frequency (translate the highest-leverage first), and cross a scan against a real MC version diff to predict class-level breaks before a mod crashes. Documented in scripts/mixin-discovery.md and CLAUDE.md. Flips broken-mixin discovery from reactive (scroll a crash log) to predictive.
  • Corpus-mined 26.x vanilla method renames (top-40 NeoForge 1.21.1 audit). A member-level link-check of the 40 most-downloaded NeoForge 1.21.1 mods (transformed to 26.2, scored against the real 26.2 jar) surfaced several common unresolved calls Retromod didn’t yet rewrite; each added redirect is owner+descriptor-scoped so a generic name only touches the one overload: Minecraft.getTimer() -> getDeltaTracker() (11 mods), Camera.getPosition() -> position() (9), GameRenderer.getMainCamera() -> mainCamera() (10, a 26.2 rename), and the JOML const-interface widening VertexConsumer/BufferBuilder.addVertex(Matrix4f,FFF) -> the Matrix4fc param (12 + 7; the concrete Matrix4f is-a Matrix4fc, so only the descriptor changes). Tested (Corpus26xRenameRedirectTest).
  • score <dir> --json corpus mode. The score command now accepts a directory and dumps each jar’s residual missing classes/methods/fields (resolved against a chosen --mc-jar) as JSON in one JVM, so a whole mod corpus can be transformed then ranked by highest-leverage break. This is the audit tool the renames above came out of.
  • Corpus-mined 26.x descriptor adaptations (top-50 Fabric+NeoForge 1.21.1 audit). A deeper link-check (50 Fabric + 40 NeoForge mods transformed to 26.2, scored against the real 26.2 jar) surfaced a tier of breaks where the target method still exists on 26.1 but a primitive TYPE or its STATIC form changed, so a plain rename can’t fix them: the descriptor has to be adapted at the call site. Two small, owner+descriptor-scoped mechanisms handle them, targeting real vanilla methods (no reflection, no embedded helper, cross-loader), each verified against the 26.1 AND 26.2 jars (old signature gone, new present) and by a re-audit (each target 24/24/16/16/15/12→0 residual mods, 11 jars improved, 0 regressed, 0 new breaks): registerConvertingRedirect (retarget + one primitive conversion on the last arg and/or the return, or a POP) covers Mth.cos(F)F/sin(F)F widened to (D)F (F2D the arg; 16+12 mods), Window.getGuiScale()D narrowed to ()I (I2D the result; 16 mods) and SoundManager.play(SoundInstance)V now returning a SoundEngine$PlayResult (POP the result; 24 mods); registerSingletonStaticRedirect re-expresses a no-arg static helper that became an instance method, covering Screen.hasControlDown()/hasShiftDown()/hasAltDown() -> Minecraft.getInstance().hasX() (24+24+12 mods); and the existing arg-drop redirect covers CompoundTag.getList(String,int) -> getListOrEmpty(String) (drop the type-hint int; 15 mods). Structural residuals from the same audit (ClickEvent/HoverEvent now interfaces, InputConstants input-handle redesign, the removed GlStateManager blend-factor enums, Minecraft.screen) are left for later. Tested (Corpus26xDescriptorAdaptationTest).
  • NBT read-API adaptations (1.21.5 refactor; found via a top-60 Fabric 1.20.1 audit, benefits 1.20.1 AND 1.21.1 mods). A separate 1.20.1 -> 26.2 corpus pass surfaced a broad save/load break: the 1.21.5 NBT refactor changed the common accessors, so a mod’s serialization code links against signatures that are gone. Each fix is owner+descriptor-scoped (the co-existing new Optional-returning overloads are untouched) and verified against the 26.1 AND 26.2 jars (old gone, new present), by re-audit (each target 12/10/9/9/9 -> 0 residual mods, 5 jars improved, 0 regressed, 0 new breaks) and by BasicVerifier (0 new verify errors on the NBT-heavy jars): CompoundTag.contains(String,int) -> contains(String) (drop the tag-type-hint int), CompoundTag.getCompound(String) and ListTag.getCompound(int) -> getCompoundOrEmpty(...) (the plain getters now return Optional), CompoundTag.remove(String)V -> remove(String)Tag (POP the now-returned removed tag), and TagParser.parseTag(String) -> parseCompoundFully(String). Tested (Corpus26xDescriptorAdaptationTest). (The same audit confirmed the 1.20.1 residuals are otherwise dominated by the multi-version render-pipeline redesign - BufferBuilder/Tesselator/RenderSystem/GlStateManager/GuiGraphics drawing - which is structural, and that 1.20.1 intermediary “skew” is NOT mapping-fixable: the dangling ids resolve to removed/renamed APIs, so the version-skew composer yielded no clean additions there, unlike the 1.21.1 FastColor case.)
  • ClickEvent/HoverEvent constructor bridges (1.21.5 text-component rework; 14+8 mods across the audits). Both became sealed INTERFACES with per-action record subtypes (ClickEvent.RunCommand/OpenUrl/ChangePage/…, HoverEvent.ShowText/…), so the old new ClickEvent(Action,String) / new HoverEvent(Action,Object) constructors are gone and a class-move can’t express the typed values. A constructor-to-factory redirect rewrites those news to com.retromod.polyfill.minecraft.RetroTextEvents, a reflective (Minecraft-free), fail-safe factory that dispatches on the action enum to the right subtype (open-url -> OpenUrl(URI), run/suggest-command + copy -> the String subtypes, change-page -> ChangePage(int), open-file -> OpenFile(File); an unmappable action or bad value goes inert as null). This is the exact dispatch the old constructor did internally, so it is correct by construction. The factory is registered as a synthetic so the Forge/NeoForge per-mod embedder relocates a JPMS-split-package-safe copy (Fabric injects it directly); the redirect appends a CHECKCAST since the reflective factory returns Object. Verified by re-audit (ctor residuals 14 -> 0 and 8 -> 0, 2 jars improved) and BasicVerifier (0 new verify errors on the affected jars). Gated 26.1+ (interfaces on every such host, so the old ctor is genuinely gone). Tested (Corpus26xDescriptorAdaptationTest: the ctor-to-factory rewrite + the factory’s fail-safe). Runtime click/hover behaviour (that the reflective subtype construction fires) still wants an in-game check.
  • Offline (batch/AOT) path now remaps access wideners and mixin refmaps to the official namespace (found by an in-game 26.2 Fabric launch). A live launch surfaced two crashes the headless member-linkcheck can’t see, because they fire during Fabric’s loadClassTweakers / mixin bootstrap - BEFORE any member resolution, so there is no crash-report and latest.log stops at the mod list. The RUNTIME (FabricModTransformer) path already remapped these; the offline batch/transform/AOT path (RetromodCli) did NOT, a parity gap: (1) a mod’s access widener stays accessWidener v1 intermediary, and 26.1+ runs the official namespace, so Fabric’s classTweaker reader throws ClassTweakerFormatException: Namespace (intermediary) does not match current runtime namespace (official) and the game dies (hit via cloth-config bundled inside AppleSkin); (2) a mixin refmap keeps its intermediary selectors, so an @Inject resolves to net/minecraft/class_XXXX and Mixin rejects it (InvalidInjectionException). Both are now handled in the offline path (top-level AND nested jar-in-jar), remapping the header/namespace and every class_/method_/field_ token to Mojang. The refmap remapper also gained the combined data."named:intermediary" -> "named:official" form current Fabric loom emits (AppleSkin’s shape), which the previous plain-data.intermediary logic missed on BOTH paths - so this also fixes the runtime path for that refmap format. Extracted to shared AccessWidenerRemapper / MixinRefmapRemapper (one source of truth for runtime + offline). Tested (AccessWidenerRemapperTest, MixinRefmapRemapperTest); verified in-game (the classTweaker + mixin crashes clear, the mod’s mixins apply). (The same launch confirmed snapshot.2 itself reaches the title screen on 26.2 Fabric with translated mods, and noted two follow-ups: the single-jar transform command doesn’t apply the full intermediary bytecode remap that batch/runtime do, and AppleSkin needs a Fabric-API PayloadTypeRegistry networking shim - separate from these transform fixes.)
  • 26.x GUI 2D-transform migration, Phase 0 (GuiGraphics.pose().pushPose() peephole). 26.x moved GUI rendering off the 3D PoseStack onto a 2D org.joml.Matrix3x2fStack (GuiGraphics.pose() now returns the 2D stack, whose ops are pushMatrix()/popMatrix(), not pushPose()/popPose()), but PoseStack still exists for 3D world rendering, so a type-blind redirect corrupts 3D (design RFC: docs/design/gui-2d-transform-migration.md). This first phase rewrites the IMMEDIATE chain a 1.21.1 mod compiles for guiGraphics.pose().pushPose() / .popPose() (the pose() call adjacent to the void op, its result consumed on the spot) to pose():Matrix3x2fStack + pushMatrix()/popMatrix() + POP: a self-contained two-instruction peephole with no dataflow and no local retyping. Strictly conservative: a stored/non-adjacent stack, the arg-carrying ops (translate/scale/mulPose), and any frame-recompute failure are left untouched (unresolved exactly as before, never worse). NeoForge/Forge, gated 26.1+. Verified on Jade (16 of 20 immediate push/popPose migrated; 0 new bytecode-verify errors across the jar via ASM CheckClassAdapter; the migrated class verifies clean). Tested (Gui2DTransformMigrationTest). Visual pixel correctness needs in-game verification; the stored-stack and arg ops are later phases per the RFC.

Fixed

  • transform CLI command now loads the removed-API polyfills (parity with analyze/batch/runtime). The standalone transform command registered only the version-shim chain (or the all-shims fallback) and skipped PolyfillRegistry.loadAndRegister, so a transform-only pass left references to removed-vanilla classes dangling: e.g. net/minecraft/util/LazyLoadedValue (removed in 26.1, referenced by Jade) stayed named in the output where the runtime/analyze/batch paths redirect it onto the embedded polyfill. transformCommand now runs the polyfill pass in BOTH branches, so a transform-only output no longer understates compatibility. Tested (TransformPolyfillRegressionTest).
  • batch and AOT now apply the removed-API polyfills too. The shared registerAuxiliaryRedirects step (the ONLY way the batch JIT path and the AOT compiler reach the transformer) registered class-moves, API shims and member mappings but NOT the polyfills, so a batch-transformed mod referencing a removed-vanilla class (net/minecraft/util/Tuple, LazyLoadedValue, …) was left dangling, even though batch/AOT is the recommended offline NeoForge flow. It now loads polyfills in that shared step (corpus-verified: raw net/minecraft/util/Tuple refs across the 40-mod corpus went 40 -> 0). Tested (TransformPolyfillRegressionTest).
  • Fabric intermediary-map version skew (Minecraft.getTimer/getDeltaTracker). A top-40 Fabric 1.21.1 corpus audit found the bundled intermediary-to-mojang.tsv (harvested with 26.x intermediary ids) misses the 1.21.1-era id for any method Mojang RENAMED since 1.21.1: getTimer() became getDeltaTracker(), so a distributed 1.21.1 mod calling method_60646 was left dangling (10 of the top-40 Fabric mods, verified 10 -> 0). Added method_60646 -> getDeltaTracker (the 26.x id method_61966 already mapped). Tested (IntermediaryMethodSkewTest). (The audit also surfaced two systematic gaps documented for future work: broader renamed-method version skew, and nested intermediary classes getting outer-only remapping, e.g. GlStateManager$class_4535.)
  • Intermediary version skew, systematized (FastColor$ARGB32/ABGR32 -> ARGB color helpers). Following up the getTimer skew fix, a reusable composer (scripts/harvest-intermediary-skew.py) resolves dangling intermediary ids for a given MC version by composing that version’s intermediary tiny (obf<->intermediary) with Mojang’s ProGuard mappings (mojang->obf). Running it over the top-50 Fabric corpus’s dangling ids found the skew is otherwise MINIMAL for 1.21.1 (the bundled tsv is comprehensive; the remaining danglers are genuine removals - Explosion particle getters, ItemInteractionResult, the ARMOR_MATERIAL registry, model-loading internals - not mappable renames), and pinned ONE real re-minting cluster: the 26.1 FastColor$ARGB32/ABGR32 -> ARGB class rename re-minted the color-helper method ids. The enclosing class already resolved (tsv class entry + the FastColor -> ARGB class-move), so only the 1.21.1-era method ids were missing; added the 6 dangling ones (method_57173/57174/58144/59553/59554/60676 -> color/opaque/color/as8BitChannel/colorFromFloat/average), each verified present on 26.2 ARGB, each id absent from the 26.x map, and 0 id conflicts (intermediary ids are append-only, so an id absent from the current map can be given its older mapping with no risk of clobbering another member). Re-audit: the 5 corpus-hit ids went to 0 residual, 0 regressions. Tested (IntermediaryMethodSkewTest).
  • Security + quality hardening (multi-agent adversarial review of this snapshot’s changes and the untrusted-input surface). Confirmed and fixed: (1) a save-data @Inject handler shared across BOTH a write (addAdditionalSaveData) and a read (readAdditionalSaveData) target is now DECLINED by the ValueIO adapter (one CompoundTag param can’t become both ValueOutput and ValueInput) and soft-fail-stripped instead of adapted to one type and left throwing InvalidInjectionException on the other; (2) collectUnrepairable now scopes its stale-CompoundTag check to captures BEFORE the CallbackInfo trailer, so a modern handler with a CompoundTag @Local is no longer wrongly stripped; (3) the ValueIO strip fallback now keys on name+descriptor, so an unrelated overloaded @Inject sharing a stripped handler’s name survives; (4) SyntheticEmbedder.embedIntoJar now bounds its reads with ZipSecurity (per-entry + aggregate cap) instead of raw readAllBytes (decompression-bomb DoS on the offline embed path); (5) the AOT cache no longer re-stamps a directory whose wipe left stale entries behind (cache-poisoning: it would have served a previous build’s transforms as current); (6) the offline transform entry-copy loop gained the aggregate decompressed-size cap the runtime extract paths already had; and (7) clearRedirectsForTesting now clears the new converting/singleton redirect maps (test isolation). Tested (new cases in MixinValueIoAdapterTest; the rest exercised by the existing suite).

Research (documented, not yet shipped)

  • Yung’s API BeardifierMixin confirmed genuinely un-translatable (headless-proven, stays stripped). Its companion NoiseChunkMixin shipped as a real translation (see Added), but BeardifierMixin did NOT: research showed it is byte-translatable (its @Inject targets forStructuresInChunk/compute survive on 26.2) and it applies cleanly, yet the headless worldgen pass proved it is not launch-safe. With BeardifierMixin active, chunk generation throws IllegalStateException: Parent chunk missing (ChunkMap.applyStep) during initial spawn generation and the dedicated server never reaches Done - its enhanced cross-chunk terrain adaptation is incompatible with 26.2’s stricter chunk-dependency scheduler, a runtime-behaviour break no bytecode remap fixes. A/B verified on a real NeoForge 26.2 server: both-mixins-translated crashes; NoiseChunk-only (shipped) and both-stripped both reach Done and generate 49 force-loaded chunks. So BeardifierMixin + the dead BeardifierAccessor stay whole-class stripped; the terrain-adaptation feature stays inert and the mod loads. The two mixins are separable in practice (NoiseChunk’s attach-to-beardifier handler is an instanceof no-op while Beardifier is stripped). This is the same “genuinely un-translatable” class as True Darkness #68, but found at launch rather than statically - and exactly why the hold required a worldgen pass before retiring the strip.
  • YUNG’s NeoForge structure mods (Better Strongholds): StructureProcessorType registration is structurally incompatible on 26.2, not mechanically bridgeable. With the two fixes above, Better Strongholds (NeoForge 1.21.1) now constructs and its mixins apply on a 26.2 dedicated server, but structure registration can’t be mechanically translated: 26.2 DELETED the StructureProcessorType-as-SAM model the mod is built on. Investigated (attempted a DeferredRegister bridge) against the NeoForge 26.2-beta server jars and found three stacked blockers: (1) StructureProcessorType lost its codec() abstract method (now a non-functional marker interface; Registries.STRUCTURE_PROCESSOR holds MapCodec directly), so the mod’s StructureProcessorType SAM lambdas reach the MapCodec registry unconverted (ClassCastException: …$$Lambda cannot be cast to MapCodec -> unbound processor_list -> FatalStartupException); (2) StructureProcessor.getType():StructureProcessorType was renamed to codec():MapCodec, so the mod’s processor subclasses (which override getType() and return their StructureProcessorType field) don’t implement 26.2’s abstract codec() and would be un-instantiable at decode, with no mechanical way to reconstruct the StructureProcessorType->MapCodec relationship the deleted SAM expressed; and (3) the mod BUILDS those SAMs via invokedynamic against the now-methodless StructureProcessorType. Converting only the DeferredRegister registration (blocker 1) leaves 2 and 3, so no bridge makes the mod actually generate. This is the same “structurally redesigned API” class as True Darkness #68 - a re-authoring job, not a redirect. Documented in Mods That Can’t Be Translated.

[1.3.0-snapshot.1] - 2026-07-11

First snapshot of the 1.3.0 line (“mixin translation”). The theme: stop soft-failing mixins and actually translate them. Where 1.2.0 could only STRIP a mixin handler whose target drifted (the feature went inert), 1.3.0 rewrites the handler so the feature keeps working. This snapshot lands the first batch of real translations, retiring four blocklist soft-fails, plus two regression fixes to the re-signature engine caught in review.

Added

  • ValueIO save-data adapter (#48). A 1.21.1 mod’s @Inject into Entity.addAdditionalSaveData/readAdditionalSaveData (or the BlockEntity saveAdditional/loadAdditional) captures a CompoundTag; the 1.21.5 ValueIO refactor made those methods pass ValueOutput/ValueInput, so Mixin rejected the handler (InvalidInjectionException) and NeoForge cascaded into “broken mod state”. MixinValueIoAdapter keeps the mod’s handler body unchanged (still operating on a real CompoundTag) and wraps it: the synthesized handler converts the ValueIO param to a CompoundTag through a reflective, Minecraft-free runtime bridge (ValueIoBridge, per-mod-embedded on NeoForge/Forge for JPMS split-package safety). Repair-or-strip: if frame recomputation fails, the handler is stripped (the old soft-fail), so it can never ship a VerifyError. Retires the Darker Depths PlayerMixin blocklist entry.
  • @Inject / @Redirect / @WrapOperation / @Overwrite signature-drift re-signaturing (#69). The 1.21.5 “world/level threading” refactor prepended a ServerLevel to many LivingEntity/Entity/Mob/Player methods (doHurtTarget(Entity) becomes doHurtTarget(ServerLevel, Entity), and so on). MixinHandlerResignature inserts the new leading param into the handler descriptor, shifts every local slot, and rewrites the drifted @At(INVOKE) injection-point targets, so the handler matches the modern method again. Gated to 1.21.5+ hosts (a no-op where the signatures are intact), re-emitted under COMPUTE_FRAMES with a fall-back to the prior soft-fail. Retires Revamped Phantoms’ SweepAttackMixin doHurtTarget entry.
  • MixinExtras injector dispatch. @ModifyReturnValue/@ModifyExpressionValue/@WrapOperation/… selectors are now routed through the same intermediary-to-Mojang / drift remap as core injectors, with the require=0 soft-fail net added only when a selector was actually rewritten (so a working MixinExtras mixin stays byte-identical).
  • Revamped Phantoms PhantomMixin (#50): a real translation via a deleted-superclass owner-alias. FlyingMob was deleted in ~1.21.2 and Phantom now extends Mob directly, but Phantom STILL declares getDefaultDimensions(Pose) (its body calls Mob.getDefaultDimensions, the former super), so the mod’s @ModifyExpressionValue’s only break was its @At(INVOKE) target still naming FlyingMob. A FlyingMob.getDefaultDimensions to Mob.getDefaultDimensions method redirect (in the three 1.21.11 to 26.1 shims, intermediary key on Fabric) re-owns the injection point, so the phantom-size feature works instead of being stripped. Retires the PhantomMixin blocklist entry. Known limitation: on the rarely-used intermediate host range 1.21.2 to 1.21.10, where FlyingMob is already gone but that redirect (and the intermediary to Mojang remap) does not apply, the feature hard-fails rather than soft-failing; the maintained 26.1+ path is fully covered, and pre-26.1 hosts are the #55 bridge’s scope.
  • Mixin selector remapping is now applied on ALL loaders and the offline CLI, not just the Fabric runtime. Previously the @Mixin/@At/method= selector remap (method + class redirects) ran only inside the Fabric-runtime path; the NeoForge/Forge and CLI-batch paths did the blocklist strip + re-signature but skipped the selector remap, so an API-rename redirect (the #50 FlyingMob alias, the #28 Painting class-move on the NeoForge Deeper and Darker, the GameNarrator/setScreen scan-renames) never reached a NeoForge/Forge or offline-transformed mixin’s annotation strings. Those paths now run the same full pipeline. (Without this, #50 and the class-moves would have silently regressed to hard injection failures on their actual loaders.)
  • Scan-rename redirects (from a 19-jar corpus scan of mixin selectors against the 26.x jars): GameNarrator.sayNow to saySystemNow, FriendlyByteBuf.readJsonWithCodec to readLenientJsonWithCodec (26.1 epoch), Minecraft.setScreen to setScreenAndShow (26.2 epoch).
  • 1.21.11 to 26.1 vanilla class moves: entity/decoration/Painting + PaintingVariant into entity/decoration/painting/, entity/monster/Husk into entity/monster/zombie/, and the MobSpawnType to EntitySpawnReason rename (all verified against the 26.1 and 26.2 jars).

Fixed

  • #28 (Deeper and Darker): a stale blocklist entry that protected nothing. The #28 entry named a PaintingItemMixin/deeperdarker$decrementStackOnServer handler that does NOT exist in the current jar (a silent no-op that hid the mod’s real drift). Replaced with entries for the REAL classes: HangingEntityItemMixin.appendHoverText (its target was re-signatured List<Component> into TooltipDisplay + Consumer, a param split, and its body reads the deleted Painting.VARIANT_MAP_CODEC/CustomData.read, so it is genuinely unrepairable and is stripped, the variant tooltip going inert) and PaintingMixin.dropItem (a ServerLevel signature drift on a generic name, stripped). The Painting class-move fixes the @Mixin target so PaintingMixin.getPickResult now applies natively.
  • Re-signature engine: a @Local captured after the CallbackInfo trailer could turn a soft-fail into a hard crash. The parameter-annotation decline guard was bounded at the CallbackInfo index, but MixinExtras @Local/@Share locals sit AFTER it, and the re-signature does not shift the parameter-annotation arrays, so an inserted leading param misaligned the @Local onto the wrong parameter, an InvalidInjectionException (which COMPUTE_FRAMES can’t catch) on exactly the #69 doHurtTarget-family methods. The guard is now full-width, so any parameter-annotated handler declines and keeps its soft-fail.
  • Re-signature engine: the @Inject method= selector was rewritten decoupled from the handler re-signature. rewriteAnnotationDrift eagerly rewrote an @Inject’s top-level method= selector to new-form even when insertParams later declined, shipping a new-form selector against an un-re-signatured handler (a hard descriptor mismatch). The top-level @Inject method= rewrite is now owned solely by the coupled insertParams path; the @At target= injection-point rewrites (independent of the handler signature) are unchanged.
  • Owner-only mixin method redirects were silently dropped. The mixin target-redirect builder only recorded a redirect when the method NAME changed, so an owner-only redirect (the #50 FlyingMob.getDefaultDimensions to Mob.getDefaultDimensions alias) never reached the @At/method= remap. It now records the owner-qualified form for ANY change (owner, name, or descriptor); the owner-agnostic bare-name form still requires a genuine name change on a globally-unique obfuscated name.

Research (documented, not yet shipped)

  • True Darkness (#68) confirmed genuinely un-translatable. LightTexture was deleted and redesigned into Lightmap (a GPU texture / UBO produced by a separate LightmapRenderStateExtractor), so the mixin’s @Shadow of the old CPU DynamicTexture field has no equivalent and the flicker/dirty state moved to a different class. A real fix is porting the mod to the 26.x render pipeline, outside a mechanical transformer. The whole-class strip stays.
  • Yung’s API BeardifierMixin/NoiseChunkMixin confirmed repairable, held pending verification. Research (javap on 26.1/26.2) confirmed both are mechanically repairable (Beardifier: un-strip the mixin, strip only the dead BeardifierAccessor; NoiseChunk: demote the removed @Shadow @Final noiseSettings to a @Unique field captured from the surviving constructor param). They are HELD in this snapshot because re-enabling interdependent worldgen mixins requires a headless-server worldgen pass first (a mis-repair cascades to all chunk generation). The blocklist reasons carry the full mechanism.

[1.2.0] - 2026-07-09

Stable release (“the general update”). Promoted from 1.2.0-rc.2 with no code changes since that candidate: a final full build + test pass, self-hash re-embedded. The 1.2.0 line accumulated across 1.2.0-snapshot.1 to snapshot.9rc.1rc.2 (the sections below have the details); headline work over 1.1.0 includes Forge 26.2 hosts (EventBus 6→7 bridge), 26.x worldgen for structure mods (Yung’s family), a 1.12.2 pre-1.13 Forge baseline, Sinytra-parity correctness fixes, and Forge→NeoForge synthetic bridges. The final rc.2 fixes: OreBlockDropExperienceBlock (#145, Isle of Berk), the nested ExtendedScreenHandlerType$ExtendedFactory redirect (#147, VillagerViewer), and the docs-site Probe security fix (DOM-XSS + jar-reader hardening) with an accuracy pass (deep-integration mods such as Applied Energistics 2 now correctly read as incompatible; the mixin note counts actual mixin classes).

[1.2.0-rc.2] - 2026-07-07

Fix-forward release candidate: two removed-class crashes reported against rc.1, plus a security fix to the docs-site Probe tool.

Security

  • Probe: fixed a stored DOM-XSS in the Modrinth search. The client-side Probe page escaped </>/& but not quotes, so a mod published on Modrinth with a crafted title or icon URL could break out of an HTML attribute and run script in the docs-site origin when a visitor searched for it (or opened a shared ?mod= link). The Modrinth result rows are now built with the DOM API (dataset / textContent / a validated-https image src) instead of innerHTML string concatenation, so untrusted fields can never become markup. Also hardened the in-browser jar reader: a per-entry decompressed-size cap (zip-bomb guard), a Modrinth-CDN origin allowlist plus download size cap, zip64 / corrupt-central-directory detection, and array-descriptor class-reference handling. Probe-only; does not affect the mod jars. Fixed a false RISKY verdict for any modern mod that references CreativeModeTab (a live class), and a dead package-match path so known-incompatible frameworks are now also detected by referenced package. Expanded the known-incompatible list to match the documented deep-integration mods (Applied Energistics 2, Tinkers’ Construct, IndustrialCraft, Thaumcraft, Botania, Sodium/Iris/Embeddium, and others), which previously read as likely-to-work, and the mixin note now counts actual mixin classes rather than config files.

Fixed

  • OreBlock no longer crashes a 1.18.x mod on a 1.19+ host (#145, Isle of Berk). net.minecraft.world.level.block.OreBlock was renamed to DropExperienceBlock in 1.19 (ore XP moved onto the block), so a 1.18.2 Forge/NeoForge mod that extends or references it died NoClassDefFoundError on a 1.20.1 host. BlockPolyfill now redirects the post-Flattening OreBlock name onto DropExperienceBlock (the pre-Flattening BlockOre was already mapped). Note: a mod that also calls the old super constructor may then surface a NoSuchMethodError, since the ctor signature differs; that is a separate bridge. Tested (Rc2RemovedClassRedirectTest).
  • Nested ExtendedScreenHandlerType$ExtendedFactory now redirects on 26.1+ (#147, VillagerViewer). The 1.21.11 -> 26.1 Fabric shim redirected the outer ExtendedScreenHandlerType to ExtendedMenuType but not the nested $ExtendedFactory, so a mod referencing the factory still crashed NoClassDefFoundError at class-load. Added the nested-class redirect to ExtendedMenuType$ExtendedFactory (target verified present in fabric-menu-api-v1 on 26.2). Tested (Rc2RemovedClassRedirectTest).

[1.2.0-rc.1] - 2026-07-05

First release candidate for 1.2.0 (“the general update”). Promotes the accumulated snapshot line (snapshot.1 through the unreleased snapshot.9) to a release candidate. Across the 1.2.0 line this delivers MC 26.1.x + 26.2 as the maintained targets on Fabric, NeoForge, and Forge (Forge 65.x, EventBus 6 -> 7), the Forge -> NeoForge migration spine, the 26.2 render-API stand-ins, worldgen on 26.x, and the transform-level 1.12.2 groundwork plus its load layer. The changes new since snapshot.8 are under Fixed below; this rc also closed a cleanup pass:

  • Phantom-target cleanup. (done) Deleted the 10 third-party API shims that were entirely phantom (AutoRegLibApiShim, JadeWailaApiShim, PatchouliApiShim, BaublesApiShim, ThermalApiShim, NeiApiShim, WailaApiShim, FabricShieldLibApiShim, OwoLibApiShim, LbaApiShim): every redirect they registered pointed at a never-written embedded class, so they were dead weight (removed their .java files and their META-INF/services/com.retromod.core.VersionShim entries). That takes the phantom count from 97 to 51. The remaining 51 live inside otherwise-valid core version shims (mixed with real registrations) and are paid down over time. New PhantomBaselineTest fences the set against phantom-baseline.txt so any NEW phantom fails the build (and fixing one prompts shrinking the baseline). The snapshot.8 runtime guard already made all of these fail-safe; this removes the underlying debt.
  • method_14452 PlacementModifier mapping fix. (done, see Fixed below.)
  • Multi-mod mcmod.info tomls. (done, see Fixed below.)

Deferred out of 1.2.0 (tracked, roll to a 1.2.x follow-up, not a blocker for this rc):

  • ClientPlayNetworking v1 raw-bytes bridge (#51), the §B3 armor-layer EquipmentClientInfo bridge (#70), and the Sinytra Tier B bundle (#94, covering #9/#11/#12/#13). These were staged at the transform level during the snapshot cycle but still need in-game launch validation on their target hosts, so they follow the rc rather than gate it.
  • Fully running real 1.12.2 mods (beyond the class-load + registration-event stubs this rc ships) is a per-mod chase through a decade of removed systems and earns its own release: the 1.4.0 line, “The 1.12.2 update”, in ROADMAP.md.

Added

  • Probe: an in-browser mod-compatibility pre-check (/probe/). Drop a local jar, or search Modrinth and pick a mod and version, and Probe screens, entirely in your browser (no upload, no server), whether Retromod is likely to translate it: it reads the mod’s loader and built-for MC version, unzips and scans its classes for referenced Minecraft/loader APIs, and cross-references a shipped database distilled from Retromod’s rename / class-move tables (scripts/build-probe-db.py -> docs/assets/probe-db.json). A mod chosen from Modrinth is fetched from Modrinth’s CDN straight to your browser and analyzed the same way, with its built-for MC and loader taken from Modrinth’s version metadata, and gets a shareable deep link (?mod=<slug>&target=<mc>) that reopens straight to its version picker. Verdicts are Likely OK / Needs transform / Risky / Out of range / Incompatible, listing the removed-but-bridged, removed-with-no-bridge, and known-incompatible references it found. It is a static pre-flight screen, not a guarantee (it does not run the real transform or launch the game); the community Compatibility DB and actually running a mod remain the ground truth. Unlike a server-side checker it is client-side, upload-free, and covers all three loaders across versions.

Fixed

  • new ResourceLocation(namespace, path) no longer corrupts to a VerifyError (#135 Wyrms of Nyrus, #136, #121). The polyfill that rewrites the removed ResourceLocation constructors to the static factories registered them as plain method redirects, which only swapped the target and left the uninitialized NEW/DUP on the stack with an INVOKESPECIAL against the factory: VerifyError: Bad type on operand stack (“Type uninitialized 0 … is not assignable”) at class load in any mod building a ResourceLocation (the canonical 1.12.2 idiom). It now uses registerConstructorRedirect (which strips the NEW/DUP and emits INVOKESTATIC), gated to hosts >= 1.20.5 where those constructors were actually removed (on 1.20.1-1.20.4 the ctor still exists, and firing it there was itself the corruption). One fix clears the same VerifyError across three reports. Validated in-game on Forge 1.20.1: Wyrms of Nyrus advanced past the VerifyError (zero VerifyErrors in the crash).
  • 1.12.2 removed registration/model events bridged so the mod loads (#134). A 1.12.2 mod’s @SubscribeEvent handler taking RegistryEvent.Register (the pre-1.13 registration event) or ModelRegistryEvent (the pre-1.17 model-registration event) hit NoClassDefFoundError. Retromod now embeds RegistryEvent/$Register/$MissingMappings and ModelRegistryEvent as synthetic classes that are a valid subtype of the host’s eventbus Event (a class on EventBus 6 and NeoForge, a marker interface on EventBus 7, resolved by probing the running host so the stub extends or implements accordingly). The event never fires on a modern host, so the registration-via-event stays inert, but the class loads and event-bus registration completes without throwing. Skipped on the offline CLI where the host eventbus can’t be resolved (no worse than before). Validated in-game on Forge 1.20.1: Mob Control Wands advanced past RegistryEvent$Register.
  • NeoForge AddReloadListenerEvent bridged to 26.2’s AddServerReloadListenersEvent (#139, Legendary Survival Overhaul). NeoForge renamed the event around 26.x and made addListener require a ResourceLocation/Identifier id. A 1.21.1 mod referencing the old event hit NoClassDefFoundError. Retromod class-redirects the event (fixing the crash and retargeting the @SubscribeEvent parameter, both are game-bus events) and bridges the old one-arg addListener(listener) call to the new two-arg form via a shim that synthesizes a deterministic id, so the reload listener actually registers.
  • 1.12.2 mods no longer die at class-load on removed Forge API types (#131/#132/#134/#135, load layer). A 1.12.2 Forge mod’s class implements/references a Forge type that later Forge deleted (IWorldGenerator and IGuiHandler and IMessage/IMessageHandler in 1.13, IForgeRegistryEntry in 1.17), so the class can’t even load (NoClassDefFoundError) on a modern Forge/NeoForge host. Forge_1_12_2_to_1_13_2 now embeds an empty stub interface per removed type and redirects the old name onto it, so those classes load. Scope: this fixes the reported class-load errors; the paired registration idioms (GameRegistry.registerWorldGenerator, NetworkRegistry.registerGuiHandler, SimpleNetworkWrapper) target other removed classes and stay inert until bridged, so a mod may surface a next error there. CreativeModeTab.<init>(String) (#133, a constructor-to-builder bridge) is a separate follow-up. Part of the ongoing 1.12.2 baseline (#79); the load layer is transform-tested, full in-game running still needs iteration against the real jars.
  • 1.12.2 mcmod.info with multiple mods now declares every modid (#115). A single 1.12.2 jar can list several mods in one mcmod.info (The Betweenlands ships thebetweenlands + mclib). The synthesized mods.toml previously kept only the first entry, so the extra modids were never declared, FML never registered them, and any class or registration keyed on the second modid failed. ForgeModTransformer.generateTomlFromMcmodInfo now JSON-parses the mcmod.info (both the array form and the {"modList":[...]} v2 wrapper) and emits a [[mods]] block plus its own dependency tables per entry, with a lenient regex fallback preserving the old single-mod behavior for an unparseable file. Because the JSON path decodes escapes into real quotes/newlines, the emitted version is validated against a [A-Za-z0-9._-] allowlist (falling back to 1.0.0) so a crafted mcmod.info can’t break out of the quoted TOML value to inject keys or spoof another modid, and the parse degrades to the regex fallback on any Throwable (including a StackOverflowError from deeply-nested input).
  • Custom PlacementModifier.getPositions no longer mis-renamed to count (#114). The Fabric intermediary short-name method_14452 is ambiguous: verified against Fabric Yarn 1.18.2 through 1.21.4 it names BOTH PlacementModifier.getPositions(context, random, BlockPos) -> Stream (Mojang getPositions) AND a separate getCount(random, BlockPos) -> int (Mojang count). The name-only 1.21.4 intermediary-to-mojang.tsv kept just count, so a mod’s getPositions override was renamed to count: it stopped overriding the abstract method and threw AbstractMethodError during chunk generation on 26.x (any mod with a custom placement modifier). IntermediaryToMojangMapper.applyTo now registers all four descriptor variants (two eras: 1.18.x java/util/Random, 1.19+ RandomSource) so the transformer’s ambiguous-name fallback resolves each site by descriptor. The systemic follow-up (a descriptor-qualified harvest so the tsv itself carries ambiguity) is noted in code.
  • Removed Fabric v0 ClientTickCallback bridged to ClientTickEvents (#129 Chat Bubbles). net.fabricmc.fabric.api.event.client.ClientTickCallback (SAM tick(MinecraftClient), static EVENT) was deleted from Fabric API around 1.16, so a 1.15-era mod referencing it dies with NoClassDefFoundError while its entrypoint class is even being loaded. Retromod now embeds a synthetic ClientTickCallback interface (kept under com/retromod/... so it can’t split-package with the loader module) that preserves the tick SAM and the EVENT field; EVENT is wired in <clinit> to an array-backed event whose invoker fires once per client tick via ClientTickEvents.END_CLIENT_TICK. Gated to 26.1+ (the SAM uses the Mojang Minecraft type). Implemented pending in-game verification.
  • AOT no longer aborts a whole mod on one un-recomputable class (#125 MineColonies, #127 The Flying Things). The AOT compiler’s last-resort per-class path recomputes stack-map frames with COMPUTE_FRAMES, which can throw deep inside ASM (Frame.merge -> ArrayIndexOutOfBoundsException / NegativeArraySizeException) on a class whose frames can’t be recomputed after our rewrites. That exception propagated out and failed the entire jar’s AOT, dropping the mod back to un-transformed bytes. It now falls back per-class to the robust main (JIT) transformer (which has its own ship-original frame-corruption guard), then to the original bytes, and never lets one class take down the jar. This mirrors the snapshot.8 frame-corruption fallback that already protected the JIT path.

[1.2.0-snapshot.8] - 2026-07-03

The three remaining 1.2.0 pillars landed, plus a full in-game acceptance pass for Apollo’s Enchantment Rebalance on Fabric 26.2 (#119). §A the pre-26.1 Fabric acceptance pass: real 1.16.5 Serilum mods run clean through full server start on a 1.20.1 host (Text/Entity-field/Material bridges + a rebuilt constructor-to-factory pass). §D the 1.12.2 in-game layers (SRG member namespace, the pre-1.13 FML lifecycle idiom, a 1.20.1-target class-move table). §B the 26.2 render-API stand-ins (MultiBufferSource, Tesselator, VertexFormat$Mode). And #119: the anvil-crash phantom redirect, a systemic phantom-target safety net, and the blank-enchanted-books tooltip rename, all fixed and verified in-game.

Added

  • 26.2 render-API stand-ins (SS B): render-referencing mods now LOAD on 26.2. 26.2 deleted MultiBufferSource/MultiBufferSource$BufferSource and Tesselator, and replaced the VertexFormat$Mode enum with PrimitiveTopology (all verified against the real 26.1.2 and 26.2 client jars), so a 26.1-translated mod referencing any of them died NoClassDefFoundError at class load on 26.2, even when the render path never ran. New per-mod-embedded stand-ins reproduce the exact 26.1 shapes: BufferSource.getBuffer(renderType) returns a REAL 26.2 BufferBuilder built from the RenderType’s own format()/primitiveTopology(), and Tesselator.getInstance().begin(...) works the same way, so mod geometry code actually executes; the endBatch family is staged (warns once and drops the batch: frame submission through the new SubmitNodeCollector.submitCustomGeometry pipeline is the follow-up). VertexFormat$Mode is class-redirected to PrimitiveTopology (identical constants and public fields). Registered in the 26.1 to 26.2 layer on all loaders. Tested (RenderBufferSyntheticsTest).
  • 1.12.2 SRG member namespace (SS D, #103/#108/#117). Distributed 1.12.2 Forge mods reference members by the OLD SRG names (func_NNNNN_x/field_NNNNN_x), a different namespace from the modern m_/f_ one, so the SRG remap never fired for them. A new bundled dictionary (srg-1.12.2-to-mojang.tsv, 7,736 entries) maps them to readable names: harvested from the MCPBot 1.12.2 stable CSVs and kept ONLY where the name exists in Mojang’s official 1.20.1 mappings (a wrong rename is worse than none). The remapper’s SRG branch now matches both eras, and the dictionary loader merges both files. Also fixed a latent core bug the new tests flushed out: an SRG-only registration built no remapper at all (masked before because real runs always carry class redirects). Tested (Srg1122MemberRemapTest).
  • 1.12.2 class moves for 1.20.x hosts (SS D). The 1.12.2 class-move table only existed with 26.1-target names, but the reported 1.12.2 hosts (#103/#108/#117) all run 1.20.1. A new validated variant (forge-1.12.2-class-moves-1201.tsv, 125 rows, every target checked against Mojang’s official 1.20.1 mappings by scripts/harvest-1.12.2-class-moves-1201.py) loads on 1.20.x hosts instead, and additionally carries the families 26.1 removed but 1.20.x still has (ItemSword -> SwordItem, EnumAction -> UseAnim, CreativeTabs -> CreativeModeTab, tool/armor tiers, …). The Block/Item Properties constructor bridge and the Material static-field nuller now apply on 1.20.x hosts too (verified: 1.20 already removed Material, and Properties.of()/Item$Properties() are present with the same shapes as 26.1). 1.21.x hosts get no table (none validated against those jars). Tested (Forge1122ClassMovesTest).
  • 1.12.2 in-game FML lifecycle layer (SS D, #103/#108/#117). A 1.12.2 mod’s @Mod(modid=...) annotation shape is invisible to a modern loader (it reads @Mod(value)), and nothing ever calls its @Mod.EventHandler setup methods, so even a mod that LOADED did nothing. The upgrade pass rewrites the annotation to the modern shape and injects a constructor-time call into an embedded lifecycle bridge that constructs the legacy FMLPreInitialization/FMLInitialization/FMLPostInitialization event stand-ins (config-dir surface included: getSuggestedConfigurationFile yields config/<modid>.cfg) and reflectively invokes the mod’s handlers in order. Active only when the 1.12.2 shim chain registered. Tested (Forge1122LifecycleTest, including a functional fire test).
  • Pre-26.1 Fabric bridge chain from the LIVE acceptance pass (SS A, #55). Real 1.16.5 Fabric content mods (Serilum’s Double Doors, Mineral Chance, Stack Refill plus their shared Collective library) were run on an actual 1.20.1 Fabric server and every crash in the chain was fixed at the transform level:
    • Pre-1.19 Text bridge. The 1.19 chat rework removed the TranslatableText/LiteralText constructors; new TranslatableText("key") died NoSuchMethodError. Constructor-to-factory redirects target Text.translatable/Text.literal (plus the with-args overload), registered with the InterfaceMethodref form (Text is an interface on 1.19+: a plain-Methodref INVOKESTATIC dies IncompatibleClassChangeError), and both old classes class-redirect to MutableText so the mod’s downstream fluent calls (formatted, setStyle, append: same intermediary ids on MutableText since 1.16, verified on the 1.20.1 jar) type-check against the factory’s return value.
    • Pre-1.17 Entity field bridge. Entity.onGround went non-public in the 1.17 access cleanup; direct access died IllegalAccessError. GETFIELD/PUTFIELD rewrite to isOnGround()/setOnGround(boolean).
    • Pre-1.20 Material bridge. MC 1.20 deleted the Material system, and ONE List<Material> in a <clinit> (Collective’s GlobalVariables) took the whole class down with NoClassDefFoundError. class_3614 class-redirects to a shipped MaterialPolyfill (44 distinct constants under their exact 1.16.5 intermediary field ids, harvested from the official intermediary mappings), and BlockState.getMaterial() devirtualizes to a staged fromState (returns a sentinel matching no constant, so material-membership features quietly no-op instead of crashing).
    • All three bridges are host-introspecting (probe with Class.forName(..., false, ...), never initialize): they register only where the host actually lost the API, so 1.16.x-1.18.x hosts are untouched. Tested (Pre1_17EntityFieldBridgeTest, Pre1_20MaterialBridgeTest, plus the real failing Collective class as a gated repro).
    • Also fixed while here: the fuzzy method resolver never activated on pre-26.1 Fabric hosts (its MC-jar probe only knew the Mojang-named SharedConstants; intermediary hosts need the class_310/MinecraftServer probes), and Fabric_1_16_5_to_1_17 carried four 26.x-only tick-event renames (*WorldTick to *LevelTick) that broke the very hosts the shim targets; the correct copies live in the 26.1 shim.

Fixed

  • 1.12.2 Forge mods no longer die “mods.toml missing metadata for modid null” on 1.20.1+ (#120, Betweenlands / Scape and Run Parasites). A 1.12.2 @Mod annotation is @Mod(modid="x", name=..., version=..., dependencies=...); modern Forge’s @Mod has ONLY value() (which IS the modid), so it read value() = null from the old shape and aborted mod loading with a null modid. The Forge1122LifecycleSynthetics @Mod modernization (added this cycle) collapses the whole legacy annotation to @Mod(value=modid) so Forge reads a real id. Verified on the real SRPMain class from Scape and Run Parasites (@Mod(modid="srparasites", ...) -> @Mod(value="srparasites")). The runtime Forge path already activated the pass via the 1.12.2 shim chain; the CLI transform now activates it too (at startup, self-gating on the old annotation shape so it never touches modern mods), so pre-transformed jars get the same fix. Tested (Forge1122LifecycleTest, incl. the full 4-element annotation shape).
  • Custom ClientTooltipComponent renderers now draw on 26.1+ (Apollo’s Enchantment Rebalance blank enchanted books, #119). 26.1 renamed ClientTooltipComponent.renderText/renderImage to extractText/extractImage (the params take GuiGraphics, itself renamed to GuiGraphicsExtractor). The intermediary->Mojang table is 1.21.4-based and mapped those two intermediary ids (method_32665/method_32666) to the OLD names, so a distributed 1.21.x Fabric mod’s overrides were renamed to renderText/renderImage and no longer overrode the interface: the vanilla empty default methods ran, and the mod’s custom tooltip drew nothing (AER’s enchanted books showed no enchantment lines, even though the enchantments themselves applied and functioned). Corrected the two id mappings to the 26.1 names; the fix is id-scoped, so the four unrelated renderText methods are untouched. Tested (ClientTooltipRenderRenameTest, both the mapping and a transformed override). This is one instance of a broader class: the 1.21.4-based intermediary table carries stale names for any method renamed between 1.21.4 and 26.1, which breaks overrides on a 26.1+ host; more such renames may need the same correction as they surface.
  • Apollo’s Enchantment Rebalance no longer crashes the anvil on Fabric 26.2 (#119). The mod’s anvil createResult calls Enchantment.getMaxLevel(), which Retromod was rewriting to a com/retromod/shim/*/embedded/EnchantmentShim that was never written (a phantom target): the anvil combined an item + book, the rewritten call resolved, and it died NoClassDefFoundError on first use. Three compounding faults, all fixed:
    • The Fabric transform path had no loader-type filter. RetromodPreLaunch registered EVERY version shim regardless of getModLoaderType(), unlike the other three entry points. So a NeoForge shim’s Mojang-named redirect (NeoForge_1_20_6_to_1_21) fired on a Fabric mod once the intermediary->Mojang harvest made the names match. It now applies the same {fabric, common} filter the NeoForge/Forge/onInitialize paths already use, which closes the entire class of cross-loader phantom bites on Fabric (NeoForge/Forge enchantment, ItemStack-NBT, DamageSource, Material, Tag redirects, etc.).
    • The redirects were wrong regardless. Enchantment.getMaxLevel()/getMinLevel() never left the API (present 1.21 through 26.2, backed by the data-driven definition); redirecting them was pointless. Both the NeoForge and the Yarn-keyed Fabric copies (dead code per the intermediary-namespace note) are deleted. getRarity was genuinely removed but can’t be bridged by a call-site redirect alone (its Rarity return type is gone too), so it’s deleted pending a proper synthetic bridge.
    • A phantom-target safety net. The transformer now sweeps its redirect tables once, before the first transform, and drops any redirect whose com/retromod/* target is neither a registered synthetic nor a loadable class, logging one warning instead of letting it detonate at first use. This retroactively neutralizes several other never-written targets found across the shim tree (FlatteningShim, SidedProxyShim, a wrong-package GameRegistryShim, …): the affected feature quietly no-ops instead of crashing whatever runs it. This also surfaced and fixed a latent bug where a redirect set containing only super-constructor + class redirects skipped the method-body visitor entirely (the 1.12.2 Block/Item super(Material)->super(Properties) bridge relied on an unrelated method redirect being present to stay active). The reported blank-enchanted-books symptom is a separate client mixin-apply matter still under investigation.
  • Constructor-to-factory rewriting no longer corrupts stack frames when the args contain branches (found live on Collective’s DuskConfig). The old deferral buffered ONE pending NEW and flushed it as soon as a nested plain new appeared, i.e. mid-expression: with a ternary in the constructor args the buffered NEW+DUP landed on one branch path only, ASM’s frame computation failed (Frame.merge AIOOBE), and the silent COMPUTE_MAXS fallback then shipped the class with its ORIGINAL StackMapTable whose offsets the rewrite had invalidated: ClassFormatError: StackMapTable format error: bad offset for Uninitialized at load, taking the whole mod down. The machinery is now a proper deferral STACK flushed only at the <init> convergence point (branch-safe by construction, and nested redirect-eligible news compose), it moved UPSTREAM of the class remapper into its own pass (CtorRedirectPrePass, so constructor redirects stay keyed on distinct pre-remap owners even when several old classes retype onto ONE new class, as the Text bridge needs), the fallback now logs the primary failure instead of swallowing it, and a fallback pass that made frame-invalidating rewrites refuses to ship the result (degrades to the untransformed class instead of a corrupt one). Regression-tested with synthesized pathological shapes (CtorRedirectBranchedArgsTest) plus the real failing class.

[1.2.0-snapshot.7] - 2026-07-01

The “big update” cycle: the Forge-family deep work (Forge -> NeoForge migration, and the Forge 26.2 EventBus 6->7 break), the 1.12.2 in-game stack, and the 26.x worldgen/datapack chain taken from “registers nothing” all the way to structures generating in-world. Two milestones this cycle. (1) The acceptance set of small Forge content mods (Macaw’s Roofs / Trapdoors / Bridges), built for Forge 1.21.1, now fully loads on NeoForge 26.2 - verified in-game (all three scan, construct, register their blocks/items, their resources reload, the sound engine starts, and the client reaches the main menu with no crash). (2) YUNG’s API + Better Dungeons (Fabric 1.21.1) now register AND generate on a 26.2 dedicated server: /locate finds a YUNG structure and /place builds one in-world with zero registry, parse, or generation errors.

Added

  • Forge -> NeoForge: the construct + register spine. A Forge 1.20.1 content mod now gets scanned, constructs, and registers its content on NeoForge 26.2. The pieces (each target verified against the NeoForge 26.2 jar):
    • FMLJavaModLoadingContext bridge (#85/#115). get().getModEventBus() is the first line of nearly every Forge @Mod constructor, and NeoForge deleted the class. Retromod embeds a per-mod synthetic bridge that delegates to ModLoadingContext.get().getActiveContainer().getEventBus(). The Forge -> NeoForge redirect that lets the embedder match the reference now lives on the API-shim path, so it applies on both the offline CLI/AOT batch and the live runtime (previously it was on a runtime-only shim the offline chain skipped). Verified in-game.
    • Registry-id bridge (#87). MC 1.21.3+ requires the registry id stamped on a Block/Item Properties before the object is built, but Forge’s DeferredRegister.register(String, Supplier) constructs it inside the supplier with no id (so RegisterEvent fails “Block/Item id not set”). A per-mod synthetic threads the id through a thread-local: registration is rerouted through the id-aware register(String, Function) overload, and the Properties factories (of/ofFullCopy/ofLegacyCopy, new Item.Properties()) stamp setId from it. Verified in-game (blocks/items register).
    • Extension-interface + lifecycle/server event migration. IForgeItem/IForgeBlock/IForgeEntity/IForgeBlockEntity -> NeoForge’s I*Extension; FMLCommonSetupEvent, FMLClientSetupEvent (-> net/neoforged/fml/event/lifecycle/*), ServerStartingEvent (-> the neoforge event package). These join the shim’s existing registry/capability/config/network redirects.
  • 26.2 ColorCollection consolidation (all loaders). 26.2 folded each per-color family (16 colors) of Blocks/Items constants into a single ColorCollection field and removed the per-color statics, so a 1.21.x mod reading Blocks.BLACK_WOOL (or Items.WHITE_CARPET, …) hits NoSuchFieldError. The 26.1->26.2 shim already rewrote four block families to <field>.pick(DyeColor.<COLOR>) accessors; this extends it to all 14 block families and 14 item families (wool, carpet, concrete, concrete_powder, stained glass + panes, beds, banners, wall banners, glazed + dyed terracotta, shulker boxes, candles, dyes, bundles), each verified against the 26.2 jar (the collection field name is irregular, e.g. WOOL vs DYED_CANDLE) with the correct Block-vs-Item cast. Shared across Fabric/NeoForge/Forge. Tested (DyedBlockAccessorTest); it was the last blocker to the Macaw’s loading in-game.
  • Forge 26.2 EventBus 6 -> 7 bridge (#85) - old Forge mods now load on Forge 26.2. Forge 26.2 (65.x) replaced EventBus 6 with EventBus 7: IEventBus and the old Event base are gone (BusGroup + per-type EventBus<T> instead), so every old Forge mod died at construction. A synthetic LegacyEventBus interface (so the mod’s interface call sites stay verifiable) bridges the old idiom onto BusGroup: getModEventBus() -> getModBusGroup() wrap, DeferredRegister.register(bus) -> register(BusGroup), MinecraftForge.EVENT_BUS -> a BusGroup.DEFAULT-backed bridge, old @SubscribeEvent -> EventBus 7’s annotation, plus workarounds for EventBus 7’s stricter policies (single-listener classes are wired to their event’s own BUS reflectively; @Mod.EventBusSubscriber classes EventBus 7 would reject are skipped with the EventBus 6 semantics preserved). Also fixed along the way, each hit in-game: Forge 26.2 RESTORED the IForgeItem-style names (the 26.1 “I-drop” rename is now version-gated), a Forge-flavored registry-id bridge (Forge has no id-aware register overload; the id comes from RegistryObject.getKey()), and the synthetic embedder now embeds transitively (a bridge referencing a helper synthetic no longer dangles). Verified in-game: Macaw’s Bridges (Forge 1.21.1) constructs, registers its content, and reaches the menu on Forge 26.2.
  • EventBus 7: lambda setup listeners + priority overloads now bind (#101). The final piece of the EventBus 6->7 bridge. bus.addListener(this::onSetup) compiles to a LambdaMetafactory invokedynamic followed by the call; EventBus 6 read the event type from the lambda’s constant pool, EventBus 7 cannot. Retromod now recovers it at transform time: a call fed directly by a Consumer-producing indy is retargeted to a typed helper with the lambda’s reified event type appended as a Class constant, so it resolves the event’s own BUS exactly (a non-adjacent/stored Consumer falls through to runtime generics recovery, as before). This un-inerts the lifecycle-setup callbacks the previous build soft-failed (verified: Macaw’s Bridges’ FMLCommonSetupEvent/FMLClientSetupEvent listeners now bind). Also added: EventBus 6’s deleted EventPriority enum is supplied as a real-enum stand-in (constants, values()/valueOf(), enum-switch all work) carrying the matching EventBus 7 Priority byte, and the bridge gained the priority-carrying addListener overloads ((EventPriority, Consumer), (EventPriority, boolean, Consumer), (EventPriority, boolean, Class, Consumer)). Tested (ForgeEventBusBridgeTest).
  • Forge entry point never loaded the polyfills. RetromodForge was missing the PolyfillRegistry wiring that Fabric, NeoForge, and the CLI all have, so all removed-API polyfills (e.g. the #24 DirectionProperty bridge) silently never applied on Forge hosts - surfaced on Forge 26.2 as NoSuchMethodError: EnumProperty.create(String, Direction[]) killing block class-init. Now loaded (with the NeoForge-specific category off, as on NeoForge).
  • Sinytra Connector techniques (clean-room, credited). A descriptor-qualified member-name fallback (correctly disambiguates overloaded members), an AccessWidener -> NeoForge AccessTransformer converter, and an offline Forge -> NeoForge mode (--target-loader neoforge) so the migration runs from the CLI without a live NeoForge host.
  • Fuzzy-resolver report for offline bridge authoring (config/retromod/fuzzy-report.tsv). The fuzzy resolver’s near-miss band (50-84% confidence: “this looks like a rename but I won’t auto-apply it”) is exactly the data needed to hand-author a precise shim bridge from a real failing mod, but scraping it out of an interleaved launch log is painful. Retromod now also writes each outcome as a machine-readable TSV row (one per distinct unresolved reference, fresh per run): NEAR rows are the bridge-authoring backlog, AUTO rows are applied auto-redirects to review, SUPPRESSED rows are type-incompatible matches that need a hand-written adapter. Reporting failures never break a transform. Tested (FuzzyMatchReportTest).
  • 1.12.2 metadata generation: mcmod.info -> mods.toml (#79). A pre-1.13 Forge mod ships only mcmod.info (the mods.toml format postdates 1.13), so modern Forge/NeoForge never scanned it and snapshot.6’s class-move + ctor bridges never ran. ForgeModTransformer now synthesizes a mods.toml from mcmod.info (id/version/name/description extracted by regex, ranges relaxed to [1,)), which promoteToNeoForgeToml then renames to neoforge.mods.toml on a NeoForge host. This is the first 1.12.2 in-game prerequisite found in snapshot.6 testing: the mod now gets scanned (the SRG member + registry-idiom layers are still ahead). Tested (McmodInfoTomlGenTest).

Fixed

  • Post-review hardening (multi-agent code + security review of this cycle). Thirteen adversarially-verified findings fixed, the notable ones: redirects to interface synthetics now force the InterfaceMethodref flag (a pre-1.19.3 mod’s Registry.register call would otherwise die IncompatibleClassChangeError at its first registration on 26.2 targets); the 26.x became-interface opcode fixes (IntProvider/FloatProvider) are host-gated so pre-26.x translations aren’t corrupted; the Fabric entry point now applies the standard “shim target must not exceed host” gate; ForgeCorePolyfill no longer class-redirects a LIVE MinecraftForge on real Forge hosts; the CLI syncs its default target version into the shim gates (previously only --target runs did); plus zip-slip/zip-bomb guards on the AOT paths, atomic jar rewrites in the synthetic embedder, and the client item-definition synthesis now also covers 1.21.4-1.21.11 targets.
  • Rewritten jars now keep their ZIP directory entries - fixes silently-broken classpath scanning. Gradle-built mod jars carry an entry per directory; Retromod’s tree-rezip paths emitted only file entries. A jar classloader resolves package resources (ClassLoader.getResources("com/example")) only from directory entries, so Reflections/ClassGraph-style scanners found NOTHING in a transformed jar - with no exception anywhere. Found on a headless Fabric 26.2 dedicated server: YungsApi’s Reflections-based @AutoRegister system scanned “0 urls, producing 0 keys”, none of its worldgen types registered, and every YUNG structure failed datapack load as “Unknown registry key” (original jar: 48 directory entries; transformed: 0). Fixed on every jar-writing path (runtime Fabric/Forge/Quilt, AOT, synthetic embedder); verified on the server: the scan now finds the package and ALL worldgen types register. This was very likely the root cause of the long-standing “worldgen-heavy mods stall on 26.x” pattern.
  • Classes no redirect touches now ship byte-identical. The transform loop re-serialized every class (fresh constant pool + recomputed stack frames) even when nothing matched. For a class whose type hierarchy isn’t loadable at transform time (a JiJ’d library), frame recomputation merges unknown sister types to java/lang/Object and the rewritten class fails JVM verification at runtime - YungsApi’s bundled javassist died with VerifyError in ConstPool.readOne, taking the whole registration path with it. The transform now compares pass 1 against an identity re-serialization and ships the ORIGINAL bytes when nothing semantically changed (cost-neutral: it replaces the old stability pass). Tested (UntouchedClassPreservationTest).
  • IntProvider/FloatProvider became interfaces on 26.x, and their static range-codec factories moved. INVOKEVIRTUAL on them is now auto-corrected to INVOKEINTERFACE (and, on the fast path too, static calls get the InterfaceMethodref flag - a static method on an interface dies with IncompatibleClassChangeError otherwise). The removed IntProvider.codec(min,max) / FloatProvider.codec(min,max) factories are bridged to the 26.x companion dispatch codecs (IntProviders.CODEC / FloatProviders.CODEC; the removed factories only added range validation on top of the same codec). All hit in-game by YUNG’s Jigsaw structure codec at datapack load on the 26.2 server.
  • Worldgen milestone: YUNG’s API + Better Dungeons (Fabric 1.21.1) fully load on a 26.2 dedicated server. On top of the directory-entry and byte-identity fixes above, three more 26.x breaks were bridged, each captured live on the headless server: (1) the STRUCTURE_PROCESSOR registry now holds MapCodecs directly, so a 1.21.x mod’s StructureProcessorType SAM-lambda registration broke every processor_list that referenced it - a new bridge intercepts Registry.register into that registry and converts the value via its old codec() SAM (registering the codec, returning the mod’s original value so its fields still hold); (2) StructureProcessor itself became an interface, so processor subclasses died at class definition - a new superclass-rebase variant rewrites extends to Object + implements, including the super() constructor call; (3) IntProvider/FloatProvider’s static codec factories and constants moved to IntProviders/FloatProviders with identical signatures (plain owner redirects; also the IntProvider.CODEC miss in #114’s gap report). Result: server reaches “Done” with zero registry errors and every YUNG worldgen registry bound.
  • Worldgen generation: YUNG structures now generate in-world on 26.2, not just register. Past registration, three more 26.x breaks in the jigsaw generation path were bridged, each captured live on the server during /place and /locate: (1) StructurePoolElement.getShuffledJigsawBlocks still returns a List, but its elements are now StructureTemplate$JigsawBlockInfo wrappers (26.1) instead of StructureBlockInfo, so a 1.21.x caller’s element cast died ClassCastException - a bridge unwraps each element via JigsawBlockInfo.info(); (2) JigsawBlock.canAttach was re-typed from (StructureBlockInfo, StructureBlockInfo) to (JigsawBlockInfo, JigsawBlockInfo) - a bridge wraps both args via JigsawBlockInfo.of; (3) MC renamed Registry.getHolder/getHolderOrThrow to get/getOrThrow (now inherited from HolderGetter), so a 1.21.1 mod’s holder lookup during structure assembly died NoSuchMethodError - fixed as a same-owner rename. Result: /locate structure finds a YUNG structure and /place structure generates one in-world with no errors. Tested (IsOverloadBridgeTest).
  • The fuzzy resolver no longer rewrites a reference that already resolves. The last-resort fuzzy matcher scored candidates even for calls that were already valid, and could pick a same-descriptor sibling declared directly on the owner over the real inherited method (an exact-class score beats a related-class one). Live failure: Registry.get(ResourceKey) (inherited from HolderGetter, returns an Optional<Holder>) was rewritten to the value getter Registry.getOptional, and YUNG’s worldgen died ClassCastException: StructureTemplatePool cannot be cast to Holder at the first structure piece. Both fuzzy entry points now short-circuit when the reference is present (hierarchy-aware), so a working call is never touched. Tested (FuzzyMatchReportTest).
  • Placement crash: 26.1 removed the single-arg is() overloads. BlockState.is(Block)/is(TagKey), ItemStack.is(Item)/is(TagKey), and all of FluidState.is(...) are gone on 26.1+ (verified against the real jars; only the Predicate-taking forms survive), so a 1.21.x mod dies with NoSuchMethodError at first use - e.g. Macaw’s Bridges crashed the server tick loop placing a bridge (Bridge_Block.onPlace). A per-mod synthetic (IsOverloadBridge) now carries faithful static reimplementations of the removed overloads (via getBlock()/getItem()/getType() identity and builtInRegistryHolder().is(...) tag/holder/key checks), and old call sites are rewritten onto it from the 1.21.11 -> 26.1 shims on all three loaders. Tested (IsOverloadBridgeTest).
  • Invisible items: 1.21.4+ client item definitions are now synthesized. MC 1.21.4 split “client item” definitions out of item models: every item id needs an assets/<ns>/items/<id>.json naming its model, and items without one render as the purple/black missing model (the checkerboard creative tab and the giant magenta held-item cube). Pre-1.21.4 mods only ship models/item/*.json, so ALL their items were invisible on 26.x (Macaw’s Bridges: 146 item models, 0 definitions). The data migrator now generates the missing definitions on every transform path (runtime and CLI). Verified in-game: “Missing item model” warnings went from 400+ to 0. Tested (ModDataMigratorTest).
  • Retromod didn’t initialize on NeoForge 26.2 (#90). Declaring the mod-file-locator SPI made FancyModLoader claim the whole Retromod jar as an “early service” and then skip it for @Mod scanning, so RetromodNeoForge never constructed and the entire runtime transform was inert. Removed the service declaration from the jar (the locator class stays; mods/Retromod/ CurseForge-export loading moves to a separate stub jar). Confirmed via a real GUI launch; this was the prerequisite for the Forge -> NeoForge work above to run at all.
  • IForgeItem -> ForgeItem shim conflict on NeoForge. The Forge-26.1 rename shim (drop the “I”, stay forge-packaged) was clobbering the NeoForge migration’s IForgeItem -> IItemExtension (last-writer-wins across ServiceLoader shims), leaving a net/minecraftforge/common/extensions/ForgeItem reference that exists on no NeoForge classpath -> NoClassDefFoundError on a custom Item. The forge-extension renames are now gated to Forge hosts only.
  • MinecraftForge.EVENT_BUS NoSuchFieldError on NeoForge. A polyfill class-redirected MinecraftForge to a capabilities shim with no EVENT_BUS field, clobbering the migration’s correct MinecraftForge -> NeoForge (which has the field). Gated off on NeoForge so a Forge mod’s MinecraftForge.EVENT_BUS.register(this) resolves.
  • Apollo’s Enchantment Rebalance produced a broken datapack on 26.x (#114). The mod’s entrypoint died before registering its custom enchantment effect/condition types, and its data then failed to load, so the datapack looked broken. Three distinct 26.x breaks were bridged: (1) ConditionalEffect.codec(Codec) dropped its ContextKeySet validation argument (an arg-dropping redirect handles the old two-arg call); (2) the loot-type registries were MapCodec-ized and the LootItemFunctionType/LootItemConditionType wrappers deleted (wrapper stand-ins plus registration-time value conversion, alongside the same treatment for the structure/density registries); (3) 26.x namespaces the keys in entity predicate JSON ("type" -> "minecraft:entity_type", flags/location/movement/… -> minecraft:*), so an old advancement/loot predicate silently failed to match - the data migrator now rewrites those keys (subset-guarded so it only touches genuine entity predicates, recursing into vehicle/passenger/targeted_entity and advancement predicate slots). Two new intermediary IDs (TooltipDisplay, Weapon) were appended for the same mod. Result: 0 errors, custom types register, datapack loads. Tested (ModDataMigratorTest).
  • The AOT cache now truly auto-clears when Retromod changes. The per-jar cache entries already carried version + self-hash headers (checked on cache hits), but two paths never validated at all: the Hybrid engine’s per-class preload served every .class file in config/retromod/aot-cache/ blindly, and the full-AOT cache (retromod-cache/full-aot/) had no version check either, so after updating Retromod both could keep serving the PREVIOUS build’s transforms until the folder was deleted by hand. A generation stamp (.cache-stamp, Retromod version + self-hash of its own classes) now guards every cache directory: when the owning build differs, the whole cache is wiped and re-stamped automatically at startup. Nobody needs to remember rm -rf config/retromod/aot-cache/ anymore. Tested (AotCacheStampTest).
  • CLI transform: the “all shims” fallback didn’t embed synthetics. When a mod’s source MC version can’t be detected, the CLI applies every shim and transforms, but that branch never called the synthetic embedder - so a jar whose references were rewritten onto a deleted-class bridge (e.g. the EventBus 7 LegacyEventBus) shipped with dangling references (NoClassDefFoundError at load). It now embeds on that path too, matching the version-detected path.

[1.2.0-snapshot.6] - 2026-06-29

Added

  • 1.12.2 (pre-1.13) Forge class-move baseline (#103/#108/#113). Old 1.12.2 Forge mods reference Minecraft classes the 1.13 “Flattening” + 1.17 repackaging moved/renamed (net.minecraft.util.math.BlockPos -> core.BlockPos, world.World -> world.level.Level, util.ResourceLocation -> resources.Identifier, the block/item/entity packages, NBT, containers, …). A bundled, jar-validated 1.12.2 -> 26.1 class-move table (117 entries in forge-1.12.2-class-moves.tsv, harvested by scripts/harvest-1.12.2-class-moves.py) is loaded by the 1.12.2 shim, gated to a 26.1 host. These are transform-level building blocks, unit-tested at the bytecode level, not yet in-game loading. A 1.12.2 mod ships only mcmod.info (the mods.toml format postdates 1.13), so running one end-to-end on modern Forge/NeoForge additionally needs: mcmod.info -> mods.toml metadata generation (so the loader scans the mod at all), the 1.12.2 SRG member namespace (func_/field_ -> Mojang), and the old @EventHandler/FMLPreInitializationEvent registry idiom. None of those are built yet, so 1.12.2 mods do not load in-game in this build. Class-moves are the dominant bytecode gap (308 of 344 issues on a real 1.12.2 mod). Transform-tested (Forge1122ClassMovesTest).
  • 1.12.2 Block/Item Properties-constructor bridge (#80). After the class moves, a 1.12.2 custom block/item still wouldn’t construct: 26.1 made Block and Item Properties-constructed, but a 1.12.2 block calls super(Material) and a 1.12.2 item calls super(), neither of which has a matching ctor (and Material itself was removed). Two new transformer mechanisms bridge this: a super-constructor replace (pop the old args, push a fresh default Properties) turns super(Material) into super(BlockBehaviour.Properties.of()), and the existing insert-defaults path now constructs a real default for Properties types (not null, which would NPE) so super() becomes super(new Item.Properties()). A static-field nuller neutralizes reads of removed-class constants (Material.IRON) so the field read doesn’t fault before the super call. Default properties only (material-specific settings are lost). At the transform level the block/item now constructs; like the class-move table this is a bytecode building block gated behind the same in-game prerequisites noted above (metadata generation, SRG members, registry idiom). Gated to a 26.1 host. Transform-tested (Forge1122ClassMovesTest).

Fixed

  • The Betweenlands: missing ISound sound class (#113). The 1.12.2 client sound interface net.minecraft.client.audio.ISound was relocated by the 1.17 repackaging to net.minecraft.client.resources.sounds.SoundInstance (same name on 1.20.1 and 26.1), and Retromod didn’t map it. Added the class move (ungated; the target exists on every supported host). It was The Betweenlands’ only transform gap, so the mod should now transform cleanly.
  • Chunk force-loading crash on 26.1 (Chunky and other 1.18-era chunk mods). 26.1 renamed ServerChunkCache.addRegionTicket(TicketType, ChunkPos, int, T) to addTicketWithRadius(TicketType, ChunkPos, int) and dropped the value argument, so an old chunk-loading mod’s 4-arg call had no target on 26.1 and crashed worlds on load. A plain rename can’t fix it (the arity changed), so Retromod gained a generic arg-dropping method redirect (it pops the dropped trailing value, then calls the shorter renamed method) and registered the chunk-ticket case. Verified on the real Chunky jar: FabricWorld now calls the 3-arg addTicketWithRadius. Mechanism tested by ArgDropRedirectTest. (Chunky also mixins ThreadedAnvilChunkStorage, so full Chunky compatibility needs that checked in-game; this removes the addRegionTicket crash.)

[1.2.0-snapshot.5] - 2026-06-29

Fixed

  • Retromod didn’t initialize at all on Forge (#102). Retromod’s main jar registered a Forge IModLocator service (the #78 mods/Retromod/ locator). Forge’s ModDirTransformerDiscoverer claims any mods/ jar that declares that service onto the early service/transformer layer, where it loads before mod discovery and is never scanned as a @Mod. So Retromod loaded as a bare locator, its RetromodForge @Mod entry never ran, and it transformed nothing; an old mod (Cart’s, 1.16.5) was then rejected by Forge’s own version check and looked like Retromod “ignored” it. The service registration is removed, so Retromod loads as a normal Forge mod again. This regressed in snapshot.2 when the Forge locator landed, and the snapshot.2 in-game check only verified the locator loading mods/Retromod/, not Retromod’s own @Mod, so it was masked. The locator class is kept for a future locator-only stub jar; until then mods/Retromod/ loading is NeoForge-only and Forge users place jars directly in mods/. Regression-tested (RetromodForgeModLocatorTest asserts the service stays unregistered).
  • Transform aborted when a mod referenced a class in a protected Mixin package (#102). The transform verifier (a diagnostic that checks a transformed mod’s references against the target MC) loads referenced classes to probe them. When one is a @Mixin class in a package owned by a *.mixins.json, Mixin throws IllegalClassLoadError on the direct load, an Error rather than an Exception. The probes caught only Exception, so the Error escaped and killed the entire transform pass, taking every input mod down with it (seen with the Mine Mine No Mi addon Cart’s, which references a mineminenomi mixin class). The three resolution probes and the verifier’s top-level guard now catch Throwable, so this diagnostic can never abort a transform. Regression-tested (TransformVerifierTest locks the probes’ no-throw contract).

  • CLI/AOT emitted broken ResourceLocation constructor bytecode (CLI did not match an in-game boot). The offline transform/batch/aot paths applied the class-move table but not the ResourceLocation(String) -> Identifier.parse / (String, String) -> Identifier.fromNamespaceAndPath constructor redirects that the Fabric/NeoForge/Forge runtime applies, so any mod that constructs a ResourceLocation got a raw new Identifier(...) call (a constructor 26.1 removed) and would fail to load when prepared via the CLI/AOT, even though the same mod transformed in-game worked. AOT is the recommended NeoForge flow, so this hit the recommended path. Fixed by routing all five transform paths (three runtime entry points + CLI + AOT) through one shared registerIdentifierCtorRedirects helper, removing the “keep in sync by hand” drift. Verified byte-for-byte: 26 classes across a 6-mod corpus that previously emitted invokespecial Identifier.<init> now emit invokestatic Identifier.parse / fromNamespaceAndPath, matching the runtime. Regression-tested (RetromodCliAuxRedirectsTest).

Changed

  • Internal code cleanup and hot-path optimizations, no behavior change. Tightened comments and structure across the source, and applied behavior-preserving performance fixes to the transform core: an O(1) constructor-redirect fast path replacing a per-NEW-opcode key scan, constant sets and regexes hoisted out of per-class loops, single map lookups in the reflection remapper instead of contains-then-get, and a skipped guaranteed-no-op pass in the intermediary remapper. Verified byte-identical transform output across a 6-mod corpus (785 classes, MC 1.16.5-1.20.4 to 26.1) on top of the full test suite.
  • Resource and allocation hardening from a leak audit, no behavior change. Wrapped unclosed Files.walk/Files.list streams and jar.getInputStream() reads in try-with-resources across the AOT, hybrid, quilt, legacy, datapack, and pre-launch paths (file-descriptor leaks), shut down an executor in a finally, deleted temp dirs in a finally, and replaced a per-class-load full-String copy in the JIT agent’s marker scan with a direct byte search. Verified byte-identical transform output and the full test suite.

[1.2.0-snapshot.4] - 2026-06-24

Starts the real Forge -> NeoForge migration for 1.20.1 Forge mods on a NeoForge 26.x host (#85): the registry and event surfaces that nearly every Forge content mod touches. See the roadmap.

Added

  • Forge -> NeoForge registry migration (#85). A 1.20.1 Forge mod’s registration and lookup idioms now translate onto NeoForge 26.x. ForgeRegistries.BLOCKS (and the other 13 vanilla registries) is represented as the vanilla BuiltInRegistries registry instance, which serves both Forge idioms at once: DeferredRegister.create(ForgeRegistries.BLOCKS, id) maps onto NeoForge’s create(Registry, String), and ForgeRegistries.ITEMS.getValue(loc) / .getKey(v) / .containsKey(loc) map onto the matching net/minecraft/core/Registry methods (getValue is get on 26.x; the others are identical). RegistryObject becomes DeferredHolder. This also removes a pre-existing crash: three shims blanket-redirected ForgeRegistries to (three different, wrong) classes, and any such class redirect rewrote the field-read owner before the field redirect could match, so a transformed mod died at <clinit> with NoSuchFieldError: NeoForgeRegistries.BLOCKS. Those broken redirects are gone, and IForgeRegistry is supplied as an empty synthetic marker interface for stray type references. Every mapping (field types, create overloads, Registry method names) was verified against the real NeoForge 26.1 jar. NeoForge runtime only; transform-tested (ForgeRegistryMigrationTest).
  • Forge -> NeoForge event-package migration (#85). NeoForge forked Forge’s event package wholesale (net.minecraftforge.event to net.neoforged.neoforge.event), keeping most class names. Retromod now class-redirects 308 cleanly-renamed event classes (core event/** and client/event/**, top-level and inner), generated by diffing the real Forge 1.20.1 and NeoForge 26.1 jars and shipped as the forge-event-renames.json data table. The handful NeoForge genuinely renamed or merged (EntityJoinWorldEvent to EntityJoinLevelEvent, LivingHurtEvent to LivingDamageEvent, world/* to level/*, and so on) stay hand-mapped and take precedence over the bulk table. Events that were reshaped rather than renamed (the TickEvent phase to Pre/Post redesign, the capability system) are out of scope: a class redirect can’t rewrite a changed handler body. NeoForge runtime only; transform-tested (ForgeEventMigrationTest).
  • Forge -> NeoForge DistExecutor + Dist markers (#85). DistExecutor (the client/server-split helper, DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> clientOnly())) was deleted in the Forge -> NeoForge split, so there is no class to redirect to. Retromod now supplies it as a per-mod-embedded synthetic that reimplements all nine run/call/forDist methods (plus the safe* variants’ serializable SAMs) by delegating to FMLEnvironment.getDist(). The Dist enum itself is package-renamed (net.minecraftforge.api.distmarker.Dist -> net.neoforged.api.distmarker.Dist) and @OnlyIn mapped to a no-op, both wired into the migration shim so they apply on the NeoForge runtime path (the equivalent AnnotationPolyfill redirects only run on Fabric and the CLI). NeoForge runtime only; transform-tested (ForgeNeoForgeSyntheticsTest).
  • Forge -> NeoForge FML lifecycle migration (#85). Every @Mod runs its setup through FML, which NeoForge package-renamed from net.minecraftforge.fml.* to net.neoforged.fml.*. Retromod now class-redirects the 20 mod-facing FML classes NeoForge kept under the same name: the lifecycle and config events (FMLCommonSetupEvent, FMLClientSetupEvent, FMLConstructModEvent, FMLDedicatedServerSetupEvent, FMLLoadCompleteEvent, InterModEnqueueEvent/InterModProcessEvent, ModConfigEvent and its inners) plus IExtensionPoint, InterModComms, ModList, and ModContainer. The table (forge-fml-renames.json) was generated from the NeoForge loader jar (every target verified present) and excludes the FML classes already handled (ModLoadingContext/FMLPaths/@Mod in the migration shim, the FMLJavaModLoadingContext synthetic). This closes the gap the event migration left: it covered event/** but not fml/event/**, where the setup events that every Forge mod subscribes to actually live. NeoForge runtime only; transform-tested (ForgeEventMigrationTest).

Fixed

  • Retromod’s mods/Retromod/ locator crashed NeoForge 1.21.x servers at boot (#100). The locator (the #78 mods-subfolder feature) called ILaunchContext.gameDirectory(), which only exists on NeoForge loader 11.x (MC 26.x); on loader 4.x (MC 1.21.x) that method is absent, so the call threw NoSuchMethodError during mod discovery and crashed the server before any mod loaded. The game directory is now resolved version-safely (reflectively off the launch context where the method exists, otherwise FMLPaths.GAMEDIR, the same route the Forge locator and in-game screen already use), so Retromod itself loads on 1.21.x again. Verified against the real NeoForge 21.1.x and 26.x loader jars; regression-tested (RetromodModLocatorTest asserts no direct gameDirectory() call survives in the compiled locator).
  • The NeoForge in-place runtime transform applied no polyfills (now it does, safely). PolyfillRegistry.loadAndRegister ran only on the Fabric entry, so a NeoForge user transforming mods at runtime silently missed every PolyfillProvider: the Forge -> NeoForge Dist/@OnlyIn redirects (the AnnotationPolyfill ones, distinct from the hand-wired migration-shim copy added earlier this snapshot), the removed-vanilla-class bridges, and the third-party API bridges (GeckoLib 3->4, JEI, EMI, …). RetromodNeoForge now loads them, mirroring the Fabric entry. The 36 providers were re-audited for a Mojang-named runtime, where CLAUDE.md #17 inverts (Mojang-keyed redirects that are dead against intermediary Fabric mods are LIVE against NeoForge mods): intermediary (class_XXXX) and old-MCP/Yarn-named redirects no-op, the Forge -> NeoForge migration applies, and the version-specific removed-class providers (DirectionProperty, LazyLoadedValue, the RenderType accessors, the ItemStack NBT methods) now self-gate on RetromodVersion.TARGET_MC_VERSION so they only fire where the class is actually gone, not on a pre-removal host. The neoforge-category transfer polyfills stay disabled on this path: the host-gated NeoForge_1_21_8_to_1_21_9 shim already owns the 1.21.9 transfer-API rework, so running both would conflict and the un-gated polyfill would NoClassDefFoundError on a pre-1.21.9 host. NeoForge runtime; transform-tested (RetromodNeoForgePolyfillTest, plus host-gating in BlockPropertyPolyfillTest).
  • OptiFine-compat hardening. Closed several hangs in the OptiFine compatibility path: infinite wait-loops in the compatibility dialog (and its broken cancel path) and in five config parsers, an AOT-prompt dialog that could hang, and a hardcoded version reference.

Notes

  • This is the registry + event slice of the Forge -> NeoForge migration. The Forge-host branch (a Forge 26.x runtime, which needs a Forge 26.x jar to verify against) and the deeper API redesigns remain follow-ups; see CLAUDE.md #13.

[1.2.0-snapshot.3] - 2026-06-23

More of the “general update”: closes the constructor-reference gap snapshot.2 flagged, finishes the pre-26.1 Fabric model bridge, and makes the gap report descriptor-aware. See the roadmap.

Added

  • Pre-1.17 Fabric model bridge: the removed abstract model bases (§A1). The bridge that reconstructs Minecraft’s pre-1.17 model system now also synthesizes the AgeableListModel-family abstract bases the 1.17 rewrite removed: AnimalModel (head + body parts), CompositeEntityModel (single parts list), and the tinted AnimalModel subclass. It also class-redirects the dead intermediary names onto them. So a ≤1.16 custom-entity-model mod (the classic pre-26.1 Fabric break) whose model extends one of those now loads on a pre-26.1 host. Pre-26.1 Fabric only; the baby head-scale path is intentionally simplified (babies render at adult scale). Transform-tested (Pre1_17AbstractBaseTest, plus a composed end-to-end Pre1_17ModelBridgeAcceptanceTest).
  • Descriptor-aware gap report (retromod gaps). The gap report now emits a distinct BAD_SIGNATURE verdict when a referenced method/field still exists on the target by NAME but no descriptor variant matches (a signature/type change a name-keyed shim won’t bridge), separate from an outright rename or removal. Backed by hierarchy-aware name probes on the MC symbol index, so “this API changed shape” reads distinctly from “this API is gone.” Tested (ReferenceVerifierTest).
  • Standalone aot <mod> now embeds the Forge→NeoForge deleted-class bridges. The single-mod aot command registers and per-mod-embeds the DeferredSpawnEggItem / FMLJavaModLoadingContext synthetics, matching what batch --aot already did. A one-off aot of a NeoForge mod that references them now produces a working jar instead of a dangling reference.

Fixed

  • Constructor references (X::new) are now rewritten (the gap snapshot.2 flagged). A constructor-reference invokedynamic (e.g. ChunkPos::new captured in a codec lambda by Resourceful Lib, the blocker for Chipped/Handcrafted on 26.x) now has its bootstrap method-handle rewritten from the removed constructor to the redirected static factory (ChunkPos.unpack), so the lambda links at runtime. The direct new X(...) form was already handled; this covers the method-reference form. Also fixed a transform early-skip that bypassed mods whose only redirects were constructor- or field-accessor-based. Tested (CtorReferenceRedirectTest).
  • FMLEnvironment.distgetDist() on NeoForge 26.x. 26.x removed the static dist field, so the near-universal FMLEnvironment.dist client/server check now rewrites to INVOKESTATIC FMLEnvironment.getDist() instead of throwing NoSuchFieldError. Tested (FmlEnvironmentDistTest).

[1.2.0-snapshot.2] - 2026-06-21

Continues the “general update”: the deeper transform-engine fix and the rest of #78. See the roadmap.

Added

  • Forge mods/Retromod/ locator (#78) + Forge MC 26.2 builds. Now that Forge has shipped a 26.2 build (forge 65.x), Retromod’s RetromodForgeModLocator implements Forge’s net.minecraftforge.forgespi.locating.IModLocator (registered as a mods/-scanned service, the Forge analogue of NeoForge’s early-service locator), so mods/Retromod/ loads in-place on Forge too, completing #78 across all three loaders. It reflection-delegates to Forge’s own ModsFolderLocator for the jar→IModFile step and soft-fails to a no-op if that ever moves, so it can’t disrupt Forge discovery. build-all.sh now produces Forge +26.2 jars.
  • AOT-first guidance for NeoForge (#95). On NeoForge, Retromod’s in-place transform runs at mod-constructor time, after the module layer is built, so module-layer failures (split packages, skipped Forge-named JiJ jars) crash before Retromod can act. When Retromod transforms mods in place on NeoForge it now recommends running the offline/AOT transform once (retromod prepare <minecraft-dir> / retromod batch mods/), which processes the jars before the loader scans and removes that whole “too late” class of failure. The offline tooling already existed (the prepare/batch commands + the Full AOT cache); this makes it the recommended NeoForge flow.
  • 26.2 removed-field polyfill: per-color block fields → ColorCollection (new static-field→accessor transform). 26.2 deleted the 16-per-color Blocks fields (candles, candle cakes, shulker boxes, terracotta; 64 fields total: WHITE_CANDLE, BLACK_SHULKER_BOX, …) in favour of Blocks.DYED_CANDLE etc. (a ColorCollection<Block> indexed by DyeColor). A 1.21.x mod reading Blocks.WHITE_CANDLE hits NoSuchFieldError on 26.2. The new registerStaticFieldAccessor transform rewrites the GETSTATIC into Blocks.DYED_<FAMILY>.pick(DyeColor.<COLOR>) + checkcast Block, a field access that has to become a method call, which a plain field redirect can’t express. The 26.1→26.2 shim registers all 64 (Fabric/NeoForge/Forge). Found while transforming YUNG’s Extras (SwampFeatureProcessor builds a candle list); it only fires when targeting 26.2, since the fields still exist on 26.1. Transform-tested (DyedBlockAccessorTest).
  • Jar-in-Jar recursion (#95). Retromod now recurses its full transform (bytecode rewrite and metadata relaxation) into the libraries a mod bundles in META-INF/jars/ (Fabric) and META-INF/jarjar/ (NeoForge/Forge), and into jars nested inside those, depth-capped and soft-failing per jar. Previously a bundled library that referenced a relocated class loaded broken or was reported “missing”, and a Forge-built nested library on a NeoForge host was skipped at scan; the nested Forge lib now also gets the mods.tomlneoforge.mods.toml promotion. Applies across all four pipelines: the offline transform/batch and AOT compiler, and the Fabric and NeoForge/Forge runtime paths. Regression-tested (NestedJarRecursionTest: a nested and a doubly nested library both get transformed).
  • CLI --target <mc-version> flag. retromod transform/batch/aot were hardcoded to target MC 26.1, so a jar prepped offline for a 26.2 pack never received the 26.1→26.2 fixes (the candle ColorCollection polyfill, the advancements package moves, …). --target 26.2 now preps mods for that host, required for the offline/mods/Retromod/-locator deploy path on 26.2. Verified end-to-end: YUNG’s Extras + Better Strongholds + YUNG’s API transformed with --target 26.2 load to the title screen on a 26.2 NeoForge instance (Better End Island loads too, with its dragon-fight overhaul soft-failed; see incompatible-mods).
  • NeoForge deleted-class bridges, embedded per-mod (#52, #85). NeoForge deleted some classes that 1.20.1-1.21.x mods still reference, so they can’t be redirected to anything that exists. They have to be supplied. A new per-mod embedding engine (SyntheticEmbedder) generates an ASM bridge for each and embeds it into the mods that reference it, under a unique com/retromod/embedded/<mod>/ package, rewriting that mod’s references there. Embedding under a Retromod-owned, per-mod package is deliberate: the deleted classes’ original packages are still owned by the loader/neoforge JPMS modules, so embedding a bridge at its original name would split-package with the loader (a hard crash) and two mods embedding it would split-package with each other (see CLAUDE.md #13). The first two bridges:
    • DeferredSpawnEggItem (#52), deleted in 26.x. 26.2’s SpawnEggItem takes only Item.Properties (the entity type moved to a data component), so the bridge extends SpawnEggItem and its old (Supplier,int,int,Item.Properties) constructor best-effort sets the type via Item.Properties.spawnEgg(EntityType) (the supplier is resolved in a try/catch, so registration order can’t crash construction) before calling super. Verified end-to-end on Luminous: Nether (a 1.21.1 MCreator mod) targeted to 26.2: the bridge embeds as a valid SpawnEggItem subclass and the mod’s references rewrite to the embedded copy, with zero copies left at the loader name.
    • FMLJavaModLoadingContext (#85), deleted in the Forge→NeoForge split. get().getModEventBus() bridges to ModLoadingContext.get().getActiveContainer().getEventBus() (verified against NeoForge loader-11.0.13.jar).
    • Wired into both the runtime (RetromodNeoForge, gated on the original being absent on the host) and the offline transform/batch paths (registered for NeoForge/Forge mods on 26.1+ targets; a cross-loader mod that ships both mods.toml + neoforge.mods.toml is detected as “forge” yet runs on NeoForge, so both JPMS loaders are covered; the embed is reference-gated, so a pure-Forge mod with no net/neoforged/ references is a no-op). Transform-tested (ForgeNeoForgeSyntheticsTest, SyntheticEmbedderTest).
  • 26.x worldgen/data-pack JSON migration for bundled mod data (ModDataMigrator), now across every transform path. The transform rewrote bytecode + loader metadata but never the data/ JSON a mod bundles. Two classes of 26.x data change broke structure mods, and both are now handled (gated to 26.x targets):
    • Strict JSON parsing (the big worldgen blocker). 26.1 parses data-pack JSON with a strict Gson reader, so the // / /* */ comments and trailing commas that hand-authored worldgen JSON has relied on for years (Jigsaw template_pools, processor_lists) now throw MalformedJsonException and leave the registry entry unbound. One mod’s unbound template_pool is FATAL (FatalStartupException) and aborts the shared worldgen registry load, so co-loaded structure mods then look like they never registered their own worldgen types (“Unknown registry key” / “Unbound values”). ModDataMigrator now strips comments + trailing commas in a string-aware pass (a // inside a URL string value is preserved). Verified on a headless 26.1.2 NeoForge server: Philips Ruins went from FatalStartupException to a clean Done with worldgen, and a co-loaded YUNG’s Extras (whose own custom Feature/PlacementModifierType registration was fine all along, just collateral to the shared-load abort) generates too.
    • Mechanical format rewrites. minecraft:chain->minecraft:iron_chain (the plain chain item split into iron/copper in 26.x), dyed_color object {"rgb":N}->bare int, custom_model_data int->{"floats":[N]} (1.21.4+ object form), advancement icon "item"->"id", and the minecraft:potion entity (in entity_type tags) ->minecraft:splash_potion + minecraft:lingering_potion (26.x’s ThrownPotionSplitFix; the potion item is untouched, so this is scoped to entity_type tag files). Now wired into every path, not just the offline transform/batch: the Fabric and NeoForge/Forge runtime transforms (FabricModTransformer/ForgeModTransformer, so in-place + retromod-input/ data migrates too), the Jar-in-Jar recursion (both loaders), the nested-jar CLI path, and the metadata-only “compatible mod” branch. Verified end-to-end on When Dungeons Arise (1.21.1): data-parse failures went from 20 to 0 (the previous residual was the minecraft:potion entity-type split, now fixed). Tested (ModDataMigratorTest, 26 cases, plus a ForgeModTransformer.transformMod runtime integration test).

Fixed

  • forge-config-api-port ConfigTracker VerifyError (#94 follow-up), broad reach. Transforming a mod that bundles Forge Config API Port produced a VerifyError in ConfigTracker.loadConfig. When ASM recomputes stack-map frames it must merge the two caught exception types (java.io.IOException and night-config’s ParsingException), but ParsingException lives in a Fabric Jar-in-Jar invisible to the transformer, so Retromod’s getCommonSuperClass fallback typed the caught value as Object where the rethrow needs a Throwable. The fallback now returns Throwable whenever either operand is one (determined by reading class bytes, not Class.forName), so the frame verifies. Fixed in both transform writers (RetromodTransformer, FabricModTransformer). Forge Config API Port is bundled by a large number of ported mods, so this reaches far beyond the original report; verified at the bytecode level (the corrupt Object frames are now Throwable).
  • retromod batch and AOT under-transformed mods: the vanilla class moves were missing. Both the batch JIT path and the AOT compiler registered only the version-shim chain, while the single-mod transform (and an in-game boot) additionally layer on the vanilla 26.1 class-move table, loader-matched API shims, and the Fabric member mappings. So mods prepped through batch/AOT (the recommended NeoForge offline flow) kept pre-26.x class names: e.g. a 1.21.x mod’s mixin @Shadow of EndDragonFight was never relocated to 26.x’s EnderDragonFight, and the mixin failed to apply. All three CLI paths now apply the same set via a shared registerAuxiliaryRedirects step. Found while transforming YUNG’s Better End Island onto 26.x.
  • Fabric intermediary member mappings were wrongly applied to NeoForge/Forge mods. The intermediary→Mojang member mappings (class_/method_/field_XXXX) are Fabric-only: distributed Fabric mods carry intermediary names, but NeoForge/Forge mods are already Mojang-named. Applying them to a NeoForge mod clobbered correct Mojang fields: it renamed YUNG’s API’s Blocks.WHITE_CANDLE to a name 26.2 no longer has, crashing construction with NoSuchFieldError. The member mappings are now gated to Fabric mods in every CLI transform path, mirroring the runtime split (RetromodPreLaunch registers them on Fabric; RetromodNeoForge never did). Class moves stay loader-agnostic (vanilla relocations apply everywhere). Regression-tested (RetromodCliAuxRedirectsTest).
  • batch under-transformed mods with unreadable version metadata. A mod whose mods.toml/neoforge.mods.toml ships a literal ${…} placeholder or otherwise unparseable Minecraft version (a common build misconfiguration, e.g. some MCreator output) read as “unknown version”, which batch treated as “already compatible” and only metadata-patched, skipping the class moves, removed-API polyfills, and deleted-class bridges. On a 26.x target such a mod then crashed at load/construct (NoSuchFieldError / NoClassDefFoundError). batch now forces the full bytecode transform whenever it can’t confirm a mod is 26.x-native (a genuine 26.x mod has readable 26.x metadata and is unaffected, and the class moves no-op on already-26 names, so a false positive is harmless). Found via Luminous: Nether, whose neoforge.mods.toml ships ${minecraft_version_range}.
  • 26.x member redirects + method-reference-aware renames. Three more 26.x API changes that broke decoration mods (Chipped, Handcrafted) on 26.1 now translate: ItemNameBlockItemBlockItem (26.x folded that BlockItem subclass into BlockItem; same constructor), ResourceKey.location()identifier(), and ChunkPos(long)ChunkPos.unpack(long). The ResourceKey rename needed a new capability: RetromodTransformer.registerMethodRename routes an owner-scoped method rename through the ClassRemapper’s mapMethodName, so it rewrites method references (e.g. ResourceKey::location captured in a codec lambda) as well as direct calls; the existing method-redirect pass only saw direct INVOKE* sites. Helps any mod that method-references a renamed method, not just these. (Constructor references such as ChunkPos::new in Resourceful Lib’s codec library are a known remaining gap, tracked.)

[1.2.0-snapshot.1] - 2026-06-18

The “general update”: the deep work redirects can’t do, plus the transform-engine and library fixes the 1.1.0 issue stream surfaced. See the roadmap for the full plan (pre-26.1 Fabric bridge, deep-API polyfills, the forge-config-api-port ConfigTracker VerifyError, JiJ-recursion, AOT-first NeoForge, the CurseForge-export locator, …).

Added

  • mods/Retromod/ loading for CurseForge-export compatibility (#78). Retromod can now load jars from a dedicated mods/Retromod/ subfolder, so Retromod (on both Modrinth and CurseForge) and its transformed *-retromod.jar outputs can ship as CurseForge pack overrides instead of top-level mods/ jars, which CurseForge pack export rejects.
    • NeoForge: a custom IModFileCandidateLocator (registered as an FML early service) discovers the subfolder’s jars and feeds them to mod discovery, loaded in-place, no restart. Verified against NeoForge loader 10.x/11.x (MC 1.21.0 → 26.2).
    • Fabric: since Fabric has no locator SPI (pre-launch runs after mod discovery), RetromodPreLaunch drains mods/Retromod/: it moves the loader-ready jars into mods/ and shows the usual one-time restart prompt. Alternatively, launch with -Dfabric.addMods=<gamedir>/mods/Retromod to load them in-place with no restart (Retromod detects this and skips the drain).
    • Follow-ups: Forge (a different forgespi SPI) and the thin CurseForge “Retromod Loader” distribution stub.

[1.1.0] - 2026-06-17

The stable 1.1.0 release, the 26.x-coverage line. It carries everything from the snapshot/rc track: the full-catalog audit and the fixes it forced, ~20 renamed/removed-event API bridges (verified firing in a live world), the Cardinal Components package move, the Tuple polyfill, NeoForge redirect corrections, first-day MC 26.2 support (Fabric + NeoForge), the two #94 fixes (both confirmed in-game), and the first round of Vulkan compatibility for 26.2’s new renderer. (One deeper issue, a VerifyError in forge-config-api-port’s ConfigTracker, pre-existing and unrelated to these fixes, is tracked for 1.2.0.)

Added

  • MC 26.2 NeoForge builds. 26.2’s Vulkan-capable client now has a NeoForge loader, so the build matrix produces retromod-1.1.0+26.2 for Fabric and NeoForge. Forge still has no 26.2 build, so no Forge 26.2 jar is shipped (the matrix entry is gated and flips on automatically once a Forge 26.2 release exists). The 26.1→26.2 transform shims were already in place since snapshot.4. NeoForge’s only 26.2 build so far is a pre-release (26.2.0.0-beta); since Maven version ordering sorts a -beta qualifier below the bare release, the dependency floor is [26.2.0.0-beta,) (not [26.2,), which would reject the beta). Verified in-game that the jar loads against 26.2.0.0-beta.
  • Vulkan compatibility: prefer the OpenGL backend for old mods (Tier 0). 26.2 adds a Vulkan rendering backend alongside OpenGL and makes Vulkan the default. Old mods doing raw OpenGL render correctly on the still-present OpenGL backend but not on Vulkan, and translating arbitrary GL→Vulkan in bytecode isn’t feasible. So on a 26.2+ client GraphicsBackendCompat sets preferredGraphicsBackend:"opengl" in options.txt. It is non-destructive (only when you haven’t chosen a backend; an explicit vulkan choice is respected with a warning) and opt-out via -Dretromod.graphics.noPreference=true. Mods using Minecraft’s modern render API are unaffected (they run on Vulkan too). See the roadmap for the full OpenGL→Vulkan strategy, including the 26.3 (OpenGL-removed) plan.
  • Render-state soft-fail for removed RenderSystem setters (Tier 2). The imperative RenderSystem state calls (enableBlend/blendFunc/depthMask/colorMask/…) were deleted in the blaze3d GpuDevice/RenderPipeline refactor with no method to redirect to. The transformer can now neutralize such a removed call (drop it, pop its args, push a default return) so an old mod loads and runs instead of crashing with NoSuchMethodError. The state is inert (soft-fail), not translated. New registerRemovedMethodNeutralize transform API + RemovedRenderStateNeutralize shim, wired for Fabric, NeoForge, and Forge; covered by a load-and-run regression test.

Fixed

  • #94: mods that bundle Forge Config API Port crashed on 26.1 with NoSuchMethodError: PayloadTypeRegistry.configurationClientbound() (CoroUtil / Watut, Fabric 26.1.2). fabric-api renamed the static PayloadTypeRegistry accessors twice (configurationClientbound()/playClientbound()/… (≤0.111) → configurationS2C()/playS2C()/… (1.21.4) → clientboundConfiguration()/clientboundPlay()/… (26.1)), and a JiJ’d library built against either older fabric-api hits the gone name on a 26.1 host. The 26.1 Fabric shim now maps both old generations to the 26.1 names (S2C = server→client = clientbound; C2S = serverbound), verified against fabric-api 0.119.4 and 0.145.4. Forge Config API Port is a dependency of a large number of mods, so this unblocks well beyond the two reported. Verified in-game: the error is gone.
  • Retromod was breaking every mod that uses Forge Config API Port. ForgeConfigApiPortShim redirected net.minecraftforge.common.ForgeConfigSpec (and $Builder, ModLoadingContext, ModConfig$Type) onto fuzs.forgeconfigapiport.api.config.v2.* targets that don’t exist. The Fabric port re-implements the Forge config classes at their original net.minecraftforge.* names (the bundled port ships 17 ForgeConfigSpec classes there), so a mod’s reference already resolves with no redirect needed. The bogus redirect rewrote working references onto absent classes → NoClassDefFoundError, and was even inconsistent (it moved ForgeConfigSpec/$Builder but left $ConfigValue/$IntValue at the old name). Caught while verifying #94 (CoroUtil crashed at ModConfigDataFabric.writeConfigFile). The shim’s redirects are removed; the port provides the originals. Regression-tested; verified in-game the NoClassDefFoundError is gone.

Docs

  • #95: Troubleshooting entry for the JPMS “Modules X and Y export package Z” error when both names are mods: a duplicate library (one bundled jar-in-jar, one standalone). The fix is to remove the standalone copy; the crash happens at module-layer build before Retromod runs, so it can’t be patched at runtime. Roadmap notes the deeper gap (recursing transform + metadata-patching into jar-in-jar dependencies, entangled with Forge→NeoForge for Forge-built nested libs).

[1.1.0-snapshot.5] - 2026-06-14

The rc-stabilization snapshot: the API bridges from snapshot.4 are now verified firing in a live world (an in-game harness caught four that didn’t load), plus the scoped coverage items (Tuple, the full Cardinal Components package move) and two issue fixes.

Fixed

  • The renamed-SAM bridge synthetics from snapshot.4 didn’t load, caught by a new in-game bridge-verification harness. A test mod compiled against the old (1.20.1) Fabric APIs now registers a listener through each bridge on a 26.1 host (exercising the real invokedynamic lambda sites), and the first run failed 4 of 10: the generated interfaces used ClassFormatError-illegal field modifiers (interface fields must be ACC_FINAL) and the reflective-EVENT <clinit> lacked stack-map frames (VerifyError). Both fixed in SamBridgeSynthetics; a new load-and-initialize JUnit test (with a stub Event) now defines the generated classes in a real classloader so this class of bug fails in CI, not in-game. After the fix: 0 failures, with ConventionalItemTags, EntityModelLayerRegistry, and LivingEntityFeatureRendererRegistrationCallback confirmed firing (handler actually invoked by the game), the rest registering cleanly.
  • Pathfinding enum renames (PathType). The same in-game pass caught a real gap behind LandPathNodeTypesRegistry: 1.21.2’s pathfinding refactor renamed the whole damage family (DAMAGE_FIREFIRE, DANGER_FIREFIRE_IN_NEIGHBOR, DAMAGE_OTHERDAMAGING, DANGER_OTHERDAMAGING_IN_NEIGHBOR), so a 1.20.1 mod registering custom path types hit NoSuchFieldError. Field redirects added to both the Fabric and NeoForge 1.21.1→1.21.2 shims.
  • #92: a NeoForge mod built for ≤1.20.6 crashed with IllegalAccessError constructing a ResourceLocation (Rings of Ascension on a 1.21.1 host: new ResourceLocation(namespace, path) → “tried to access private method ResourceLocation.<init>(String,String)”). That 2-arg constructor was made private in 1.21 in favour of the static factory fromNamespaceAndPath; the Fabric 1.20.6→1.21 shim already rewrote the ctor, but the NeoForge chain never did, so any NeoForge mod constructing a ResourceLocation by ctor crashed at <clinit> on a 1.21+ host. Added the constructor→factory redirect to the NeoForge 1.20.6→1.21 shim (covers 1.21.x and 26.1 hosts). Transform-level regression test included.

Added

  • util/Tuple polyfill (26.2 removal). 26.2 removed net.minecraft.util.Tuple; a trivial mutable-pair reimplementation is now redirected in for mods that use it (both the Mojang name and intermediary class_3545). Host-gated to 26.2+: Tuple still exists on 26.1, where redirecting it would point live code at the stub.
  • Cardinal Components: the full dev.onyxstudios.ccaorg.ladysnake.cca package move (CCA 5→6). The shim previously hand-curated ~7 of the famous classes; harvesting the real jars (5.2.3 vs 6.1.3) by sub-path showed the move is overwhelmingly a clean package rename, so 49 more public api/ classes (every one verified present in both, inner classes included) now redirect, turning “whole CCA package missing → NoClassDefFoundError” into a resolved package for the CCA-dependent ecosystem (Origins, Trinkets-style mods, etc.). The classes CCA 6 genuinely removed are deliberately left unredirected (no phantom targets): the item-component family (ItemComponent, ItemComponentFactoryRegistry, CcaNbtType, since CCA 6 dropped item components in favour of vanilla data components), PlayerComponent/PlayerCopyCallback, and the block/util/Sided*Compound helpers; mods using those need the 6.x API and are beyond a redirect. The curated method/field redirects for API-level changes are unchanged.

[1.1.0-snapshot.4] - 2026-06-11

Opened for the fixes that landed after snapshot.3 was published, plus a codebase-wide hang/dialog-bug sweep prompted by the OptiFine fix, plus first-day support for the upcoming Minecraft 26.2. The Compatibility DB fix is docs-only and goes live as soon as the site is pushed, no jar release involved.

Added

  • Seven more Fabric APIs un-trapped: the renamed-SAM bridge batch. Seven event/registry interfaces whose 26.1 successor renamed the functional method were either covered by a plain class redirect (a “lambda trap”: LambdaMetafactory throws the moment a mod registers a listener) or not covered at all. Each now gets the extends+default synthetic treatment (old SAM stays the only abstract method so old lambdas keep linking; a default method forwards the new SAM; EVENT fields are mirrored reflectively and soft-fail to inert), generated by a new shared SamBridgeSynthetics helper and verified against the real fabric-api jars (0.141.4+1.21.11 vs 0.145.4+26.1.2): ClientWorldEvents (field and SAM renamed, so the old redirect was a double break), ServerEntityWorldChangeEvents (ditto, and the old redirect’s source path event/lifecycle/v1/ never existed; the real class is at entity/event/v1/, so this event was never actually bridged), LivingEntityFeatureRendererRegistrationCallback (registerRenderersregisterLayers), DrawItemStackOverlayCallback (onDrawItemStackOverlayonExtractItemDecorations), TooltipComponentCallbackClientTooltipComponentCallback (non-void SAM, previously unbridged), ServerChunkEvents$LevelTypeChange$FullChunkStatusChange (inner + field redirect; the outer holder survives), and LandPathNodeTypesRegistryLandPathTypeRegistry (class+method renames plus two provider-SAM synthetics, previously unbridged). All gated 26.1+ (the old APIs are alive on older hosts). Like the other reflective bridges: transform-tested, needs the shared in-game pass.
  • Minecraft 26.2 support (verified in-game on 26.2-rc-1: the Fabric test mod passes 214/214 after transform). New 26.1→26.2 shims for all three loaders carrying 188 verified class moves/renames plus 209 field redirects. 26.2 also extracted every registry constant out of EntityType and BlockEntityType into new EntityTypes/BlockEntityTypes holder classes (the Blocks/Items pattern), which the first in-game run caught as 27 NoSuchFieldErrors before the field redirects went in. Harvested by the new scripts/harvest-mc-diff.py (member-fingerprint matching over the real 26.1.2 and 26.2-rc-1 client jars, both still Java 25 bytecode, so the bundled ASM 9.8 reads 26.2 as-is) and hand-checked. The bulk: 26.2 split advancements/criterion/* into advancements/triggers/* + advancements/predicates/*. Notable refactors covered: Slime/MagmaCube moved to monster/cubemob/ with an extracted AbstractCubeMob base (Slime’s AI inner classes followed it; SlimePredicateCubeMobPredicate), the contextualbar renderers dropped their Renderer suffix, and SubmitNodeStorage’s per-feature *Submit records moved into renderer/feature/*FeatureRenderer$Submit. Version aliases cover the whole 26.2 pre/rc/snapshot family, and the Fabric build matrix gains a 26.2 target (Forge/NeoForge 26.2 jars stay gated off until those loaders actually ship 26.2 builds). Deliberately untouched: client/gui/Gui, which 26.2 split (HUD half went to the new Hud class) but did not remove. Redirecting it would hijack a live class. Known 26.2 removals that need future bridges, on the list: MultiBufferSource (huge, most rendering mods reference it), the Tesselator/VertexFormat old vertex layer, and util/Tuple.

Fixed

  • #87: a transformed 1.20.1-era mod could kill the whole NeoForge module layer at boot (ResolutionException: Modules blueprint and mixin_synthetic export package org.spongepowered.asm.synthetic.args). Blueprint 7.x (and mods built from its template) ship a placeholder Dummy.class in Mixin’s runtime-generated args package, an old-Forge hack so the mod’s module exports that package. NeoForge 1.20.2+ has its own mixin_synthetic module that owns it, so the moment any module reads both exporters (in the report, Connector Extras’ ModMenu bridge), JPMS refuses to build the layer and the game dies seconds into launch. The transform now strips org/spongepowered/asm/synthetic/ from mod jars on NeoForge 1.20.2+ hosts (nothing references the dummy, so removal is safe; empty parent dirs are pruned so the jar stops declaring the packages; a mod that shades full Mixin keeps its other org/spongepowered/ content). Old Forge hosts are deliberately left alone, since the hack is still load-bearing there. Verified against the real blueprint-1.20.1-7.1.4.jar; covered by a JUnit test of the strip and a NeoForge test-mod case that ships the dummy on purpose and asserts it’s gone after transform. Note: getting past the module layer doesn’t promise Endergetic itself runs on a 1.21.1 NeoForge host. A 1.20.1 Forge-API mod on modern NeoForge is the known Forge→NeoForge migration limitation tracked for 1.2.0.
  • A corrupted mod jar could hang the game mid-transform: five descriptor parsers had infinite loops on malformed input. They advanced with indexOf(';') + 1, so a method descriptor whose object type never closes with ; (corrupted download, hand-crafted jar) reset the cursor to 0 and spun forever. In RetromodTransformer’s constructor-defaults writer it also emitted bytecode on every pass. One had a disguised version: FuzzyMethodResolver’s “malformed” guard was a break inside a switch arrow-rule, which exits the switch, not the loop, so it spun too. All parse loops (TransformVerifier, FabricModTransformer, MixinCompatibilityTransformer, FuzzyMethodResolver, RetromodTransformer) now bail out on a missing ;, and ObfuscationRemapper passes the malformed tail through instead of throwing. Covered by regression tests that run the parsers on truncated descriptors under a preemptive timeout: with the bug they’d hang, not fail.
  • The Full AOT progress dialog could stay open forever if compilation failed. Listeners only heard onComplete on the success path, so an exception (or an Error from bytecode work, which wasn’t caught at all) left the modal dialog sitting at N% looking like it was still compiling, and leaked the progress listener into the next run. Completion is now notified in finally (every exit path closes the dialog) and the listener is removed even when the future completes exceptionally.
  • The in-game manager’s “native version available on Modrinth” check asked about MC 1.21.1 no matter what the actual game version was, hardcoded since before auto-detection existed. It now uses the detected target version, so the offer is made (and skipped) for the right MC.
  • Compatibility DB: the 🔬 Audit tab never opened. Clicking it hid the community reports but the audit panel itself stayed invisible: the show/hide toggle cleared the panel’s inline style, which fell straight back to the stylesheet’s display:none, so the page collapsed to the “Add your report” form. The same inline-vs-stylesheet bug also kept the “No reports match” message and the bundle-mods box (when picking Bundle in the report form) from ever appearing. All three now set an explicit display value, verified by driving the page in a real browser (tab switch both ways, 5,751 rows loading and paginating, verdict filters, search, bundle toggle, empty state). The community medal legend now also hides in audit mode, since it describes report badges, not audit verdicts.
  • The OptiFine warning dialog could hang mod loading forever. After showing the warning it polled every 100 ms for any visible Swing dialog in the JVM and waited until there was none, so any unrelated dialog (Retromod’s own restart prompt, another mod’s window) kept the mod-transform thread stuck indefinitely; conversely, if the warning was slow to appear the “wait” exited immediately. The user’s Cancel choice was also thrown on the Swing event thread, where the transformer’s cancelled-check could never see it, so Cancel never actually cancelled. The dialog now blocks cleanly via invokeAndWait (no polling to wedge), returns the choice to the loading thread so Cancel really aborts the transform, and is skipped entirely in headless environments.

[1.1.0-snapshot.3] - 2026-06-10

Still in development. The two crash fixes below (#82, #84) turned up after snapshot.2 went out, so they roll into this build. The authenticity line changed wording, the tick-event SAMs got their 26.1 rename, and the audit-driven API bridges for the bigger remaining blockers are in progress.

Added

  • Bridge for the removed Fabric HudRenderCallback (single global HUD-render event), sole-blocking about 6 HUD-overlay mods. 26.1 replaced it with the layered hud/HudElementRegistry. The synthetic keeps the old onHudRender SAM and extends the new HudElement, bridging the renamed SAM with a default method, so old lambdas link, every listener is a HudElement, and the event’s combined invoker is attached to the registry as one layer. Needs the same in-game pass as the other bridges; fails soft.
  • The removed Fabric convention tags v1 (tag/convention/v1/Conventional*Tags) now redirect to v2: all six holder classes, plus explicit field renames for the 19 tags whose v2 name changed (SHEARSSHEAR_TOOLS, RAW_ORESRAW_MATERIALS, the bucket singulars→plurals, the gem renames, …), each verified against the real fabric-api jars (0.92.7 for v1, 0.145.4 for v2). 54 of 79 item-tag fields kept their names and ride the class redirect untouched; the handful with no certain successor degrade to a per-field NoSuchFieldError instead of the old whole-class NoClassDefFoundError.
  • Ten more 26.1 Fabric API renames, verified against fabric-api 0.145.4 and the official migration map: the particle API’s factory→provider naming (ParticleFactoryRegistry, its $PendingParticleFactory inner (SAM method unchanged, so lambda-safe), FabricSpriteProviderFabricSpriteSet, ParticleRendererRegistryParticleGroupRegistry, FabricBlockStateParticleEffectFabricBlockParticleOption), AtlasSourceRegistrySpriteSourceRegistry, ComponentTooltipAppenderRegistryItemComponentTooltipProviderRegistry, PointOfInterestHelperPoiHelper, FabricTrackedDataRegistryFabricEntityDataRegistry, and FabricGameRuleVisitorFabricGameRuleTypeVisitor.
  • NeoForge: BlockEvent$BreakEventlevel/block/BreakBlockEvent (26.1 moved block events into a subpackage; same constructor shape) and the RenderLevelStageEvent stage renames ($AfterEntities$AfterOpaqueFeatures, $AfterParticles$AfterTranslucentParticles), all verified directly against the NeoForge 26.1.2 universal jar.
  • The settings screen now shows the build’s authenticity status (Verified build / Modified / Not Retromod / Unknown) on a small line above Done, the same status the launch log reports. The docs described this line but it had never actually been built; the docs and the screen now agree.
  • Bridge for the removed Fabric item-group events v1 API (itemgroup/v1/ItemGroupEvents), adding items to creative tabs, the single biggest functional-interface gap in the audit at about 83 mods. The successor creativetab/v1/CreativeModeTabEvents renamed both the registration method (modifyEntriesEventmodifyOutputEvent) and the SAM method (ModifyEntries.modifyEntriesModifyOutput.modifyOutput), so a class redirect would both NoSuchMethodError on the lookup and lambda-trap the handler. The bridge injects a synthetic ItemGroupEvents holder (keeping modifyEntriesEvent and the MODIFY_ENTRIES_ALL field) plus synthetic ModifyEntries/ModifyEntriesAll interfaces that keep the old SAM names, and wires each to its live v2 event by reflection. The entries object also renamed its mutators (addAfter/addBeforeinsertAfter/insertBefore); all 12 overloads of each are renamed with descriptors taken verbatim from fabric-api 0.145.4, and a transform test confirms the class redirect and the method rename compose (so FabricItemGroupEntries.addAfter really becomes FabricCreativeModeTabOutput.insertAfter). Bytecode and the redirect composition are tested; the reflective event wiring still needs an in-game check and fails soft (logged, inert) if it can’t wire up.
  • Bridge for the removed Fabric command v1 API (command/v1/CommandRegistrationCallback), server command registration, sole-blocking about 38 mods in the audit. The v1 SAM is the 2-arg register(dispatcher, dedicated); the surviving v2 one is the 3-arg register(dispatcher, CommandBuildContext, CommandSelection), so a class redirect would crash LambdaMetafactory (the mod’s handler lambda hard-codes the 2-arg shape). The bridge keeps a synthetic v1 interface (the 2-arg SAM plus the EVENT field the mod reads) and its static initializer wires EVENT to the live v2 callback by reflection, mapping the v2 CommandSelection down to the v1 dedicated boolean (DEDICATED → true). This one is clean because the v1 lambda body only touches the brigadier CommandDispatcher (a stable, never-remapped type), so nothing inside the handler needs adapting. The bytecode is tested; the reflective wiring still needs an in-game check, and it fails soft (logged, callbacks inert) rather than crashing if it can’t wire up.
  • Bridge for the removed Fabric ServerWorldEvents lifecycle API (server level load/unload), sole-blocking about 21 mods. 26.1 renamed both the holder (ServerWorldEventsServerLevelEvents) and the SAM methods (onWorldLoad/onWorldUnloadonLevelLoad/onLevelUnload), so the old class redirect was a lambda trap. The bridge keeps synthetic ServerWorldEvents (with LOAD/UNLOAD) and $Load/$Unload SAM interfaces at the old names, wired to ServerLevelEvents by reflection (forwarding 1:1; the only param change, ServerWorldServerLevel, is handled by the harvest). Replaces the previous straight redirect. Bytecode tested; the reflective wiring needs an in-game check and fails soft.
  • Bridge for the removed Fabric EntityModelLayerRegistry entity-model-layer API, sole-blocking about 18 mods. The successor ModelLayerRegistry renamed the provider SAM (TexturedModelDataProvider.createModelDataTexturedLayerDefinitionProvider.createLayerDefinition), a lambda trap, but the return type is unchanged (Yarn TexturedModelData is Mojang LayerDefinition, the same class). The bridge keeps a synthetic EntityModelLayerRegistry holder and a TexturedModelDataProvider SAM at the old name; registerModelLayer wraps the old provider in the new one (calling the old createModelData and returning its LayerDefinition) and forwards to ModelLayerRegistry. Bytecode tested; the reflective wiring needs an in-game check and fails soft.
  • Tick-event inner interfaces now carry their 26.1 rename on the Fabric chain: ClientTickEvents$StartWorldTick/$EndWorldTick and the ServerTickEvents pair redirect to $StartLevelTick/$EndLevelTick. The 26.1 Fabric API finished the World→Level rename on these nested SAMs while leaving the outer class and the onStartTick/onEndTick method untouched, so this is a plain redirect, not a re-typed handler, verified against fabric-api 0.145.4 that the host ships only the $*LevelTick inners with the same descriptor. The entries already existed in the 1.16.5→1.17 shim, but that chain only covers ≤1.16 mods; a 1.19-1.21 mod reaches 26.1 through the 26.1 shim, which didn’t have them, so ClientTickEvents$EndWorldTick was a sole blocker for about 10 mods in the audit.

Changed

  • The 26.1 Fabric API bridges (item-group events, ServerWorldEvents, EntityModelLayerRegistry, client-networking v1) now register only on 26.1+ hosts. On a pre-26.1 host those APIs still exist in the Fabric API, so redirecting them would have hijacked working APIs and wired them to 26.1-only successors (the #21/#31 class of bug, caught in review before release, never shipped). The command v1 bridge stays unconditional on purpose: command/v1 has been gone since the 1.19 era on every modern host, its command/v2 target exists from 1.19.1 on, and its bridge is namespace-agnostic. The Fabric pre-launch now also publishes the detected host version before shims register, so this kind of gate works during pre-launch at all.
  • The build authenticity check now reports VERIFIED instead of OFFICIAL when the embedded self-hash matches. The old wording overclaimed: a match means the running bytecode is byte-for-byte what was published, but the hash carries no secret key, so anyone can recompute it after editing. It can’t prove a build is the genuine one. “Verified” says what’s actually true (the hash checks out), and the in-game line now reads Verified build. isOfficial() is kept as a deprecated alias for the new isVerified().

Added (docs)

  • A community Compatibility DB as its own ProtonDB-style section (/compatdb/: dark standalone UI with medals, search, filters, and stats, separate from the docs theme). It holds ProtonDB-style reports with badges (Diamond / Gold / Iron / Copper / Borked), the mod + Retromod + host versions used, links, and details, with search and filters, plus bundle reports (“I ran all these mods at once”) since combinations often behave differently than single mods. It also has a second tab with the full 5,000-mod static audit (the top Modrinth mods run through the snapshot.2 transform, sorted by downloads, searchable and filterable by verdict), clearly labeled as static analysis, not gameplay verdicts, with community reports as the ground truth. Submissions are nearly hands-free: the DB page has a form that pre-fills the GitHub report, and a GitHub Action converts each report into a ready-to-merge pull request against docs/_data/compatdb.yml automatically (merge = published; direct PRs also welcome); seeded with six honest entries from real test runs.

Fixed

  • The 1.12-era Gui polyfill redirect hijacked the modern HUD class. net.minecraft.client.gui.Gui is era-ambiguous: in 1.12 MCP it was the static drawing helper, but in Mojang mappings (1.14.4+) the same name is the HUD class every overlay mod mixins into. The legacy GuiGuiComponent redirect chained into GuiComponentGuiGraphicsExtractor and rewrote modern HUD mixin targets (AppleSkin’s InGameHudMixin had its @Mixin retargeted to GuiGraphicsExtractor while its @Inject selector still said Gui → the whole HUD mixin was rejected; caught in the in-game pass). The legacy redirect is removed, so the modern meaning wins; pre-1.13 drawing-helper references are below the supported floor.
  • The reflective bridges resolved Event.register/invoker against the runtime class instead of the public Event interface. Fabric’s ArrayBackedEvent impl isn’t public, so the lookups threw IllegalAccessException and the bridge wiring soft-failed (caught live in the in-game pass: “could not wire the v2 forwarder”). All six lookup sites across the four event bridges now resolve against the public interface; the relaunch shows zero bridge soft-fails.
  • Instance→static bridge redirects missing the devirtualize flag corrupted the call’s stack shape, caught by the snapshot.3 in-game pass (spawn-mod’s MobBucketItemMixin crashed the 26.1 bootstrap with an ASM Frame.merge ArrayIndexOutOfBoundsException). The ItemStack NBT polyfill (and ~100 other registrations found by a sweep) used the 6-arg registerMethodRedirect form, so ItemStack.getTag() was rewritten to INVOKEVIRTUAL ItemStackNbtBridge.getTag(Object), an instruction that pops a receiver plus an argument where only the receiver was pushed. Fixed at the mechanism level: the transformer now auto-devirtualizes any instance call whose redirect target takes exactly one extra parameter (receiver-as-arg0 is the only meaning that shape can have), which repairs every such registration at once, past and future. Regression-tested with the polyfill’s real registrations.
  • Removed four NeoForge redirects that pointed at classes that don’t exist. The 26.1 shim “dropped-I-prefix” entries (IItemHandlerItemHandler, IItemHandlerModifiableItemHandlerModifiable, IFluidHandlerFluidHandler, IEnergyStorageEnergyStorage) were wrong on every count. Verified against the NeoForge 26.1.2 universal jar, the old capability interfaces still exist in 26.1 (deprecated since the 1.21.9 Transfer Rework, not removed), ItemHandler/FluidHandler were never real classes, and EnergyStorage is the concrete implementation, not a renamed interface. Any item/fluid/energy-handler mod the redirects touched was being rewritten onto missing classes. They’re gone; the surviving interfaces need no redirect at all. (The ForgeSpawnEggItemNeoForgeSpawnEggItem entry was also dropped, since neither class exists in 26.1.)
  • The #82 RecipesUpdatedEventRecipesReceivedEvent redirect moved from the 26.1 shim to the 1.21.3→1.21.4 shim where the rename actually happened (the old event last shipped in NeoForge 1.21.1; the successor exists from 1.21.4 on). It now also covers a 1.21.1 mod landing on a 1.21.4-1.21.11 host, not just the 26.1 hop.
  • #70: Arcanus (and other 1.20.1 Fabric mods) crashed at load with InvalidInjectionException: Expected (class_583) but found (LegacyModelBase_583). The pre-1.17 model bridge registered its concrete-base swaps (class_583 etc. → LegacyModelBase_*) as global class redirects, which rewrote every reference to the base, including a modern mod’s mixin @Inject handler that merely captured EntityModel as a parameter, so the handler descriptor no longer matched its target. Replaced those with a new superclass rebase that rewrites only the inheritance edge (the extends clause and the super(...) constructor call) and leaves all other references to the base type alone. The legacy base is a subtype of the original, so untouched references stay valid, and a 1.20.1 mod that doesn’t actually extend the old base is no longer disturbed. (New RetromodTransformer.registerSuperclassRebase; covered by SuperclassRebaseTest.)
  • #84: Forge mods built for the host’s own MC version were being backed up and rewritten when they should have been left alone. A Forge mod declares its MC dependency as a range ([1.20,)), so the detector reads the floor 1.20, and the old check compared that against a 1.20.1 host at patch precision: 1.20 < 1.20.1 looked like “needs transformation.” needsTransformation now compares at the precision the mod actually declared: a bare major.minor floor like 1.20 is matched minor-to-minor (so any 1.20.x mod on a 1.20.1 host is skipped), while a specific patch like 1.21.1 is still compared in full, so a 1.21.1 mod on a 1.21.11 host is correctly translated. The redundant sameMinorVersion guard at the loader call sites (which would have wrongly blocked that 1.21.1 → 1.21.11 case) was removed; the decision now lives entirely in needsTransformation. An unreadable version is left alone instead of transformed on a guess.
  • #82: EMI crashed on a 26.1 NeoForge host with NoClassDefFoundError: net/neoforged/neoforge/client/event/RecipesUpdatedEvent. That event was renamed to RecipesReceivedEvent in 26.1. Added the class redirect. EMI’s listener only uses the event as a type and calls its own EmiReloadManager.reloadRecipes() with no arguments, so the rename is all it needs (the event’s reshaped accessors don’t matter here).

[1.1.0-snapshot.2] - 2026-05-30

Still in development. Most of this came out of running the compatibility audit against the top Modrinth mods and fixing what it turned up: the deleted FabricBlockSettings API, NeoForge getting the same 26.1 class moves Fabric already had, and a batch of removed-API bridges. Plus a few real crash fixes from issue reports.

Added

  • Bridge for the old ClientPlayNetworking raw-packet API. The pre-1.20.5 channel handler (raw FriendlyByteBuf) was replaced by typed CustomPacketPayload/StreamCodec and is gone in 26.1. The bridge keeps the old handler interface so mod lambdas still link, and routes its register/send calls onto the new payload API by reflection. Around 29 mods were stuck on this alone: AppleSkin, the Cobblemon add-ons, Exposure, Origins. The bytecode transform is tested; the runtime side still needs an in-game check, and it falls back to a no-op rather than crashing if it can’t wire up. See docs-dev/cpn-v1-bridge-design.md.
  • Fixed 14 Fabric API redirects that named the right renamed class but the wrong subpackage, so they didn’t resolve. WorldRenderEvents/WorldRenderContext moved under rendering/v1/level, the C2S channel events under client/networking/v1, the transfer ReadView/WriteView became serialization/v1/value/FabricValueInput/Output, plus a handful of others. Checked against the real fabric-api 0.145.4 for 26.1.2.
  • Yarn Item.Settings → Mojang Item.Properties, and the GlStateManager blend enums (SourceFactor/DestFactor moved to the top of the platform package). These were the last thing blocking about 18 small content mods.
  • NeoForge now gets the 1.21.11→26.1 class moves through the shim chain like Fabric does (GuiGraphics, RenderType, BlockAndTintGetter). They already applied at runtime; this just gives the chain path parity. Helps the 1.21.1-on-NeoForge case from #64.
  • GlStateManager (blaze3d.platform.opengl) and VillagerTrades (entity.npcitem.trading) package moves. The outer types resolve now; their restructured inner members are still to do.
  • FabricBlockSettings bridge. This was the most-referenced removed Fabric class in the audit (~880 refs). It used to extend BlockBehaviour.Properties, so redirecting the class to Properties fixes the load and every builder that kept its name; the renamed ones (hardnessdestroyTime, soundssound, and so on) are method-redirected. Builders that changed signature, like luminance(int)lightLevel(ToIntFunction), still need work. Also covers the older pre-0.50 path.
  • BlockRenderLayerMap stub. The render-layer registry was dropped from the 26.1-era Fabric API; Retromod injects a no-op so mods that call it still load. Render-layer registration does nothing (a cutout block renders solid) but the mod runs.

Fixed

  • #71: a mod that registers its content through a bundled library loaded but registered nothing. A lot of Fabric mods don’t touch the registry directly. They go through a small registration lib shipped as jar-in-jar (Critter, Botarium, Moonlight, and so on). Retromod was remapping that nested library’s bytecode and then throwing it away unless the library’s metadata also changed, so a pure-code library kept its old intermediary names and its register calls quietly did nothing on 26.1. The mod loaded with no content. Rubinated Nether hit this through Critter. The runtime now keeps the remapped nested classes, and the CLI does the same and applies the member mappings it had been skipping. (I’d written this off as an Architectury problem earlier; it wasn’t.)
  • #79: a mixin pointed at a class that no longer exists took the whole game down. If a mod’s @Mixin targets a vanilla class that was deleted (not renamed, so there’s nothing to remap it to), Mixin throws ClassMetadataNotFoundException partway through the transform and the game dies at bootstrap, usually with a vanilla Blocks or MobEffects stacktrace that doesn’t mention the mod. Spelunkery does this with LootDataManager, gone since the 1.21 loot rewrite. Retromod now checks each mixin’s target against the running game and, if it’s really gone, skips that one mixin so the rest of the mod loads. Works on all three loaders, and doesn’t need a per-mod list.
  • The CLI transform command now applies the full 26.1 class-move table, same as a real launch. It didn’t before, so its output didn’t match the game and the compat audit over-reported gaps.
  • #74: no config.json on Forge or NeoForge. The default config was only written by the Fabric entrypoint, so the other two loaders ended up with an empty config/retromod/ folder. Moved that into a shared helper all three call at startup.
  • #66: a mod-specific method rename was leaking onto unrelated mixins. Method redirects were stored under the bare name, so AutoRegLib’s NetworkHandler.registerregisterPacket rewrote every mixin that targeted something called register, including Reinforced Barrels’ invoker on vanilla BlockEntityType, which then crashed with No candidates matching registerPacket. Bare-name redirects are now only used for the obfuscated names that are genuinely unique; ordinary names have to match the owner too.

A note on the deeper gaps. Some of what’s left in the audit is classes that were actually removed, not renamed, and the code around them changed too, so a stub would pass a static check but crash in game. Those need the mod’s feature rewritten rather than redirected: ArmorItem (its constructor takes a removed type), MobType (its getter is gone too), Tier (now a record), InteractionResultHolder (folded into InteractionResult), and the 26.1 render-pipeline classes (ItemRenderer, BakedModel, ModelResourceLocation, and friends). Deferred for now.

[1.1.0-snapshot.1] - 2026-05-29

First snapshot of the 1.1.0 line. The headline is much broader pre-26.1 Fabric coverage: old mods (1.16-1.21.x) now translate onto current Fabric far more often. It also brings a batch of API-relocation shims, a systemic redirect bug fix, and a new whole-class mixin soft-fail. A 1000-mod compatibility audit against the most-downloaded Modrinth mods moved from ~20-30% “transforms cleanly” to ~52% with these changes.

Added

  • Pre-1.17 Fabric entity-model bridge (#55). The 1.17 model-system rewrite removed the old self-building ModelPart idiom (new ModelPart(this, u, v) + addBox/texOffs/setRotationPoint). Retromod now injects a synthetic LegacyModelPart extends class_630 per affected mod, de-virtualizes the old construction calls onto it, bridges each concrete vanilla model base’s old super() ctor, and emits a render override that draws the recorded cubes through the modern VertexConsumer chain. Lets ≤1.16-era custom-model mods load on a current Fabric host.
  • Pre-1.21.2 InteractionResult bridge. 1.21.2 rebuilt class_1269 (InteractionResult) from a plain enum into a sealed interface; the constant fields (field_5811/PASS, etc.) kept their names but changed type, so pre-1.21.2 mods hit NoSuchFieldError. Retromod auto-probes the host and rewrites the field descriptors. (First seen via AutoConfig on Earth2Java.)
  • Pre-1.20.5 Identifier/ResourceLocation ctor bridge. The public (String) / (String,String) constructors were removed in 1.20.5 in favor of parse / fromNamespaceAndPath. The bridge auto-discovers the host’s factory method names and rewrites new class_2960(...) accordingly, on the intermediary (pre-26.1 Fabric) path the existing polyfill didn’t cover.
  • Pre-1.18.2 Biome.Category bridge. class_1959$class_1961 was deleted in 1.18.2 (categories → tags). Retromod injects a stand-in enum and rewrites Biome.getCategory() so pre-1.18.2 spawn-helper mods load instead of crashing in <clinit>. The category-keyed spawn logic is inert, but the mod runs.
  • Fabric renderer-API relocation shim. fabric-renderer-api-v1 moved every type from …/renderer/v1/* to …/client/renderer/v1/* around Fabric API 0.110. Surviving classes (mesh/*, model/ModelHelper, Renderer) are now redirected; the removed material/* + render-pipeline types (RenderMaterial, MaterialFinder, BlendMode, MeshBuilder, RenderContext, SpriteFinder, …) get injected no-op synthetic stubs so mods load with custom rendering inert (Continuity, EMF, ETF, …).
  • Fabric networking V0 soft-fail. The original net/fabricmc/fabric/api/network/* channel API (ClientSidePacketRegistry, ServerSidePacketRegistry, PacketContext, PacketConsumer) was removed; Retromod injects no-op stand-ins so V0-era mods (Xaero’s Minimap/World Map, REI 1.14 builds, Comforts) load; custom packets are silently dropped.
  • Whole-class mixin strip ("strip": "class" in the blocklist, #68/#69). Some broken mixins can’t be soft-failed by removing handler methods, chiefly ones that add an interface to their target (True Darkness’s MixinLightTexture implements LightmapAccess) or have an interdependent critical @Inject failure (Revamped Phantoms’ SweepAttackMixin). The new mode repoints the @Mixin annotation at a non-existent target so the framework skips the whole class gracefully. True Darkness (#68) and Revamped Phantoms (#69) now load with the affected feature inert.
  • AOT cache auto-invalidation. Cached transforms are now stamped with Retromod’s own self-hash, so any change to Retromod’s code (release-to-release and dev iteration within a snapshot) invalidates stale cache entries automatically. No more manual rm -rf config/retromod/aot-cache/.

Fixed

  • Identity class-redirects silently clobbered real ones. registerClassRedirect now ignores A → A entries. Several legacy shims registered identity redirects as “compat-surface markers,” but Map.put semantics meant they overwrote legitimate redirects from other shims: e.g. RenderingBackendShim’s MatrixStack → MatrixStack wiped Fabric_1_16_5_to_1_17’s real MatrixStack → PoseStack rename, and SodiumIrisApiShim’s identity entries wiped the renderer relocation for QuadEmitter/MeshBuilder/Renderer. This single guard cleared the largest class of cross-shim interference (Lithium and Continuity both went from 80+ unresolved references to 0 after transform).
  • CLI transform ran a subset of what the runtime applies. The retromod transform command only applied the version-jump shim chain, never the API shims (FabricApiShim, the renderer relocation, etc.), so CLI-produced jars diverged from what an in-game boot produces. It now picks up all registered shims via ServiceLoader, matching the runtime.
  • #57: FabricItemGroupBuilderShim.build(Identifier, Supplier) overload added (the form Earth2Java’s <clinit> calls), with a soft-fail returning null so a build failure can’t take down the mod’s static init.
  • #60: MixinExtrasShim (previously a phantom redirect target → ClassNotFoundException) is now a real no-op IMixinConfigPlugin; and the “all mods translated” over-eager in-place scan is gated on RetromodVersion.sameMinorVersion, so a mod already on the host’s minor version is skipped.
  • #62: Forge/NeoForge transforms now inject a license line into mods.toml when the original lacks one, so NeoForge no longer rejects the transformed jar for missing license metadata.
  • #67: Demoted the noisiest per-resolution [Retromod-Fuzzy]/[Retromod-Pattern], per-class transformClass, and Mixin-redirect log lines from INFO to DEBUG. A normal startup’s Retromod log shrinks ~10×.
  • #68: True Darkness Biomes no longer crashes on world create (ClassCastException: LightTexture cannot be cast to LightmapAccess); the base True Darkness render mixins are whole-class-stripped.
  • #69: Revamped Phantoms no longer crashes with the SweepAttackMixin critical injection failure; that mixin is whole-class-stripped while the goals/size features stay live.

Changed

  • 1.16→1.17 package-reorganization class redirects expanded (Entity, World→Level, BlockPos, Direction, Box→AABB, BlockState, ItemStack, Identifier→ResourceLocation, MinecraftClient→Minecraft, MatrixStack→PoseStack, …) so ≤1.16 mods resolve against the modern package tree.
  • 1.21.11→26.1 class moves expanded (GuiGraphics→GuiGraphicsExtractor, RenderType→rendertype/RenderType, BlockAndTintGetter relocation, ClientTickEvents $Start/EndWorldTick$Start/EndLevelTick).

Known gaps (tracked for snapshot.2): NeoForge mods don’t yet get the 1.21.11→26.1 renames (no NeoForge 26.1 shim chain); FabricBlockSettings (deleted Fabric API) needs a method-rewrite bridge; assorted Mojang 26.1 renames (MobSpawnTypeEntitySpawnReason, armor/tool restructure).

[1.0.1] - 2026-05-26

A small correctness patch on the Fabric path and the (informational) transform verifier, from working the recent Fabric reports (#57, #58). No change to transform behaviour in the common case: these fix a runtime mis-detection and a bogus warning.

Fixed

  • Fabric client mis-detected as a dedicated server on pre-26.1 hosts. EnvironmentDetector’s client probe only knew the Mojang (net.minecraft.client.Minecraft) and Yarn (net.minecraft.client.MinecraftClient) client class names. On a real pre-26.1 Fabric runtime the client class is the intermediary net.minecraft.class_310, so the probe found no client class, concluded “dedicated server”, and skipped client-side shims. The intermediary name is now probed in both detectClient() and detectDedicatedServer(). Surfaced while investigating #57.
  • Verifier reported present classes as “Missing” (#58). TransformVerifier flagged a class as missing whenever its own classloader couldn’t load it, but that classloader can’t see client classes from a server context, nor other-module classes under NeoForge’s modular loading, even though they exist in the target MC. So the verify report listed classes that are actually present (e.g. BlockColor/class_322, BakedModel/class_1087, ModelResourceLocation/class_1091) as gaps. canResolveClass now falls back to Retromod’s own mapping before declaring a gap. The verify report is informational only, so this never affected transforms, but the false alarms were misleading noise.

The FabricItemGroupBuilderShim NoSuchMethodError also reported in #57 is not fixed here. Its proper fix is part of the pre-26.1 Fabric bridge planned for 1.1.0 (it’s the same intermediary-name/descriptor mismatch that release addresses).

[1.0.0] - 2026-05-25

Stable release. Promoted from 1.0.0-rc.1 after a final security + correctness pass, with no blocking issues. Changes since rc.1:

Added

  • Addon API v1. Third-party mods can extend Retromod with their own VersionShims and PolyfillProviders (via ServiceLoader), plus the file-based mixin-blocklist.json / config.json. A stable public API across the 1.x line; see Writing an Addon.

Changed

  • Build authenticity is now a self-hash integrity check (no signing, no keystore). Official builds embed a SHA-256 of their own classes; the verifier reports OFFICIAL when the running code matches, otherwise it logs a fork notice. It detects modification/corruption, not a determined attacker. For real verification, compare a download’s SHA-256 against the one published on the releases page. See Authenticity.

Fixed

  • Forge/NeoForge duplicate-zip-entry guard. ForgeModTransformer now skips duplicate JAR entries instead of throwing ZipException and silently dropping the mod (the guard FabricModTransformer already had).

Roadmap (rescheduled)

  • 1.1.0 = the pre-26.1 Fabric bridge. Old Fabric mods on a pre-26.1 host lose bridging for changed/removed APIs today (most simple mods still work via intermediary stability; pre-1.17 custom entity models don’t). 1.1.0 reimplements those redesigned subsystems as polyfills.
  • The deep-API-change polyfills (structurally redesigned APIs; Forge-mod-on-NeoForge) that rc.1 listed for 1.1.0 now ship in 1.2.0, keeping one theme per release.

Versioning

  • From 1.0.0: patch releases (1.0.1, …) ship directly; minor/major (1.1.0, …) get a snapshot then an rc (Minecraft-style).

[1.0.0-rc.1] - 2026-05-24

First release candidate. The transform pipeline is feature-complete; what remains are documented deep-API-change limitations (below), targeted for 1.1.0. This release is a large correctness pass on the mapping/transform core, driven by the NeoForge 1.21.11 reports (#50-#53).

NeoForge host-version detection (the keystone fix). On FancyModLoader 10.x (NeoForge 21.11 / MC 1.21.11) Retromod silently mis-detected the host MC version and fell back to a hardcoded default, which then gated out every version shim newer than that default, so core renames like ResourceLocationIdentifier were never applied, and transformed mods crashed with NoClassDefFoundError on classes that had simply been renamed. RetromodVersion.detectFmlMcVersion() now reads the version across FML API generations (the 10.x getCurrent().getVersionInfo() shape and older static forms) and logs loudly instead of silently falling back. This underlies #50/#51/#52.

Host-version-aware vanilla class moves (NeoForge/Forge). The net/minecraft/* rename/package-move table is no longer gated coarsely on “26.1+”. Each rename is applied on a host only when that host actually has the new class and not the old one, i.e. the rename already happened by that host version. So a 1.21.11 host correctly gets the subset of renames that landed by 1.21.11 (≈800 of them), without the prior hazard of rewriting a working reference to a name that doesn’t exist yet. Class existence is queried via the classloader that loaded Minecraft rather than a fragile MC-jar-on-disk search, which also fixed the fuzzy method resolver silently indexing the wrong jar (it had matched a net/minecraftforge/… library by substring → 0 classes).

Much larger, corrected mapping tables.

  • SRG → Mojang (Forge reobf names): regenerated from MCPConfig + official Mojang mappings (the same join ForgeGradle performs), giving ~53,000 verified entries, replacing a 117-entry starter set in which 56 entries were wrong (e.g. getNamespace/getPath swapped; f_50122_ mislabeled).
  • Intermediary → Mojang (Fabric): a multi-version method/field harvest (1.16.5 / 1.20.1 / 1.21.11) so a mod built for an older version whose intermediary ids were dropped from newer tiny files still resolves.
  • Vanilla class moves/renames: grew from 606 to ~900 entries, covering package reorganizations plus simple-name-changed renames paired by member-set similarity (FarmBlockFarmlandBlock, MobSpawnTypeEntitySpawnReason, UseAnimItemUseAnimation, ElytraLayerWingsLayer, …).

Fuzzy method resolver: stack-safety guard. When it redirects a call matched by name + arity, it now refuses the redirect if the original argument/return types aren’t stack-compatible with the candidate (e.g. AnimationUtils.swingWeaponDown(…,Mob,…) became (…,HumanoidArm,…) in 1.21.11). Rewriting such a call emitted bytecode that fails verification (a class-load VerifyError that crashes the whole mod) instead of degrading to a far milder, often-never-hit NoSuchMethodError. This protects every transformed mod.

Mod-specific.

  • #50 Revamped Phantoms: its PhantomMixin @ModifyReturnValue on getDefaultDimensions couldn’t resolve a target on 1.21.11; the handler is self-contained, so it’s soft-failed via the mixin blocklist and the mod loads with its goals feature intact.
  • #53 Ultracraft: all unresolved intermediary names now map (the reported class_195 and 88 method_NNNN gaps are gone); remaining gaps are 26.1 rendering-API removals (a complex, rendering-heavy mod).

Mapping-data attribution added to the README (Mojang obfuscation maps; Fabric intermediary, CC0-1.0; Forge MCPConfig). Retromod’s bundled name tables are derived/transformed works, not redistributions of the originals.

Known limitations carried into 1.1.0: #51 (Illagers Wear Armor) and #52 (Luminous: Nether) now construct and load far further than before, but their core feature code uses Minecraft/NeoForge APIs that were structurally redesigned or removed (armor-layer rendering → EquipmentClientInfo; DeferredSpawnEggItem deleted), not renamed, so they need hand-written polyfills, planned for 1.1.0. See Mods That Can’t Be Translated.


[1.0.0-beta.10] - 2026-05-23

Startup-crash hotfix (all loaders). Retromod’s mere presence could crash an otherwise-working, native mod at launch, even a mod Retromod doesn’t transform at all. EnvironmentDetector (the client/server/headless probe run from each loader’s entry point during mod construction) checked for Minecraft classes with the single-argument Class.forName(name), which initializes the class. That forced net.minecraft.server.MinecraftServer’s static initializer to run far too early; for a mod that mixins into MinecraftServer/LevelSettings (e.g. Legacy4J), it cascaded into wily.legacy.client.PackAlbum.<clinit> reading Minecraft.getInstance().gameDirectory before the client singleton existed → NullPointerException, and the mod “failed to load correctly.” The probe now uses Class.forName(name, false, loader) (initialize = false): it observes class presence without ever triggering a static initializer. Dedicated-server detection was also corrected to key on the absence of a client class (Minecraft’s merged jar carries server classes on a client too, so their presence isn’t a reliable signal). Reported in #46 (Legacy4J on NeoForge 1.21.11), reproduced down to a 3-mod minimal case. Recommended for everyone: the bug was loader-agnostic.

Transformed mods with spaces in their filename couldn’t resolve core Minecraft classes (NeoForge/Forge). A mod that ships no module-info / Automatic-Module-Name (common for MCreator and small mods) gets its JPMS module name derived from the jar filename, and spaces or odd characters there break the module’s reads, so the transformed mod then dies in its own <clinit> with ClassNotFoundException: net.minecraft.resources.ResourceLocation (or another core class). Retromod now sanitizes the transformed jar’s output filename so the derived module name is always valid. Confirmed by a controlled test: the same mod (Luminous Nether) failed with spaces in the filename and loaded once renamed without them. Reported in #47.

The mixin soft-fail blocklist now runs on NeoForge/Forge, not just Fabric. When a mod’s mixin @Inject targets a Minecraft method whose signature changed (the canonical case being Entity.addAdditionalSaveData / readAdditionalSaveData switching CompoundTagValueOutput / ValueInput in the 1.21.5 serialization refactor), Mixin rejects the injection (InvalidInjectionException: Invalid descriptor … expected ValueOutput … found CompoundTag), the mod fails to construct, and the loader’s “broken mod state” then surfaces a misleading downstream NullPointerException (e.g. in ModelManager.reload). Retromod can’t re-type the handler (it’s not a rename; ValueOutput is a different API), so it strips the offending handler via the mixin blocklist and the mod loads with that one feature inert. That stripping had only ever been wired into the Fabric transform path; it now runs on NeoForge/Forge too. Caveat: this cleanly soft-fails a self-contained handler, but a mod whose mixin feature is spread across interdependent members (Darker Depths’ death-anchor save/load, #48) strips clean of the injection error yet still may not fully construct. Reported in #48. (Not a GeckoLib problem; GeckoLib loaded fine, verified against the reporter’s own log.)


[1.0.0-beta.9] - 2026-05-22

NeoForge hotfix. 1.20.1 (Neo)Forge mods were silently skipped on NeoForge 1.21.x (“File X is for Minecraft Forge or an older version of NeoForge, and cannot be loaded”) and never loaded, even though Retromod processed them. NeoForge renamed its metadata file from META-INF/mods.toml to META-INF/neoforge.mods.toml in 1.20.2, and modern NeoForge skips any jar that still has only the old mods.toml at scan time, before bytecode transformation runs. Retromod patched the toml’s contents (version ranges) but never renamed it, so NeoForge rejected the mod before the shims could help. Now, on a NeoForge host (1.20.2+), Retromod promotes mods.tomlneoforge.mods.toml, relaxes the top-level loaderVersion (the FancyModLoader version, which doesn’t track the Forge number an old mod declares), and repoints the mod’s forge loader dependency at neoforge (NeoForge has no mod with id forge, so the mod would otherwise be rejected with “requires forge … MISSING” right after passing the file-scan stage). NeoForge then accepts the mod and the bytecode shims do their job. NeoForge-only: Forge (which still uses mods.toml) and Fabric are untouched. Reported in #42, and this was the real cause of #38, which was wrongly attributed to the shim gate (that never ran, because the mod was skipped first).

Known limitation (NeoForge): this gets a 1.20.1 Forge mod scanned by NeoForge, but loading it fully is a separate, much larger job. 1.20.1 was NeoForge’s first release, when it still shared Forge’s API (ForgeRegistries/IForgeRegistry, the old DeferredRegister.create(IForgeRegistry, …) signature, FMLJavaModLoadingContext, …); NeoForge then replaced all of that in 1.20.2+. Translating such a mod onto modern NeoForge means redoing the whole Forge→NeoForge API migration (registries, the mod-event-bus idiom, events, data gen), which is planned for 1.1.0, not the beta line. For now a 1.20.1 Forge mod is best run on the Forge host, where those APIs still exist natively. See Mods That Can’t Be Translated.


[1.0.0-beta.8] - 2026-05-21

NeoForge / Forge hotfix. beta.7 crashed on launch on NeoForge and Forge, even with no mods, with NoClassDefFoundError: net/fabricmc/loader/api/entrypoint/PreLaunchEntrypoint (“Retromod has failed to load correctly”). beta.7’s shim-version gate had the NeoForge/Forge entry points call a version-compare helper that lived on RetromodPreLaunch, and that class implements the Fabric-only PreLaunchEntrypoint; loading it on a non-Fabric loader can’t resolve that interface, so mod construction aborted. The version-math helpers moved to the loader-agnostic RetromodVersion (which exists precisely so entry points don’t drag in another loader’s classes), so the NeoForge/Forge paths no longer touch any Fabric type. Fabric beta.7 is unaffected: only NeoForge and Forge need this build. Reported in #40; #41 was downstream (mods couldn’t convert while Retromod itself failed to load).


[1.0.0-beta.7] - 2026-05-21

A focused follow-up to beta.6. Several reporters on beta.6 still hit “old mod won’t load on a pre-26.1 host” crashes: beta.6 gated the intermediary→Mojang remap, but still applied 26.1 shims (which rename API classes) on older hosts. This finishes that fix on every loader, handles the private ResourceLocation constructor on 1.21.x, and adds an in-game restart prompt. If you’re on beta.6 and translating onto a pre-26.1 host, upgrade. (Translating onto 26.1 is unchanged.)

Fixed

  • Pre-26.1 hosts crash on 26.1-only API names (Fabric: NoClassDefFoundError: net/fabricmc/fabric/api/client/screen/v1/ScreenEvents$BeforeExtract, …/menu/v1/ExtendedMenuProvider, VerifyError against …/LevelRenderContext; NeoForge: 26.1 names like ItemHandler/FluidHandler). Companion to beta.6’s remap gate: Retromod registered every version shim regardless of host, so the 1.21.11→26.1 shim (which renames API classes: Fabric BeforeRenderBeforeExtract, WorldRenderContextLevelRenderContext; NeoForge IItemHandlerItemHandler) ran on 1.21.8/1.21.1 hosts and rewrote mods to names that only exist in 26.1. Unlike the intermediary remap, API names are identical in the mod and the runtime, so these renames bite even on Fabric. Shims are now gated on every loader (Fabric, NeoForge, Forge): a shim is registered only if its target version is ≤ the host. Reported on Fabric 1.21.8 (Puzzles Lib #31, Tidal #32), Fabric 1.21.1 (Arcanus #35), and NeoForge 1.21.1 (#38). (On a 26.1 host every shim still applies, unchanged.)
  • Fabric: IllegalAccessError calling the private ResourceLocation(String, String) constructor on 1.21.x. That two-arg constructor became private in 1.21, so a mod built for ≤1.20 (new ResourceLocation(ns, path)) crashes at bootstrap on a 1.21.x host. The 1.20.6→1.21 shim already rewrote it to ResourceLocation.fromNamespaceAndPath, but only under the Mojang name; on a pre-26.1 Fabric host the bytecode keeps the intermediary class_2960 (the remap is off), so the redirect missed. Added the intermediary-keyed variant (class_2960method_60655). Reported against Haema and Rubinated Nether on 1.21.1 (#36, #37).

Added

  • In-game restart prompt. When Retromod converts mods during launch, it now shows a Yes/No prompt on the title screen (“converted N mod(s); the game needs to restart to load them”) instead of only logging it. Clicking Yes cleanly closes the client so you can relaunch from your launcher (true in-process relaunch isn’t attempted; it can’t be done reliably across launchers). Works on all loaders, declining returns to the title screen, and it’s gated by restart_prompt in config/retromod/config.json (default on; set false to suppress). Requested in #33.

[1.0.0-beta.6] - 2026-05-20

A JPMS-conflict hotfix for javax.annotation (same family as the Gson/ASM conflicts), a correctness fix for Fabric mods running on a pre-26.1 host (Retromod was over-translating them to 26.1 names that don’t exist on older Minecraft), a polyfill for the removed DirectionProperty block-state class, and a mixin blocklist that turns a class of fatal MixinExtras crashes into inert features.

Fixed

  • Crash on launch from a MixinExtras @WrapOperation/@Local that can’t bind to a reworked vanilla method (VerifyError: Bad local variable type). When a mod captures a @Local from a vanilla method whose body was restructured in 26.1, MixinExtras resolves the local to the wrong slot and emits an invalid bridge method that the JVM rejects at class-load: fatal, before any soft-fail can run. We can’t safely auto-detect this (the local often still exists, just at a different slot, so a naive check would strip working mixins), so Retromod now ships a curated mixin blocklist (retromod/mixin-blocklist.json, extendable via config/retromod/mixin-blocklist.json) that surgically removes the offending handler so the framework never processes it, so the mod loads with that one feature inert. Seeded with Deeper & Darker’s PaintingItemMixin#deeperdarker$decrementStackOnServer, which wraps ItemStack.shrink in HangingEntityItem.useOn and crashed bootstrap on Fabric 26.1.2 (#28).
  • Crash on launch with NoClassDefFoundError: net/minecraft/world/level/block/state/properties/DirectionProperty. 26.1 removed the DirectionProperty class: what used to be a subclass of EnumProperty<Direction> is now just EnumProperty.create(name, Direction.class). A mod that references the removed type (Caverns & Chasms pulled it into Blocks.<clinit> via a mixin and killed bootstrap on NeoForge) crashes the game. Retromod now polyfills it: the type redirects to the surviving EnumProperty, and the four old DirectionProperty.create(...) factories are bridged to supply the Direction.class argument the modern API needs. Reported on NeoForge 26.1.2 (#24). Note: a mod can clear this crash and still have other features that don’t work if it @Injects into vanilla methods 26.1 reworked; those injections soft-fail (non-fatal). See Mods That Can’t Be Translated.
  • Fabric: old mods crash on a pre-26.1 host with ClassNotFoundException for 26.1-only names (net.minecraft.resources.Identifier, net.minecraft.core.particles.ParticleType, net.minecraft.core.registries.BuiltInRegistries, net.minecraft.network.chat.Component, …). Retromod applied its intermediary→Mojang remap unconditionally, but that translation is only valid for MC 26.1+ (the first unobfuscated release). On any earlier host the Fabric runtime still exposes Minecraft under intermediary names (class_XXXX), so a mod built for, say, 1.21.1 already matches the runtime, and rewriting its references to 26.1 Mojang names produces classes that don’t exist yet, killing the mod at load. The remap (bytecode and mixin-config/refmap metadata) is now gated on the host actually being 26.1+. Mods translated for a pre-26.1 host keep their working intermediary names. Reported on 1.21.8 Fabric (#21) and against several 1.21.1 mods (#29). If you’re translating onto 26.1 nothing changes: that path is byte-for-byte identical.
  • Forge + NeoForge crash on launch: Modules jsr305 and retromod export package javax.annotation to module mixin_synthetic. We bundle javax/annotation/{Nullable,Nonnull} polyfill stubs so old mods referencing those annotations resolve. But Forge and NeoForge bundle Guava, which transitively provides jsr305 as a JPMS module that also exports javax.annotation, and strict JPMS refuses two modules exporting the same package. Reported by @epiktechno on NeoForge 1.21.1 + Forge 1.20.1 (#20). The stub can’t be relocated (a polyfill only works at the real package name), so build-all.sh now strips javax/annotation/ from the Forge and NeoForge per-loader mod JARs; jsr305 provides the real ones there. Fabric keeps the stub (no JPMS enforcement, so no conflict; it stays as a harmless fallback) and so does the standalone CLI.

Note

This is the third package in the bundled-dependency-vs-loader-module conflict family (Gson → relocate, ASM → strip, javax.annotation → strip on Forge/NeoForge). The remaining bundled polyfill packages (baubles/api, cofh/api, codechicken/nei, mcp/mobius/waila) are stubs for ancient 1.7-1.12 mods that are essentially never present as JPMS modules on modern MC, so they don’t collide. They are left in place so the polyfill still works for the rare user translating one of those.


[1.0.0-beta.5] - 2026-05-19

Hotfix for ASM classloader conflicts that beta.4 introduced on Fabric and NeoForge. Everyone on beta.4 should upgrade: beta.4 crashes on launch on Fabric and NeoForge.

Fixed

  • Fabric + NeoForge crash on launch with ASM LinkageError / loader-constraint violation. beta.4 un-relocated ASM to fix a Forge problem (the EventSubclassTransformer issue, #12), but that made the bundled ASM collide with the loader’s own ASM: Fabric and NeoForge each bundle ASM and load org.objectweb.asm.tree.ClassNode in their classloader, so our duplicate copy in the mod classloader is a second class with the same name → LinkageError. Reported by @Derpgamer22 / @Rivmo / @LwkySad (#18, NeoForge) and @berwulf / @benio1394 (#19, Fabric).
  • Root cause, properly fixed this time: the mod JAR no longer bundles ASM at all. Every mod loader (Fabric, Forge, NeoForge) provides its own ASM and exposes it to mods, so Retromod just uses the loader’s copy: exactly one ASM per runtime, no relocation, no duplicate, no conflict on any loader. build-all.sh strips org/objectweb/asm/ from each per-loader mod JAR. The standalone CLI keeps its bundled ASM (it has no loader to borrow one from). This resolves the beta.3-vs-beta.4 flip-flop where relocating broke Forge and not-relocating broke Fabric/NeoForge; neither was right, and the answer was to not ship ASM in the mod at all.

Note

If you tested beta.4 and it crashed on Fabric/NeoForge, that’s this bug. beta.5 is the fix. The classtweaker, NeoForge-loaderVersion, and Forge-AOT fixes from beta.4 are all still here; beta.5 only adds the ASM-stripping on top.


[1.0.0-beta.4] - 2026-05-19

Bug-fix release closing out the wave of beta.2/beta.3 issue reports from the early-Modrinth-publish users. Three independent fixes; everyone running beta.2 or beta.3 should upgrade.

Fixed

  • Forge: AOT crash with Could not find parent com/retromod/shaded/asm/signature/SignatureVisitor. Beta.3’s ASM relocation broke Forge’s EventSubclassTransformer, which runs on the system AppClassLoader and looked up the relocated ASM classes by string, but those classes only existed in Retromod’s mod-classloader. Every Forge transform pass failed. Un-relocated ASM (kept Gson, SLF4J, and toml4j relocated, since those are what actually conflict with Forge’s JPMS module resolver). Reported by @No11290 on issue #12.
  • NeoForge: needs language provider java:26.1 or above to load, we have found 11.0. loaderVersion in neoforge.mods.toml is the FancyModLoader version, not the NeoForge version. Our build-all.sh was setting it to the MC-version-based number ([26.1,) for MC 26.1, etc.), which doesn’t match anything because FML versions don’t track MC versions (FML 11.x ships with MC 26.1 NeoForge). Changed to permissive [1,); the actual NeoForge version constraint is in the [[dependencies.retromod]] modId = "neoforge" block and is unchanged. Reported by @Derpgamer22 (#9) and @maksmanus (#10).
  • Fabric: Failed to read classTweaker file from mod X / Namespace mismatch crash on launch. Mods built for one MC era ship classtweaker / accesswidener files with a namespace header (intermediary or official) that doesn’t match the host MC’s runtime namespace. The existing remapper only handled intermediary → official (so a 1.21.x mod on a 26.1 runtime worked); the reverse direction silently failed and Fabric Loader crashed at launch with ClassTweakerFormatException. New behavior: strip the classTweakers and accessWidener declarations from the mod’s fabric.mod.json during transformation. The mod loses whatever class-opening the classtweaker provided (some mixin targets may now fail to find their hooks), but the mod loads instead of killing the game. That’s the correct tradeoff for old-mod-on-new-MC. Reported across issues #11, #13, #14, #15, #16, #17 against satin, azurelib, tidal, dsurround, rubinated_nether, immersive_aircraft, comforts, moonlight, supplementaries.

Notes for the issue reporters

  • #9, #10 (NeoForge): fixed by the loaderVersion change. Re-download beta.4 and the JPMS error goes away.
  • #11 (4 mods on Fabric 26.1.2): three of those mods (immersive_aircraft, comforts, moonlight, supplementaries) likely had classtweaker dependencies via their depend chain; the strip should unblock them. Worth re-testing.
  • #12 (1.12.2 mods on 1.20.1 Forge): the ASM unrelocation should fix the SignatureVisitor crash; 1.12.2 → 1.20.1 is still a massive hop, so individual content mods may have other issues, but at least the AOT pipeline runs to completion.
  • #13, #14, #15, #16, #17: directly fixed by the classtweaker strip.

[1.0.0-beta.3] - 2026-05-18

Single-purpose hotfix release. Only difference from beta.2 is the shaded-dependency relocation: no other code changes, no new features.

Fixed

  • Forge 1.20.1+ “Modules retromod and com.google.gson export package X to module minecraft” crash on launch. The shaded fat JAR bundled Gson, SLF4J, and toml4j without relocation, so Forge’s strict JPMS module resolver saw two modules claiming to export the same package to minecraft and refused to start. Every bundled dependency is now relocated under com.retromod.shaded.* (ASM was already done, this round adds Gson, SLF4J, and toml4j). Fabric/Quilt/NeoForge builds weren’t crashing but received the same relocation for consistency and future-proofing.

If you’re on beta.2 and the mod loads for you fine, beta.3 is bytes-different but behavior-identical, so there’s no urgency to upgrade. If you’re on beta.2 and you hit the JPMS crash above (Forge 1.20.x specifically), beta.3 is the fix.


[1.0.0-beta.2] - 2026-05-17

This release focused on broadening the Java + loader-version range so the right Retromod build is actually loadable on every MC version we publish, plus carrying the security/quality improvements from the past few weeks.

Fixed

  • Java requirement now matches each MC version. Retromod’s compiled bytecode targets Java 17 (was Java 25 in beta.1, which silently required all users to be on Java 25 regardless of MC version). The same JAR runs on Java 17, 21, and 25. build-all.sh now declares the appropriate per-MC "java" constraint in fabric.mod.json: >=17 for MC 1.20-1.20.4, >=21 for MC 1.20.5-1.21.x, >=25 for MC 26.x. This directly fixes the GitHub issue where MC 1.20.1 + Java 17 was rejected (reported on the issue tracker).
  • Forge loaderVersion is now correct per MC version. beta.1 hardcoded [52,) which silently rejected every Forge build for MC 1.20.x (Forge 47-50). The MC 1.20.1/1.20.2/1.20.4 Forge builds couldn’t actually load on the user’s Forge. build-all.sh now maps MC → Forge loader version (e.g. MC 1.20.1 → Forge 47).
  • NeoForge loaderVersion is now correct per MC version. Same pattern: beta.1 had a hardcoded [4,) minimum that didn’t match NeoForge’s actual versioning (20.2.x for MC 1.20.2, 21.0.x for MC 1.21, etc.). Now per-MC.
  • issueTrackerURL in build-all.sh pointed at the old MC-Retromod repo name in Forge/NeoForge metadata. Now correctly points at Retromod.
  • Duplicate “Made by the Developers of revivalsmp.net” line in Forge and NeoForge mod descriptions (sandwich top + bottom) is now a single line at the end, matching the Fabric description.

Added

  • quilt.mod.json so Quilt Loader sees Retromod as a first-class Quilt mod (icon and metadata in Quilt’s mod list, instead of being shown as a Fabric-mod-loaded-via-compat). Same Fabric JAR, no separate “Quilt build” needed; Quilt Loader runs Fabric mods natively.
  • SRG → Mojang member-name remap for old reobf’d Forge mods. Forge 64.x dropped the SRG remap layer for MC 26.1; Retromod now provides the dictionary (~120 starter entries; expandable via src/main/resources/retromod/srg-to-mojang.tsv; see Adding SRG Mappings in the docs).
  • CHANGELOG.md (this file).
  • Documentation: new pages for “Mods That Can’t Be Translated” and “Adding SRG Mappings”, new FAQ entries about Quilt support and Retromod’s offline-by-default network policy.

Security

  • AutoFix engine opt-in flag is now honored on Forge and NeoForge entry points too. beta.1 gated the crash-log parser behind -Dretromod.autoFix=true on Fabric only; the Forge/NeoForge entry points ran it unconditionally. A malicious mod that emitted a crafted log line through SLF4J could have caused Retromod to register attacker-chosen method/field redirects in the shared transformer. Fixed across all three loader entry points.
  • JIJ (Jar-in-Jar) extraction in FabricModTransformer.processNestedJiJJar is now bounded against zip-bomb-style nested archives. Previously, an inner JAR that lied about its compressed-vs-decompressed size could expand to arbitrary size during transform.
  • Output JAR write paths in AotCompiler and ApiEmbedder now validate entry names via ZipSecurity.safeEntryName(...) to stop input-side zip-slip payloads from propagating into output JARs (where a downstream tool extracting them would have inherited the vulnerability).
  • Resource-pack and data-pack extraction in ResourcePackTransformer, DataPackTransformer, and QuiltModTransformer now use the bounded ZipSecurity.copyBounded helper instead of unbounded Files.copy(InputStream, Path).
  • SignatureVerifier no longer uses XOR-obfuscated strings. The fork-detection notice template was previously stored as a XOR-encoded byte array and decoded at runtime, a textbook malware-scanner pattern even though the content was benign. Replaced with a plain string constant; the soft anti-tamper trade-off wasn’t worth the scanner flag.
  • Premain-Class / Agent-Class / Can-Redefine-Classes manifest entries are no longer in the main mod JAR or the shaded CLI fat JAR, only in the dedicated -agent.jar classifier (which exists for explicit -javaagent use). The mod JAR no longer advertises JVM-class-hijack capability it doesn’t use.
  • All three Runtime.exec(new String[]{"xdg-open", url}) browser-launch fallbacks are removed. Desktop.isDesktopSupported() is the primary path on every system Minecraft runs on; if it fails, users now get a dialog with the URL to copy instead of a process-exec fallback. The codebase has zero Runtime.exec calls now.

Changed

  • Project is named “Retromod” (capital R only) throughout the codebase, docs, and metadata. The previous “RetroMod” (capital R + capital M) casing was a typo on my part that propagated for a while. All class names, comments, log output, READMEs, and docs are now consistent. The GitHub repo and Modrinth project have been renamed to match.
  • Network policy is offline by default. Retromod never reaches the internet on its own: no telemetry, no auto-update checks, no silent downloads. Optional Modrinth native-version lookup is gated behind a config flag; the CLI’s archive download command prompts before any HTTP request.
  • fabric.mod.json and quilt.mod.json now use ${project.version} via Maven resource filtering, so future version bumps only need to touch pom.xml.
  • Per-loader output JARs from build-all.sh are now properly metadata-stripped: the Fabric build contains fabric.mod.json AND quilt.mod.json (so it works on both Fabric and Quilt loaders); the Forge build contains only mods.toml; the NeoForge build contains only neoforge.mods.toml. No more “multi-loader fat JAR” being shipped where one loader’s mod scanner might trip over another loader’s metadata.

Known limitations (unchanged from beta.1)

  • Deep-integration mods (Create, Applied Energistics 2, Tinkers’ Construct, IndustrialCraft, OptiFine, etc.) will not work. See the Mods That Can’t Be Translated page for the full list and the general “if a mod uses X” rules.
  • Server software (Paper, Spigot, Bukkit, Purpur) is not supported: Retromod targets the mod loaders, not server plugin platforms. Plugin support is planned but development has not started yet.

[1.0.0-beta.1] - 2026-04

Initial public Modrinth release. Core transformation pipeline (145 version shims, 30 polyfill providers, intermediary→Mojang mapping, mixin compatibility, AOT compiler), CLI tool, in-game GUI, multi-loader (Fabric / NeoForge / Forge) support.