17 KiB
Horizon mods
Two kinds of mod run in Horizon:
| Kind | File | What it is | Where it runs |
|---|---|---|---|
| Content mod | .hmod |
Data and assets: blocks, items, recipes, loot, tags, models, textures, structures. No code. | Read when the game starts |
| Native mod | .hnmod |
A horizon-mixins bundle: Rust that changes the game's own source before it is compiled. | In a game the launcher builds for the player (native-mods.md) |
Fabric and NeoForge jars do not run as they are: their code calls and patches Minecraft's Java classes, and Horizon has none. What carries over is the data and assets inside a jar (modpacks.md).
Mods are off by default. A fresh install has no horizon-mods folder and plays exactly like vanilla 26.3.
Playing with mods
- Install a mod from the launcher's Mods page (mod-browser.md), or put its files by hand in the
game folder (the one holding
options.txt): a content mod's.hmod(or a folder holdinghorizon-mod.toml) inhorizon-mods/. Native mods go inhorizon-native-mods/, and the launcher builds them (native-mods.md). - Options > Mods lists them with what each may do ("May add blocks, items and mob effects to your worlds") and a
switch. They start off. The choice is kept in
horizon-mods.txt(mod.<id>:true). - A content mod's blocks, items and mob effects load once, when the game starts: restart the game after a switch.
- A mod that can't load shows why, in red, on its row.
Two turned-on content mods never own the same namespace: the first by file name loads, and the other stays out with the reason in the log.
Benchmarks (--benchmark) never load mods.
Making a content mod
cargo install --git https://github.com/horizon-rust/horizon cargo-horizon-mod
cargo horizon-mod new my-mod # a content mod: manifest, one block with its model, loot and recipe
cd my-mod
cargo horizon-mod run # build my-mod.hmod, then start the game with it turned on
cargo horizon-mod build # only build: target/horizon-mod/my_mod.hmod
cargo horizon-mod check target/horizon-mod/my_mod.hmod # the registry's checks; --json for JSON
new <path> [--name "Name"]writes the template. It makes content mods only: any other--templateis refused.buildpackshorizon-mod.toml,assets/anddata/intotarget/horizon-mod/<id>.hmod, with the same checks the game makes when it reads the content. It refuses a folder with aCargo.toml(code is native now) and a manifest without capabilityregistries.runbuilds, then startshorizon-client(onPATH, or--game PATH, orHORIZON_CLIENT) with--dev-mod target/horizon-mod/<id>.hmod, always on. Arguments after--go to the game. The game reads content once at start, so rebuild and run again after an edit.check <file>runs what the registry runs: the file's size and SHA-256, its manifest and content, and a signature with--key <public key> --signature <file>.minisig. It reads.hmod,.hnmod,.hpack, resource pack.zipand.hshaderfiles.registry build|review|check|list|release|pageis the registry repository's CI (api/mod-index.md).
The manifest
id = "my_mod" # [a-z0-9_]{1,64}: settings key, log name
name = "My mod"
version = "0.1.0"
description = "..."
authors = ["..."]
api = "0.2" # content mods
side = "both"
capabilities = ["registries"]
namespaces = [] # optional: more namespaces the mod's files use (below)
Unknown keys are refused, so a typo is an error, not a silently ignored permission.
Content mods (API 0.2)
A content mod has capability registries and carries assets/ and data/ in the .hmod. It adds blocks and items
by declaring them in data/<id>/horizon/block/*.json and data/<id>/horizon/item/*.json, each copying a vanilla
template (modpacks.md), and mob effects (below):
{ "template": "minecraft:iron_block",
"properties": { "hardness": 4.0, "light": 3, "sounds": "minecraft:amethyst_block" },
"item": true }
An item copies its template's data components, with max_stack_size and
rarity changed, and any other 26.3 component set or removed in
components, written as a data component patch is: minecraft:<type> sets
one, !minecraft:<type> with {} removes it. That is how a modded sword
gets its own damage and durability, or a tool its mining speed:
{ "template": "minecraft:iron_sword",
"components": {
"minecraft:max_damage": 900,
"minecraft:attribute_modifiers": [{ "type": "minecraft:attack_damage",
"id": "minecraft:base_attack_damage", "amount": 7.0,
"operation": "add_value", "slot": "mainhand" }],
"!minecraft:repairable": {} } }
A block's item takes the same components, over the components of its
template block's item; a block with "item": false can't have them. Block files
have no max_stack_size or rarity keys, so those two go in components here
(an integer 1 to 99, and one of common, uncommon, rare, epic):
{ "template": "minecraft:iron_block",
"components": {
"minecraft:max_stack_size": 16,
"minecraft:damage_resistant": { "types": "#minecraft:is_fire" } } }
A component type no vanilla item has is refused, so a misspelt one fails the load instead of doing nothing. Behaviour the game ties to a vanilla item's id doesn't come along yet: a copy of the trident doesn't throw (#3305).
A mod effect is declared in data/<id>/horizon/mob_effect/<name>.json with what vanilla's MobEffect takes: its
category (beneficial, harmful or neutral) and its RGB colour (0 to 0xFFFFFF), which its particles take:
{ "category": "harmful", "color": 9154528 }
It is the effect <id>:<name>: /effect give @s ruby:glow 30 gives it, saves and mobs keep it, and it reaches LAN and
dedicated server players who run the same mods (its network id follows vanilla's). Its name is the translation
effect.<id>.<name> from the mod's assets/<id>/lang/. Its icon is not drawn yet: the HUD shows the effect's frame and
the inventory its frame and name, without an icon. It does nothing on its own yet: no tick behaviour and no attribute
modifiers. A world opened without the mod drops the effect from its players and mobs. Clients join a server only with
the same mods' effects, as with its blocks.
crafting_remainder names what the item leaves in the crafting grid when a
recipe uses it, as a milk bucket leaves a bucket (vanilla's
Item.Properties.craftRemainder, not a data component): a vanilla item or
one of the mod's own, refused at load otherwise. It holds on the crafting
table, the inventory grid and the crafter, not yet as furnace fuel or in a
brewing stand. What the item leaves once eaten
or drunk is the minecraft:use_remainder component instead:
{ "template": "minecraft:mushroom_stew", "crafting_remainder": "minecraft:bowl" }
The fence: files go in the mod's own namespaces (assets/<id>/,
data/<id>/, and those its manifest lists), plus additions to vanilla tags
(data/minecraft/tags/) and to the convention tags NeoForge and Fabric mods
share (data/c/tags/).
Templates and sounds name vanilla blocks; unknown keys are refused.
Some Java mods ship their files under other names than their mod id (Towns and Towers, mod id t_and_t, under
kaisyn and towns_and_towers). A content mod lists those in its manifest:
id = "t_and_t"
api = "0.2"
capabilities = ["registries"]
namespaces = ["kaisyn", "towns_and_towers"]
A listed namespace is the mod's as its id is: its assets/ and data/ files load, and the game reads its tags,
recipes, names and loot. Blocks, items and mob effects are declared in data/<id>/horizon/ only. minecraft, c and
horizon cannot be listed. Two mods that are on may not own the same namespace (one's id or listed namespace being
the other's): the game keeps the first in load order and leaves the other's content out, with "mods A and B both use
the namespace N; turn one of them off" in the log. Paths use vanilla's resource characters, at most 65,536 files, 16
MiB each and 512 MiB in all. A mod whose content is refused is left out, with the reason in the log. The game reads
this content into its block and item tables (horizon_client::mod_content, modpacks.md). A tag entry
naming an id nobody registers (a block a port left out, another mod's item) is dropped, so the rest of the tag still
loads; vanilla would drop the whole tag.
Tags merge as a data pack's do. Every mod's file for one tag (vanilla's data/minecraft/tags/, the convention
data/c/tags/, or another mod's) adds its values after vanilla's and the mods' before it, in load order. A file with
"replace": true first drops what came before it, vanilla's entries and the earlier mods', and the mods after it
still add theirs, as a higher data pack does. Since the result depends on load order, the game says so in the log:
"mod towns replaces data/minecraft/tags/worldgen/biome/has_structure/village_plains.json: vanilla's entries and those
of adds are left out, ...". Tags are the only minecraft files a mod may hold: its other content stays in its own
namespaces.
A mod adds to structure sets, template pools and processor lists that are not its own (vanilla's villages, another
mod's houses) with a set, pool or processor list file in its own namespace holding horizon:add_to, a key vanilla data
never has. Its entries are appended to each set, pool or list named, after every mod's own files, by
horizon:priority (lower first, 1000 when it has none), then in mod and file order; the file is no set, pool or list
itself. A name nobody defines is skipped.
{ "horizon:add_to": "minecraft:villages",
"structures": [ { "structure": "mymod:village_beach", "weight": 10 } ] }
A pool addition holds "elements" as a pool does, a processor list addition "processors". This is what
Lithostitched's add_structure_set_entries, add_template_pool_elements and add_processor_list_processors modifiers
do on NeoForge and Fabric, and horizon-port-pack carry writes the last two from those.
A pool element may be one of Lithostitched's: lithostitched:limited (delegate, limit, min_depth) places its
delegate at most limit times in one structure start, and lithostitched:guaranteed (delegate, count,
min_depth) is tried before its pool's other elements at each jigsaw that reaches that pool (as target or fallback)
until count are placed. Neither goes below a piece shallower than min_depth (the start is at depth 0).
A structure (and any of its pool elements, in place of the structure's) may hold
horizon:enhanced_terrain_adaptation: MoogsStructureLib's terrain kernel, which carves or buries terrain around the
pieces on top of vanilla's terrain_adaptation. kernel_size and kernel_distance shape the kernel; top and
bottom (carve, bury or none) act above and below a piece's floor; bottom_offset, padding (x, top,
bottom, z) and MoogsStructureLib's band (bottom, top, piece_heights) move the box it measures from. On a
structure, rigid_junctions adds rigid pieces' junctions as well and inflate grows the start's box by the kernel
radius. kernel_size 0 is none.
{ "horizon:enhanced_terrain_adaptation": { "kernel_size": 24, "kernel_distance": 24,
"top": "carve", "bottom": "none", "rigid_junctions": true, "inflate": true } }
Native mods
Behaviour that needs code (a new key, a HUD, a tool that changes how mining works) is a native mod. It is a bundle of
a horizon-mixin.toml and a few .rs helpers that hook into named functions of the game. Start from
horizon-native-mod-template. How the launcher builds
one, what it pins and why it is not sandboxed: native-mods.md.
A native mod's config files go in the game's shared config/ folder, beside options.txt (on a dedicated server,
beside server.properties), as Fabric and NeoForge mods' do: the mod picks its own file names, such as
config/<mod id>.json. It asks for the folder with Game::native_config_dir() (hook points API 26), which needs no
game, so it works from any hook, and makes the folder when missing. Settings kept with native_set_setting stay where they were, in horizon-mods/.horizon-mod-data/<id>/.
Publish your mod
Horizon's registry lists mods, modpacks (.hpack) and resource packs made for Horizon. A project is reviewed
once, when it is listed; its later releases are checked by machine, not read by a person. Players find them at
playhorizon.dev/mods and in the launcher (Mods, then Horizon beside Modrinth),
which checks every file before it installs it.
- Run
cargo horizon-mod checkon the file (above): the registry runs the same checks. - Put the source in a public GitHub repository, under a licence that lets players run it. A native mod's
.hnmodis its source. - Publish a GitHub release in it with the file as an asset (
.hmod,.hnmod,.hpack,.hshader, or a resource pack's.zip), and its.minisigbeside it if you sign. The release notes are the changelog. - List the project once: open playhorizon.dev/mods/submit, sign in with GitHub
and give the repository and the page (name, summary, licence, optionally a minisign public key). It opens a pull
request in horizon-rust/horizon-mods adding
mods/<id>.toml, with you as owner. A reviewer reads the repository and its source and merges it.
Releasing a version is only step 3. Publish a GitHub release (not a draft or a pre-release); within the hour the
registry reads it, runs the checks a submission runs (the file's own, the signature, the id and the version in the
file's manifest), and lists it. A release that fails is skipped and reported in an issue of the registry, never
published. Nobody opens a pull request and nobody reviews it. The version is the one in the file's manifest (the tag
when the file has none), the date and the changelog are the release's, the SHA-256 is computed by the registry, and
the launcher downloads the asset from your repository and refuses any other bytes. A published file never changes:
publish a new version. revoked.toml takes a bad one down.
Once it is published, its page and its card in the launcher show its rating: players signed in with GitHub on its page give it 1 to 5 stars, one rating per account, which they can change. Its owners can't rate it, and nobody sees who rated.
Your page can live in your repository too: a horizon-registry.toml at its root (on the newest release's tag) with
name, summary, description, authors and license fills what the registry's entry leaves out; the entry's own
words win. Its owners, key, kind and icon are the registry's, changed by a pull request. Projects listed before this
(versions in their entry) keep their hosted versions. The formats: api/mod-index.md.
Integrity and signing
- Integrity. Every download is checked against the SHA-256 the index lists before the file reaches the game folder.
- Signing. Authors sign files with an Ed25519 key (minisign format: a
.minisigbeside the file); the index pins each project's public key, so an update signed by another key is refused. Unsigned local files stay allowed, with an "unsigned" mark. - Takedowns. A file the registry takes down (
revoked.toml) is refused by the launcher and turned off where installed.
Horizon's official mods
Horizon's own mods are native. They live in
horizon-rust/horizon-mods under native/<name>/:
| Mod | Folder | Replaces |
|---|---|---|
| FPS Counter | native/fps-overlay |
Extras > FPS counter |
| Keystrokes | native/keystrokes |
Extras > Keystrokes |
| Zoom | native/zoom |
Extras > Zoom |
| Minimap | native/minimap |
Extras > Minimap |
| Recipes | native/recipes |
The Recipes mod (item panel and recipe pages, on hook points version 2) |
| Coordinates HUD | native/coords-hud |
A small example mod (position and facing, /where, /coords) |
The game keeps the parts that must be fast, such as drawing the minimap's map, and a native mod picks the options (extras.md). Far View is not a mod any more: its distance and detail are Graphics > LOD Distance and Far Detail (graphics.md).