Files
RustNotMC/docs/modpacks.md
T
SashegDev 874b6f20c2 Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
2026-10-29 22:04:28 +03:00

78 KiB
Raw Blame History

Modpacks: porting any pack to Horizon

Goal (Ludovic, 2026-10-16): support modpacks like All the Mods 10 far better than Minecraft does, and be able to port any of them. "Better" is measured: pack load time, RAM, FPS, TPS and world-generation speed against the same pack on NeoForge or Fabric. "Any of them" means a pipeline, not a list of hand ports. It takes a pack as players download it and produces native Horizon mods. The pipeline is AI agents in dependency order, each port checked against the original mod running in a dev-only Java instance.

This page is the design. It builds on mods.md (the mod API, WASM in wasmtime, and why Java jars cannot run in Horizon, so we port instead). The ATM10 inventory behind the numbers below is modpacks/atm10-mods.csv.

In one picture

 pack (.mrpack / CurseForge zip or instance / Prism instance)
   │  import: normalise to pack.json (MC version, loader, files, hashes, overrides)
   ▼
 inventory: every jar's metadata (mods.toml / fabric.mod.json, licence, deps, file census)
   │  classify: content · client-ui · library · performance · server-only · integration · other
   │  tier:     data · logic · machines · deep · client · none
   ▼
 dependency order (libraries first, then the mods that need them)
   │
   ├─ carry-over (mechanical, no agent): assets/ + data/ upgraded to 26.3 formats,
   │    block and item registrations from the oracle's facts export  →  <mod>.hmod
   ├─ port (one agent per mod, tiers logic and up): Rust → WASM against the Horizon
   │    mod API, iterating until its harness passes                  →  <mod>.hmod
   ├─ harness per mod: the Java mod in a dev-only NeoForge/Fabric oracle vs the port
   ▼
 coverage report: ported · partial · not yet · not needed, per mod and per class

Everything runs on the player's (or server owner's) machine, on jars they downloaded. The results stay there. See Licences.

All the Mods 10, measured

Source. ATM10 is on CurseForge. Its GitHub repository (AllTheMods/ATM-10) carries the pack's own mod list, config/crash_assistant/modlist.json, written by the Crash Assistant mod in the pack. We used the version committed on 2026-10-15. It lists jar names, mod ids, versions, CurseForge fingerprints and Modrinth SHA-1s. ATM10 8.x runs Minecraft 1.21.1 on NeoForge 21.1.259. The list has 495 jars (494 mod ids: CC: Tweaked ships two jars) plus one datapack (Universal Sawmill's).

Metadata only. Modrinth's version_files endpoint resolved 321 mods by hash, with their categories, client/server sides, licence and required dependencies. A Modrinth name search found 70 more CurseForge-only mods with a listing. The remaining 104 are CurseForge-only, so CurseForge's API (which needs a key) would be the only source. They were classified from their names and what they are known to do. 20 of those calls are marked guess in the CSV. No jar was downloaded.

Classes

Class What it is Port? ATM10
content adds or changes gameplay: blocks, items, machines, mobs, worldgen, magic yes: carry-over or agent 287
library APIs and code shared by other mods (Architectury, Curios, GeckoLib, Kotlin for Forge, Balm...) no: maps to the Horizon API or the SDK 80
integration glue between two other mods (JEI plugins, AE2 ↔ Mekanism, Create add-on wiring) after both sides are ported 44
client-ui client-only screens, HUDs, tooltips, maps, recipe viewers yes: client API, or built in 32
other pack tooling and engine features: KubeJS, Almost Unified, shader mods, CTM, crash/version tools no: folded into the pipeline or the engine 25
performance Sodium, FerriteCore, ModernFix, ImmediatelyFast, spark, AI and recipe speed-ups no: Horizon is the replacement 16
server-only admin and server features: ranks, essentials, backups, claims, spawn control yes: server API 11
495

Tiers (the API a port needs)

Tier Needs ATM10 of which content
data registries (blocks, items) + carried-over data and assets; no code 68 56
logic + behaviour callbacks (use, place, break, ticks), server events, config 72 55
machines + block entities, menus and screens, capabilities (energy, items, fluids), mod networking 101 89
deep + entities and AI, custom renderers and animated models, dimensions, new vanilla hooks (what Mixins do) 91 87
client the client interfaces (keys, target, inventory-read, ui, render) 38 0
none nothing to port 125 0

Class by tier:

data logic machines deep client none
content 56 55 89 87
library 80
integration 11 7 12 3 8 3
client-ui 30 2
other 1 24
performance 16
server-only 10 1

What this says:

  • A quarter of ATM10 needs no port at all (125: libraries, performance mods, pack tooling). Horizon's renderer and simulation replace the performance mods. Libraries become API features: Curios becomes accessory slots, GeckoLib an animated-model format, Patchouli, Modonomicon and GuideME a guide-book reader, the config libraries a config schema.
  • 68 mods are data tier. They are the structure packs (YUNG's, Moog's, Formations, CTOV, Towns and Towers, When Dungeons Arise), the decoration blocks (Macaw's doors, fences, paths, roofs, stairs; Chipped; XTones; Factory Blocks) and the ore and compression sets (AllTheOres, AllTheCompressed). They come over without an agent once registries and worldgen carry-over exist.
  • The big systems are deep tier. Create, AE2, Mekanism, Ars Nouveau, Twilight Forest, MineColonies and Applied/Refined Storage are a long campaign of agent ports. They reach the most dependents: AE2 has 9 required dependents in the pack, Create 7, Ars Nouveau 6 and Mekanism 5.
  • Dependency depth is shallow. Of the 321 mods with Modrinth data, 188 need nothing else in the pack, 103 sit one layer up, 29 two layers up and one (Apotheosis) three. The most required libraries are YUNG's API (10 dependents), GeckoLib, Resourceful Lib, SuperMartijn's core (8, 8, 7), then Curios, Bookshelf, Prickle and McJtyLib (6 each).

Licences in ATM10

Class permissive (MIT, Apache, BSD, CC0, CC-BY) copyleft (GPL, LGPL, MPL, AGPL) All Rights Reserved custom non-commercial / no-derivatives unknown (CurseForge only)
content 93 46 62 16 9 61
library 27 20 9 6 4 14
integration 16 8 9 0 0 11
client-ui 13 5 3 5 1 5
other 6 8 4 1 0 6
performance 6 5 1 1 0 3
server-only 6 0 1 0 0 4
all 167 92 89 29 14 104

The "unknown" column is a gap in our metadata, not in the mods: NeoForge's mods.toml has a mandatory license field, and the inventory reads it from each jar at import.

Licences

Mods have their own licences: many are All Rights Reserved, others MIT, LGPL or MPL. A port is a derivative work. These rules are part of the pipeline, not advice to users:

  1. Local by default. The pipeline runs on the player's or server owner's machine, on jars they downloaded themselves. The ports it makes stay there. This works like prepare with Minecraft's own files: nothing is redistributed.
  2. The licence is recorded per mod in the inventory (license from mods.toml or fabric.mod.json, plus Modrinth's SPDX id where one exists) and travels with the port into its horizon-mod.toml (ported-from = { id, version, license, url }).
  3. Publishing needs permission. A port can go into a mod index, the launcher browser or a server's download list only if its licence allows redistributing derivatives (permissive, or copyleft with the port under the same licence and its source published) or the author has given written permission, recorded in the index entry. The tooling refuses to package anything else. No-derivatives and non-commercial licences (14 in ATM10) are never published. For them, even a local port gets a warning that the licence may not allow it.
  4. Mod code never enters this repository, and nobody reads it. Mod jars, Java oracle builds with mod code, and ports of mods we have no permission to publish all live outside the repo: in the user's pipeline work directory, or for our own development in ~/code/horizon-parity/modports/ (dev-only, never pushed). A mod's code is never decompiled, read, quoted or paraphrased, by people or agents. Ports are written only from the mod's observed behaviour (the oracle and the harness), its own data files and its public documentation. The repository holds the pipeline, the API, metadata (ids, names, versions, licences, counts) and test fixtures we write ourselves.
  5. Contacting authors or publishing any port is Ludovic's call. Agents ask first.

This is engineering policy, not legal advice. Before anything is published, Ludovic decides, with advice if needed.

The mod API: what packs need, against API 0.1

API 0.1 is client-side only: log, game (player and clock), chat, hud, plus tick, frame, chat and command hooks. Server mods do not run yet. What a content pack needs:

Feature API 0.1 Needed by (ATM10 content+integration, by tier) Planned in Shape
Block registry: ids, states, properties, shapes, sounds, light — data and up (all 331) 0.2 declared in data/<mod>/horizon/block/*.json, each from a vanilla template block (below)
Item registry, block items — data and up 0.2 data/<mod>/horizon/item/*.json, from a vanilla template item
Recipes, loot tables, tags, advancements, lang, blockstates, models, textures, sounds in the mod's namespace vanilla namespace only data and up 0.2 the 26.3 data-pack and resource-pack formats, as they are
Worldgen JSON: features, structures, template pools, processors, structure NBT, biome additions vanilla bundle only about 30 data-tier structure and biome mods 0.2.x carried into the worldgen/ bundle
Block and item behaviour callbacks: use, place, break, attack, random tick, scheduled tick, neighbour change, entity inside — logic and up (253) 0.3 server interface; hooks per registered id, budgeted
Server events, typed commands, game rules — logic and up, server-only (11) 0.3 server interface
Config: typed schema in the manifest, per world and per server, a settings page — most mods with code 0.3 config + the Options > Mods page
Block entities: per-block NBT state, ticking, client sync — machines and up (191) 0.4 block-entity interface, ticks batched per mod
Menus and screens: slots, progress bars, buttons, fluid and energy gauges — machines and up 0.4 a declared screen layout the game draws (ui), with server-side menu logic in the mod
Capabilities: energy (FE), item, fluid handlers on blocks and items — machines and up 0.4 capabilities interface; FE as the common unit, others as mod-defined resources
Data attachments on entities, chunks, levels — machines and up 0.4 storage per entity/chunk/level
Accessory slots (what Curios does) — Curios users (6 require it, more use it optionally) 0.4 built-in slots API
Mod networking between its client and server halves — machines and up 0.4 net interface (already planned in mods.md)
Custom recipe types: machine recipes with their own JSON — machines and up 0.4 recipe-type registry: the mod declares its JSON schema and a display layout for the recipe viewer
Entities: types, attributes, AI goals and brains, spawning — deep (87) 0.5 entity interface, AI in the mod with vanilla goals reusable
Custom renderers and animated models (block-entity, entity, item; GeckoLib's format) — deep 0.5 data models (Bedrock geometry + animations), plus a budgeted draw-list interface for the rest
Dimensions: dimension types, levels, chunk generators — about 10 (Twilight Forest, Aether, Ad Astra, Undergarden, ...) 0.5 JSON part with worldgen; custom generators as mod code
New hooks into vanilla behaviour (what Mixins do) — deep as asked each becomes a named hook, added for everyone
Client interfaces: keys, target, inventory-read, ui, render, world-read planned client tier (38) 0.2–0.4 as in mods.md

Two rules from mods.md stay: the sandbox (no files, sockets or processes; memory cap; fuel per call; a crashing mod stops alone) and free when unused (no mods means vanilla code paths and the vanilla resource root, so the parity harnesses test the same game).

Registries 0.2: data-driven blocks and items

A .hmod may now carry assets/ and data/ beside (or instead of) mod.wasm. A content-only mod needs no code:

ruby.hmod
├── horizon-mod.toml          id = "ruby", api = "0.2", side = "both", capabilities = ["registries"]
├── data/ruby/horizon/block/ruby_block.json
├── data/ruby/horizon/item/ruby.json
├── data/ruby/recipe/ruby_block.json            (26.3 data-pack formats)
├── data/ruby/loot_table/blocks/ruby_block.json
├── data/minecraft/tags/block/mineable/pickaxe.json   (adds to a vanilla tag)
├── assets/ruby/blockstates/ruby_block.json     (26.3 resource-pack formats)
├── assets/ruby/models/block/ruby_block.json
├── assets/ruby/items/ruby.json
├── assets/ruby/textures/block/ruby_block.png
└── assets/ruby/lang/en_us.json

A block declaration names a template, a vanilla block whose behaviour class, state properties and shapes the new block takes, and overrides what differs:

{
  "template": "minecraft:iron_block",
  "properties": { "hardness": 5.0, "explosion_resistance": 6.0, "sound": "minecraft:metal",
                  "map_color": 33, "light": 0, "requires_tool": true },
  "item": true
}

Most decoration and resource blocks are vanilla classes with other numbers: Macaw's doors are DoorBlocks, XTones are full cubes, ore sets are DropExperienceBlocks. So templates cover the data tier without code. Templates also give mod blocks every behaviour Horizon already implements and the harnesses already check. Items work the same way ("template": "minecraft:diamond", overriding max_stack_size, rarity and so on).

How the game loads them, without touching vanilla's path:

  • Horizon reads everything from one resource root. Resource packs already work as an overlay root (packs::startup_root) with vanilla's files linked and the overridden ones rewritten. Content mods add one more layer: the enabled mods' assets/ and data/, and the prepared per-state tables (facts.json, gameplay-facts.json, block-behaviours.json, the collision, occlusion and movement tables, model-states.json, render-definitions.json, items.json, ...). Each table gets the mod blocks' rows appended, copied from the template's rows with the declared overrides. The loaders then read one tree and need no mod-specific code.
  • Vanilla state ids do not move. Mod states are appended after vanilla's, mods in id order, so the same mod set gives the same ids. Saves store block names, so ids never reach the disk.
  • Loaders that read only data/minecraft/ (recipes, loot, tags) read every namespace, as vanilla does. With no mods there is only minecraft, so nothing changes.
  • Namespaces. A mod's files go under its id, and under the namespaces its manifest lists when its Java original ships them so (namespaces = ["kaisyn", "towns_and_towers"] for Towns and Towers, mod id t_and_t; docs/mods.md). No namespace has two owners: a mod whose id or listed namespace another mod that is on already owns is left out, with both named in the log. horizon-port-pack avoids that inside a pack: a namespace two carried mods both claim (YUNG's structure mods all ship files under yungsapi, the namespace of YUNG's API, whose own jar isn't carried) is left to none of them, and its files stay outside. The launcher applies the game's rule before it starts a pack (pipeline::loadable) and lists what it left off.
  • The overlay is built on the first start with a set of mods and kept for later starts. A stamp (horizon-mod-content.stamp) hashes the game executable, the base tables and every mod file's size and mtime. Adding, removing or editing a mod, or a new build of Horizon, rebuilds it. For ATM10's 40 data-tier mods the build takes about a minute of the first start on a busy machine.
  • Worldgen. Mods' structures, structure sets, template pools, processor lists, configured and placed features join the worldgen/*.json bundles. Their structure templates go under worldgen/structure/<ns>/. Their biome tags are merged into worldgen/biome_tags.json, minus the biomes and tags Horizon does not have. Mod biomes, noise and density functions are not carried yet.
  • Limits. Chunk sections size their global palette to the registry: 16 bits for vanilla's 35,723 states (the same chunks, saves and packets as before), one more bit each time content mods double it, up to 2^20 states (1,048,576). Past that the content root is refused with the states per mod. ATM10's 42 carried mods take the registry to 193,762 states (18 bits). On the network the width is the registry's: unchanged for vanilla clients as long as no content mod is on, wider only between Horizons that share the same mods; a Horizon with content mods still reads a vanilla server's 16-bit chunks. Block sprites fill texture arrays of 2048 layers, eight at most (16,384; vanilla takes 1,250, identical textures share a layer; ATM10's 42 take 9,790).
  • Broken mod files load as vanilla loads them. A blockstate definition that does not bind (a multipart value the block does not have), a block model that is missing or does not parse, and a recipe that does not parse are logged and skipped: the block gets no model or the missing model, the recipe is absent, everything else loads. Vanilla's own files never hit these paths.
  • Not in 0.2: multiplayer registry sync (single player and the integrated server first), worlds opened without a mod they used (vanilla turns unknown blocks into air; we will ask first), vanilla ids redefined by a mod (refused).

The pipeline: horizon-port-pack

A Rust tool in this repo (crates/horizon-port-pack, a CLI the launcher also calls), modelled on the version-port pipeline (tools/version_port.py: detect, diff, regen, harnesses, report, publish). Each step reads and writes files in a work directory, so it can be rerun, resumed and inspected:

<work>/                       ~/.horizon/ports/<pack-slug>/ for players; ~/code/horizon-parity/modports/ for us
├── pack.json                 normalised import
├── inventory.json            per mod: metadata, licence, census, class, tier, deps, layer
├── jars/                     the user's own jars (copied or linked from their instance), never uploaded
├── mods/<id>/
│   ├── carry/                assets/ + data/ upgraded to 26.3
│   ├── oracle/               facts, registries and traces exported from the Java oracle
│   ├── port/                 the agent's Rust crate (tiers logic and up)
│   ├── harness/              per-mod harness results and logs
│   └── <id>.hmod
└── coverage.json, coverage.md

Status. import, inventory, fetch, carry and report run today, and port runs them all for what a player's machine can port (content mods of the data tier), as the launcher's "Import modpack" does:

cargo run -p horizon-port-pack -- import ~/Downloads/pack.mrpack ~/.horizon/ports/pack
cargo run -p horizon-port-pack -- inventory ~/.horizon/ports/pack     # --offline reuses modrinth.json
cargo run -p horizon-port-pack -- fetch ~/.horizon/ports/pack         # the portable mods' jars, or name them
#   --from <folder>: copy the player's own jars (an installed instance, a downloads folder)
cargo run -p horizon-port-pack -- carry ~/.horizon/ports/pack         # content mods with a jar, or name them
#   --resources <prepared root>: only templates Horizon's tables have
cargo run -p horizon-port-pack -- report ~/.horizon/ports/pack        # coverage.json and coverage.md
cargo run -p horizon-port-pack -- port ~/Downloads/pack.zip ~/.horizon/ports/pack [--from <folder>] [--resources <root>]

check <jar> <out> takes one mod jar on its own, as the launcher's mod browser installs it (mod-browser.md): it carries the jar into <out>/carry/ and reads its class files (constant pool, superclass, declared methods; no decompiling) for what the carry can't take, then writes the verdict to <out>/check.json. Works: content only, and the code just registers it. Partly: the content ports, and the code features listed don't (block entities, menus, entities, blocks and items with behaviour of their own, recipe types, Mixins, game events). Doesn't run: nothing to port, or a jar made for 1.20 or older. Each mod class is followed up to the first game class it extends; a block subclass that only overrides how it looks or is placed (getStateForPlacement, rotate...) is registration. Game classes are read by Mojang's names (26.1 and later, and NeoForge since 1.20.5); older Fabric jars name them in intermediary, so a class extending one counts as code that can't be read. On ten 26.3 jars (2026-10-22): Macaw's Fences, Windows and Furniture, Ecologics, Compressed Blocks, More Decorative Blocks and Moog's Nether Structures are Partly; Sodium and GeckoLib don't run (Mixins, renderers, nothing to port); Fabric API doesn't run (its id has a dash, which content mods can't use). A check takes 0.2 s to 3 s, and 24 s for Compressed Blocks (18 MB, 9,360 recipes, writing the carry dominates).

fetch gets each jar the player's way, never from us: copied from a folder they have (matched by SHA-1, or through a CurseForge app instance's minecraftinstance.json), else downloaded from the link the pack itself gives (a .mrpack's, on the format's hosts only) or, for a CurseForge pack, the same file on Modrinth's public CDN, found by its SHA-1. A jar is kept only when its hash matches. A file only CurseForge has is listed as "CurseForge downloads coming soon" (its API needs a paid key; Ludovic, 2026-10-22), with the player's installed instance as the way round it. <work>/fetch.json says what happened to each file. On All the Mods 10 8.3's CurseForge zip (2026-10-17): of the 54 data-tier content mods, 42 come from Modrinth's CDN (84 MB) and 12 need the key or the instance; port then carries the 42 over (0 ported, 42 partial: the harness is a dev step).

inventory reads metadata only: Modrinth answers by file hash, and the jars only if the user's instance has them (an imported instance folder, or jars put in <work>/jars/). Without jars, classes come from the reviewed table, Modrinth categories and sides; with jars, from their mods.toml / fabric.mod.json and file census too. On Create+ 6.0.0 alpha f (155 files, metadata only): content 54, library 33, client-ui 26, performance 25, other 14, unknown 2, server-only 1; 40 rows matched the reviewed ATM10 table; three dependency layers (93 / 57 / 5).

carry copies a jar's assets/<id>/, data/<id>/, data/minecraft/tags/ and data/c/tags/ into mods/<id>/carry/, a folder content mod (API 0.2) the game loads with --dev-mod, and keeps what a content mod may not hold (other namespaces, vanilla data) in outside/. Files of the mod's own content that the game's content loader would refuse whole also go to outside/, listed with the reason in the report's unreadable: paths vanilla does not read (upper case), and JSON that does not parse. Vanilla skips them one by one. A vanilla or convention tag with "replace": true is carried as it is: the game honours it as a data pack's, with a warning (docs/mods.md); Towns and Towers' seven minecraft:has_structure/* biome tags are such. A byte-order mark is dropped. For ATM10's 42 mods that is 6 files in 4 mods, and the 42 load as carried. No class file is copied. It upgrades from 1.21.1 to 26.3: vanilla-type recipes (ingredients), loot tables (conditions lists become one condition, functions become modifier, kinds move to type, tool matches on silk touch or shears become minecraft:tool/can_silk_touch and minecraft:tool/can_shear, and number providers written as a bare {"min", "max"} get "type": "minecraft:uniform"), and the items/ definitions 1.21.4 introduced. 1.21.1 read such a bare range as uniform and 26.3 refuses it: 304 of ATM10's carried tables have them (rolls, counts, enchantment levels), and before the carry typed them their chests came out empty or short.

Blocks and items come from the NeoForge oracle when <work>/oracle/facts.json exists (step 6): every block the mod registers gets the vanilla template of the same vanilla class and states whose shapes, sounds, set type and wood type match best, with the mod's own hardness, blast resistance, light, tool need, friction, map colour and sounds as overrides. --resources <prepared> limits templates to blocks Horizon's tables have. carry-report.json lists, per block, what the template does not reproduce (other shapes, sounds no vanilla block has, a class of its own), and the blocks left out: those whose vanilla class has no vanilla block with the same states (Macaw's garage doors, multi-part gates). Recipes get the convention tags they use (c:dyes/red), filled from the oracle with vanilla's and the mod's own members. Without the oracle, templates are guessed from the blockstate's properties: the template whose properties include them with the fewest left over (models often ignore powered or waterlogged), the block's name breaking ties (_pane, _gate, _trapdoor). Walls and vines have the same property names; sides tested as true / false make a vine (Chipped's 11 vines, which the game refused as walls: their blockstates did not load).

1. Import

Format Read Jars come from
Modrinth .mrpack modrinth.index.json: dependencies (minecraft, neoforge / fabric-loader / forge / quilt-loader), files[] with path, sha1/sha512, env (client/server), download URLs; overrides/, client-overrides/, server-overrides/ downloaded from the listed Modrinth URLs, hash-checked
CurseForge zip manifest.json: minecraft.version, modLoaders[], files[] (projectID, fileID, required), overrides. When the overrides hold Crash Assistant's config/crash_assistant/modlist.json (All the Mods 10 does), its jar names, mod ids and SHA-1s replace the bare ids the user's installed instance (CurseForge app minecraftinstance.json + mods/), or the same file on Modrinth by SHA-1. CurseForge's API needs a key, so we take what the user already downloaded
Prism / MultiMC instance mmc-pack.json components (net.minecraft, net.neoforged, net.fabricmc.fabric-loader), instance.cfg, .minecraft/mods/*.jar, packwiz .pw.toml files if present the instance folder
packwiz pack.toml, index.toml, mods/*.pw.toml as listed
Crash Assistant modlist.json jar name → mod id, name, version, Modrinth SHA-1; the loader entry (NeoForge 21.1.x is Minecraft 1.21.1). Packs such as ATM10 publish it in their repository metadata only; jars put in <work>/jars/

pack.json holds the Minecraft version, the loader and its version, each file with its hashes and side, and the overrides (configs, KubeJS scripts, datapacks).

2. Inventory and classification

Per jar, without decompiling:

  • Metadata: META-INF/neoforge.mods.toml / META-INF/mods.toml / fabric.mod.json: id, version, license, authors, URLs, dependencies with ordering and side, mixin configs, access transformers.
  • Census of files: blockstates (≈ blocks), item models (≈ items), recipes, loot tables, tags, worldgen files, structure NBTs, languages, sounds, textures; the number of mixins and the vanilla classes they target; whether the jar has code beyond its data.
  • Census of classes: the superclass names in the class files (constant pool only): how many block entities, menus, screens, entities, renderers, recipe serializers, payload types.
  • Known mods: a reviewed table in the repo (id → class, tier, Horizon equivalent: "sodium: performance, built in", "jei: client-ui, recipe viewer built in") wins over heuristics. It starts with ATM10's 495 rows.

Class rules, in order: the known-mods table; client-only side → client-ui; server-only side → server-only; mixins only, into rendering or ticking, no content → performance; no content and depended on by others → library; depends on two or more content mods and adds little → integration; otherwise content. Tier rules: content with no code beyond registration → data; behaviour callbacks only → logic; block entities or menus → machines; entities, renderers, dimensions or mixins into gameplay → deep.

3. Dependency order

A topological order over required dependencies, libraries first. Each mod gets a layer (0: needs nothing in the pack). A mod is ported only after its dependencies are ported or mapped to the API. A library mapped to the API ("Curios → accessory slots") counts as done when that API feature exists. Cycles (rare: optional mutual integrations) are broken at optional edges.

4. Carry-over: data and assets

Mechanical, the same for every mod, and no agent:

  1. Extract assets/ and data/ from the jar into mods/<id>/carry/.
  2. Upgrade to 26.3. ATM10 is 1.21.1 and Horizon is 26.3. Item models moved to items/ (1.21.4), recipe ingredients and results changed shape (1.21.2), some definitions moved to files of their own, and some registries and tags were renamed. The upgrader is a table of these changes generated from vanilla's own datafixers and pack-format notes, applied per file. A file it cannot upgrade is listed, not guessed.
  3. Registrations. Block and item ids registered in Java code are read from the oracle (step 6): its facts exporter writes each mod block state's hardness, sounds, shapes, light, map colour and class chain. These are the same exporters that produce Horizon's vanilla gameplay-facts.json, run on the modded game. The class chain picks the template (a DoorBlock → a vanilla door). Without an oracle, the blockstate's property set gives the template (facing, half, hinge, open, powered is a door), with the template's numbers.
  4. The pack's own data. KubeJS scripts, Almost Unified and config-driven recipes change recipes, tags and loot after load. We do not translate scripts. The oracle exports the final recipe, tag and loot registries of the whole pack once, and the pack layer carries those over.
  5. Write <id>.hmod. For a data-tier mod, it is now ported.

Two cases the oracle fills in:

  • Loot registered in code. Chipped ships 7 loot tables for about 7,000 blocks; the rest drop themselves through Java code. When a declared block has no table in the jar and the oracle shows vanilla's drop-self, carry writes that table in 26.3's shape. When only the states with one property value drop it (a door's lower half), the entry gets vanilla's match_block condition. Chipped: 6,843 tables added.

  • Worldgen carry-over. Structure libraries give vanilla's types other names. Carry maps these to vanilla's:

    • lithostitched:jigsaw, MoogsStructureLib's and Repurposed Structures' generic jigsaws become minecraft:jigsaw;
    • their advanced_random_spread becomes minecraft:random_spread;
    • their versioned, legacy and empty pool elements become vanilla's.

    Three more rewrites make the mapped structures decode and start where their mod would:

    • Block states. 1.21.1 writes a block or fluid state as {"Name", "Properties"}, 26.3 as {"id", "properties"} and refuses the old shape, which failed the whole processor list, feature or pool. Carry rewrites them (#816; 1,005 files in 17 of ATM10's mods).
    • Start and placement rules (#820 in the game, #821 in carry). Repurposed Structures' and Moog's jigsaws refuse to start in liquid and check biomes and terrain height in a radius; their spreads keep a minimum distance from the world origin and away from other sets (named by a structure set tag). Carry writes these under horizon:start_checks, horizon:min_distance_from_world_origin and horizon:exclusion_zone; the game runs them (horizon-world's structure/start_checks.rs). Vanilla data has none of the keys. Moog's also spreads its structures 1.65 times as far apart as its files say, which carry now applies. MVS went from 829 starts to 90 in a 128×128-chunk square (seed 1234): 294 from the spacing, the rest from the start checks.
    • Mo' Structures (LGPL-3.0; 38 files). Its mostructures:generic is vanilla's jigsaw that starts only where the terrain under the 5×5 chunks around it varies by no more than its heightRange: carry writes minecraft:jigsaw with horizon:start_checks (terrain_radius 2, terrain_range). Its placement mostructures:type takes spacing and separation from the mod's config, not its files; carry writes the config's defaults (keyed by config_key) and the sets it avoids within 3 chunks as horizon:exclusion_zone with "their_zones": false: like the mod, the game tests those sets by their spread and frequency alone, not by their own exclusion zones. Its two block-ageing processors (jungle_rot_processor, simple_cobblestone, 3 uses) are still dropped, as is its mixin that keeps lakes out of its structures. Over chunks -64..63 that adds 13 starts at seed 1234 (pirate ships, churches, a tavern, the castle in the sky...) and 16 at seed 2, and every other start stays as it was.
    • Processors written in Java are taken out of their lists (#822). The game, like vanilla, refuses a list holding a type it does not know, and with it every pool naming the list. The structures now generate undecorated (no pillars under them, spawners as the template has them); 1,427 processors of 146 types in ATM10's mods, the most common lithostitched:block_swap, Formations' biome_replacements and Repurposed Structures' pillar, fluid and void processors.
    • Spawner processors carried as data (horizon-port-pack/src/processors.rs). Most chests in the mods' templates already name their loot table; what the Java processors add is the spawners' mobs. Moog's and Repurposed Structures' spawner randomizing processors become horizon:spawner_mobs (a weighted mob per spawner, picked with the placement's random as theirs do, and the spawner NBT they write; Repurposed's rs_spawners lists are written in). YUNG's Better Dungeons and Better Desert Temples turn a spawner or a placeholder block (gravel, bone block, yellow wool...) into a spawner of a fixed mob: that is vanilla's rule processor with an append_static block entity. Moog's vanilla loot swap acts only when a "replace vanilla" preset is on, and they ship off. Moog's trial spawner and vault processors keep the block and change its block entity: one rule per block state, with the game's horizon:edit modifier, which takes out the state the template was saved with (a vault saved with state_updating_resumes_at far in the future never opens; a spawner's saved spawn_data blocks its own mobs) and merges in the spawner's normal and ominous configs (by id: the carry upgrades trial_spawner/ files' mobs to 26.3's equipment) or the vault's loot table and key. Better Desert Temples' placeholder blocks (orange, red and white glass, blue and yellow concrete, red wool, polished diorite, blank pots) become what its processors make of them: the fixed swaps as vanilla's rule, the random ones (suspicious sand, sand and sandstone, upside-down stairs, cobwebs, sherds on the pots) as the game's horizon:pick, on the same nextFloat as the mod. Better Dungeons' small dungeons keep one chest and each other one with a fifth's chance, two at most (the game's horizon:count_cap, counted per placement as the mod's DungeonContext is): 12 chests in five small dungeons at seed 1234 become 6. Lithostitched's processors (all in ChoiceTheorem's Overhauled Village in ATM10) are the game's own types, read as the mod writes them. block_swap keeps the properties the new block can have: a farm's ripe wheat becomes ripe carrots, but beetroot, whose age stops at 3, keeps its default; a target block the game does not have (most are Nature's Spirit's and Croptopia's) leaves the block as it is. reference runs the lists it names, condition tests the template's block or the world's, apply_random and set_block draw per block or per piece as the data says. Lithostitched's own random is not known yet, so the shares of a random choice are the Java pack's but the blocks chosen differ. Lists named by tag and state providers other than simple and weighted ones are dropped and counted (ATM10 has none). CTOV's compat hooks for other mods (its modular_compat, 30 of them in 17 lists) become a reference when the mod they name is ported along, and are taken out (a no-op, as in the mod) when it is not. In ATM10 they all name Nature's Spirit, which the pack does not have. In Java, Lithostitched also lays out every jigsaw structure its own way (villages, outposts, trial chambers; CTOV's villages are lithostitched:jigsaw, carried as vanilla's jigsaw). Horizon keeps vanilla's layouts, so with Lithostitched in a pack, structure layouts differ from the Java pack's for the same seed (their positions do not). The processors that build outside their template are carried too: Repurposed's and Moog's pillar processors become the game's horizon:pillar (a column from a placeholder block down to the ground, with the processor list they run on each of its blocks written in), Repurposed's bottom pillars horizon:bottom_pillar, their fluid fix (a wall of the structure's block over water and lava sources next to its open blocks) horizon:close_off_fluid_sources, and YUNG's pillars (Better Fortresses, Better Jungle Temples) and Better Fortresses' stair supports horizon:yung_pillar and horizon:stair_pillar. They write only in the chunk being generated, as the mods do. Dropped processors went from 570 to 456 in Repurposed Structures (its pillars, bottom pillars and fluid fixes: 242 lists), 14 to 1 in Moog's, 10 to 5 in Better Fortresses and 52 to 50 in Better Jungle Temples; the structure starts are the same (382 starts, 37,451 pieces in chunks -64..63, seed 1234). Around spawn at seed 1234, 1,515 blocks change: the 382 red glass placeholders under Repurposed's ocean outpost and pyramids become prismarine columns, and their gravel and prismarine pillars reach the sea floor. In the Nether, the Better Fortresses fortress at chunk (-23, 9) stands on its pillars: its terracotta placeholders become nether bricks and 41,487 blocks of air (and 2,163 of lava) under it become brick and magma columns down to the netherrack, 64,568 blocks changed in its 180 chunks (the huge fungi there grow around the columns). Better Ocean Monuments' ten processors are carried as well, so its monuments are no longer dry: its templates are mostly air, which its air processor turns to water below the sea level (the game's horizon:flood_air); its waterlog processor (horizon:waterlog) waterlogs what can be and marks the water on the chunk's edges for postprocessing, its structure void keeps the world's block (horizon:keep_world_block), its placeholder blocks become slabs, sponges, seagrass, gravel and aged copper (vanilla's rule, and horizon:pick with a new nextFloat for each chance, as the mod draws), and its legs and Better Witch Huts' log and fence legs are horizon:yung_pillar. In the monument at chunk (2, 8) (seed 1234), 318,512 blocks of air become water and none is left between y 30 and 60 (227,034 before); its 5,142 placeholder blocks are gone. Repurposed's opposite fix, which walls off the air and other liquids beside its ocean mineshafts' water, is horizon:close_off_air_sources (its 4 lists). It runs in the mod's order, so the walls it builds keep the later blocks of the template from the mod's "where the world is air, keep it" rules: around the ocean mineshaft at chunk (-18, -54), seed 1234, 650 blocks of air and cave air become prismarine and 614 more template water blocks stay water (1,303 blocks change in its 81 chunks). Its mineshafts' sky view processor (71 lists) is horizon:mineshaft_sky_view: a block of the template at or above the world's OCEAN_FLOOR_WG height in its column is not placed, so a mineshaft cut by a valley or a ravine is not built in the open, except plain rails over a block that can occlude, which stay (waterlogged where the world holds water). Repurposed's dropped processors go from 253 to 182. Still taken out: Formations' biome replacements, blocks aged or propped up. In the 128×128-chunk square (seed 1234), every spawner of a mod structure now has its mob: in the Overworld 83 Better Dungeons spawners held the template's placeholder pig and 17 of Moog's were empty (763 spawners with a mob, up from 628); in the Nether the 52 empty ones in Moog's Nether Structures are filled (162, up from 110). Chests are unchanged: 2,777 and 1,405 with a loot table. The game read loot tables from data/minecraft/ only, so every one of those chests naming a mod's table opened empty; it now reads every namespace, and an integer field written as a float ("rolls": {"min": 3.0, ...}, as Towns and Towers writes them) counts as vanilla's int codecs count it. Of the 675 chest and other non-block tables in ATM10's carried mods, 534 now fill a chest (seed 1; none before). Repurposed's structure voids keep the world's block, as vanilla's minecraft:block_ignore does (its 82 lists: mineshafts, the ocean and swamp villages, the ocean and taiga temples, the jungle pyramid's hidden room). Before, the game placed them like any template block: around the ocean mineshaft at chunk (-18, -54), seed 1234, 974 structure voids took the place of 464 prismarine blocks of its walls, 225 of water (dry holes in the sea), 279 of air and cave air and 6 rails and walls.
    • Rule tests written in Java. Repurposed Structures' mineshafts and Nether monument test blocks with three of the mod's own rule tests inside vanilla rule processors: what a block is made of (solid, liquid, air, with rails and chains), a chance over the distance from the template's origin along one axis, and a world height range. The game runs them as horizon:matter_phase, horizon:piece_origin_axis_aligned_linear_pos and horizon:y_value with the mod's fields, and carry renames them (213 in 75 lists).
    • Waterlogging. Repurposed's waterlogging_when_replacing_water_processor (the bottom pillars of its villages and outposts, 16 lists) is horizon:waterlog_in_world_water: a block that can be waterlogged is where the world holds water, dry where it does not, and one placed in water gets a block tick. Its dropped processors go from 370 to 253 (with the structure voids carried); around the ocean village at chunk (12, 10), seed 1234, 6 spruce fences of its pillars under the sea are now waterlogged.
    • Namespaces besides the mod id. Towns and Towers (mod id t_and_t) ships all 60 of its structures under kaisyn and towns_and_towers, which carry used to leave in outside/. Carry now claims a namespace for the mod when the mod's own files (its id's, its tag additions and the namespaces already claimed) name ids in it, and no mod of the pack has it as its id; it lists them in the manifest's namespaces (docs/mods.md). Files for mods it supports (CTOV's wares and monobank, YUNG's morevillagers tags) and configs of pack mods (cristellib) stay outside: of ATM10's 42 jars only T&T claims any. A tag of a mod's own namespace that replaces is carried as an addition (no pack before it defines it). In the 128×128-chunk square T&T has 7 starts at seed 1234 (its ocean wreckages, an ocean village and outpost) and 7 at seed 2 (forest and snowy taiga villages, snowy outposts), from none.
    • Renamed ids. 26.3 calls 1.21.1's chain the iron chain (its datafixers rename it in worlds). Carry renames it in recipes, loot tables and worldgen files (112 files in 5 mods: Repurposed's mineshafts, Better Dungeons' processors, Regions Unexplored's hanging sign recipes...), which otherwise fail to load in 26.3. With both, the 5 Nether mineshaft pools load: 47 of Repurposed's Nether mineshafts in the 128×128-chunk square (seed 1234), under the lava sea.
    • Advancements. An enter_block criterion names its block as blocks in 26.3; 26.3 skips 1.21.1's block, so the criterion matched any block the player entered. Carry renames it (20 advancements in Regions Unexplored: "Downer", "From the Tree Tops" and "Light as a Frog" were granted on the first dive into the sea, and its boat recipes' "in water" criteria).
    • Big templates (#838). Vanilla reads structure templates with no limit on their tag count; Horizon now does too (nbt::Limits::templates). Seven of ATM10's 8,962 carried templates are past the old 1,000,000-tag cap: YUNG's monument start, four When Dungeons Arise main pieces and two Nullscape pieces. They used to load as empty templates, so their structures never started.
    • Additions to other sets and pools (#832 in the game, #833 in carry). A set or pool file holding horizon:add_to appends its entries to the sets or pools it names (docs/mods.md). Carry writes these from lithostitched's add_structure_set_entries and add_template_pool_elements modifiers whose load conditions hold for the mods ported along, and from CTOV's Java registration: its 21 village types and 11 outposts join vanilla's villages and pillager_outposts with the mod's default weights.
    • YUNG's API (#839, #846 in the game; #840, #847 in carry). yungsapi:yung_jigsaw and its elements become vanilla's jigsaw and single element, with their rules under horizon:yungsapi. The game assembles them as YUNG's JigsawManager does: priority pieces, caps by name, depth ranges, ignore_bounds and the conditions tree, with piece_in_range. Dead-end pools swap a piece that got no child for one of its dead-end pool, and once the layout is done each piece's modifiers can turn it into another template (transform) or place it last (delay_generation). Better Fortresses' bridges need all three: over seed 1234's Nether chunks -64..63 it goes from 3,765 to 4,106 pieces. YUNG's placements become horizon: keys on a random spread: horizon:exclusion_zone for the enhanced exclusion zone, horizon:avoid_biomes for the temples' ocean and river search, horizon:ring_sections for Better Strongholds' rings. horizon:disables on a YUNG structure turns off the vanilla type its mod's mixin turns off. It is written only when the game can start the replacement, so vanilla mineshafts stay while Better Mineshafts (a Java type) can't.
    • Repurposed Structures' and Moog's own assembler (#850 in the game, #851 in carry). Both lay out their jigsaws with PieceLimitedJigsawManager, not vanilla's. Their structures, the Nether variants too, get horizon:piece_limited: y_allowance, burying, the pools whose fallbacks ignore the bounds, liquid, the Nether land search and ledge, and each structure's piece caps and required pieces from the mod's *_pieces_spawn_counts files. The game assembles them the mods' way and runs their post-layout steps (centring, burying, the Nether land search).
    • Enhanced terrain adaptation. YUNG's API and MoogsStructureLib add a kernel of their own to the Beardifier: its own size and falloff, carving or burying above and below each piece's floor. Carry writes it as horizon:enhanced_terrain_adaptation on the structure and on pool elements with their own (docs/mods.md), and the game sums it after vanilla's terrain density. In seed 1234's terrain before structures are placed, Better Fortresses' pieces held 39 to 41 % netherrack and now 3 to 7 %, the mod's open bridges; Moog's mega fortress band 45 % and now 30 %; Better Strongholds' pieces, buried, go from 92 to 97 % solid to 99 %, closing the caves that cut through them.

    Each mod's carry-report.json lists what was mapped, the rules and processors lost by name, and the types left as they are. NeoForge's convention biome tags (#c:is_forest, ...) come from the loader jar (<work>/loader/neoforge-<version>-universal.jar, fetched dev-only and SHA-1 checked) and are written into the carry. The game skips the structures it cannot decode.

    On ATM10's 42 data-tier mods with a jar (seed 1234, chunks -64..63, chunkgen_bench --starts over the carried worldgen): 286 starts with 15,097 pieces, 150 of them vanilla's (unchanged), and no pool that fails to decode (65 before #816 in the 64×64 centre).

    • 1,061 files were mapped (MVS 413, MNS 210, MSS 157, MES 110, Repurposed Structures 104, ChoiceTheorem's Overhauled Village 66), and 103 convention biome tags written.
    • When Dungeons Arise, Structory and Towns and Towers use vanilla's types already (T&T's namespaces are carried since the namespaces rule above).
    • Still lost, written in Java:
      • Repurposed's mineshafts, mansions and monuments, Better Mineshafts and two Better Dungeons types: Java structure types;
      • the block processors written in Java (YUNG's banners and decorations, Better Strongholds' legs, Lithostitched's block swaps);
    • Formations (Overworld and Nether: 50 structures, 128 pool elements) places one template per start at a height found in the noise columns under its corners and centre: on the surface, on water or lava, or in a cave (on its floor, under its ceiling, buried in stone, on a lava lake). Carry writes its formations:simple as the game's horizon:formations (horizon-world's structure/formations.rs, which reads the blocks of ChunkGenerator.getBaseColumn through the structure context) and its pool element as vanilla's with its ground_level under horizon:ground_level. In chunks -64..63 that adds 39 Overworld and 672 Nether starts at seed 1234 (51 and 643 at seed 2), and leaves every other start as it was. Its processors (biome replacements, aged blocks, bed colours, plants) are still dropped, so its structures keep their templates' blocks. Its one PiecedStructure, the fountain, starts the same way and then lays out the element's jigsaw children with vanilla's jigsaw: carry writes it as horizon:formations with horizon:pieced (max_depth, max_distance_from_center), and the game's assembler takes the placed start piece (jigsaw::add_children), the children kept within that distance of the start's centre, one block above the height found. It shares Formations' "uncommon" structure set, so where the set now picks it, it takes the place of a statue or a house: 1 fountain at seed 1234 and 3 at seed 2 in chunks -64..63, one with a child piece.

    With every structure PR above, the same square (seed 1234, chunks -64..63) holds 362 overworld starts with 22,026 pieces. CTOV has 7, Better Dungeons 72 and Better Ocean Monuments 3, and vanilla's monuments are gone as YUNG's mod has them. In the Nether there are 512 starts, up from 322: Moog's Nether Structures 147, Repurposed's Nether structures 40, Better Fortresses 7, with no vanilla fortress. Over chunks -256..255: Better Strongholds 10 (vanilla 0), Better Jungle Temples 5, Better Witch Huts 11. Vanilla's own worlds are unchanged: the structure-start oracle still matches 35 of 35.

    Repurposed's Nether city, Nether shipwrecks and End stronghold are its generic jigsaw with one more start check each, carried as minecraft:jigsaw with the check named in horizon:start_checks.repurposed: open air from sea level + 20 to + 45 around the city's chunk, no solid block in a 33×30×33 grid over a shipwreck, and land 45 blocks up at nine points 35 and 70 blocks apart for the stronghold. Over chunks -320..319 in the Nether there are 2 cities and 45 shipwrecks (30 and 865 without the checks); the End stronghold keeps 8,000 blocks from the centre, and chunks 500..1139 × -320..319 hold 2 (43 without).

    In a world with the 40 mods, seed 1234, /locate structure mvs:azelea_house finds one 27 blocks from spawn and it stands there as the template builds it; When Dungeons Arise's bandit towers and Structory's chapel locate too.

5. Code: one agent per mod

For tiers logic, machines and deep, in dependency order, as many agents at once as the AI budget allows:

  • Input: the inventory record, the carried-over data, the oracle's exports and traces, the harness, the Horizon mod API docs, the ports of its dependencies, and the mod's public documentation. Never its code: the agent doesn't decompile, read or quote it.
  • Output: a Rust crate in mods/<id>/port/ built with cargo horizon-mod build, and its .hmod.
  • Loop: write → build → run the harness → read the differences → fix, until the harness passes or the agent reports what blocks it. Usually that is an API gap, which becomes an issue against the mod API ("needs a scheduled-tick hook"), the way version ports turn differences into work items.
  • Rules: the agent writes the port from observed behaviour, the harness and the mod's own data files only; ports follow the mod's licence (Licences). Players without an AI agent get the carry-over tiers. A port the licence lets us publish can be downloaded instead of regenerated.

6. The Java oracle and per-mod harnesses

The oracle is dev-only, like Horizon's vanilla oracles in tools/: the pack's own Minecraft version (1.21.1 for ATM10) with NeoForge or Fabric, the mod and its dependencies, plus a small exporter mod (written by us, never shipped to players). It runs headless and is driven over a socket, as the vanilla oracles are.

A harness per mod, generated from the tier:

Tier What is compared, oracle vs Horizon with the port
data every registered block state's facts (hardness, sounds, shapes, light, map colour, drops via loot), every item's facts, every recipe's result for its inputs, loot table outputs over fixed seeds, tags; structure placement over fixed seeds and chunks
logic scripted interactions: place, use, break, tick N times; the world diff and the inventories after each step
machines a fixture world with the machine, its inputs and power: the output, energy and progress per tick over N ticks; the menu's slots and data values; NBT after save and load
deep entity scenarios (spawn, tick N, positions and states, drops), and render captures where the look is the point, compared as images with a tolerance

The comparison skips vanilla 1.21.1-versus-26.3 differences: it normalises ids and components through the step-4 upgrader and compares only the mod's own ids and effects.

Status: the data-tier harness runs.

python3 tools/neoforge_oracle.py export --jars <mods> --deps <their libraries> --out <work>/oracle/facts.json
cargo run -p horizon-port-pack -- carry <work> --resources .local/horizon-client/26.3
python3 tools/mod_port_harness.py --work <work>      # or --jars/--deps to export first
  • tools/neoforge_oracle.py installs a NeoForge server (21.1.259, ATM10's) under .local/neoforge-oracle/, builds the exporter mod in mods/horizon/src/neoforge-oracle/ (our code, dev only), starts the server headless with the jars and writes, for every block of the mods' namespaces and every vanilla block: the class chain, states, per-state hardness, light, tool need, map colour, sounds, outline and collision shapes; drops for hand, diamond tools, shears and silk touch; placement on a floor and a wall; BlockSetType and WoodType. Then items (class, stack size, rarity, food), recipes (type, result, count, resolved ingredients) and tags. One run with the seven mods below takes about 30 s.
  • crates/horizon-client/examples/mod_port_probe.rs builds the content root from the carried mods and dumps what Horizon's loaders make of the same blocks, drops, recipes and tags.
  • tools/mod_port_harness.py compares them per mod (blocks, properties, sounds, shapes, drops, placement against the template's in the oracle, vanilla tags, recipes, items) and writes <work>/mods/<id>/harness/result.json. It maps 1.21.1 sound events to 26.3's through the vanilla blocks both versions have (block.metal.* became block.iron.*) and ignores vanilla items 1.21.1 did not have (pale oak in #minecraft:logs).

The oracle needs the mods' jars, so it is not a tools/parity.py harness: it runs where the user's jars are, and its facts and results stay in the work folder. The first real mods (ATM10's own jars from Modrinth, all MIT; checks passed / compared):

mod blocks properties sounds shapes drops placement tags recipes items
Macaw's Trapdoors 1.1.5 179/179 11456/11456 11456/11456 11456/11456 10024/10024 358/358 534/534 191/191 191/191
Macaw's Doors 1.1.5 239/245 15296/15296 15296/15296 12352/15296 13384/13384 478/478 941/941 257/257 251/257
Macaw's Fences and Walls 1.2.1 150/180 4800/4800 4800/4800 4032/4800 33600/33600 300/300 432/432 222/222 150/180
Macaw's Paths and Pavings 1.1.1 234/315 4602/4602 4602/4602 4472/4602 6734/6734 468/468 416/416 315/315 234/315
Glassential-renewed 3.4.7 105/114 5177/5177 5177/5177 4727/5177 6447/6447 210/210 105/105 143/143 106/115
Factory Blocks 1.4.0 42/49 42/42 0/42 42/42 294/294 84/84 42/42 1/1 42/49
Cloud Glass 1.1.0 16/16 16/16 16/16 0/16 112/112 32/32 – 31/31 16/16
Chipped 4.0.2 6857/6981 46212/46443 46443/46443 46396/46443 149093/149093 13548/13714 11984/12378 6/13 6849/6973

What fails is what API 0.2 cannot express yet: shapes of their own (Japanese doors, hedges, paths a sixteenth high, glass without collision), sound events of the mod's own (factory_blocks:metal_sound), and blocks whose vanilla class has no vanilla block with the same states (garage doors, double gates, engraved and facing paths, fans). Every drop, recipe and tag of the declared blocks matches. Chipped's 124 missing blocks are its own classes (workbenches and DirectionBlock), and its workbench recipes need those blocks. Its drops match since carry writes the self-drop tables it registers in code (step 4).

7. Coverage report

coverage.json and coverage.md, per mod and totalled per class and tier:

  • ported: .hmod built, harness green;
  • partial: .hmod built, harness passes the listed parts (e.g. blocks and recipes yes, machine processing not yet), with the failing checks and the API gaps blocking them;
  • not yet: waiting on an API tier or a dependency, which is named;
  • not needed: performance, library mapped to the API, pack tooling folded into the pipeline, client-ui built in.

Plus the licence summary (how many ports could be published) and, for a pack, "playable?": whether every required content mod is ported or partial with its blocks and items present.

horizon-port-pack report <work> writes the per-mod rows and the class, tier and licence totals from the inventory, each mod's carry-report.json and its harness result. A data-tier mod is ported when its harness is green and nothing was left out (blocks with a class of their own are counted and named, since facts cannot show their extra code); partial with the failing checks, the blocks left out and what the templates miss; not yet with the API tier it waits for, or the dependency; not needed for libraries, performance mods, pack tooling and what Horizon has built in. Unclassified mods (the 20 CurseForge-only rows the reviewed table only guesses) count as not yet.

ATM10, measured (import of ATM10's modlist.json from its repository on 2026-10-16, 496 files; inventory; carry and the harness on the eight mods we downloaded; report):

class ported partial not yet not needed all
content 1 7 268 0 276
library 0 0 0 77 77
integration 0 0 40 3 43
client-ui 0 0 27 2 29
other 0 0 1 23 24
unknown 0 0 20 0 20
performance 0 0 0 16 16
server-only 0 0 11 0 11
all 1 7 367 121 496

By tier, the 367 not yet: data 57 (carry-over and the harness, no agent: their jars were not downloaded), logic 66 (API 0.3), machines 97 (0.4), deep 92 (0.5), client 35, unclassified 20. Ported: Macaw's Trapdoors. Partial: Macaw's Doors, Fences and Walls, Paths and Pavings, Glassential, Factory Blocks, Cloud Glass, Chipped (see the harness table) and MVS, whose structures now generate (see step 4). Of the 9, 7 are MIT, one LGPL and Chipped is under the Terrarium License; nothing is published.

Measuring "10x"

tools/perf_bench.py already measures Horizon against vanilla and vanilla with performance mods on the same machine, world and camera path (benchmark.md). With --game neoforge --mod-jars <dir> --deps <dir> it runs the mods on their own loader (NeoForge 21.1.259 for ATM10, installed under .local/neoforge-oracle, -Xmx2G like the launcher), driven by the same steps through a dev-only NeoForge port of the bench mod (mods/horizon/src/neoforge-bench). --game horizon --port-work <work> loads a work folder's carried-over mods into Horizon. A whole pack (--pack, its recommended JVM arguments) is next.

Metric How Horizon side
Load time start → title screen; start → in a fresh world, every chunk around the spawn drawn the same with the ported pack's .hmods enabled
RAM peak and steady RSS of the process, sampled 4 times a second from outside (the whole JVM for Java) the whole process
FPS the four flight scenes: mean, 1 % and 0.1 % lows, p99 frame time same
TPS / MSPT a fixed server-load world: N loaded chunks with the pack's machines and mobs placed by the harness fixtures; mean and p99 milliseconds per tick over 60 s (NeoForge: /neoforge tps and spark; Horizon: the integrated server's tick times, which today feed only the debug chart, exposed through horizon-ctl) same fixture, the ported content
World generation chunks per second pregenerating a fixed square (Chunky on NeoForge; Horizon gets a pregeneration command for this) with the pack's worldgen same
Disk and download install size of the pack against its .hmods

Two comparisons, both published:

  1. Same content. NeoForge with only the mods Horizon has ported (plus their dependencies) against Horizon with those ports. Content matches, so this is the honest ratio.
  2. What players run today. The full pack on NeoForge as a reference row, labelled as such, beside Horizon's partial pack. This is not a like-for-like ratio.

"10x" is claimed per metric only where the same-content ratio reaches it over the repeats' confidence interval. Other ratios are reported as they come out. Caveat (in every report): the pack runs Minecraft 1.21.1 and Horizon runs 26.3, so the vanilla parts differ by a few versions.

Measured: six ported mods against the same mods on NeoForge

Cloud Glass, Factory Blocks, Glassential, Macaw's Fences, Paths and Trapdoors (Doors left out: with Trapdoors it passes the block states chunks hold, see Limits), carried over and loaded as dev mods in Horizon, against their jars plus Architectury and Fusion on NeoForge 21.1.259. Mac mini M4 Pro, 1280x720, seed 1234, the four flight scenes, 2 games per row, 2026-10-17. Means; FPS is the mean of the four scenes. "Title" is start to title screen, "world" start to a fresh world drawn around the spawn, "scene load" a teleport to every chunk drawn.

RD game title s world s scene load s FPS RSS steady / peak MB load avg
8 Horizon + 6 mods 9.8 16.2 4.0 122 2,644 / 2,771 32
8 NeoForge + 6 mods 19.5 50.0 30.2 72 2,325 / 2,592 56
8 Horizon 5.0 11.0 4.0 124 1,559 / 1,629 51
8 NeoForge 12.8 36.1 21.9 104 2,334 / 2,558 30
16 Horizon + 6 mods 10.8 27.0 11.3 90 2,925 / 3,032 40
16 NeoForge + 6 mods 26.4 82.5 64.3 25 3,016 / 3,519 45
16 Horizon 3.5 10.0 7.4 119 1,732 / 1,871 56
16 NeoForge 16.9 73.7 43.2 36 2,748 / 3,425 48

Same content, Horizon against NeoForge: the world is ready 3.1x sooner at 8 chunks and 3.1x at 16, the title screen 2.0x and 2.4x sooner, and the flights run 1.7x and 3.6x the frames. RAM is not lower: Horizon's steady RSS grows 1.1 GB with the mods (61,816 block states against vanilla's 35,723; the per-state tables are the next thing to measure), while the JVM stays inside its 2 GB heap. Caveats, so none of this is a "10x" claim yet: other sessions and CI runners kept the machine's load average between 30 and 56 on 14 threads, so the confidence intervals are as wide as the means (perf_bench.py --table); Horizon's frames are paced near the display's 144 Hz at 8 chunks; Horizon's title time includes building the content overlay (about 5 s for six mods; it was rebuilt on every start then, and is kept for later starts now); and the pack runs 1.21.1 against Horizon's 26.3. A quiet rerun with more games is the next measurement.

Where the RAM goes (2026-10-17). RSS overstates it on macOS. ps counts pages the allocator freed and marked reusable, and the content tables free a lot at load. perf_bench.py now also samples the physical footprint (footprint, what the OS charges the process). The machine was shared and very busy (load average 90-138 on 14 threads). RD 8, the same six mods, one or two games per row:

game RSS steady MB footprint steady MB
Horizon 1,598-1,636 1,277-1,295
Horizon + 6 mods 2,996-3,052 1,736-1,869

So the six mods cost about 0.5 GB of footprint, not 1.1-1.4 GB. ATM10's 42 mods (193,762 states) take the game to 7.7 GB RSS but 3.9 GB footprint. The rest is startup garbage the allocator keeps.

crates/horizon-client/examples/resource_memory.rs breaks it down per loader: the live heap (a counting allocator), each step's peak, RSS and the footprint. Live heap after every loader, vanilla against ATM10's 42 mods:

loader vanilla MB ATM10 42 mods MB
block registry 24 125
assets (registry, models, sprites: 153 MB of pixels for 9,790 layers) 94 657
block facts and behaviours +29 +75
items, recipes +14 +82
light, mesh, collision, motion +24 +53
worldgen +79 +182
live / footprint 240 / 551 1,049 / 2,724

The block registry kept a second copy of every state as the key of its id map; it now indexes ids by a hash checked against the states: -22 MB live for vanilla, -124 MB for ATM10's, -30 MB and -140 MB of footprint. The startup peaks are what is left. Each step parses its per-state tables whole: as JSON trees, or as NBT trees of 300-600 MB each for ATM10 (block-properties.nbt, the collision, support, occlusion and movement tables). The peak reaches 3.9 GB during worldgen loading, and the footprint stays near the peak.

The NBT tables now stream: the readers check each state's identity as it is read, and no states tree is built (#753). That tree was about 240 MB for each table. With the carried ATM10 mods (153,511 states), the peak of each step falls by about 210 MB:

step before MB after MB
light 911 699
collision 945 733
motion 1,427 1,215
worldgen, uncached 3,000 2,788

The fluid tables no longer decode the 211 MB gameplay-facts.json a second time (#754). The content root is kept between starts (#756). What is left at startup:

  • facts.json went through a JSON tree, then an NBT tree, in both the registry and the assets step. It now streams (#825): at ATM10's size (214,338 states) each step's peak fell from 816 MB to 153 MB (registry) and 235 MB (assets), and the RSS after the assets step from 1,344 MB to 442 MB;
  • on a first start, the content-root build and Behaviours' decode of gameplay-facts.json (cached afterwards in the behaviour snapshot).

Against NeoForge, same 40 mods (2026-10-17). The 40 data-tier mods carried from ATM10 (the 42 without Regions Unexplored and MO Structures, whose libraries are only on CurseForge), against their jars on NeoForge 21.1.259 with -Xmx2G. perf_bench.py --port-work, RD 8, one game each, load average 80-125 on 14 threads; --warm measures a later start:

game title / world s RSS steady / peak MB footprint steady / peak MB
Horizon before #756 (every start) 81.1 / 102.8 4,008 / 4,164 2,368 / 2,554
Horizon, first start 105.2 / 128.8 3,795 / 3,993 2,306 / 2,560
Horizon, later starts 21.6 / 33.7 2,629 / 2,935 2,445 / 2,731
NeoForge + 40 mods 53.2 / 119.9 3,099 / 3,203 3,304 / 3,381

On later starts, Horizon uses 15 % less RSS and 26 % less footprint than NeoForge with the same mods. It reaches the title 2.5x sooner and the world 3.6x sooner, and its flights ran at 93-233 FPS against NeoForge's 27-62. The first start is slower than NeoForge's to the title, because it builds the content root once. One game per row on a loaded machine is not a benchmark; a quiet rerun with repeats is still to do.

The working version for 2026-10-23

In scope (each is a PR or a small stack):

  1. This design merged.
  2. Registries 0.2 in the game: content-only .hmods with blocks (full cubes and the vanilla templates the first pack needs: doors, trapdoors, fences, walls, slabs, stairs, panes, glass), items, block items, recipes, loot tables, tags, blockstates, models, textures and lang, in single player. Proven by a harness: a test mod's block placed, rendered, broken with the right time and tool, dropping its loot, crafted from its recipe; every per-state fact equal to its template's with the overrides applied. Parity unchanged with no mods.
  3. horizon-port-pack: import (.mrpack, CurseForge zip and instance, Prism), inventory, classify, licence record, dependency order, data carry-over for data-tier mods and their .hmods, coverage report. Run on ATM10's real list (coverage report published in the PR, metadata only) and on a small real pack end to end.
  4. First pack end to end, locally: 5–10 data-tier mods with permissive licences from ATM10 (candidates: Macaw's Doors, Fences and Walls, Trapdoors and Paths (MIT), Factory Blocks (CC0), XTones Reworked (MIT), Cloud Glass (MIT), Glassential (MIT), Crystalix (LGPL)), each with its harness against the NeoForge 1.21.1 oracle. The ports stay local even though these licences would allow publishing: we ask first.
  5. Launcher: "Import modpack" (.mrpack / CurseForge zip / instance) showing the coverage report (ATM10's first), and "Port and play" for what is portable.
  6. Measured: load time, RAM and FPS of that pack on NeoForge 1.21.1 against Horizon, with the full ATM10 on NeoForge as the reference row.

Out of scope for that date, and next in order: the rest of worldgen carry-over (mods' structures, sets, pools and features generate now, see step 4; mod biomes, noise and density functions, and the structure types written in Java such as YUNG's and Formations', do not yet), behaviour callbacks and server events (0.3), block entities, menus, capabilities and net (0.4), the first agent-ported logic-tier mod as the pilot of step 5, then machines. Porting all of ATM10 is a campaign. Each step publishes its coverage number; we make no claim beyond what the report shows.

Where ATM10's data mods stand (2026-10-22)

Measured with #1175, #1176, #1177 and #1191 on main, over the 42 data-tier mods carried from ATM10 (the launcher's --dev-mod carries), seeds 1234 and 2.

  • Structures. The carries define 687 structures in 25 mods. 419 of them start somewhere in chunks -128..127 of the Overworld, the Nether or the End at one of the two seeds, and 264 in chunks -64..63 (249 on 2026-10-19). Most of the rest are biome-bound and rare: no desert lies in either square, and ChoiceTheorem's Overhauled Village (25 of 78) and Towns and Towers (15 of 60) have a variant per biome. 27 never start because their type is written in Java: Better Mineshafts 13, Repurposed Structures' mansions 8, monuments 4 and End mineshaft 1, and one Better Dungeons type.
  • Processors. 1,560 carried and 605 dropped (1,347 and 818 on 2026-10-19). The dropped ones by mod: Formations 227 (Overworld 182, Nether 45), Repurposed Structures 182, Better Dungeons 69, Better Jungle Temples 50, Better Strongholds 41, Better Desert Temples 14, Villages and Pillages 10, Better Fortresses 5, Better Witch Huts 3, Mo' Structures 3, Moog's Nether Structures 1.
  • Content. 190,550 block states and 12,931 items load. 5 block models are missing (4 of Macaw's Doors' cherry waffle door, Regions Unexplored's blue crystalite): those blocks draw the missing model.
  • Load time (Mac mini M4 Pro, offscreen, render distance 12). The first start builds the content root: about 25 to 35 s, and again after every game update, since the root's stamp includes the executable. Later starts: 6.6 to 8.2 s to "resources loaded", against 1.3 to 2.5 s for vanilla.
  • Play. No crash at either seed: spawn, Mo' Structures' pirate ship, castle in the sky, barn house and villager tower, CTOV villages, Formations' fountains (one with its chamber), the Nether.

What is missing, ranked by how much a player notices:

  1. Code mods. 333 of ATM10's 496 mods (machines, magic, mobs, tools, storage): the pack's gameplay. They need behaviour APIs (0.3 and later) and a port per mod.
  2. The 12 mods only on CurseForge, which the carry has not fetched.
  3. Plainer structures. The 605 dropped processors are what makes structures look aged, overgrown or fitted to their biome in Java: Formations' biome swaps and block ageing (its tables are All Rights Reserved code), Repurposed Structures' random and noise replacements and floating-block removal, and YUNG's decorations (Better Dungeons, Jungle Temples, Strongholds).
  4. Structures that never generate: the 27 Java types above. Repurposed Structures' mansions and monuments are about 3,000 lines of LGPL layout code; whether to port them into this MIT repository is a licence question.
  5. The first start after installing or updating: 25 to 35 s. Later starts are about four times vanilla's.
  6. Smaller gaps: Mo' Structures' mixin that keeps lakes out of its structures and its two block-ageing processors, and the 5 missing block models.

Three packs through the mod browser, measured

2026-10-22, Mac mini M4 Pro (offscreen, a debug launcher, release-profile horizon-port-pack and game), while other sessions kept the CPU at 85 to 93 %, so times are upper bounds. The two Modrinth packs were imported with horizon-launcher mods install <slug> (the browser's Import), ATM10 from its CurseForge zip with modpack import (CurseForge support is coming soon), then each ported with --resources for 26.3 and opened as a survival world with every ported mod on (horizon-ctl launch --dev-mod ..., from a scratch script).

Pack Files Import Port Ported mods Coverage Declared World load (resources) Crashes
Vanilla Perfected 26.3 (Fabric, small) 108 30.4 s 1.0 s 2 2 partial, 79 not yet, 27 not needed 0 blocks (a trail effect, a torch-flower texture) 5.2 s (2.1 s) none
Slimes Adventure 1.13.4 (Fabric 1.21.1, medium) 105 18.8 s 22.8 s 20 20 partial, 46 not yet, 39 not needed 8,208 blocks, 41 items, 1,376 recipes 23.0 s (15.0 s) none
All the Mods 10 8.3 (NeoForge 1.21.1, CurseForge zip) 496 3.1 s 29.4 s 42 42 partial, 333 not yet, 121 not needed 10,000 blocks, 1,273 items, 4,178 recipes 38.3 s (28.7 s) none

The same world with no mods loads in 5.4 s (resources 1.1 s). Import is the pack download and Modrinth's answers by file hash; Vanilla Perfected's 30 s is mostly its 108 lookups and its download. Port is the jars (downloaded for the two Modrinth packs; ATM10's 42 copied from a folder, 12 more are on CurseForge only: CurseForge downloads coming soon) and the carry, which writes tens of thousands of small files. World load grows with the blocks the pack declares: ATM10 adds about 155,000 block states, and loading their models and textures is most of its 28.7 s.

Every world loaded and stayed up, with no panic and no error in the log. Before the namespace fix above, Slimes Adventure lost three of YUNG's mods at start ("both use the namespace yungsapi"); after it all 20 load. As a share of the mods that need porting (not counting performance mods and libraries), 2 of 81 ported partly, 20 of 66 and 42 of 375. Every ported mod is Partial: their Java code (block entities, entities, Mixins) doesn't come along.

Open questions for Ludovic

  • Contacting authors of permissively licensed mods to publish ports (none contacted).
  • Whether players should run code-port agents themselves (their own Claude Code or Codex login, as Zenith does) or only download published ports.
  • CurseForge: dropped for now (its API needs a paid key; Ludovic, 2026-10-22), shown as coming soon. CurseForge packs import from their zip or an installed instance.