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.N → 1.0.0-rc.N → stable 1.0.0; from 1.1.0 on, minor/major releases use snapshot.N → rc.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 APINoiseChunkMixintranslated with it (worldgen). NewMixinShadowFieldDemotion: 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@Injecthandler captures, the field is demoted to a plain@Uniquemixin field and the handler is prepended withthis.<field> = <capturedParam>.NoiseChunk.noiseSettingswas deleted in the 1.21.5 worldgen refactor (verified absent on 26.x, present on 1.21.1) while theNoiseSettingsconstructor argument is unchanged, so YUNG’sNoiseChunkMixin(whose aquifer-override readsnoiseSettings.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 theNoiseChunkMixinblocklist strip. Tested (MixinShadowFieldDemotionTest, incl. a byte-for-byte check against the shipped YungsApi 5.1.6 jar). Registryvalue getterget(Identifier)->getValue(Identifier)(26.1 rename). TheResourceLocation->Identifierrename had a companion: the registry VALUE getterRegistry.get(Identifier)(returns the valueT) becamegetValue(Identifier), whileget(Identifier)now returnsOptional<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 vanishedget(Identifier)Objectand died at construct time withNoSuchMethodError: DefaultedRegistry.get(Identifier)(YUNG’s Better Strongholds, verified) - and the fuzzy resolver couldn’t help becauseget(Identifier)still “resolves” by name+params. A descriptor-scoped method redirect (registered forRegistry/DefaultedRegistry/MappedRegistry/DefaultedMappedRegistry, keyed on the post-remapIdentifiervalue-getter descriptor so theOptional-returninggetis untouched) renames just that overload togetValue, 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).@InjectTRAILING-parameter re-signature (MixinHandlerResignature). The #69 engine inserted a LEADING param (theServerLevelprepend); it now also handles a param appended at the END of a target’s capture list, right before theCallbackInfotrailer (shifting only theCallbackInfoslot). 26.1 appended aResourceKey<Level>(the dimension key) toChunkGenerator.tryGenerateStructure, so a 1.21.1@Injectthat captured the old 9 params was missing the trailing arg and diedInvalidInjectionException. Registered fortryGenerateStructurewith aStructureSet$StructureSelectionEntryfirst-param guard (YUNG’s Better StrongholdsDisableVanillaStrongholdsMixin, verified applied on 26.2). Tested (MixinHandlerResignatureTest).- Mixin discovery tooling (
mixin-scanCLI +mixin-rank/mixin-crossjoin/mixin-refmap-harvestscripts). Enumerate every@Mixininjector 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 inscripts/mixin-discovery.mdand 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 wideningVertexConsumer/BufferBuilder.addVertex(Matrix4f,FFF)-> theMatrix4fcparam (12 + 7; the concreteMatrix4fis-aMatrix4fc, so only the descriptor changes). Tested (Corpus26xRenameRedirectTest). score <dir> --jsoncorpus mode. Thescorecommand 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) coversMth.cos(F)F/sin(F)Fwidened to(D)F(F2D the arg; 16+12 mods),Window.getGuiScale()Dnarrowed to()I(I2D the result; 16 mods) andSoundManager.play(SoundInstance)Vnow returning aSoundEngine$PlayResult(POP the result; 24 mods);registerSingletonStaticRedirectre-expresses a no-arg static helper that became an instance method, coveringScreen.hasControlDown()/hasShiftDown()/hasAltDown()->Minecraft.getInstance().hasX()(24+24+12 mods); and the existing arg-drop redirect coversCompoundTag.getList(String,int)->getListOrEmpty(String)(drop the type-hint int; 15 mods). Structural residuals from the same audit (ClickEvent/HoverEventnow interfaces,InputConstantsinput-handle redesign, the removedGlStateManagerblend-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 byBasicVerifier(0 new verify errors on the NBT-heavy jars):CompoundTag.contains(String,int)->contains(String)(drop the tag-type-hint int),CompoundTag.getCompound(String)andListTag.getCompound(int)->getCompoundOrEmpty(...)(the plain getters now returnOptional),CompoundTag.remove(String)V->remove(String)Tag(POP the now-returned removed tag), andTagParser.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/GuiGraphicsdrawing - 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.1FastColorcase.) ClickEvent/HoverEventconstructor 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 oldnew 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 thosenews tocom.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 asnull). 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 aCHECKCASTsince the reflective factory returnsObject. Verified by re-audit (ctor residuals 14 -> 0 and 8 -> 0, 2 jars improved) andBasicVerifier(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 andlatest.logstops at the mod list. The RUNTIME (FabricModTransformer) path already remapped these; the offlinebatch/transform/AOT path (RetromodCli) did NOT, a parity gap: (1) a mod’s access widener staysaccessWidener v1 intermediary, and 26.1+ runs theofficialnamespace, so Fabric’s classTweaker reader throwsClassTweakerFormatException: 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@Injectresolves tonet/minecraft/class_XXXXand Mixin rejects it (InvalidInjectionException). Both are now handled in the offline path (top-level AND nested jar-in-jar), remapping the header/namespace and everyclass_/method_/field_token to Mojang. The refmap remapper also gained the combineddata."named:intermediary"->"named:official"form current Fabric loom emits (AppleSkin’s shape), which the previous plain-data.intermediarylogic missed on BOTH paths - so this also fixes the runtime path for that refmap format. Extracted to sharedAccessWidenerRemapper/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-jartransformcommand doesn’t apply the full intermediary bytecode remap thatbatch/runtime do, and AppleSkin needs a Fabric-APIPayloadTypeRegistrynetworking 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 3DPoseStackonto a 2Dorg.joml.Matrix3x2fStack(GuiGraphics.pose()now returns the 2D stack, whose ops arepushMatrix()/popMatrix(), notpushPose()/popPose()), butPoseStackstill 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 forguiGraphics.pose().pushPose()/.popPose()(thepose()call adjacent to the void op, its result consumed on the spot) topose():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
transformCLI command now loads the removed-API polyfills (parity with analyze/batch/runtime). The standalonetransformcommand registered only the version-shim chain (or the all-shims fallback) and skippedPolyfillRegistry.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.transformCommandnow runs the polyfill pass in BOTH branches, so a transform-only output no longer understates compatibility. Tested (TransformPolyfillRegressionTest).batchand AOT now apply the removed-API polyfills too. The sharedregisterAuxiliaryRedirectsstep (the ONLY way thebatchJIT 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 thoughbatch/AOT is the recommended offline NeoForge flow. It now loads polyfills in that shared step (corpus-verified: rawnet/minecraft/util/Tuplerefs 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 bundledintermediary-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()becamegetDeltaTracker(), so a distributed 1.21.1 mod callingmethod_60646was left dangling (10 of the top-40 Fabric mods, verified 10 -> 0). Addedmethod_60646 -> getDeltaTracker(the 26.x idmethod_61966already 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->ARGBcolor helpers). Following up thegetTimerskew 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 -Explosionparticle getters,ItemInteractionResult, theARMOR_MATERIALregistry, model-loading internals - not mappable renames), and pinned ONE real re-minting cluster: the 26.1FastColor$ARGB32/ABGR32->ARGBclass rename re-minted the color-helper method ids. The enclosing class already resolved (tsv class entry + theFastColor -> ARGBclass-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.2ARGB, 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
@Injecthandler shared across BOTH a write (addAdditionalSaveData) and a read (readAdditionalSaveData) target is now DECLINED by the ValueIO adapter (oneCompoundTagparam can’t become bothValueOutputandValueInput) and soft-fail-stripped instead of adapted to one type and left throwingInvalidInjectionExceptionon the other; (2)collectUnrepairablenow scopes its stale-CompoundTagcheck to captures BEFORE theCallbackInfotrailer, so a modern handler with aCompoundTag@Localis no longer wrongly stripped; (3) the ValueIO strip fallback now keys on name+descriptor, so an unrelated overloaded@Injectsharing a stripped handler’s name survives; (4)SyntheticEmbedder.embedIntoJarnow bounds its reads withZipSecurity(per-entry + aggregate cap) instead of rawreadAllBytes(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 offlinetransformentry-copy loop gained the aggregate decompressed-size cap the runtime extract paths already had; and (7)clearRedirectsForTestingnow clears the new converting/singleton redirect maps (test isolation). Tested (new cases inMixinValueIoAdapterTest; the rest exercised by the existing suite).
Research (documented, not yet shipped)
- Yung’s API
BeardifierMixinconfirmed genuinely un-translatable (headless-proven, stays stripped). Its companionNoiseChunkMixinshipped as a real translation (see Added), butBeardifierMixindid NOT: research showed it is byte-translatable (its@InjecttargetsforStructuresInChunk/computesurvive on 26.2) and it applies cleanly, yet the headless worldgen pass proved it is not launch-safe. WithBeardifierMixinactive, chunk generation throwsIllegalStateException: Parent chunk missing(ChunkMap.applyStep) during initial spawn generation and the dedicated server never reachesDone- 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 reachDoneand generate 49 force-loaded chunks. SoBeardifierMixin+ the deadBeardifierAccessorstay 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 aninstanceofno-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):
StructureProcessorTyperegistration 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 theStructureProcessorType-as-SAM model the mod is built on. Investigated (attempted aDeferredRegisterbridge) against the NeoForge 26.2-beta server jars and found three stacked blockers: (1)StructureProcessorTypelost itscodec()abstract method (now a non-functional marker interface;Registries.STRUCTURE_PROCESSORholdsMapCodecdirectly), so the mod’sStructureProcessorTypeSAM lambdas reach theMapCodecregistry unconverted (ClassCastException: …$$Lambda cannot be cast to MapCodec-> unboundprocessor_list->FatalStartupException); (2)StructureProcessor.getType():StructureProcessorTypewas renamed tocodec():MapCodec, so the mod’s processor subclasses (which overridegetType()and return theirStructureProcessorTypefield) don’t implement 26.2’s abstractcodec()and would be un-instantiable at decode, with no mechanical way to reconstruct theStructureProcessorType->MapCodecrelationship the deleted SAM expressed; and (3) the mod BUILDS those SAMs viainvokedynamicagainst the now-methodlessStructureProcessorType. Converting only theDeferredRegisterregistration (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
@InjectintoEntity.addAdditionalSaveData/readAdditionalSaveData(or the BlockEntitysaveAdditional/loadAdditional) captures aCompoundTag; the 1.21.5 ValueIO refactor made those methods passValueOutput/ValueInput, so Mixin rejected the handler (InvalidInjectionException) and NeoForge cascaded into “broken mod state”.MixinValueIoAdapterkeeps the mod’s handler body unchanged (still operating on a realCompoundTag) and wraps it: the synthesized handler converts the ValueIO param to aCompoundTagthrough 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 aVerifyError. Retires the Darker DepthsPlayerMixinblocklist entry. @Inject/@Redirect/@WrapOperation/@Overwritesignature-drift re-signaturing (#69). The 1.21.5 “world/level threading” refactor prepended aServerLevelto manyLivingEntity/Entity/Mob/Playermethods (doHurtTarget(Entity)becomesdoHurtTarget(ServerLevel, Entity), and so on).MixinHandlerResignatureinserts 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 underCOMPUTE_FRAMESwith a fall-back to the prior soft-fail. Retires Revamped Phantoms’SweepAttackMixindoHurtTargetentry.- MixinExtras injector dispatch.
@ModifyReturnValue/@ModifyExpressionValue/@WrapOperation/… selectors are now routed through the same intermediary-to-Mojang / drift remap as core injectors, with therequire=0soft-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.FlyingMobwas deleted in ~1.21.2 and Phantom now extendsMobdirectly, but Phantom STILL declaresgetDefaultDimensions(Pose)(its body callsMob.getDefaultDimensions, the formersuper), so the mod’s@ModifyExpressionValue’s only break was its@At(INVOKE)target still namingFlyingMob. AFlyingMob.getDefaultDimensionstoMob.getDefaultDimensionsmethod 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 thePhantomMixinblocklist entry. Known limitation: on the rarely-used intermediate host range 1.21.2 to 1.21.10, whereFlyingMobis 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 #50FlyingMobalias, the #28Paintingclass-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.sayNowtosaySystemNow,FriendlyByteBuf.readJsonWithCodectoreadLenientJsonWithCodec(26.1 epoch),Minecraft.setScreentosetScreenAndShow(26.2 epoch). - 1.21.11 to 26.1 vanilla class moves:
entity/decoration/Painting+PaintingVariantintoentity/decoration/painting/,entity/monster/Huskintoentity/monster/zombie/, and theMobSpawnTypetoEntitySpawnReasonrename (all verified against the 26.1 and 26.2 jars).
Fixed
- #28 (Deeper and Darker): a stale blocklist entry that protected nothing. The
#28entry named aPaintingItemMixin/deeperdarker$decrementStackOnServerhandler 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-signaturedList<Component>intoTooltipDisplay+Consumer, a param split, and its body reads the deletedPainting.VARIANT_MAP_CODEC/CustomData.read, so it is genuinely unrepairable and is stripped, the variant tooltip going inert) andPaintingMixin.dropItem(aServerLevelsignature drift on a generic name, stripped). ThePaintingclass-move fixes the@Mixintarget soPaintingMixin.getPickResultnow applies natively. - Re-signature engine: a
@Localcaptured 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/@Sharelocals sit AFTER it, and the re-signature does not shift the parameter-annotation arrays, so an inserted leading param misaligned the@Localonto the wrong parameter, anInvalidInjectionException(whichCOMPUTE_FRAMEScan’t catch) on exactly the #69doHurtTarget-family methods. The guard is now full-width, so any parameter-annotated handler declines and keeps its soft-fail. - Re-signature engine: the
@Injectmethod=selector was rewritten decoupled from the handler re-signature.rewriteAnnotationDrifteagerly rewrote an@Inject’s top-levelmethod=selector to new-form even wheninsertParamslater 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 coupledinsertParamspath; 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.getDefaultDimensionstoMob.getDefaultDimensionsalias) 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.
LightTexturewas deleted and redesigned intoLightmap(a GPU texture / UBO produced by a separateLightmapRenderStateExtractor), so the mixin’s@Shadowof the old CPUDynamicTexturefield 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/NoiseChunkMixinconfirmed repairable, held pending verification. Research (javap on 26.1/26.2) confirmed both are mechanically repairable (Beardifier: un-strip the mixin, strip only the deadBeardifierAccessor; NoiseChunk: demote the removed@Shadow @Final noiseSettingsto a@Uniquefield 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.9 → rc.1 → rc.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: OreBlock → DropExperienceBlock (#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-httpsimagesrc) instead ofinnerHTMLstring 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 referencesCreativeModeTab(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
OreBlockno longer crashes a 1.18.x mod on a 1.19+ host (#145, Isle of Berk).net.minecraft.world.level.block.OreBlockwas renamed toDropExperienceBlockin 1.19 (ore XP moved onto the block), so a 1.18.2 Forge/NeoForge mod that extends or references it diedNoClassDefFoundErroron a 1.20.1 host.BlockPolyfillnow redirects the post-FlatteningOreBlockname ontoDropExperienceBlock(the pre-FlatteningBlockOrewas already mapped). Note: a mod that also calls the old super constructor may then surface aNoSuchMethodError, since the ctor signature differs; that is a separate bridge. Tested (Rc2RemovedClassRedirectTest).- Nested
ExtendedScreenHandlerType$ExtendedFactorynow redirects on 26.1+ (#147, VillagerViewer). The 1.21.11 -> 26.1 Fabric shim redirected the outerExtendedScreenHandlerTypetoExtendedMenuTypebut not the nested$ExtendedFactory, so a mod referencing the factory still crashedNoClassDefFoundErrorat class-load. Added the nested-class redirect toExtendedMenuType$ExtendedFactory(target verified present infabric-menu-api-v1on 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.javafiles and theirMETA-INF/services/com.retromod.core.VersionShimentries). 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. NewPhantomBaselineTestfences the set againstphantom-baseline.txtso 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_14452PlacementModifier mapping fix. (done, see Fixed below.)- Multi-mod
mcmod.infotomls. (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
EquipmentClientInfobridge (#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 removedResourceLocationconstructors to the static factories registered them as plain method redirects, which only swapped the target and left the uninitializedNEW/DUPon the stack with anINVOKESPECIALagainst the factory:VerifyError: Bad type on operand stack(“Type uninitialized 0 … is not assignable”) at class load in any mod building aResourceLocation(the canonical 1.12.2 idiom). It now usesregisterConstructorRedirect(which strips theNEW/DUPand emitsINVOKESTATIC), 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
@SubscribeEventhandler takingRegistryEvent.Register(the pre-1.13 registration event) orModelRegistryEvent(the pre-1.17 model-registration event) hitNoClassDefFoundError. Retromod now embedsRegistryEvent/$Register/$MissingMappingsandModelRegistryEventas synthetic classes that are a valid subtype of the host’s eventbusEvent(a class on EventBus 6 and NeoForge, a marker interface on EventBus 7, resolved by probing the running host so the stubextendsorimplementsaccordingly). 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 pastRegistryEvent$Register. - NeoForge
AddReloadListenerEventbridged to 26.2’sAddServerReloadListenersEvent(#139, Legendary Survival Overhaul). NeoForge renamed the event around 26.x and madeaddListenerrequire aResourceLocation/Identifierid. A 1.21.1 mod referencing the old event hitNoClassDefFoundError. Retromod class-redirects the event (fixing the crash and retargeting the@SubscribeEventparameter, both are game-bus events) and bridges the old one-argaddListener(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 (IWorldGeneratorandIGuiHandlerandIMessage/IMessageHandlerin 1.13,IForgeRegistryEntryin 1.17), so the class can’t even load (NoClassDefFoundError) on a modern Forge/NeoForge host.Forge_1_12_2_to_1_13_2now 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.infowith multiple mods now declares every modid (#115). A single 1.12.2 jar can list several mods in onemcmod.info(The Betweenlands shipsthebetweenlands+mclib). The synthesizedmods.tomlpreviously 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.generateTomlFromMcmodInfonow JSON-parses themcmod.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 emittedversionis validated against a[A-Za-z0-9._-]allowlist (falling back to1.0.0) so a craftedmcmod.infocan’t break out of the quoted TOML value to inject keys or spoof another modid, and the parse degrades to the regex fallback on anyThrowable(including aStackOverflowErrorfrom deeply-nested input). - Custom
PlacementModifier.getPositionsno longer mis-renamed tocount(#114). The Fabric intermediary short-namemethod_14452is ambiguous: verified against Fabric Yarn 1.18.2 through 1.21.4 it names BOTHPlacementModifier.getPositions(context, random, BlockPos) -> Stream(MojanggetPositions) AND a separategetCount(random, BlockPos) -> int(Mojangcount). The name-only 1.21.4intermediary-to-mojang.tsvkept justcount, so a mod’sgetPositionsoverride was renamed tocount: it stopped overriding the abstract method and threwAbstractMethodErrorduring chunk generation on 26.x (any mod with a custom placement modifier).IntermediaryToMojangMapper.applyTonow registers all four descriptor variants (two eras: 1.18.xjava/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
ClientTickCallbackbridged toClientTickEvents(#129 Chat Bubbles).net.fabricmc.fabric.api.event.client.ClientTickCallback(SAMtick(MinecraftClient), staticEVENT) was deleted from Fabric API around 1.16, so a 1.15-era mod referencing it dies withNoClassDefFoundErrorwhile its entrypoint class is even being loaded. Retromod now embeds a syntheticClientTickCallbackinterface (kept undercom/retromod/...so it can’t split-package with the loader module) that preserves thetickSAM and theEVENTfield;EVENTis wired in<clinit>to an array-backed event whose invoker fires once per client tick viaClientTickEvents.END_CLIENT_TICK. Gated to 26.1+ (the SAM uses the MojangMinecrafttype). 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$BufferSourceandTesselator, and replaced theVertexFormat$Modeenum withPrimitiveTopology(all verified against the real 26.1.2 and 26.2 client jars), so a 26.1-translated mod referencing any of them diedNoClassDefFoundErrorat 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.2BufferBuilderbuilt from the RenderType’s ownformat()/primitiveTopology(), andTesselator.getInstance().begin(...)works the same way, so mod geometry code actually executes; theendBatchfamily is staged (warns once and drops the batch: frame submission through the newSubmitNodeCollector.submitCustomGeometrypipeline is the follow-up).VertexFormat$Modeis class-redirected toPrimitiveTopology(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 modernm_/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 byscripts/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 theMaterialstatic-field nuller now apply on 1.20.x hosts too (verified: 1.20 already removedMaterial, andProperties.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.EventHandlersetup 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 legacyFMLPreInitialization/FMLInitialization/FMLPostInitializationevent stand-ins (config-dir surface included:getSuggestedConfigurationFileyieldsconfig/<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/LiteralTextconstructors;new TranslatableText("key")diedNoSuchMethodError. Constructor-to-factory redirects targetText.translatable/Text.literal(plus the with-args overload), registered with the InterfaceMethodref form (Textis an interface on 1.19+: a plain-Methodref INVOKESTATIC diesIncompatibleClassChangeError), and both old classes class-redirect toMutableTextso the mod’s downstream fluent calls (formatted,setStyle,append: same intermediary ids onMutableTextsince 1.16, verified on the 1.20.1 jar) type-check against the factory’s return value. - Pre-1.17 Entity field bridge.
Entity.onGroundwent non-public in the 1.17 access cleanup; direct access diedIllegalAccessError. GETFIELD/PUTFIELD rewrite toisOnGround()/setOnGround(boolean). - Pre-1.20 Material bridge. MC 1.20 deleted the Material system, and ONE
List<Material>in a<clinit>(Collective’sGlobalVariables) took the whole class down withNoClassDefFoundError.class_3614class-redirects to a shippedMaterialPolyfill(44 distinct constants under their exact 1.16.5 intermediary field ids, harvested from the official intermediary mappings), andBlockState.getMaterial()devirtualizes to a stagedfromState(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 theclass_310/MinecraftServerprobes), andFabric_1_16_5_to_1_17carried four 26.x-only tick-event renames (*WorldTickto*LevelTick) that broke the very hosts the shim targets; the correct copies live in the 26.1 shim.
- Pre-1.19 Text bridge. The 1.19 chat rework removed the
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
@Modannotation is@Mod(modid="x", name=..., version=..., dependencies=...); modern Forge’s@Modhas ONLYvalue()(which IS the modid), so it readvalue()= null from the old shape and aborted mod loading with a null modid. TheForge1122LifecycleSynthetics@Modmodernization (added this cycle) collapses the whole legacy annotation to@Mod(value=modid)so Forge reads a real id. Verified on the realSRPMainclass 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
ClientTooltipComponentrenderers now draw on 26.1+ (Apollo’s Enchantment Rebalance blank enchanted books, #119). 26.1 renamedClientTooltipComponent.renderText/renderImagetoextractText/extractImage(the params takeGuiGraphics, itself renamed toGuiGraphicsExtractor). 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 torenderText/renderImageand 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 unrelatedrenderTextmethods 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
createResultcallsEnchantment.getMaxLevel(), which Retromod was rewriting to acom/retromod/shim/*/embedded/EnchantmentShimthat was never written (a phantom target): the anvil combined an item + book, the rewritten call resolved, and it diedNoClassDefFoundErroron first use. Three compounding faults, all fixed:- The Fabric transform path had no loader-type filter.
RetromodPreLaunchregistered EVERY version shim regardless ofgetModLoaderType(), 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.getRaritywas genuinely removed but can’t be bridged by a call-site redirect alone (itsRarityreturn 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-packageGameRegistryShim, …): 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/Itemsuper(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.
- The Fabric transform path had no loader-type filter.
- 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 pendingNEWand flushed it as soon as a nested plainnewappeared, 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.mergeAIOOBE), 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 Uninitializedat 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-eligiblenews 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):
FMLJavaModLoadingContextbridge (#85/#115).get().getModEventBus()is the first line of nearly every Forge@Modconstructor, and NeoForge deleted the class. Retromod embeds a per-mod synthetic bridge that delegates toModLoadingContext.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
Propertiesbefore the object is built, but Forge’sDeferredRegister.register(String, Supplier)constructs it inside the supplier with no id (soRegisterEventfails “Block/Item id not set”). A per-mod synthetic threads the id through a thread-local: registration is rerouted through the id-awareregister(String, Function)overload, and thePropertiesfactories (of/ofFullCopy/ofLegacyCopy,new Item.Properties()) stampsetIdfrom it. Verified in-game (blocks/items register). - Extension-interface + lifecycle/server event migration.
IForgeItem/IForgeBlock/IForgeEntity/IForgeBlockEntity-> NeoForge’sI*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
ColorCollectionconsolidation (all loaders). 26.2 folded each per-color family (16 colors) ofBlocks/Itemsconstants into a singleColorCollectionfield and removed the per-color statics, so a 1.21.x mod readingBlocks.BLACK_WOOL(orItems.WHITE_CARPET, …) hitsNoSuchFieldError. 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.WOOLvsDYED_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:
IEventBusand the oldEventbase are gone (BusGroup+ per-typeEventBus<T>instead), so every old Forge mod died at construction. A syntheticLegacyEventBusinterface (so the mod’s interface call sites stay verifiable) bridges the old idiom ontoBusGroup:getModEventBus()->getModBusGroup()wrap,DeferredRegister.register(bus)->register(BusGroup),MinecraftForge.EVENT_BUS-> aBusGroup.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 ownBUSreflectively;@Mod.EventBusSubscriberclasses 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 theIForgeItem-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 fromRegistryObject.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 aLambdaMetafactoryinvokedynamic 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 aConsumer-producing indy is retargeted to a typed helper with the lambda’s reified event type appended as aClassconstant, so it resolves the event’s ownBUSexactly (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/FMLClientSetupEventlisteners now bind). Also added: EventBus 6’s deletedEventPriorityenum is supplied as a real-enum stand-in (constants,values()/valueOf(), enum-switch all work) carrying the matching EventBus 7Prioritybyte, and the bridge gained the priority-carryingaddListeneroverloads ((EventPriority, Consumer),(EventPriority, boolean, Consumer),(EventPriority, boolean, Class, Consumer)). Tested (ForgeEventBusBridgeTest). - Forge entry point never loaded the polyfills.
RetromodForgewas missing thePolyfillRegistrywiring that Fabric, NeoForge, and the CLI all have, so all removed-API polyfills (e.g. the #24DirectionPropertybridge) silently never applied on Forge hosts - surfaced on Forge 26.2 asNoSuchMethodError: 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):NEARrows are the bridge-authoring backlog,AUTOrows are applied auto-redirects to review,SUPPRESSEDrows 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 onlymcmod.info(themods.tomlformat postdates 1.13), so modern Forge/NeoForge never scanned it and snapshot.6’s class-move + ctor bridges never ran.ForgeModTransformernow synthesizes amods.tomlfrommcmod.info(id/version/name/description extracted by regex, ranges relaxed to[1,)), whichpromoteToNeoForgeTomlthen renames toneoforge.mods.tomlon 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.registercall would otherwise dieIncompatibleClassChangeErrorat 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;ForgeCorePolyfillno longer class-redirects a LIVEMinecraftForgeon real Forge hosts; the CLI syncs its default target version into the shim gates (previously only--targetruns 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@AutoRegistersystem 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/Objectand the rewritten class fails JVM verification at runtime - YungsApi’s bundled javassist died withVerifyErrorinConstPool.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/FloatProviderbecame interfaces on 26.x, and their static range-codec factories moved.INVOKEVIRTUALon them is now auto-corrected toINVOKEINTERFACE(and, on the fast path too, static calls get theInterfaceMethodrefflag - a static method on an interface dies withIncompatibleClassChangeErrorotherwise). The removedIntProvider.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_PROCESSORregistry now holdsMapCodecs directly, so a 1.21.x mod’sStructureProcessorTypeSAM-lambda registration broke everyprocessor_listthat referenced it - a new bridge interceptsRegistry.registerinto that registry and converts the value via its oldcodec()SAM (registering the codec, returning the mod’s original value so its fields still hold); (2)StructureProcessoritself became an interface, so processor subclasses died at class definition - a new superclass-rebase variant rewritesextendstoObject+implements, including thesuper()constructor call; (3)IntProvider/FloatProvider’s static codec factories and constants moved toIntProviders/FloatProviderswith identical signatures (plain owner redirects; also theIntProvider.CODECmiss 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
/placeand/locate: (1)StructurePoolElement.getShuffledJigsawBlocksstill returns aList, but its elements are nowStructureTemplate$JigsawBlockInfowrappers (26.1) instead ofStructureBlockInfo, so a 1.21.x caller’s element cast diedClassCastException- a bridge unwraps each element viaJigsawBlockInfo.info(); (2)JigsawBlock.canAttachwas re-typed from(StructureBlockInfo, StructureBlockInfo)to(JigsawBlockInfo, JigsawBlockInfo)- a bridge wraps both args viaJigsawBlockInfo.of; (3) MC renamedRegistry.getHolder/getHolderOrThrowtoget/getOrThrow(now inherited fromHolderGetter), so a 1.21.1 mod’s holder lookup during structure assembly diedNoSuchMethodError- fixed as a same-owner rename. Result:/locate structurefinds a YUNG structure and/place structuregenerates 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 fromHolderGetter, returns anOptional<Holder>) was rewritten to the value getterRegistry.getOptional, and YUNG’s worldgen diedClassCastException: StructureTemplatePool cannot be cast to Holderat 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 ofFluidState.is(...)are gone on 26.1+ (verified against the real jars; only the Predicate-taking forms survive), so a 1.21.x mod dies withNoSuchMethodErrorat 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 (viagetBlock()/getItem()/getType()identity andbuiltInRegistryHolder().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>.jsonnaming 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 shipmodels/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
@Modscanning, soRetromodNeoForgenever 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->ForgeItemshim conflict on NeoForge. The Forge-26.1 rename shim (drop the “I”, stay forge-packaged) was clobbering the NeoForge migration’sIForgeItem->IItemExtension(last-writer-wins across ServiceLoader shims), leaving anet/minecraftforge/common/extensions/ForgeItemreference that exists on no NeoForge classpath ->NoClassDefFoundErroron a custom Item. The forge-extension renames are now gated to Forge hosts only.MinecraftForge.EVENT_BUSNoSuchFieldErroron NeoForge. A polyfill class-redirectedMinecraftForgeto a capabilities shim with noEVENT_BUSfield, clobbering the migration’s correctMinecraftForge->NeoForge(which has the field). Gated off on NeoForge so a Forge mod’sMinecraftForge.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 itsContextKeySetvalidation argument (an arg-dropping redirect handles the old two-arg call); (2) the loot-type registries were MapCodec-ized and theLootItemFunctionType/LootItemConditionTypewrappers 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 intovehicle/passenger/targeted_entityand 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
.classfile inconfig/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 rememberrm -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 (NoClassDefFoundErrorat 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 inforge-1.12.2-class-moves.tsv, harvested byscripts/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 onlymcmod.info(themods.tomlformat postdates 1.13), so running one end-to-end on modern Forge/NeoForge additionally needs:mcmod.info->mods.tomlmetadata generation (so the loader scans the mod at all), the 1.12.2 SRG member namespace (func_/field_-> Mojang), and the old@EventHandler/FMLPreInitializationEventregistry 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
BlockandItemProperties-constructed, but a 1.12.2 block callssuper(Material)and a 1.12.2 item callssuper(), neither of which has a matching ctor (andMaterialitself was removed). Two new transformer mechanisms bridge this: a super-constructor replace (pop the old args, push a fresh defaultProperties) turnssuper(Material)intosuper(BlockBehaviour.Properties.of()), and the existing insert-defaults path now constructs a real default forPropertiestypes (notnull, which would NPE) sosuper()becomessuper(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
ISoundsound class (#113). The 1.12.2 client sound interfacenet.minecraft.client.audio.ISoundwas relocated by the 1.17 repackaging tonet.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)toaddTicketWithRadius(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:FabricWorldnow calls the 3-argaddTicketWithRadius. Mechanism tested byArgDropRedirectTest. (Chunky also mixinsThreadedAnvilChunkStorage, so full Chunky compatibility needs that checked in-game; this removes theaddRegionTicketcrash.)
[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
IModLocatorservice (the #78mods/Retromod/locator). Forge’sModDirTransformerDiscovererclaims anymods/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, itsRetromodForge@Modentry 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 loadingmods/Retromod/, not Retromod’s own@Mod, so it was masked. The locator class is kept for a future locator-only stub jar; until thenmods/Retromod/loading is NeoForge-only and Forge users place jars directly inmods/. Regression-tested (RetromodForgeModLocatorTestasserts 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
@Mixinclass in a package owned by a*.mixins.json, Mixin throwsIllegalClassLoadErroron the direct load, anErrorrather than anException. The probes caught onlyException, 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 amineminenomimixin class). The three resolution probes and the verifier’s top-level guard now catchThrowable, so this diagnostic can never abort a transform. Regression-tested (TransformVerifierTestlocks the probes’ no-throw contract). - CLI/AOT emitted broken
ResourceLocationconstructor bytecode (CLI did not match an in-game boot). The offlinetransform/batch/aotpaths applied the class-move table but not theResourceLocation(String)->Identifier.parse/(String, String)->Identifier.fromNamespaceAndPathconstructor redirects that the Fabric/NeoForge/Forge runtime applies, so any mod that constructs aResourceLocationgot a rawnew 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 sharedregisterIdentifierCtorRedirectshelper, removing the “keep in sync by hand” drift. Verified byte-for-byte: 26 classes across a 6-mod corpus that previously emittedinvokespecial Identifier.<init>now emitinvokestatic 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.liststreams andjar.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 afinally, deleted temp dirs in afinally, and replaced a per-class-load full-Stringcopy 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 vanillaBuiltInRegistriesregistry instance, which serves both Forge idioms at once:DeferredRegister.create(ForgeRegistries.BLOCKS, id)maps onto NeoForge’screate(Registry, String), andForgeRegistries.ITEMS.getValue(loc)/.getKey(v)/.containsKey(loc)map onto the matchingnet/minecraft/core/Registrymethods (getValueisgeton 26.x; the others are identical).RegistryObjectbecomesDeferredHolder. This also removes a pre-existing crash: three shims blanket-redirectedForgeRegistriesto (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>withNoSuchFieldError: NeoForgeRegistries.BLOCKS. Those broken redirects are gone, andIForgeRegistryis supplied as an empty synthetic marker interface for stray type references. Every mapping (field types,createoverloads,Registrymethod 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.eventtonet.neoforged.neoforge.event), keeping most class names. Retromod now class-redirects 308 cleanly-renamed event classes (coreevent/**andclient/event/**, top-level and inner), generated by diffing the real Forge 1.20.1 and NeoForge 26.1 jars and shipped as theforge-event-renames.jsondata table. The handful NeoForge genuinely renamed or merged (EntityJoinWorldEventtoEntityJoinLevelEvent,LivingHurtEventtoLivingDamageEvent,world/*tolevel/*, 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+Distmarkers (#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 ninerun/call/forDistmethods (plus thesafe*variants’ serializable SAMs) by delegating toFMLEnvironment.getDist(). TheDistenum itself is package-renamed (net.minecraftforge.api.distmarker.Dist->net.neoforged.api.distmarker.Dist) and@OnlyInmapped to a no-op, both wired into the migration shim so they apply on the NeoForge runtime path (the equivalentAnnotationPolyfillredirects only run on Fabric and the CLI). NeoForge runtime only; transform-tested (ForgeNeoForgeSyntheticsTest). - Forge -> NeoForge FML lifecycle migration (#85). Every
@Modruns its setup through FML, which NeoForge package-renamed fromnet.minecraftforge.fml.*tonet.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,ModConfigEventand its inners) plusIExtensionPoint,InterModComms,ModList, andModContainer. 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/@Modin the migration shim, theFMLJavaModLoadingContextsynthetic). This closes the gap the event migration left: it coveredevent/**but notfml/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) calledILaunchContext.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 threwNoSuchMethodErrorduring 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, otherwiseFMLPaths.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 (RetromodModLocatorTestasserts no directgameDirectory()call survives in the compiled locator). - The NeoForge in-place runtime transform applied no polyfills (now it does, safely).
PolyfillRegistry.loadAndRegisterran only on the Fabric entry, so a NeoForge user transforming mods at runtime silently missed everyPolyfillProvider: the Forge -> NeoForgeDist/@OnlyInredirects (theAnnotationPolyfillones, 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, …).RetromodNeoForgenow 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, theRenderTypeaccessors, theItemStackNBT methods) now self-gate onRetromodVersion.TARGET_MC_VERSIONso they only fire where the class is actually gone, not on a pre-removal host. Theneoforge-category transfer polyfills stay disabled on this path: the host-gatedNeoForge_1_21_8_to_1_21_9shim already owns the 1.21.9 transfer-API rework, so running both would conflict and the un-gated polyfill wouldNoClassDefFoundErroron a pre-1.21.9 host. NeoForge runtime; transform-tested (RetromodNeoForgePolyfillTest, plus host-gating inBlockPropertyPolyfillTest). - 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 tintedAnimalModelsubclass. 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-endPre1_17ModelBridgeAcceptanceTest). - Descriptor-aware gap report (
retromod gaps). The gap report now emits a distinctBAD_SIGNATUREverdict 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-modaotcommand registers and per-mod-embeds theDeferredSpawnEggItem/FMLJavaModLoadingContextsynthetics, matching whatbatch --aotalready did. A one-offaotof 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-referenceinvokedynamic(e.g.ChunkPos::newcaptured 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 directnew 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.dist→getDist()on NeoForge 26.x. 26.x removed the staticdistfield, so the near-universalFMLEnvironment.distclient/server check now rewrites toINVOKESTATIC FMLEnvironment.getDist()instead of throwingNoSuchFieldError. 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’sRetromodForgeModLocatorimplements Forge’snet.minecraftforge.forgespi.locating.IModLocator(registered as amods/-scanned service, the Forge analogue of NeoForge’s early-service locator), somods/Retromod/loads in-place on Forge too, completing #78 across all three loaders. It reflection-delegates to Forge’s ownModsFolderLocatorfor the jar→IModFilestep and soft-fails to a no-op if that ever moves, so it can’t disrupt Forge discovery.build-all.shnow produces Forge+26.2jars. - 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 (theprepare/batchcommands + 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-colorBlocksfields (candles, candle cakes, shulker boxes, terracotta; 64 fields total:WHITE_CANDLE,BLACK_SHULKER_BOX, …) in favour ofBlocks.DYED_CANDLEetc. (aColorCollection<Block>indexed byDyeColor). A 1.21.x mod readingBlocks.WHITE_CANDLEhitsNoSuchFieldErroron 26.2. The newregisterStaticFieldAccessortransform rewrites theGETSTATICintoBlocks.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 (SwampFeatureProcessorbuilds 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) andMETA-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 themods.toml→neoforge.mods.tomlpromotion. Applies across all four pipelines: the offlinetransform/batchand 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/aotwere 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 candleColorCollectionpolyfill, the advancements package moves, …).--target 26.2now 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.2load 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 uniquecom/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’sSpawnEggItemtakes onlyItem.Properties(the entity type moved to a data component), so the bridge extendsSpawnEggItemand its old(Supplier,int,int,Item.Properties)constructor best-effort sets the type viaItem.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 validSpawnEggItemsubclass 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 toModLoadingContext.get().getActiveContainer().getEventBus()(verified against NeoForgeloader-11.0.13.jar).- Wired into both the runtime (
RetromodNeoForge, gated on the original being absent on the host) and the offlinetransform/batchpaths (registered for NeoForge/Forge mods on 26.1+ targets; a cross-loader mod that ships bothmods.toml+neoforge.mods.tomlis detected as “forge” yet runs on NeoForge, so both JPMS loaders are covered; the embed is reference-gated, so a pure-Forge mod with nonet/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 thedata/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 (Jigsawtemplate_pools,processor_lists) now throwMalformedJsonExceptionand leave the registry entry unbound. One mod’s unboundtemplate_poolis 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”).ModDataMigratornow 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 fromFatalStartupExceptionto a cleanDonewith 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_colorobject{"rgb":N}->bare int,custom_model_dataint->{"floats":[N]}(1.21.4+ object form), advancement icon"item"->"id", and theminecraft:potionentity (inentity_typetags) ->minecraft:splash_potion+minecraft:lingering_potion(26.x’sThrownPotionSplitFix; the potion item is untouched, so this is scoped toentity_typetag files). Now wired into every path, not just the offlinetransform/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 theminecraft:potionentity-type split, now fixed). Tested (ModDataMigratorTest, 26 cases, plus aForgeModTransformer.transformModruntime integration test).
- Strict JSON parsing (the big worldgen blocker). 26.1 parses data-pack JSON with a strict Gson reader, so the
Fixed
- forge-config-api-port
ConfigTrackerVerifyError(#94 follow-up), broad reach. Transforming a mod that bundles Forge Config API Port produced aVerifyErrorinConfigTracker.loadConfig. When ASM recomputes stack-map frames it must merge the two caught exception types (java.io.IOExceptionand night-config’sParsingException), butParsingExceptionlives in a Fabric Jar-in-Jar invisible to the transformer, so Retromod’sgetCommonSuperClassfallback typed the caught value asObjectwhere the rethrow needs aThrowable. The fallback now returnsThrowablewhenever either operand is one (determined by reading class bytes, notClass.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 corruptObjectframes are nowThrowable). retromod batchand AOT under-transformed mods: the vanilla class moves were missing. Both thebatchJIT path and the AOT compiler registered only the version-shim chain, while the single-modtransform(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 throughbatch/AOT (the recommended NeoForge offline flow) kept pre-26.x class names: e.g. a 1.21.x mod’s mixin@ShadowofEndDragonFightwas never relocated to 26.x’sEnderDragonFight, and the mixin failed to apply. All three CLI paths now apply the same set via a sharedregisterAuxiliaryRedirectsstep. 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’sBlocks.WHITE_CANDLEto a name 26.2 no longer has, crashing construction withNoSuchFieldError. The member mappings are now gated to Fabric mods in every CLI transform path, mirroring the runtime split (RetromodPreLaunchregisters them on Fabric;RetromodNeoForgenever did). Class moves stay loader-agnostic (vanilla relocations apply everywhere). Regression-tested (RetromodCliAuxRedirectsTest). batchunder-transformed mods with unreadable version metadata. A mod whosemods.toml/neoforge.mods.tomlships a literal${…}placeholder or otherwise unparseable Minecraft version (a common build misconfiguration, e.g. some MCreator output) read as “unknown version”, whichbatchtreated 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).batchnow 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, whoseneoforge.mods.tomlships${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:
ItemNameBlockItem→BlockItem(26.x folded that BlockItem subclass into BlockItem; same constructor),ResourceKey.location()→identifier(), andChunkPos(long)→ChunkPos.unpack(long). TheResourceKeyrename needed a new capability:RetromodTransformer.registerMethodRenameroutes an owner-scoped method rename through the ClassRemapper’smapMethodName, so it rewrites method references (e.g.ResourceKey::locationcaptured in a codec lambda) as well as direct calls; the existing method-redirect pass only saw directINVOKE*sites. Helps any mod that method-references a renamed method, not just these. (Constructor references such asChunkPos::newin 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 dedicatedmods/Retromod/subfolder, so Retromod (on both Modrinth and CurseForge) and its transformed*-retromod.jaroutputs can ship as CurseForge pack overrides instead of top-levelmods/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),
RetromodPreLaunchdrainsmods/Retromod/: it moves the loader-ready jars intomods/and shows the usual one-time restart prompt. Alternatively, launch with-Dfabric.addMods=<gamedir>/mods/Retromodto load them in-place with no restart (Retromod detects this and skips the drain). - Follow-ups: Forge (a different
forgespiSPI) and the thin CurseForge “Retromod Loader” distribution stub.
- NeoForge: a custom
[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.2for 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-betaqualifier 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 against26.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
GraphicsBackendCompatsetspreferredGraphicsBackend:"opengl"inoptions.txt. It is non-destructive (only when you haven’t chosen a backend; an explicitvulkanchoice 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
RenderSystemsetters (Tier 2). The imperativeRenderSystemstate 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 withNoSuchMethodError. The state is inert (soft-fail), not translated. NewregisterRemovedMethodNeutralizetransform API +RemovedRenderStateNeutralizeshim, 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 staticPayloadTypeRegistryaccessors 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.
ForgeConfigApiPortShimredirectednet.minecraftforge.common.ForgeConfigSpec(and$Builder,ModLoadingContext,ModConfig$Type) ontofuzs.forgeconfigapiport.api.config.v2.*targets that don’t exist. The Fabric port re-implements the Forge config classes at their originalnet.minecraftforge.*names (the bundled port ships 17ForgeConfigSpecclasses 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 movedForgeConfigSpec/$Builderbut left$ConfigValue/$IntValueat the old name). Caught while verifying #94 (CoroUtil crashed atModConfigDataFabric.writeConfigFile). The shim’s redirects are removed; the port provides the originals. Regression-tested; verified in-game theNoClassDefFoundErroris 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
invokedynamiclambda sites), and the first run failed 4 of 10: the generated interfaces usedClassFormatError-illegal field modifiers (interface fields must beACC_FINAL) and the reflective-EVENT<clinit>lacked stack-map frames (VerifyError). Both fixed inSamBridgeSynthetics; a new load-and-initialize JUnit test (with a stubEvent) 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, withConventionalItemTags,EntityModelLayerRegistry, andLivingEntityFeatureRendererRegistrationCallbackconfirmed firing (handler actually invoked by the game), the rest registering cleanly. - Pathfinding enum renames (
PathType). The same in-game pass caught a real gap behindLandPathNodeTypesRegistry: 1.21.2’s pathfinding refactor renamed the whole damage family (DAMAGE_FIRE→FIRE,DANGER_FIRE→FIRE_IN_NEIGHBOR,DAMAGE_OTHER→DAMAGING,DANGER_OTHER→DAMAGING_IN_NEIGHBOR), so a 1.20.1 mod registering custom path types hitNoSuchFieldError. 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
IllegalAccessErrorconstructing aResourceLocation(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 factoryfromNamespaceAndPath; 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/Tuplepolyfill (26.2 removal). 26.2 removednet.minecraft.util.Tuple; a trivial mutable-pair reimplementation is now redirected in for mods that use it (both the Mojang name and intermediaryclass_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.cca→org.ladysnake.ccapackage 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 publicapi/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 theblock/util/Sided*Compoundhelpers; 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”:
LambdaMetafactorythrows 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;EVENTfields are mirrored reflectively and soft-fail to inert), generated by a new sharedSamBridgeSyntheticshelper 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 pathevent/lifecycle/v1/never existed; the real class is atentity/event/v1/, so this event was never actually bridged),LivingEntityFeatureRendererRegistrationCallback(registerRenderers→registerLayers),DrawItemStackOverlayCallback(onDrawItemStackOverlay→onExtractItemDecorations),TooltipComponentCallback→ClientTooltipComponentCallback(non-void SAM, previously unbridged),ServerChunkEvents$LevelTypeChange→$FullChunkStatusChange(inner + field redirect; the outer holder survives), andLandPathNodeTypesRegistry→LandPathTypeRegistry(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
EntityTypeandBlockEntityTypeinto newEntityTypes/BlockEntityTypesholder classes (the Blocks/Items pattern), which the first in-game run caught as 27NoSuchFieldErrors before the field redirects went in. Harvested by the newscripts/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 splitadvancements/criterion/*intoadvancements/triggers/*+advancements/predicates/*. Notable refactors covered: Slime/MagmaCube moved tomonster/cubemob/with an extractedAbstractCubeMobbase (Slime’s AI inner classes followed it;SlimePredicate→CubeMobPredicate), thecontextualbarrenderers dropped theirRenderersuffix, andSubmitNodeStorage’s per-feature*Submitrecords moved intorenderer/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 newHudclass) 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), theTesselator/VertexFormatold vertex layer, andutil/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 placeholderDummy.classin Mixin’s runtime-generated args package, an old-Forge hack so the mod’s module exports that package. NeoForge 1.20.2+ has its ownmixin_syntheticmodule 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 stripsorg/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 otherorg/spongepowered/content). Old Forge hosts are deliberately left alone, since the hack is still load-bearing there. Verified against the realblueprint-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. InRetromodTransformer’s constructor-defaults writer it also emitted bytecode on every pass. One had a disguised version:FuzzyMethodResolver’s “malformed” guard was abreakinside 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;, andObfuscationRemapperpasses 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
onCompleteon the success path, so an exception (or anErrorfrom 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 infinally(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.1no 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 layeredhud/HudElementRegistry. The synthetic keeps the oldonHudRenderSAM and extends the newHudElement, bridging the renamed SAM with a default method, so old lambdas link, every listener is aHudElement, 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 (SHEARS→SHEAR_TOOLS,RAW_ORES→RAW_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-fieldNoSuchFieldErrorinstead of the old whole-classNoClassDefFoundError. - 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$PendingParticleFactoryinner (SAM method unchanged, so lambda-safe),FabricSpriteProvider→FabricSpriteSet,ParticleRendererRegistry→ParticleGroupRegistry,FabricBlockStateParticleEffect→FabricBlockParticleOption),AtlasSourceRegistry→SpriteSourceRegistry,ComponentTooltipAppenderRegistry→ItemComponentTooltipProviderRegistry,PointOfInterestHelper→PoiHelper,FabricTrackedDataRegistry→FabricEntityDataRegistry, andFabricGameRuleVisitor→FabricGameRuleTypeVisitor. - NeoForge:
BlockEvent$BreakEvent→level/block/BreakBlockEvent(26.1 moved block events into a subpackage; same constructor shape) and theRenderLevelStageEventstage 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 successorcreativetab/v1/CreativeModeTabEventsrenamed both the registration method (modifyEntriesEvent→modifyOutputEvent) and the SAM method (ModifyEntries.modifyEntries→ModifyOutput.modifyOutput), so a class redirect would bothNoSuchMethodErroron the lookup and lambda-trap the handler. The bridge injects a syntheticItemGroupEventsholder (keepingmodifyEntriesEventand theMODIFY_ENTRIES_ALLfield) plus syntheticModifyEntries/ModifyEntriesAllinterfaces that keep the old SAM names, and wires each to its live v2 event by reflection. The entries object also renamed its mutators (addAfter/addBefore→insertAfter/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 (soFabricItemGroupEntries.addAfterreally becomesFabricCreativeModeTabOutput.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-argregister(dispatcher, dedicated); the surviving v2 one is the 3-argregister(dispatcher, CommandBuildContext, CommandSelection), so a class redirect would crashLambdaMetafactory(the mod’s handler lambda hard-codes the 2-arg shape). The bridge keeps a synthetic v1 interface (the 2-arg SAM plus theEVENTfield the mod reads) and its static initializer wiresEVENTto the live v2 callback by reflection, mapping the v2CommandSelectiondown to the v1dedicatedboolean (DEDICATED→ true). This one is clean because the v1 lambda body only touches the brigadierCommandDispatcher(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
ServerWorldEventslifecycle API (server level load/unload), sole-blocking about 21 mods. 26.1 renamed both the holder (ServerWorldEvents→ServerLevelEvents) and the SAM methods (onWorldLoad/onWorldUnload→onLevelLoad/onLevelUnload), so the old class redirect was a lambda trap. The bridge keeps syntheticServerWorldEvents(withLOAD/UNLOAD) and$Load/$UnloadSAM interfaces at the old names, wired toServerLevelEventsby reflection (forwarding 1:1; the only param change,ServerWorld→ServerLevel, 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
EntityModelLayerRegistryentity-model-layer API, sole-blocking about 18 mods. The successorModelLayerRegistryrenamed the provider SAM (TexturedModelDataProvider.createModelData→TexturedLayerDefinitionProvider.createLayerDefinition), a lambda trap, but the return type is unchanged (YarnTexturedModelDatais MojangLayerDefinition, the same class). The bridge keeps a syntheticEntityModelLayerRegistryholder and aTexturedModelDataProviderSAM at the old name;registerModelLayerwraps the old provider in the new one (calling the oldcreateModelDataand returning itsLayerDefinition) and forwards toModelLayerRegistry. 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/$EndWorldTickand theServerTickEventspair redirect to$StartLevelTick/$EndLevelTick. The 26.1 Fabric API finished the World→Level rename on these nested SAMs while leaving the outer class and theonStartTick/onEndTickmethod 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$*LevelTickinners 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, soClientTickEvents$EndWorldTickwas 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/v1has been gone since the 1.19 era on every modern host, itscommand/v2target 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 newisVerified().
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 againstdocs/_data/compatdb.ymlautomatically (merge = published; direct PRs also welcome); seeded with six honest entries from real test runs.
Fixed
- The 1.12-era
Guipolyfill redirect hijacked the modern HUD class.net.minecraft.client.gui.Guiis 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 legacyGui→GuiComponentredirect chained intoGuiComponent→GuiGraphicsExtractorand rewrote modern HUD mixin targets (AppleSkin’sInGameHudMixinhad its@Mixinretargeted toGuiGraphicsExtractorwhile its@Injectselector still saidGui→ 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/invokeragainst the runtime class instead of the publicEventinterface. Fabric’sArrayBackedEventimpl isn’t public, so the lookups threwIllegalAccessExceptionand 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
MobBucketItemMixincrashed the 26.1 bootstrap with an ASMFrame.mergeArrayIndexOutOfBoundsException). The ItemStack NBT polyfill (and ~100 other registrations found by a sweep) used the 6-argregisterMethodRedirectform, soItemStack.getTag()was rewritten toINVOKEVIRTUAL 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 (
IItemHandler→ItemHandler,IItemHandlerModifiable→ItemHandlerModifiable,IFluidHandler→FluidHandler,IEnergyStorage→EnergyStorage) 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/FluidHandlerwere never real classes, andEnergyStorageis 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. (TheForgeSpawnEggItem→NeoForgeSpawnEggItementry was also dropped, since neither class exists in 26.1.) - The #82
RecipesUpdatedEvent→RecipesReceivedEventredirect 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_583etc. →LegacyModelBase_*) as global class redirects, which rewrote every reference to the base, including a modern mod’s mixin@Injecthandler that merely capturedEntityModelas a parameter, so the handler descriptor no longer matched its target. Replaced those with a new superclass rebase that rewrites only the inheritance edge (theextendsclause and thesuper(...)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. (NewRetromodTransformer.registerSuperclassRebase; covered bySuperclassRebaseTest.) - #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 floor1.20, and the old check compared that against a1.20.1host at patch precision:1.20 < 1.20.1looked like “needs transformation.” needsTransformation now compares at the precision the mod actually declared: a bare major.minor floor like1.20is matched minor-to-minor (so any 1.20.x mod on a 1.20.1 host is skipped), while a specific patch like1.21.1is still compared in full, so a 1.21.1 mod on a 1.21.11 host is correctly translated. The redundantsameMinorVersionguard 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 toRecipesReceivedEventin 26.1. Added the class redirect. EMI’s listener only uses the event as a type and calls its ownEmiReloadManager.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
ClientPlayNetworkingraw-packet API. The pre-1.20.5 channel handler (rawFriendlyByteBuf) was replaced by typedCustomPacketPayload/StreamCodecand 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. Seedocs-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/WorldRenderContextmoved underrendering/v1/level, the C2S channel events underclient/networking/v1, the transfer ReadView/WriteView becameserialization/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→ MojangItem.Properties, and the GlStateManager blend enums (SourceFactor/DestFactormoved 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) andVillagerTrades(entity.npc→item.trading) package moves. The outer types resolve now; their restructured inner members are still to do.FabricBlockSettingsbridge. This was the most-referenced removed Fabric class in the audit (~880 refs). It used to extendBlockBehaviour.Properties, so redirecting the class toPropertiesfixes the load and every builder that kept its name; the renamed ones (hardness→destroyTime,sounds→sound, and so on) are method-redirected. Builders that changed signature, likeluminance(int)→lightLevel(ToIntFunction), still need work. Also covers the older pre-0.50 path.BlockRenderLayerMapstub. 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
@Mixintargets a vanilla class that was deleted (not renamed, so there’s nothing to remap it to), Mixin throwsClassMetadataNotFoundExceptionpartway through the transform and the game dies at bootstrap, usually with a vanillaBlocksorMobEffectsstacktrace that doesn’t mention the mod. Spelunkery does this withLootDataManager, 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
transformcommand 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.jsonon Forge or NeoForge. The default config was only written by the Fabric entrypoint, so the other two loaders ended up with an emptyconfig/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.register→registerPacketrewrote every mixin that targeted something calledregister, including Reinforced Barrels’ invoker on vanillaBlockEntityType, which then crashed withNo 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 intoInteractionResult), 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
ModelPartidiom (new ModelPart(this, u, v)+addBox/texOffs/setRotationPoint). Retromod now injects a syntheticLegacyModelPart extends class_630per affected mod, de-virtualizes the old construction calls onto it, bridges each concrete vanilla model base’s oldsuper()ctor, and emits a render override that draws the recorded cubes through the modernVertexConsumerchain. Lets ≤1.16-era custom-model mods load on a current Fabric host. - Pre-1.21.2
InteractionResultbridge. 1.21.2 rebuiltclass_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 hitNoSuchFieldError. Retromod auto-probes the host and rewrites the field descriptors. (First seen via AutoConfig on Earth2Java.) - Pre-1.20.5
Identifier/ResourceLocationctor bridge. The public(String)/(String,String)constructors were removed in 1.20.5 in favor ofparse/fromNamespaceAndPath. The bridge auto-discovers the host’s factory method names and rewritesnew class_2960(...)accordingly, on the intermediary (pre-26.1 Fabric) path the existing polyfill didn’t cover. - Pre-1.18.2
Biome.Categorybridge.class_1959$class_1961was deleted in 1.18.2 (categories → tags). Retromod injects a stand-in enum and rewritesBiome.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-v1moved every type from…/renderer/v1/*to…/client/renderer/v1/*around Fabric API 0.110. Surviving classes (mesh/*, model/ModelHelper, Renderer) are now redirected; the removedmaterial/*+ 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’sMixinLightTexture implements LightmapAccess) or have an interdependent critical@Injectfailure (Revamped Phantoms’SweepAttackMixin). The new mode repoints the@Mixinannotation 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.
registerClassRedirectnow ignoresA → Aentries. Several legacy shims registered identity redirects as “compat-surface markers,” butMap.putsemantics meant they overwrote legitimate redirects from other shims: e.g.RenderingBackendShim’sMatrixStack → MatrixStackwipedFabric_1_16_5_to_1_17’s realMatrixStack → PoseStackrename, andSodiumIrisApiShim’s identity entries wiped the renderer relocation forQuadEmitter/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
transformran a subset of what the runtime applies. Theretromod transformcommand 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-opIMixinConfigPlugin; and the “all mods translated” over-eager in-place scan is gated onRetromodVersion.sameMinorVersion, so a mod already on the host’s minor version is skipped. - #62: Forge/NeoForge transforms now inject a
licenseline intomods.tomlwhen 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-classtransformClass, 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
SweepAttackMixincritical 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 (MobSpawnType→EntitySpawnReason, 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 intermediarynet.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 bothdetectClient()anddetectDedicatedServer(). Surfaced while investigating #57. - Verifier reported present classes as “Missing” (#58).
TransformVerifierflagged 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.canResolveClassnow 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
FabricItemGroupBuilderShimNoSuchMethodErroralso 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 andPolyfillProviders (via ServiceLoader), plus the file-basedmixin-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
OFFICIALwhen 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.
ForgeModTransformernow skips duplicate JAR entries instead of throwingZipExceptionand silently dropping the mod (the guardFabricModTransformeralready 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 ResourceLocation→Identifier 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/getPathswapped;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 (
FarmBlock→FarmlandBlock,MobSpawnType→EntitySpawnReason,UseAnim→ItemUseAnimation,ElytraLayer→WingsLayer, …).
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@ModifyReturnValueongetDefaultDimensionscouldn’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_195and 88method_NNNNgaps 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;DeferredSpawnEggItemdeleted), 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 CompoundTag → ValueOutput / 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.toml → neoforge.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 oldDeferredRegister.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,VerifyErroragainst…/LevelRenderContext; NeoForge: 26.1 names likeItemHandler/FluidHandler). Companion to beta.6’s remap gate: Retromod registered every version shim regardless of host, so the1.21.11→26.1shim (which renames API classes: FabricBeforeRender→BeforeExtract,WorldRenderContext→LevelRenderContext; NeoForgeIItemHandler→ItemHandler) 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:
IllegalAccessErrorcalling the privateResourceLocation(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. The1.20.6→1.21shim already rewrote it toResourceLocation.fromNamespaceAndPath, but only under the Mojang name; on a pre-26.1 Fabric host the bytecode keeps the intermediaryclass_2960(the remap is off), so the redirect missed. Added the intermediary-keyed variant (class_2960→method_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_promptinconfig/retromod/config.json(default on; setfalseto 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/@Localthat can’t bind to a reworked vanilla method (VerifyError: Bad local variable type). When a mod captures a@Localfrom 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 viaconfig/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’sPaintingItemMixin#deeperdarker$decrementStackOnServer, which wrapsItemStack.shrinkinHangingEntityItem.useOnand crashed bootstrap on Fabric 26.1.2 (#28). - Crash on launch with
NoClassDefFoundError: net/minecraft/world/level/block/state/properties/DirectionProperty. 26.1 removed theDirectionPropertyclass: what used to be a subclass ofEnumProperty<Direction>is now justEnumProperty.create(name, Direction.class). A mod that references the removed type (Caverns & Chasms pulled it intoBlocks.<clinit>via a mixin and killed bootstrap on NeoForge) crashes the game. Retromod now polyfills it: the type redirects to the survivingEnumProperty, and the four oldDirectionProperty.create(...)factories are bridged to supply theDirection.classargument 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
ClassNotFoundExceptionfor 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 bundlejavax/annotation/{Nullable,Nonnull}polyfill stubs so old mods referencing those annotations resolve. But Forge and NeoForge bundle Guava, which transitively providesjsr305as a JPMS module that also exportsjavax.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), sobuild-all.shnow stripsjavax/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 (theEventSubclassTransformerissue, #12), but that made the bundled ASM collide with the loader’s own ASM: Fabric and NeoForge each bundle ASM and loadorg.objectweb.asm.tree.ClassNodein 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.shstripsorg/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’sEventSubclassTransformer, which runs on the systemAppClassLoaderand 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.loaderVersioninneoforge.mods.tomlis the FancyModLoader version, not the NeoForge version. Ourbuild-all.shwas 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 mismatchcrash on launch. Mods built for one MC era ship classtweaker / accesswidener files with a namespace header (intermediaryorofficial) that doesn’t match the host MC’s runtime namespace. The existing remapper only handledintermediary → official(so a 1.21.x mod on a 26.1 runtime worked); the reverse direction silently failed and Fabric Loader crashed at launch withClassTweakerFormatException. New behavior: strip theclassTweakersandaccessWidenerdeclarations from the mod’sfabric.mod.jsonduring 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
minecraftand refused to start. Every bundled dependency is now relocated undercom.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.shnow declares the appropriate per-MC"java"constraint infabric.mod.json:>=17for MC 1.20-1.20.4,>=21for MC 1.20.5-1.21.x,>=25for MC 26.x. This directly fixes the GitHub issue where MC 1.20.1 + Java 17 was rejected (reported on the issue tracker). - Forge
loaderVersionis 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.shnow maps MC → Forge loader version (e.g. MC 1.20.1 → Forge 47). - NeoForge
loaderVersionis now correct per MC version. Same pattern: beta.1 had a hardcoded[4,)minimum that didn’t match NeoForge’s actual versioning (20.2.xfor MC 1.20.2,21.0.xfor MC 1.21, etc.). Now per-MC. issueTrackerURLin build-all.sh pointed at the oldMC-Retromodrepo name in Forge/NeoForge metadata. Now correctly points atRetromod.- 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.jsonso 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=trueon 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.processNestedJiJJaris 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
AotCompilerandApiEmbeddernow validate entry names viaZipSecurity.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, andQuiltModTransformernow use the boundedZipSecurity.copyBoundedhelper instead of unboundedFiles.copy(InputStream, Path). SignatureVerifierno 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-Classesmanifest entries are no longer in the main mod JAR or the shaded CLI fat JAR, only in the dedicated-agent.jarclassifier (which exists for explicit-javaagentuse). 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 zeroRuntime.execcalls 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 downloadcommand prompts before any HTTP request. fabric.mod.jsonandquilt.mod.jsonnow use${project.version}via Maven resource filtering, so future version bumps only need to touchpom.xml.- Per-loader output JARs from
build-all.share now properly metadata-stripped: the Fabric build containsfabric.mod.jsonANDquilt.mod.json(so it works on both Fabric and Quilt loaders); the Forge build contains onlymods.toml; the NeoForge build contains onlyneoforge.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.