Repository navigation
Addon API
Legacy Connector runs unmodified 1.7.10 and 1.12.2 Forge mods on NeoForge 1.21.1. The addon API lets other NeoForge 1.21.1 mods build on those legacy mods with modern code. Examples of what an addon can do:
- give modern items and blocks (copper, deepslate, bee nests, blocks of other modern mods) Thaumcraft 4 aspects;
- expose a legacy tile entity (an essentia jar, an Iron Chest, a RotaryCraft machine) as a NeoForge capability
(
IItemHandler,IFluidHandler,IEnergyStorage) so modern pipes and Create can use it; - call a legacy mod's own API with modern content, react to the legacy load phases with modern code.
The API lives in the package org.kylepitcock.legacyconnector.api (and api.event). It is part of the normal Legacy Connector
jar. Addons are ordinary NeoForge mods that compile against the API and the NeoForge 1.21.1 types only.
-
apiandapi.eventare public API. Signatures contain only JDK, Minecraft/NeoForge 1.21.1 and API types, never classes of the legacy shim. Legacy objects (a 1.7.10ItemStack, aTileEntity, aWorld) are passed asObject. - While the mod is 0.x, PATCH releases (0.3.x -> 0.3.y) never break the API; a MINOR release may, and the release notes say so.
From 1.0.0 on it is semantic versioning.
LegacyApi.API_VERSIONis the API's ownmajor.minorfor additions. -
api.internalis not API. The legacy shim (legacyconnector.v1_7_10,legacyconnector.v1_12_2) is not API either and changes freely: reach into legacy objects by their original legacy names throughLegacyAccess, exactly as the legacy mod would.
Compile against the API jar, do not bundle it (at runtime it is inside the Legacy Connector jar):
dependencies {
// From this repository: ./gradlew apiJar -> build/dist/legacyconnector-api-<version>.jar (classes and sources)
compileOnly files('libs/legacyconnector-api-0.3.6.jar')
// A maven coordinate will be published with 1.0; placeholder:
// compileOnly 'org.kylepitcock.legacyconnector:legacyconnector-api:0.3.6'
}Declare the dependency in your neoforge.mods.toml so Legacy Connector loads first:
[[dependencies.my_addon]]
modId = "legacyconnector"
type = "required"
versionRange = "[0.3.6,)"
ordering = "AFTER"
side = "BOTH"Implement LegacyAddon (public class, public no-argument constructor; every method has an empty default) and name it in your
neoforge.mods.toml with a mod property:
[modproperties.my_addon]
legacyconnector-addon = "com.example.MyAddon" # or a list: ["com.example.A", "com.example.B"]Legacy Connector reads the legacyconnector-addon property of every installed mod, so an addon needs no other registration. A
META-INF/services/org.kylepitcock.legacyconnector.api.LegacyAddon file is read as well. The class is created once. Addons run in
order of priority() (lower first), then by class name. requiredLegacyMods() lists legacy mod ids (for example
List.of("thaumcraft")): while one is missing or failed, the addon gets no callback at all, so it needs no isLoaded checks.
Exceptions in a callback are logged and do not stop the game or other addons.
NeoForge registry events (modern mods register)
| last RegisterEvent
v
DISCOVERED all legacy mods known, none constructed onLegacyModsDiscovered / LegacyModsDiscoveredEvent
| legacy construction, preInit, [1.12.2 registry events], init, IMC, postInit, deferred registrations
v
REGISTERED legacy content registered, registries open onLegacyRegistered / LegacyRegistrationFinishedEvent
| RegisterCapabilitiesEvent registerCapabilities(LegacyCapabilities)
| FMLCommonSetupEvent, legacy loadComplete
v
LOAD_COMPLETE registries frozen, main thread onLegacyLoadComplete / LegacyLoadCompleteEvent
Each phase calls the addons first, then posts the event on every mod's event bus (IModBusEvent, so modBus.addListener(...)
works for mods that are not addons), then runs actions queued with LegacyApi.runWhen(phase, action).
Which phase to use: feed modern content into a legacy mod's tables at LOAD_COMPLETE (everything legacy is complete, all modern registry entries exist; Thaumcraft registers its own vanilla aspects in postInit, so earlier registrations could be overwritten). Use REGISTERED only when the legacy mod must see your data before its own loadComplete.
| Class | Purpose |
|---|---|
LegacyApi |
mods(), mod(id), isLoaded(id), hasLegacyMods(), phase(), runWhen(phase, action), classOf(modId, className), API_VERSION
|
LegacyMod |
id(), legacyId(), name(), version(), minecraftVersion(), failed(), failureMessage(), classLoader(), classOf(name), instance(), file()
|
LegacyVersion |
V1_7_10, V1_12_2
|
LegacyPhase |
NOT_STARTED, DISCOVERED, REGISTERED, LOAD_COMPLETE
|
LegacyAddon |
the entry point (see above) |
LegacyAccess |
reflection on legacy objects: invokeStatic, invoke, newInstance, getStatic, getField, setField, setStatic, isInstance, hasMethod
|
LegacyItems |
toLegacy(ItemStack, version), fromLegacy(Object)
|
LegacyBlocks |
legacyBlock, legacyMeta, legacyState (1.12.2), fromLegacy, fromLegacyState, legacyTile(BlockEntity), legacyTile(Level, BlockPos), tileVersion, versionOf
|
LegacyWorlds |
toLegacy(Level, version) |
LegacyContentHooks |
itemKey(id or stack, version) -> LegacyItemKey(version, legacyName, damage, legacyItem), legacyStack(id, count, version), legacyBlockStack(id, count, version), modernItemId(legacyStack)
|
LegacyCapabilities |
forTile(modId, className, capability, adapter), forAnyTile(capability, adapter), adapter constants |
event.* |
LegacyModsDiscoveredEvent, LegacyRegistrationFinishedEvent, LegacyLoadCompleteEvent
|
You cannot name legacy classes at compile time (their signatures use the shim's legacy Minecraft types), so call them by name.
Methods are found by name and by the argument values (the first method whose parameters accept them, preferring exact types;
null matches any reference parameter; an Integer argument fits int, long, ...). Handles are cached. Varargs methods take
an explicit array. Failures throw LegacyAccess.LegacyAccessException; the called method's exception is its cause.
Class<?> api = LegacyApi.classOf("thaumcraft", "thaumcraft.api.ThaumcraftApi").orElseThrow();
Object list = LegacyAccess.newInstance(LegacyApi.classOf("thaumcraft", "thaumcraft.api.aspects.AspectList").orElseThrow());
LegacyAccess.invoke(list, "add", LegacyAccess.getStatic(aspectClass, "METAL"), 5);
LegacyAccess.invokeStatic(api, "registerObjectTag",
LegacyContentHooks.legacyStack(ResourceLocation.parse("minecraft:copper_ingot"), 1, LegacyVersion.V1_7_10).orElseThrow(), list);A modern item that the legacy version did not have (copper ingot) converts to a legacy stand-in item with a stable identity: the
same modern item always gives the same legacy item and damage, so a legacy mod can key data by it. LegacyContentHooks.itemKey
returns that key (legacy name and damage) for modern ids; legacyStack gives the stack to pass to a legacy API.
For 1.7.10, LegacyItems.toLegacy returns a live view of the modern stack (changes to count or damage reach it) and null for an
empty stack, as 1.7.10 did. For 1.12.2 it returns a copy and ItemStack.EMPTY.
LegacyBlocks.legacyTile(blockEntity) returns the mod's own TileEntity instance for the block entity that hosts it (empty for
vanilla and modern block entities). LegacyBlocks.versionOf(state) tells which legacy version a block state belongs to.
All legacy tile entities of a version share one modern block entity type (legacyconnector:legacy_tile_1_7_10,
legacyconnector:legacy_tile_1_12_2). A rule therefore says: if the hosted legacy tile is an instance of class X of mod M, answer
capability C with this adapter:
@Override
public void registerCapabilities(LegacyCapabilities caps) {
caps.forTile("thaumcraft", "thaumcraft.common.tiles.TileJarFillable",
Capabilities.FluidHandler.BLOCK, (tile, side, host) -> new JarView(tile));
}registerCapabilities is called while NeoForge fires RegisterCapabilitiesEvent. Addon rules are asked before the built-in ones; an
adapter returning null passes to the next rule. The class is looked up lazily by name (class, superclass or interface); with a
missing mod or class the rule never matches. NeoForge caches capability lookups until Level.invalidateCapabilities(pos): return a
handle that reads the legacy tile on each call instead of copying its state.
Already built in (do not register these again):
| Legacy | NeoForge capability | Constant |
|---|---|---|
1.7.10 IInventory, ISidedInventory
|
ItemHandler.BLOCK |
LegacyCapabilities.INVENTORY_ITEMS |
1.7.10 Forge IFluidHandler
|
FluidHandler.BLOCK |
LegacyCapabilities.FLUIDS_1710 |
1.7.10 CoFH IEnergyReceiver/IEnergyProvider (1 RF = 1 FE) |
EnergyStorage.BLOCK |
LegacyCapabilities.RF_ENERGY |
1.12.2 tile answering Forge ITEM_HANDLER_CAPABILITY
|
ItemHandler.BLOCK |
LegacyCapabilities.FORGE_ITEMS_1122 |
1.12.2 tile answering Forge CapabilityEnergy.ENERGY
|
EnergyStorage.BLOCK |
LegacyCapabilities.FORGE_ENERGY_1122 |
1.12.2 tile answering Forge FLUID_HANDLER_CAPABILITY
|
FluidHandler.BLOCK |
LegacyCapabilities.FORGE_FLUIDS_1122 |
1.7.10 Forge IFluidTank tile (not an IFluidHandler) |
FluidHandler.BLOCK (one tank) |
built in |
IC2 IEnergySink / IEnergySource tiles (energy.fePerEu, default 4 FE = 1 EU; energy.fePerEu.in/.out set one direction, 0 = off) |
EnergyStorage.BLOCK |
built in (reflection) |
BuildCraft IPowerReceptor tiles (energy.fePerMj, default 10 FE = 1 MJ; energy.fePerMj.in = 0 turns it off; receive only) |
EnergyStorage.BLOCK |
built in (reflection) |
Legacy tile entities are exposed as capabilities by default, so modern hoppers, Create belts/funnels/arms/vaults, modern pipes and energy
cables work with them without an addon; addons only add what is missing. Side handling: NeoForge's Direction maps to ForgeDirection
by index (DOWN, UP, NORTH, SOUTH, WEST, EAST), a null side is UNKNOWN/all slots; canInsertItem/canExtractItem/getAccessibleSlotsFromSide
are honoured; simulation never mutates (NeoForge's wrappers copy stacks); markDirty is called after changes. IC2 sinks get at most one
packet of their tier's voltage per call (8 * 4^tier EU) so a machine is never overloaded; an IC2 tile is not double handled because IC2's own
cables and BuildCraft pipes are legacy blocks that never ask for the capability. Cached lookups (Create) are invalidated on every neighbour
change of a legacy tile block (Level.invalidateCapabilities(pos)), and NeoForge itself does it on placement and removal.
Items (by default): 1.7.10 IEnergyContainerItem (1 RF = 1 FE), IC2 IElectricItem/ISpecialElectricItem through ElectricItem.manager
(charge/discharge with the item's tier; energy.fePerEu) and IFluidContainerItem become EnergyStorage.ITEM / FluidHandler.ITEM;
1.12.2 items that attach capabilities in Item.initCapabilities become ItemHandler.ITEM/FluidHandler.ITEM/EnergyStorage.ITEM, looked
up again on every call on a view of the live stack (stacks are copied constantly, nothing is cached). Tools: legacy pickaxes, axes, spades,
hoes, swords and shears, or any item whose Forge getToolClasses names pickaxe/axe/shovel/hoe/sword/shears, answer the matching
ItemAbilities (*_DIG, AXE_STRIP/SCRAPE/WAX_OFF, SHOVEL_FLATTEN, HOE_TILL, SHEARS_HARVEST) in both versions. Already existing:
ore dictionary names as c: tags (OreDictBridge), burn time, crafting remainders, destroy speed, enchantment value.
Not done: a view of modern block entities as legacy IInventory/IFluidHandler (legacy pipes pushing into a Create vault or a modern
chest: legacy code asks getTileEntity(...) instanceof IInventory, which needs a TileEntity view per modern block entity), BuildCraft 7 MJ
batteries (only the 6.x PowerHandler API), extracting from BuildCraft engines, 1.12.2 fluid textures for the modern fluid types,
FIRESTARTER_LIGHT and attribute modifiers for legacy items. An addon can supply these with forTile + LegacyAccess.
examples/thaumcraft-modern-aspects in this repository (a standalone Gradle build, not part of the release jar):
- registers Thaumcraft aspects for
copper_ingot,raw_iron,raw_copper,amethyst_shard,honeycomb,deepslateandbee_nestinonLegacyLoadComplete, throughThaumcraftApi.registerObjectTag; - exposes Thaumcraft's essentia jar (
TileJarFillable) as a read-onlyIFluidHandler.
Build and install:
./gradlew apiJar # in the repository root: build/dist/legacyconnector-api-<version>.jar
cd examples/thaumcraft-modern-aspects
../../gradlew build # -> build/libs/thaumcraft-modern-aspects-1.0.0.jar
Copy the jar into the mods folder next to Legacy Connector, Thaumcraft 4.2.3.5 (1.7.10) and its dependencies (Baubles/Curios
bridge is automatic). Check the log for Legacy addon example.tcmodern.ThaumcraftModernAspects and
Registered Thaumcraft aspects for 7 modern items/blocks. In game, open a Thaumonomicon/Thaumometer: scanning (or /given and
inspecting with the Thaumometer) a copper ingot should show Metallum and Permutatio.
- Server and client: addon callbacks run on both; capabilities and recipes are server logic.
- Legacy mods that fail to load are listed with
failed() == true;isLoadedis false for them. - Names used with
LegacyAccessare the legacy (deobfuscated MCP) names the mod itself uses; do not use SRG or shim names. - 1.7.10 has no flattening: modern-only blocks and items are stand-ins whose damage is always 0; a modern block with block states reports one stand-in per block, not per state.
- No tile-entity events (placed/broken) are exposed yet; use NeoForge's own block events with
LegacyBlocks.legacyTile.