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

1087 lines
78 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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](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](#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](mods.md#planned-interfaces) |
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:
```json
{
"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
`DoorBlock`s, XTones are full cubes, ore sets are `DropExperienceBlock`s. 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](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 `replace`s 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](#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](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 `.hmod`s 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 `.hmod`s | |
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 `.hmod`s 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
`.hmod`s, 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.