Retromod

Retromod Is a Minecraft mod that allows you to play older mods on newer versions.

File Details

Retromod 1.2.0-Snapshot.7 (NeoForge 26.2)

  • R
  • Jul 2, 2026
  • 3.14 MB
  • 3
  • 26.2
  • NeoForge

File Name

retromod-1.2.0-snapshot.7+26.2.jar

Supported Versions

  • 26.2

Curse Maven Snippet

NeoForge

implementation "curse.maven:retromod-1532616:8359403"
Curse Maven does not yet support mods that have disabled 3rd party sharing

Learn more about Curse Maven

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

Added

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

Fixed

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