A content root is built per set of mods (.horizon-cache/mod-content/<stamp>),
so a running game keeps reading the root it was given while a switch
builds another; roots unused for a day are removed. Turning a mod on or
off in Options > Mods, or installing one from Browse Mods, applies at once
when the mod has only assets and data: the client reloads its resources,
then the server reloads its data packs as /reload does and resyncs its
players. A mod that adds blocks, items, mob effects, entity or menu types
or changes world generation keeps loading at start, and the Mods page
says to restart.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
minecraft:wandering_trader joins the zombie and the villager as a living
template. It is the neutral one whose behaviour is all goals, so a native
mod's register_goals can replace every bit of it: a citizen, a guard or an
NPC, which a zombie (hostile, burns in daylight) and a villager (its brain
stays) can't be. Saving and entity data already follow the template's
class (class_id); the tests now cover a trader type too.
Part of #8622.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A trait mod's register_goals hook is asked for every new mob, after its
species added its goals and before its saved data is read. It may clear
the goal and target selectors and add Goal types of its own, which steer
the mob with its navigation (nav_move_to, create_path_to) and controls.
A mod's living type from #9667 thus gets its own AI. The hook runs before
the float check, so a mod's Float goal floats the navigation as
FloatGoal's constructor does.
The hook is a free function in the client library, so it goes through
NativeClientHooks as living_breathe does. mod_api re-exports Mob,
MobWorld, Goal, GoalSelector and Cx; the generator's dispatch tests get
a Mob stub and the catalog test knows mobs/ai.rs.
Part of #8622.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content mod's entity type could only copy minecraft:snowball, so a mod
had no way to add a mob. It may now copy minecraft:zombie or
minecraft:villager, as NeoForge's EntityType.Builder.of(Zombie::new, ...)
makes a type of a vanilla class, with its own default attribute values
(EntityAttributeCreationEvent) in an `attributes` map, for attributes the
template has. A thrown type still names its item; a living one has none.
The mod's row in entity-types.json is its template's with `template`
added and its attribute bases in place of the template's, every other
value kept as the template's text. MobTypes::load makes a row naming a
template into that species' MobType under the mod's id, so registry and
network ids, /summon and remote spawns, which already went through the
table and the registry, pick it up. What a class saves and syncs of its
own (a zombie's IsBaby, CanBreakDoors and conversion times, its entity
data, the villager's) follows MobType::class_id, the template's id for a
mod type; the saved and sent id stays the mod's.
A mob's default loot table, and /loot ... kill, now keep the type's
namespace (EntityType.getDefaultLootTable): ruby:ghoul rolled
minecraft:entities/ruby:ghoul, and rolls ruby:entities/ghoul. Nothing
changes without content mods: no row has a template, and class_id is the
type's own id.
First slice of #8622; goals, navigation and structure placement stay
open.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep main suffocation registration and the world damage catalog together when loading world tables. Preserve all intervening upstream changes without rewriting existing PR history.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: the 32 KiB cap is enforced on both encode and decode, a vanilla client ignores the unknown horizon:menu_extra channel, and the lan_bot test pins that the payload arrives right before its own window's OpenScreen.
Preserve ordinary player damage, enchantment predicates and synchronized registry data for enabled resource/data packs. Validate definitions before publication and keep world definitions stable through reload.
Co-Authored-By: Codex <[email protected]>
🤖 Agent: all five targets build and the four runnable ones start on their runners (x86_64-apple-darwin through Rosetta on the arm64 macos-latest); releases are created with --latest=false so a cargo-horizon-mod tag never takes GitHub's latest from the game; the YAML-grep test is gone, the three left test the manifest-to-release contract.
A mod block opening its content mod's menu type asks native_menu_extra_data for bytes
of the mod's own; a horizon:menu_extra payload carries them with the window and the
block's position to Horizon clients right before OpenScreen. The client takes the
block from it instead of the last clicked one, and native_screen_pos /
native_screen_extra_data read both while the mod's screen is open.
Fixes#8675
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: one point checked: occludes:false drops a solid render's dampening to 1 (0 with skylight_down) as getLightDampening does for non-solid full blocks; only content-mod blocks that set the key change, vanilla rows are untouched.
`cargo install --git ...` compiles the tool and its workspace, which for a
mod developer on a slow machine is a long wait to write one block (#2120).
`cargo binstall` skips the build when the release holds a binary for the
platform.
- crates/cargo-horizon-mod/Cargo.toml gains [package.metadata.binstall]:
one `cargo-horizon-mod-v<version>` release, one tar.gz per Rust target,
the binary at the archive's root; a `.sha256` beside each.
- .github/workflows/cargo-horizon-mod.yml builds those targets (Linux
x86_64 and aarch64, Windows x86_64, macOS x86_64 and arm64), starts
each binary it can run, and publishes them on the tag. A pull request
builds them without publishing.
- tools/horizon_mod_binstall.py is the one list of targets: the workflow's
matrix comes from it, and it checks the built archives and the tag
against the manifest before anything is published.
- docs/mods.md shows the binstall command first, the source build after.
Co-Authored-By: DeepSeek V4.1 Flash <[email protected]>
A content block occluded as its template does, so a thin panel or a block
drawn by its block entity on a full-cube template hid the faces of the
blocks beside it, leaving a hole in the floor under it.
`properties.occludes: false` is vanilla's `noOcclusion()`: the block's
gameplay state can't occlude and isn't a solid render, and its render
definition can't occlude (the faces beside it are drawn). A solid render's
full light dampening becomes a non-solid block's, as vanilla's data has it
for leaves and spawners: 1, or 0 when skylight passes down.
Item 2 of #5550.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01ChJKHFsqiw1XFe27mMdroL
generated.rs is no longer tracked, and xtask's generator moved to
horizon-mod-api-gen, so the dispatch tests' stand-in types get NativeConfig
and NativeConfigFile there.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: checked the output against 26.3's plains.json: spawners land in attributes[minecraft:gameplay/natural_mob_spawns].argument.spawns_by_category with count as an int or a minecraft:uniform object, the shape biome.rs already reads. One edge: an entity type missing from entity-types.json falls back to the misc category, but the entity_reg check skips such a spawner first, so that fallback never runs.
Read NeoForge's `add_spawns`, `remove_spawns`, `add_spawn_costs` and
`remove_spawn_costs` biome modifiers alongside `add_features` and
`remove_features` when building the content root:
- `add_spawns` appends its spawners (a single spawner object or a list)
to each entity type's spawn category in `spawns_by_category` under
`attributes[minecraft:gameplay/natural_mob_spawns]`, converting
`minCount` and `maxCount` to 26.3's `count` (an integer when equal,
`minecraft:uniform` otherwise) and taking the category from
`entity-types.json`;
- `remove_spawns` removes the resolved `entity_types` (an id, `#tag`,
list, or `any`/`or`/`and`/`not` holder set) from every category;
- `add_spawn_costs` sets `spawn_costs[type]` (`energy_budget`,
`charge`) and `remove_spawn_costs` removes them;
- additions run before removals in mod and file order, unknown entity
types or tags are skipped with a warning, malformed modifiers fail the
build naming the mod and file, and changed biomes also update
`data/<ns>/worldgen/biome/<path>.json` in the overlay when present.
Fixes#7478
Co-Authored-By: Claude Opus 4.6 <[email protected]>
native_register_configs lists a mod's configs: a kind (Client, Common,
Server), a file in config/ (<mod id>-<kind>.toml unless named) and the
text written there when it is missing. Client configs load on the
window when the game starts, Common once per process, Server before
native_server_about_to_start for each world, from the world's
serverconfig/ copy when there is one. native_config_loading hands over
the text, which the mod parses. The files' modification times are read
once a second; changed text fires native_config_reloading. A server's
configs fire native_config_unloading when it stops. Fixes#7950.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A content block always drew as its template does, so a block whose moving parts its block entity
draws every frame (a turning shaft, a machine's arm) still had its model baked into the chunk mesh.
`properties.render_shape` takes vanilla's `RenderShape` by name: `MODEL`, `ENTITYBLOCK_ANIMATED`
(the model is left out of the chunk mesh) or `INVISIBLE`. It sets the state's `render_shape`, which
the mesher already reads. Outline, collision, light and occlusion are unchanged.
Item 1 of #5550.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod can add sources to vanilla's atlases (assets/minecraft/atlases/*.json),
as a resource reload appends every pack's sources to the ones below it.
The fence allows those files, the content root writes vanilla's sources
followed by the mods', and carry keeps them.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
`--template native [--horizon <commit>]` writes a trait mod's bundle: its
horizon-mixin.toml, pinned to that Horizon commit or left unpinned, and an
[api] crate whose hooks() greets each player who joins. The files are
what docs/api/mod-files.md lets a bundle carry, so the launcher builds the
folder as it is and packs it into an .hnmod; a test packs it that way and
runs the registry's check on it.
A server with content mods on sends each mod's id, version, side and the
SHA-256 of its .hmod on horizon:mod_list; the client answers with its own
list, and a client that differs is refused with the mods named. Only
side = "both" mods count, so client-only mods never block a join.
Refs #7192
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
A mod block only became a sign when its name ended in _sign, and then its
text never reached a client: the chunk encoder gave a mod block its own block
entity type and dropped any entity that wasn't the mod's. Its item also placed
vanilla's wall sign, and its editor looked for a texture named after the block.
Go by the template's class, as vanilla's instanceof SignBlock does, for the
block entity, the renderer, the editor, chaining and respawning, and for the
block entities world generation gives the blocks a feature places. A content
block declares wood_type, its WoodType name, and wall_block, the wall form its
item places, one of the same mod's blocks: a pair that doesn't fit leaves out
that mod alone. Mod signs go out with vanilla's sign and hanging_sign block
entity types, so text, glow, dye, wax and saving work as they do on an oak
sign. A stone block named road_sign is no longer a sign (#5532).
Written with Claude Opus 5.5 in OpenCode.
AbstractFurnaceBlockEntity.consumeFuel takes the fuel item's own crafting
remainder: it takes the fuel slot once the stack is used up, and is dropped
at the furnace while more of the fuel is left. furnace.rs only knew the lava,
water and milk buckets by name, so a content mod's fuel with a
crafting_remainder burnt without leaving it. The remainder now comes from the
items table (the buckets carry theirs there), and Furnace::tick hands the
dropped ones to its caller, which drops them with Containers.dropItemStack's
helper.
Fixes#6524
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
🤖 Agent: Reviewed by Ludovic's AI agent (Claude): content block states laid out in StateDefinition order (names sorted, first slowest) instead of refusing a property that sorts first; ids unchanged for blocks that already loaded (test); load-time only. Union of 6247+6260+6270+6279 with main: Python tests 594 OK, stamp_native_hooks OK; verify green on b40d434d4.
A block's states were its template's states times the declared combinations,
the template's properties varying slowest, but StateTable::new reads a state
table as vanilla's StateDefinition does: every property sorted by name, the
first name slowest. A declared name that sorts before one of the template's
(`bites` before `facing` on carved_pumpkin) stopped the game at load.
The states are now laid out in that order, with one lookup from a template
state and a declared combination to the state's row, used for the template
state's new id (the default state included), the collision shape fan-out and
the cached shapes. Declared names that sort after the template's lay out as
before, so those blocks' ids do not move. The refusal this replaces is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A mod block only got random ticks when its template ticks, so a crop,
sapling or soil on a template that never ticks (dirt, stone) never grew.
`properties.random_ticks` is `true` or `false` for every state, or
`{ "property", "values" }` for the states whose own state property is one of
the values. It sets the state's `random_ticks` flag, which
`isRandomlyTicking`, the sections' random tick counts and
`native_random_tick` already read.
Fixes#6138
Co-Authored-By: Claude Opus 5.5 <[email protected]>
🤖 Agent: biome modifiers place mod features; fixed here: unread modifier types and unknown names are skipped with a warning instead of dropping every content mod. Proven on a union of main 956e35469 with 19 open PRs including this head: pr_ready --quick green for it (clippy -D warnings incl. the server build, workspace tests, content-root tests, Python tests). CI parity covers the harnesses.
🤖 Agent: content mods declare thrown entity types (snowball template); rdbtCVS found no defects; verify green. Proven on a union of main f99f8feaa with 37 open PRs including this head: pr_ready --quick green for it (fmt, clippy -D warnings incl. the server build, 3077 workspace tests, 20 content-root tests, launcher clippy + 340 tests, 510 Python tests; the one failure is #5570's own new test, not in this PR). CI parity covers the harnesses.
A modifier of a type not read yet (add_spawns, add_carvers, a mod's own
serializer) or naming a biome, feature or tag the root doesn't have (another
mod's ids, #c: convention tags) dropped every content mod. Real mods ship
those beside their feature modifiers, so that modifier is now skipped with a
warning naming the mod and file, as NeoForge skips an unresolvable holder
set (reservedbytes's review). A malformed modifier of a read type (unknown
step) still stops the build. The docs say #tag in features reads vanilla's
feature tags only for now (arismoko's review).
Co-Authored-By: Claude Opus 5.5 <[email protected]>