Compare commits

...
Author SHA1 Message Date
Ludovic MarieandClaude Opus 5.5 847210b1e7 Mod API 0.4 preview: block entities, a machine harness, and the code-mods plan
The research spike for ATM10's 333 missing code mods (docs/research/code-mods-plan.md):
the gaps of the mod API, ATM10's code mods ranked by the gaps they hit
(docs/modpacks/atm10-code-gaps.csv, metadata only), and a first port.

- WIT: interface `machine` (capability `block-entities`) and export `block-entities`,
  in a second world so components built before still load.
- horizon-mods: block entity types checked at start, one fuel-budgeted tick call per mod,
  game-owned slots and block state lent per call, item/fuel/cooking facts from the game.
- SDK: block entity methods on `Mod`, all defaulted.
- Harness: NeoForge oracle `machines` (MachineTrace.java), mod_machine_probe on Horizon's
  own tables, tools/mod_machine_harness.py comparing them tick by tick.

The proof of concept, Iron Furnaces' iron furnace (Apache-2.0), stays local; it matches
the Java mod on all 3,760 ticks of 7 scenarios.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 11:44:29 +02:00
17 changed files with 1891 additions and 27 deletions

No files matched your search

Generated
+7
View File
@@ -1510,6 +1510,13 @@ dependencies = [
"horizon-mod-sdk",
]
[[package]]
name = "horizon-mods-test-machine"
version = "0.1.0"
dependencies = [
"horizon-mod-sdk",
]
[[package]]
name = "horizon-port-pack"
version = "0.1.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
exclude = ["launcher", "crates/horizon-telemetry-server", "third_party/wgpu-hal"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
members = ["crates/horizon-core", "crates/horizon-dualsense", "crates/horizon-threads", "crates/horizon-world", "crates/voxel-kernels", "crates/horizon-client", "crates/horizon-server-files", "crates/horizon-server", "crates/horizon-bots", "crates/horizon-assets", "crates/horizon-protocol", "crates/horizon-theme", "crates/horizon-shaderpack", "crates/horizon-prepare", "crates/horizon-telemetry", "crates/horizon-bench", "crates/horizon-friends", "crates/horizon-relay", "crates/horizon-mod-sdk", "crates/horizon-mods", "crates/horizon-mods/tests/guest", "crates/horizon-mods/tests/machine", "crates/cargo-horizon-mod", "crates/horizon-port-pack", "crates/horizon-release-sign", "crates/horizon-mod-sdk/examples/coords-hud", "xtask"]
resolver = "2"
[workspace.package]
@@ -0,0 +1,160 @@
//! The Horizon side of a code mod's machine harness (tools/mod_machine_harness.py,
//! docs/research/code-mods-plan.md): runs a ported mod's block entity through
//! horizon-mods on Horizon's own item, fuel and cooking tables, and writes its
//! slots, block state and saved fields after every tick.
//!
//! cargo run -p horizon-client --example mod_machine_probe -- PREPARED MOD.hmod SCENARIOS.json OUT.json
//!
//! SCENARIOS.json is the list the NeoForge oracle replays on the Java mod:
//! `[{"name", "block", "ticks", "slots": {"0": ["minecraft:raw_iron", 8]},
//! "properties": {"lit": ["false", ["false", "true"]]}, "fields": ["CookTime"],
//! "events": [{"tick": 40, "slot": 1, "stack": ["minecraft:coal", 1]}]}]`.
//! Fields are read from the mod's save when it is a JSON object.
use horizon_client::{
Result,
assets::Assets,
blocks::BlockFacts,
containers::Providers,
crafting::Recipes,
items::{ItemStack, Items},
};
use horizon_mods::{
BlockEntityView, CookingKind, CookingRecipe, ItemFacts, Mods, Property, Stack, Status,
};
use serde_json::{Map, Value, json};
use std::{fs, path::Path, sync::Arc};
struct Facts {
items: Items,
recipes: Recipes,
providers: Providers,
}
impl ItemFacts for Facts {
fn max_stack_size(&self, id: &str) -> u32 {
self.items.get(id).map_or(0, |i| i.max_stack)
}
fn burn_time(&self, id: &str) -> u32 {
let stack = ItemStack::new(id, 1);
self.providers.burn_time(&self.items, &stack, false).max(0) as u32
}
fn remainder(&self, id: &str) -> Option<String> {
self.items.get(id)?.crafting_remainder.clone()
}
fn cooking_recipe(&self, kind: CookingKind, input: &str) -> Option<CookingRecipe> {
let kind = match kind {
CookingKind::Smelting => "minecraft:smelting",
CookingKind::Blasting => "minecraft:blasting",
CookingKind::Smoking => "minecraft:smoking",
CookingKind::Campfire => "minecraft:campfire_cooking",
};
let c = self.recipes.cook(kind, &ItemStack::new(input, 1))?;
Some(CookingRecipe {
output: Stack {
id: c.result.item.to_string(),
count: c.result.count,
},
cooking_time: c.time,
experience: c.experience,
})
}
}
fn stack(v: &Value) -> Option<Stack> {
Some(Stack {
id: v[0].as_str()?.to_owned(),
count: v[1].as_u64()? as u32,
})
}
fn run(mods: &mut Mods, scenario: &Value) -> Result<Value> {
let block = scenario["block"].as_str().ok_or("scenario without block")?;
let (handle, slots) = mods
.create_block_entity(block, &[])
.ok_or_else(|| format!("no running mod has a block entity for {block}"))?;
let mut view = BlockEntityView {
handle,
slots: vec![None; slots as usize],
..Default::default()
};
for (slot, s) in scenario["slots"].as_object().into_iter().flatten() {
view.slots[slot.parse::<usize>()?] = stack(s);
}
for (name, p) in scenario["properties"].as_object().into_iter().flatten() {
view.properties.push(Property {
name: name.clone(),
value: p[0].as_str().unwrap_or_default().to_owned(),
values: p[1]
.as_array()
.into_iter()
.flatten()
.filter_map(|v| Some(v.as_str()?.to_owned()))
.collect(),
});
}
let fields: Vec<&str> = scenario["fields"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let mut trace = Vec::new();
for tick in 0..scenario["ticks"].as_u64().unwrap_or(0) {
for e in scenario["events"].as_array().into_iter().flatten() {
if e["tick"].as_u64() == Some(tick) {
view.slots[e["slot"].as_u64().unwrap_or(0) as usize] = stack(&e["stack"]);
}
}
let mut views = [view];
mods.tick_block_entities(&mut views);
[view] = views;
let saved = mods.save_block_entity(handle).unwrap_or_default();
let saved: Value = serde_json::from_slice(&saved).unwrap_or(Value::Null);
let slots: Map<String, Value> = view
.slots
.iter()
.enumerate()
.filter_map(|(i, s)| Some((i.to_string(), json!([s.as_ref()?.id, s.as_ref()?.count]))))
.collect();
let properties: Map<String, Value> = view
.properties
.iter()
.map(|p| (p.name.clone(), json!(p.value)))
.collect();
let values: Map<String, Value> = fields
.iter()
.map(|f| (f.to_string(), saved[f].clone()))
.collect();
trace.push(json!({"slots": slots, "properties": properties, "fields": values}));
}
mods.remove_block_entity(handle);
Ok(json!({"name": scenario["name"], "trace": trace}))
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let [root, hmod, scenarios, out] = args.as_slice() else {
return Err("usage: mod_machine_probe PREPARED MOD.hmod SCENARIOS.json OUT.json".into());
};
let root = Path::new(root);
let assets = Assets::load(root)?;
let facts = BlockFacts::load(root, assets.registry)?;
let items = Items::load(root, assets.registry, &facts)?;
let recipes = Recipes::load(root, &items)?;
let providers = Providers::load(root);
let mut mods = Mods::load(&[hmod.into()], |_| true, |_| false);
if let Status::Failed(why) = &mods.entries()[0].status {
return Err(format!("{hmod}: {why}").into());
}
mods.set_item_facts(Arc::new(Facts {
items,
recipes,
providers,
}));
let scenarios: Value = serde_json::from_slice(&fs::read(scenarios)?)?;
let mut results = Vec::new();
for scenario in scenarios.as_array().into_iter().flatten() {
results.push(run(&mut mods, scenario)?);
}
fs::write(out, serde_json::to_vec_pretty(&results)?)?;
Ok(())
}
+60 -3
View File
@@ -21,18 +21,26 @@
//! ```
//!
//! Every interface but `log` is a capability: list the ones the mod uses in
//! `horizon-mod.toml` (`game-read`, `chat`, `hud`, `storage`).
//! `horizon-mod.toml` (`game-read`, `chat`, `hud`, `storage`, and
//! `block-entities` for [`machine`] with `api = "0.4"`).
//!
//! Block entities (API 0.4): a mod whose blocks are machines returns their
//! types from [`Mod::block_entity_types`], keeps each one's state by its
//! handle, and reads and writes its slots and block state through [`machine`]
//! during [`Mod::tick_block_entities`]. The game owns the slots (hoppers,
//! menus, drops and saves see them); the mod saves the rest.
wit_bindgen::generate!({
path: "wit",
world: "horizon-mod",
world: "horizon-machine-mod",
pub_export_macro: true,
export_macro_name: "__export_horizon_mod",
default_bindings_module: "horizon_mod_sdk",
});
pub use exports::horizon::mods::block_entities::{BlockEntityType, Guest as __BlockEntitiesGuest};
pub use exports::horizon::mods::events::{Guest as __Guest, Registration};
pub use horizon::mods::{chat, game, hud, log, storage};
pub use horizon::mods::{chat, game, hud, log, machine, storage};
/// The game's calls into a mod. Only the hooks [`Registration`] asks for
/// are called; the others may stay as their defaults.
@@ -51,6 +59,29 @@ pub trait Mod: Sized + 'static {
}
/// A chat line the player received, when [`Registration::chat`] is set.
fn chat(&mut self, _text: &str) {}
/// The mod's block entity types, once after [`Mod::init`]: each for one
/// of its own blocks. Needs capability `block-entities`.
fn block_entity_types(&mut self) -> Vec<BlockEntityType> {
Vec::new()
}
/// A block entity was placed (`saved` empty) or loaded (`saved` is what
/// [`Mod::save_block_entity`] returned).
fn create_block_entity(&mut self, _handle: u32, _block: &str, _saved: &[u8]) {}
/// One server tick of every loaded ticking block entity of the mod; its
/// slots and block state are readable and writable through [`machine`]
/// during this call only.
fn tick_block_entities(&mut self, _handles: &[u32]) {}
/// Its state for the save, beside the slots the game saves.
fn save_block_entity(&mut self, _handle: u32) -> Vec<u8> {
Vec::new()
}
/// Broken or unloaded.
fn remove_block_entity(&mut self, _handle: u32) {}
/// The values its menu shows (progress bars, fuel left).
fn block_entity_data(&mut self, _handle: u32) -> Vec<i32> {
Vec::new()
}
}
impl Registration {
@@ -135,6 +166,32 @@ macro_rules! export_mod {
with(|m| $crate::Mod::chat(m, &text))
}
}
impl $crate::__BlockEntitiesGuest for Export {
fn types() -> Vec<$crate::BlockEntityType> {
let mut out = Vec::new();
with(|m| out = $crate::Mod::block_entity_types(m));
out
}
fn create(handle: u32, block: String, saved: Vec<u8>) {
with(|m| $crate::Mod::create_block_entity(m, handle, &block, &saved))
}
fn tick(handles: Vec<u32>) {
with(|m| $crate::Mod::tick_block_entities(m, &handles))
}
fn save(handle: u32) -> Vec<u8> {
let mut out = Vec::new();
with(|m| out = $crate::Mod::save_block_entity(m, handle));
out
}
fn remove(handle: u32) {
with(|m| $crate::Mod::remove_block_entity(m, handle))
}
fn data(handle: u32) -> Vec<i32> {
let mut out = Vec::new();
with(|m| out = $crate::Mod::block_entity_data(m, handle));
out
}
}
$crate::__export_horizon_mod!(Export);
};
};
+86
View File
@@ -107,3 +107,89 @@ world horizon-mod {
import storage;
export events;
}
/// Capability `block-entities` (API 0.4): what the code of a mod's block
/// entities may read and change while the game ticks them. A handle names one
/// block entity and is valid only during the `block-entities` call it was
/// passed to; slots and block state belong to the game (hoppers, menus, drops
/// and saves see them), the mod keeps the rest of its state itself.
interface machine {
/// An item and how many. Item components are not carried yet.
record item-stack {
/// `minecraft:coal`, `<mod>:<path>`, ...
id: string,
count: u32,
}
enum cooking-kind {
smelting,
blasting,
smoking,
campfire,
}
record cooking-recipe {
output: item-stack,
/// Ticks the recipe takes in its vanilla block.
cooking-time: u32,
experience: f32,
}
/// The block entity's slot, none when empty or out of range.
get-slot: func(handle: u32, slot: u32) -> option<item-stack>;
/// Empties the slot with none; a stack over the item's maximum is cut to it.
/// False for a slot out of range, an unknown item or a handle not in this call.
set-slot: func(handle: u32, slot: u32, stack: option<item-stack>) -> bool;
/// A property of the block entity's block state (`lit` -> `true`).
get-property: func(handle: u32, name: string) -> option<string>;
/// False when the block has no such property or value.
set-property: func(handle: u32, name: string, value: string) -> bool;
/// The most of `id` one slot holds; 0 for an unknown item.
max-stack-size: func(id: string) -> u32;
/// Ticks `id` burns as a furnace's fuel (the game's fuel values), 0 if it
/// is no fuel.
burn-time: func(id: string) -> u32;
/// What is left of `id` once used up as fuel or an ingredient (a bucket
/// from a lava bucket).
remainder: func(id: string) -> option<string>;
/// The data pack's cooking recipe of `kind` for `input`.
cooking: func(kind: cooking-kind, input: string) -> option<cooking-recipe>;
}
/// What the game calls on a mod's block entities (API 0.4). Only for the
/// blocks `types` names, which must be the mod's own.
interface block-entities {
record block-entity-type {
/// The mod's block, `<mod>:<path>`.
block: string,
/// Slots the game keeps for each of its block entities.
slots: u32,
/// Call `tick` every server tick while it is loaded.
ticks: bool,
}
/// Once, after `init`.
types: func() -> list<block-entity-type>;
/// A block entity was placed or loaded: `saved` is what `save` returned
/// for it, empty for a new one.
create: func(handle: u32, block: string, saved: list<u8>);
/// One server tick of every loaded ticking block entity of this mod.
tick: func(handles: list<u32>);
/// Its state for the save, beside the slots the game saves itself.
save: func(handle: u32) -> list<u8>;
/// Broken or unloaded: forget it.
remove: func(handle: u32);
/// The values its menu shows (vanilla's `ContainerData`: progress
/// bars, fuel left).
data: func(handle: u32) -> list<s32>;
}
/// A mod with block entities: the SDK builds every mod against this world.
/// Mods built for `horizon-mod` alone (no `block-entities` export) still load.
world horizon-machine-mod {
include horizon-mod;
import machine;
export block-entities;
}
/// The game's side of `horizon-machine-mod` beyond `horizon-mod`.
world machine-host {
import machine;
export block-entities;
}
+32 -3
View File
@@ -12,7 +12,9 @@ pub const API: &str = "0.1";
/// The manifest `api` lines this build loads. 0.2 is 0.1 plus content:
/// `assets/` and `data/` in the `.hmod`, with blocks and items declared in
/// `data/<id>/horizon/` (docs/modpacks.md). Code still links API 0.1.
pub const APIS: [&str; 2] = [API, "0.2"];
/// 0.4 is 0.2 plus block entities: capability `block-entities` and the
/// `block-entities` export (docs/mods.md).
pub const APIS: [&str; 3] = [API, "0.2", "0.4"];
/// Namespaces no mod may claim: vanilla's, the convention tags' (mods add
/// to those, docs/mods.md) and the game's own.
const RESERVED_NAMESPACES: [&str; 3] = ["minecraft", "c", "horizon"];
@@ -30,7 +32,8 @@ pub struct Manifest {
pub description: String,
#[serde(default)]
pub authors: Vec<String>,
/// The API line the mod was built for: `0.1`, or `0.2` for content.
/// The API line the mod was built for: `0.1`, `0.2` for content, `0.4`
/// for block entities.
pub api: String,
#[serde(default)]
pub side: Side,
@@ -68,14 +71,18 @@ pub enum Capability {
/// New blocks and items from the mod's `data/` (API 0.2): no WIT
/// interface, the game reads them before the world loads.
Registries,
/// Block entities for the mod's own blocks (API 0.4): the `machine`
/// interface and the `block-entities` export.
BlockEntities,
}
impl Capability {
pub const ALL: [Capability; 5] = [
pub const ALL: [Capability; 6] = [
Capability::GameRead,
Capability::Chat,
Capability::Hud,
Capability::Storage,
Capability::Registries,
Capability::BlockEntities,
];
/// The WIT interface it grants, in `horizon:mods`.
pub fn interface(self) -> &'static str {
@@ -85,6 +92,7 @@ impl Capability {
Capability::Hud => "hud",
Capability::Storage => "storage",
Capability::Registries => "registries",
Capability::BlockEntities => "machine",
}
}
/// What the player reads before turning the mod on: "May <this>".
@@ -95,6 +103,7 @@ impl Capability {
Capability::Hud => "draw text on your screen",
Capability::Storage => "save its own settings to disk",
Capability::Registries => "add blocks and items to your worlds",
Capability::BlockEntities => "run its blocks' machines in your worlds",
}
}
/// The capability a component import needs, `Ok(None)` for one every
@@ -125,6 +134,7 @@ impl fmt::Display for Capability {
Capability::Hud => "hud",
Capability::Storage => "storage",
Capability::Registries => "registries",
Capability::BlockEntities => "block-entities",
};
f.write_str(name)
}
@@ -155,6 +165,11 @@ impl Manifest {
"{MANIFEST}: capability registries needs api = \"0.2\""
));
}
if m.capabilities.contains(&Capability::BlockEntities) && m.api != "0.4" {
return Err(format!(
"{MANIFEST}: capability block-entities needs api = \"0.4\""
));
}
if !m.namespaces.is_empty() && !m.capabilities.contains(&Capability::Registries) {
return Err(format!(
"{MANIFEST}: namespaces are for content, which needs capability registries"
@@ -382,6 +397,20 @@ capabilities = ["game-read", "hud"]
assert!(Manifest::parse(&code).unwrap_err().contains("registries"));
}
#[test]
fn block_entities_need_api_0_4() {
let base = "id = \"ironfurnaces\"\nname = \"I\"\nversion = \"1\"\nside = \"both\"\n";
let caps = "capabilities = [\"registries\", \"block-entities\"]\n";
let m = Manifest::parse(&format!("{base}api = \"0.4\"\n{caps}")).unwrap();
assert_eq!(m.check_imports(["horizon:mods/[email protected]"]), Ok(()));
let old = Manifest::parse(&format!("{base}api = \"0.2\"\n{caps}")).unwrap_err();
assert!(old.contains("needs api = \"0.4\""), "{old}");
let without = "capabilities = [\"registries\"]\n";
let m = Manifest::parse(&format!("{base}api = \"0.4\"\n{without}")).unwrap();
let e = m.check_imports(["horizon:mods/[email protected]"]).unwrap_err();
assert!(e.contains("block-entities"), "{e}");
}
#[test]
fn hmod_round_trips() {
let dir = std::env::temp_dir().join(format!("horizon-mods-{}", std::process::id()));
+340 -12
View File
@@ -1,6 +1,7 @@
//! Loading and calling mods with wasmtime's component model.
use crate::manifest::{Manifest, Package, Side};
use crate::manifest::{Capability, Manifest, Package, Side};
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant, SystemTime},
@@ -16,6 +17,15 @@ mod bindings {
world: "horizon-mod",
});
}
/// API 0.4's block entities: a second world, so mods built before it (no
/// `block-entities` export) still instantiate as `horizon-mod`.
mod machine_bindings {
wasmtime::component::bindgen!({
path: "../horizon-mod-sdk/wit",
world: "machine-host",
additional_derives: [PartialEq],
});
}
pub use bindings::horizon::mods::{
game::{Clock, Player},
hud::{Corner, Line},
@@ -25,6 +35,11 @@ use bindings::{
exports::horizon::mods::events::Registration,
horizon::mods::{chat, game, hud, log as mod_log, storage},
};
use machine_bindings::{MachineHost, horizon::mods::machine};
pub use machine_bindings::{
exports::horizon::mods::block_entities::BlockEntityType,
horizon::mods::machine::{CookingKind, CookingRecipe, ItemStack as Stack},
};
/// A mod's linear memory cap.
const MEMORY: usize = 64 << 20;
@@ -53,6 +68,43 @@ const STORAGE_WRITE_BUDGET: u64 = 1 << 20;
/// never has a manifest there, so `find` never mistakes it for a mod).
const STORAGE_DIR: &str = ".horizon-mod-data";
/// The export a block-entity mod adds (`horizon:mods/block-entities`).
const BLOCK_ENTITIES_EXPORT: &str = "horizon:mods/[email protected]";
/// Slots a mod's block entity may ask the game to keep.
const MAX_SLOTS: u32 = 256;
/// The game's item and recipe facts that block entities query (`machine`):
/// set once with [`Mods::set_item_facts`] after the data packs load.
pub trait ItemFacts: Send + Sync {
/// 0 for an unknown item.
fn max_stack_size(&self, id: &str) -> u32;
/// Ticks the item burns as furnace fuel, 0 if it is no fuel.
fn burn_time(&self, id: &str) -> u32;
/// What is left once the item is used up (a bucket from a lava bucket).
fn remainder(&self, id: &str) -> Option<String>;
fn cooking_recipe(&self, kind: CookingKind, input: &str) -> Option<CookingRecipe>;
}
/// A block state property of a block entity's block, as `machine` sees it.
#[derive(Clone, Debug, PartialEq)]
pub struct Property {
pub name: String,
pub value: String,
/// The values the block allows for it; `set-property` refuses others.
pub values: Vec<String>,
}
/// What the game lends a mod's block entity during a tick: its slots and its
/// block's properties. `changed` is set when the mod wrote either, so the
/// game saves the chunk and updates the block and open menus.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BlockEntityView {
pub handle: u32,
pub slots: Vec<Option<Stack>>,
pub properties: Vec<Property>,
pub changed: bool,
}
/// What mods may read of the game, copied once per tick by the game.
#[derive(Clone, Debug, Default)]
pub struct Snapshot {
@@ -101,8 +153,16 @@ pub struct Entry {
struct Running {
store: Store<State>,
exports: HorizonMod,
exports: Exports,
hooks: Registration,
/// Its block entity types (API 0.4), checked against its namespaces.
types: Vec<BlockEntityType>,
}
struct Exports {
events: HorizonMod,
/// Present when the component exports `block-entities`.
machine: Option<MachineHost>,
}
struct State {
@@ -119,6 +179,10 @@ struct State {
/// needs its own budget.
storage_budget: u64,
limits: StoreLimits,
/// Capability `block-entities`: the game's facts, and the block entities
/// lent to the current `tick` call (none outside it).
facts: Option<Arc<dyn ItemFacts>>,
views: Vec<BlockEntityView>,
}
/// The installed mods and the engine running them. `Mods::default()` is
@@ -129,6 +193,13 @@ pub struct Mods {
entries: Vec<Entry>,
snapshot: Arc<Snapshot>,
out: Vec<Output>,
facts: Option<Arc<dyn ItemFacts>>,
/// Each live block entity's handle: the entry of the mod that owns it.
handles: HashMap<u32, usize>,
next_handle: u32,
/// The block entities a mod held when it stopped mid-call, saved from
/// its dropped store so the game gets their slots back.
stranded: Vec<BlockEntityView>,
}
/// The `.hmod` files and mod folders in `dir`, by name.
@@ -204,6 +275,8 @@ impl Mods {
let mut linker = Linker::new(&engine);
HorizonMod::add_to_linker::<State, HasSelf<State>>(&mut linker, |s| s)
.map_err(|e| e.to_string())?;
MachineHost::add_to_linker::<State, HasSelf<State>>(&mut linker, |s| s)
.map_err(|e| e.to_string())?;
self.engine = Some((engine, linker));
}
Ok(self.engine.as_ref().expect("made above"))
@@ -242,6 +315,7 @@ impl Mods {
.flat_map(|r| r.hooks.commands.iter().cloned())
.collect();
let snapshot = self.snapshot.clone();
let facts = self.facts.clone();
let (engine, linker) = self.engine()?;
let component =
Component::new(engine, wasm).map_err(|e| format!("not a component: {e}"))?;
@@ -251,6 +325,9 @@ impl Mods {
.map(|(name, _)| name.to_string())
.collect();
m.check_imports(imports.iter().map(String::as_str))?;
let has_machine = ty
.exports(engine)
.any(|(name, _)| name == BLOCK_ENTITIES_EXPORT);
let data_dir = path
.parent()
.unwrap_or(Path::new("."))
@@ -268,16 +345,38 @@ impl Mods {
.memory_size(MEMORY)
.instances(16)
.build(),
facts,
views: Vec::new(),
};
let mut store = Store::new(engine, state);
store.limiter(|s| &mut s.limits);
store.set_fuel(FUEL_INIT).map_err(|e| e.to_string())?;
let exports =
HorizonMod::instantiate(&mut store, &component, linker).map_err(|e| reason(&e))?;
let mut hooks = exports
let instance = linker
.instantiate(&mut store, &component)
.map_err(|e| reason(&e))?;
let events = HorizonMod::new(&mut store, &instance).map_err(|e| reason(&e))?;
let machine = match has_machine {
true => Some(MachineHost::new(&mut store, &instance).map_err(|e| reason(&e))?),
false => None,
};
let mut hooks = events
.horizon_mods_events()
.call_init(&mut store)
.map_err(|e| reason(&e))??;
let mut types = Vec::new();
if let Some(machine) = &machine {
let declared = machine
.horizon_mods_block_entities()
.call_types(&mut store)
.map_err(|e| reason(&e))?;
if !declared.is_empty() && !m.capabilities.contains(&Capability::BlockEntities) {
return Err(format!(
"has block entities but not capability block-entities in {}",
crate::manifest::MANIFEST
));
}
types = check_types(m, declared)?;
}
hooks.commands.retain(|c| {
let free = !reserved(c) && !taken.contains(c);
if !free {
@@ -288,8 +387,9 @@ impl Mods {
self.out.append(&mut store.data_mut().out);
Ok(Some(Running {
store,
exports,
exports: Exports { events, machine },
hooks,
types,
}))
}
@@ -310,7 +410,7 @@ impl Mods {
&mut self,
fuel: u64,
wants: impl Fn(&Registration) -> bool,
mut f: impl FnMut(&HorizonMod, &mut Store<State>) -> wasmtime::Result<()>,
mut f: impl FnMut(&Exports, &mut Store<State>) -> wasmtime::Result<()>,
) {
for i in 0..self.entries.len() {
if self.entries[i]
@@ -328,12 +428,13 @@ impl Mods {
&mut self,
i: usize,
fuel: u64,
f: impl FnOnce(&HorizonMod, &mut Store<State>) -> wasmtime::Result<R>,
f: impl FnOnce(&Exports, &mut Store<State>) -> wasmtime::Result<R>,
) -> Option<R> {
let entry = &mut self.entries[i];
let run = entry.run.as_mut()?;
run.store.data_mut().snapshot = self.snapshot.clone();
run.store.data_mut().storage_budget = STORAGE_WRITE_BUDGET;
run.store.data_mut().facts = self.facts.clone();
let started = Instant::now();
let result = run
.store
@@ -353,6 +454,7 @@ impl Mods {
reason: reason.clone(),
});
entry.status = Status::Failed(reason);
self.stranded = std::mem::take(&mut run.store.data_mut().views);
entry.run = None;
None
}
@@ -364,7 +466,7 @@ impl Mods {
self.each(
FUEL_TICK,
|h| h.tick,
|m, s| m.horizon_mods_events().call_tick(s, tick),
|m, s| m.events.horizon_mods_events().call_tick(s, tick),
);
}
/// A rendered frame, `seconds` after the last.
@@ -372,7 +474,7 @@ impl Mods {
self.each(
FUEL_FRAME,
|h| h.frame,
|m, s| m.horizon_mods_events().call_frame(s, seconds),
|m, s| m.events.horizon_mods_events().call_frame(s, seconds),
);
}
/// A chat line the player received, as plain text.
@@ -380,7 +482,7 @@ impl Mods {
self.each(
FUEL_EVENT,
|h| h.chat,
|m, s| m.horizon_mods_events().call_chat(s, text),
|m, s| m.events.horizon_mods_events().call_chat(s, text),
);
}
/// Whether a running mod owns `/name`.
@@ -402,7 +504,7 @@ impl Mods {
return false;
};
let result = self.call(i, FUEL_EVENT, |m, s| {
m.horizon_mods_events().call_command(s, name, args)
m.events.horizon_mods_events().call_command(s, name, args)
});
match result {
Some(Ok(Some(text))) => self.out.push(Output::Feedback { text, error: false }),
@@ -430,6 +532,117 @@ impl Mods {
std::mem::take(&mut self.out)
}
/// The game's item and recipe facts, for capability `block-entities`.
pub fn set_item_facts(&mut self, facts: Arc<dyn ItemFacts>) {
self.facts = Some(facts);
}
/// The block entity types of the running mods (API 0.4).
pub fn block_entity_types(&self) -> impl Iterator<Item = &BlockEntityType> {
self.entries
.iter()
.filter_map(|e| e.run.as_ref())
.flat_map(|r| r.types.iter())
}
/// The running mod whose block entity type is `block`'s, and the type.
fn block_entity_type(&self, block: &str) -> Option<(usize, &BlockEntityType)> {
self.entries.iter().enumerate().find_map(|(i, e)| {
let t = e.run.as_ref()?.types.iter().find(|t| t.block == block)?;
Some((i, t))
})
}
/// A block entity for `block`, placed (`saved` empty) or loaded (what
/// [`Mods::save_block_entity`] returned): its handle, and the slots the
/// game must keep for it. None when no running mod has `block`, or the
/// mod stopped.
pub fn create_block_entity(&mut self, block: &str, saved: &[u8]) -> Option<(u32, u32)> {
let (i, ty) = self.block_entity_type(block)?;
let slots = ty.slots;
let handle = self.next_handle;
self.next_handle = self.next_handle.wrapping_add(1);
self.call(i, FUEL_EVENT, |m, s| {
machine_of(m)?
.horizon_mods_block_entities()
.call_create(s, handle, block, saved)
})?;
self.handles.insert(handle, i);
Some((handle, slots))
}
/// One server tick of the ticking block entities in `views`, one call
/// per mod. Each mod sees only its own; their slots and properties come
/// back changed. Views of unknown handles or stopped mods are left alone.
pub fn tick_block_entities(&mut self, views: &mut [BlockEntityView]) {
for i in 0..self.entries.len() {
let Some(run) = self.entries[i].run.as_ref() else {
continue;
};
let ticking = |v: &BlockEntityView| self.handles.get(&v.handle) == Some(&i);
if !run.types.iter().any(|t| t.ticks) || !views.iter().any(ticking) {
continue;
}
let mine: Vec<usize> = (0..views.len()).filter(|&v| ticking(&views[v])).collect();
let lent: Vec<BlockEntityView> = mine
.iter()
.map(|&v| std::mem::take(&mut views[v]))
.collect();
let handles: Vec<u32> = lent.iter().map(|v| v.handle).collect();
if let Some(run) = self.entries[i].run.as_mut() {
run.store.data_mut().views = lent;
}
self.call(i, FUEL_TICK, |m, s| {
machine_of(m)?
.horizon_mods_block_entities()
.call_tick(s, &handles)
});
// A mod that stopped in the call left its views in `stranded`,
// with what it wrote before it stopped.
let returned = match self.entries[i].run.as_mut() {
Some(run) => std::mem::take(&mut run.store.data_mut().views),
None => std::mem::take(&mut self.stranded),
};
for (slot, view) in mine.into_iter().zip(returned) {
views[slot] = view;
}
}
}
/// What the game saves for the block entity beside its slots.
pub fn save_block_entity(&mut self, handle: u32) -> Option<Vec<u8>> {
let i = *self.handles.get(&handle)?;
self.call(i, FUEL_EVENT, |m, s| {
machine_of(m)?
.horizon_mods_block_entities()
.call_save(s, handle)
})
}
/// The values the block entity's menu shows (progress, fuel).
pub fn block_entity_data(&mut self, handle: u32) -> Vec<i32> {
let Some(&i) = self.handles.get(&handle) else {
return Vec::new();
};
self.call(i, FUEL_EVENT, |m, s| {
machine_of(m)?
.horizon_mods_block_entities()
.call_data(s, handle)
})
.unwrap_or_default()
}
/// The block entity was broken or unloaded.
pub fn remove_block_entity(&mut self, handle: u32) {
if let Some(i) = self.handles.remove(&handle) {
self.call(i, FUEL_EVENT, |m, s| {
machine_of(m)?
.horizon_mods_block_entities()
.call_remove(s, handle)
});
}
}
/// Restarts the mods whose file changed since they loaded (mod
/// development: rebuild, and the game picks it up). True when any did.
pub fn reload_changed(&mut self, reserved: impl Fn(&str) -> bool) -> bool {
@@ -444,6 +657,9 @@ impl Mods {
any = true;
self.entries[i].modified = now;
self.entries[i].run = None;
// The new instance knows none of the old one's block entities:
// the game creates them again from their saves.
self.handles.retain(|_, owner| *owner != i);
let path = self.entries[i].path.clone();
let started = Package::read(&path).and_then(|p| {
self.entries[i].manifest = Some(p.manifest.clone());
@@ -467,6 +683,38 @@ impl Mods {
}
}
/// A mod's `block-entities` export; an error (the call fails, the mod goes
/// on) for a component without one, which then has no types to call it for.
fn machine_of(m: &Exports) -> wasmtime::Result<&MachineHost> {
m.machine
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("no block-entities export"))
}
/// The block entity types a mod declared, if every one is for a block in its
/// own namespaces, declared once, with at most [`MAX_SLOTS`] slots.
fn check_types(m: &Manifest, types: Vec<BlockEntityType>) -> Result<Vec<BlockEntityType>, String> {
for (n, t) in types.iter().enumerate() {
let owned = t
.block
.split_once(':')
.is_some_and(|(ns, path)| !path.is_empty() && m.owned_namespaces().any(|o| o == ns));
if !owned {
return Err(format!(
"declares a block entity for {}, which is not one of its blocks",
t.block
));
}
if types[..n].iter().any(|o| o.block == t.block) {
return Err(format!("declares {} twice", t.block));
}
if t.slots > MAX_SLOTS {
return Err(format!("{} asks for over {MAX_SLOTS} slots", t.block));
}
}
Ok(types)
}
fn modified(path: &Path) -> Option<SystemTime> {
let meta = std::fs::metadata(path).ok()?;
if meta.is_dir() {
@@ -603,3 +851,83 @@ fn storage_key_ok(key: &str) -> bool {
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"_.-".contains(&b));
chars_ok && key != "." && key != ".."
}
impl State {
/// The block entity `handle` lent to this call.
fn view(&mut self, handle: u32) -> Option<&mut BlockEntityView> {
self.views.iter_mut().find(|v| v.handle == handle)
}
}
impl machine::Host for State {
fn get_slot(&mut self, handle: u32, slot: u32) -> Option<Stack> {
self.view(handle)?
.slots
.get(slot as usize)
.cloned()
.flatten()
}
fn set_slot(&mut self, handle: u32, slot: u32, stack: Option<Stack>) -> bool {
let stack = match stack {
Some(s) if s.count > 0 => {
// Unknown items are refused; with no facts set (a test host)
// nothing is known about items, so any id passes uncut.
let max = match &self.facts {
Some(facts) => match facts.max_stack_size(&s.id) {
0 => return false,
max => max,
},
None => u32::MAX,
};
Some(Stack {
count: s.count.min(max),
id: s.id,
})
}
_ => None,
};
let Some(view) = self.view(handle) else {
return false;
};
let Some(cell) = view.slots.get_mut(slot as usize) else {
return false;
};
if *cell != stack {
*cell = stack;
view.changed = true;
}
true
}
fn get_property(&mut self, handle: u32, name: String) -> Option<String> {
let view = self.view(handle)?;
let p = view.properties.iter().find(|p| p.name == name)?;
Some(p.value.clone())
}
fn set_property(&mut self, handle: u32, name: String, value: String) -> bool {
let Some(view) = self.view(handle) else {
return false;
};
let Some(p) = view.properties.iter_mut().find(|p| p.name == name) else {
return false;
};
if !p.values.contains(&value) {
return false;
}
if p.value != value {
p.value = value;
view.changed = true;
}
true
}
fn max_stack_size(&mut self, id: String) -> u32 {
self.facts.as_ref().map_or(0, |f| f.max_stack_size(&id))
}
fn burn_time(&mut self, id: String) -> u32 {
self.facts.as_ref().map_or(0, |f| f.burn_time(&id))
}
fn remainder(&mut self, id: String) -> Option<String> {
self.facts.as_ref()?.remainder(&id)
}
fn cooking(&mut self, kind: CookingKind, input: String) -> Option<CookingRecipe> {
self.facts.as_ref()?.cooking_recipe(kind, &input)
}
}
@@ -0,0 +1,16 @@
[package]
name = "horizon-mods-test-machine"
version = "0.1.0"
edition.workspace = true
license.workspace = true
publish = false
description = "A mod with a block entity, for horizon-mods' block-entity tests"
[lib]
crate-type = ["cdylib"]
[dependencies]
horizon-mod-sdk = { path = "../../../horizon-mod-sdk" }
[lints]
workspace = true
@@ -0,0 +1,93 @@
//! A block entity for the runtime's tests (API 0.4): `test_machine:smelter`
//! has an input and an output slot and smelts one input item every recipe's
//! cooking time, `lit` while it does. Its progress is saved and shown as the
//! menu's one data value; TNT in its input makes it panic (after lighting
//! up). `test_machine:crate` holds 4 slots and never ticks.
use horizon_mod_sdk::{
BlockEntityType, Mod, Registration,
machine::{self, CookingKind, ItemStack},
};
use std::collections::HashMap;
struct Machines {
/// Each smelter's ticks of cooking so far.
progress: HashMap<u32, u32>,
}
impl Mod for Machines {
fn init() -> Result<(Self, Registration), String> {
let progress = HashMap::new();
Ok((Machines { progress }, Registration::new()))
}
fn block_entity_types(&mut self) -> Vec<BlockEntityType> {
vec![
BlockEntityType {
block: "test_machine:smelter".into(),
slots: 2,
ticks: true,
},
BlockEntityType {
block: "test_machine:crate".into(),
slots: 4,
ticks: false,
},
]
}
fn create_block_entity(&mut self, handle: u32, block: &str, saved: &[u8]) {
if block == "test_machine:smelter" {
let progress = saved.try_into().map_or(0, u32::from_le_bytes);
self.progress.insert(handle, progress);
}
}
fn tick_block_entities(&mut self, handles: &[u32]) {
for &h in handles {
let progress = self.progress.entry(h).or_default();
if machine::get_slot(h, 0).is_some_and(|s| s.id == "minecraft:tnt") {
machine::set_property(h, "lit", "true");
panic!("tnt in the smelter");
}
let recipe = machine::get_slot(h, 0)
.and_then(|input| machine::cooking(CookingKind::Smelting, &input.id));
let Some(recipe) = recipe else {
*progress = 0;
machine::set_property(h, "lit", "false");
continue;
};
machine::set_property(h, "lit", "true");
*progress += 1;
if *progress < recipe.cooking_time {
continue;
}
*progress = 0;
let out = match machine::get_slot(h, 1) {
Some(o) if o.id == recipe.output.id => ItemStack {
count: o.count + recipe.output.count,
..o
},
Some(_) => continue,
None => recipe.output,
};
if !machine::set_slot(h, 1, Some(&out)) {
continue;
}
let input = machine::get_slot(h, 0).filter(|i| i.count > 1);
let rest = input.map(|i| ItemStack {
count: i.count - 1,
..i
});
machine::set_slot(h, 0, rest.as_ref());
}
}
fn save_block_entity(&mut self, handle: u32) -> Vec<u8> {
let progress = self.progress.get(&handle).copied().unwrap_or(0);
progress.to_le_bytes().to_vec()
}
fn remove_block_entity(&mut self, handle: u32) {
self.progress.remove(&handle);
}
fn block_entity_data(&mut self, handle: u32) -> Vec<i32> {
vec![self.progress.get(&handle).copied().unwrap_or(0) as i32]
}
}
horizon_mod_sdk::export_mod!(Machines);
+189 -2
View File
@@ -1,7 +1,8 @@
//! Loads real mods built with the SDK: the coords-hud example and a guest
//! that misbehaves on command. Needs `rustup target add wasm32-unknown-unknown`.
use horizon_mods::{
Clock, Corner, Mods, Output, Package, Player, Snapshot, Status, manifest::componentize,
BlockEntityView, Clock, CookingKind, CookingRecipe, Corner, ItemFacts, Mods, Output, Package,
Player, Property, Snapshot, Stack, Status, manifest::componentize,
};
use std::{
path::{Path, PathBuf},
@@ -18,7 +19,16 @@ api = "0.1"
capabilities = ["chat", "storage"]
"#;
/// Builds both mods once and writes their `.hmod` files; returns the folder.
const MACHINE_MANIFEST: &str = r#"
id = "test_machine"
name = "Test Machine"
version = "0.1.0"
api = "0.4"
side = "both"
capabilities = ["block-entities"]
"#;
/// Builds the mods once and writes their `.hmod` files; returns the folder.
fn mods_dir() -> &'static Path {
static DIR: OnceLock<PathBuf> = OnceLock::new();
DIR.get_or_init(|| {
@@ -32,6 +42,8 @@ fn mods_dir() -> &'static Path {
"coords-hud",
"-p",
"horizon-mods-test-guest",
"-p",
"horizon-mods-test-machine",
"--target-dir",
])
.arg(&target)
@@ -48,6 +60,7 @@ fn mods_dir() -> &'static Path {
let mods = [
("coords_hud", std::fs::read_to_string(example).unwrap()),
("horizon_mods_test_guest", GUEST_MANIFEST.to_string()),
("horizon_mods_test_machine", MACHINE_MANIFEST.to_string()),
];
for (name, manifest) in mods {
let core = std::fs::read(built.join(format!("{name}.wasm"))).unwrap();
@@ -396,3 +409,177 @@ capabilities = ["registries"]
// Content has no hooks: the game's per-tick mod work stays skipped.
assert!(!mods.is_running());
}
/// Raw iron smelts into iron ingots in 3 ticks; `test:ghost` is a recipe
/// output no item table knows.
struct Facts;
impl ItemFacts for Facts {
fn max_stack_size(&self, id: &str) -> u32 {
if id == "test:ghost" { 0 } else { 64 }
}
fn burn_time(&self, _id: &str) -> u32 {
0
}
fn remainder(&self, _id: &str) -> Option<String> {
None
}
fn cooking_recipe(&self, kind: CookingKind, input: &str) -> Option<CookingRecipe> {
let output = match (kind, input) {
(CookingKind::Smelting, "minecraft:raw_iron") => "minecraft:iron_ingot",
(CookingKind::Smelting, "test:haunted") => "test:ghost",
_ => return None,
};
Some(CookingRecipe {
output: stack(output, 1),
cooking_time: 3,
experience: 0.7,
})
}
}
fn stack(id: &str, count: u32) -> Stack {
Stack {
id: id.into(),
count,
}
}
fn smelter(handle: u32, input: Option<Stack>) -> BlockEntityView {
BlockEntityView {
handle,
slots: vec![input, None],
properties: vec![Property {
name: "lit".into(),
value: "false".into(),
values: vec!["false".into(), "true".into()],
}],
changed: false,
}
}
fn machines() -> Mods {
let mut mods = load("horizon_mods_test_machine.hmod");
assert_eq!(mods.entries()[0].status, Status::Running);
mods.set_item_facts(std::sync::Arc::new(Facts));
mods
}
#[test]
fn block_entities_tick_save_and_load() {
let mut mods = machines();
let types: Vec<(&str, u32, bool)> = mods
.block_entity_types()
.map(|t| (t.block.as_str(), t.slots, t.ticks))
.collect();
assert_eq!(
types,
[
("test_machine:smelter", 2, true),
("test_machine:crate", 4, false)
]
);
assert_eq!(mods.create_block_entity("minecraft:furnace", &[]), None);
let (h, slots) = mods
.create_block_entity("test_machine:smelter", &[])
.unwrap();
assert_eq!(slots, 2);
let mut views = [smelter(h, Some(stack("minecraft:raw_iron", 2)))];
mods.tick_block_entities(&mut views);
assert!(views[0].changed, "lit turned on");
assert_eq!(views[0].properties[0].value, "true");
assert_eq!(mods.block_entity_data(h), [1]);
views[0].changed = false;
mods.tick_block_entities(&mut views);
assert!(!views[0].changed, "nothing the game keeps changed");
mods.tick_block_entities(&mut views);
assert_eq!(
views[0].slots,
[
Some(stack("minecraft:raw_iron", 1)),
Some(stack("minecraft:iron_ingot", 1))
]
);
mods.tick_block_entities(&mut views);
assert_eq!(mods.block_entity_data(h), [1]);
// Saved and loaded again (a chunk unloads and comes back).
let saved = mods.save_block_entity(h).unwrap();
mods.remove_block_entity(h);
assert_eq!(mods.block_entity_data(h), Vec::<i32>::new());
let (again, _) = mods
.create_block_entity("test_machine:smelter", &saved)
.unwrap();
assert_ne!(again, h);
assert_eq!(mods.block_entity_data(again), [1]);
assert!(mods.drain().is_empty());
}
#[test]
fn block_entities_see_only_their_own() {
let mut mods = machines();
let (h, _) = mods
.create_block_entity("test_machine:smelter", &[])
.unwrap();
// A view with a handle the mod never got, and an output no table knows:
// the stranger is not lent, and the ghost item is refused.
let mut views = [
smelter(h + 100, Some(stack("minecraft:raw_iron", 1))),
smelter(h, Some(stack("test:haunted", 1))),
];
for _ in 0..3 {
mods.tick_block_entities(&mut views);
}
assert_eq!(
views[0],
smelter(h + 100, Some(stack("minecraft:raw_iron", 1)))
);
assert_eq!(views[1].slots, [Some(stack("test:haunted", 1)), None]);
assert_eq!(mods.entries()[0].status, Status::Running);
}
#[test]
fn block_entities_need_their_capability_and_blocks() {
let wasm = Package::read(&mods_dir().join("horizon_mods_test_machine.hmod"))
.unwrap()
.wasm
.unwrap();
let cases = [
(
MACHINE_MANIFEST.replace("\"block-entities\"", ""),
"uses capability block-entities, missing from horizon-mod.toml",
),
(
MACHINE_MANIFEST.replace("id = \"test_machine\"", "id = \"other\""),
"declares a block entity for test_machine:smelter, which is not one of its blocks",
),
];
for (n, (manifest, why)) in cases.into_iter().enumerate() {
let dir = mods_dir().join(format!("machine-refused-{n}"));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("horizon-mod.toml"), manifest).unwrap();
std::fs::write(dir.join("mod.wasm"), &wasm).unwrap();
let mods = Mods::load(&[dir], |_| true, |_| false);
assert_eq!(mods.entries()[0].status, Status::Failed(why.into()));
assert_eq!(mods.block_entity_types().count(), 0);
}
}
#[test]
fn a_block_entity_that_traps_gives_its_slots_back() {
let mut mods = machines();
let (h, _) = mods
.create_block_entity("test_machine:smelter", &[])
.unwrap();
let mut views = [smelter(h, Some(stack("minecraft:tnt", 3)))];
mods.tick_block_entities(&mut views);
assert!(matches!(mods.entries()[0].status, Status::Failed(_)));
assert!(matches!(mods.drain()[..], [Output::Stopped { .. }]));
// Its slots are back, with what it wrote before it stopped.
assert_eq!(views[0].handle, h);
assert_eq!(views[0].slots, [Some(stack("minecraft:tnt", 3)), None]);
assert_eq!(views[0].properties[0].value, "true");
// A stopped mod's block entities are left alone after that.
mods.tick_block_entities(&mut views);
assert_eq!(views[0].slots, [Some(stack("minecraft:tnt", 3)), None]);
assert_eq!(mods.save_block_entity(h), None);
}
+333
View File
@@ -0,0 +1,333 @@
mod_id,name,class,tier,licence,modrinth_downloads,notice,gaps,requires
ae2,Applied Energistics 2,content,deep,custom,6316000,3,CDIMNRTV,
allthemodium,Allthemodium,content,deep,unknown,,3,BIWX,
alltheores,AllTheOres,content,data,unknown,,3,J,
apotheosis,Apotheosis,content,deep,permissive,373000,3,DEHIMRTX,
ars_nouveau,Ars Nouveau,content,deep,all-rights-reserved,4014000,3,BDHIMNRTVX,
create,Create,content,deep,custom,27867000,3,BCDHINRTVX,
farmersdelight,Farmer's Delight,content,machines,permissive,25505000,3,BDIMRTV,
ftbquests,FTB Quests,content,machines,unknown,,3,EIMNU,
irons_spellbooks,Iron's Spells 'n Spellbooks,content,deep,unknown,,3,ADEIMNRTX,
jade,Jade 🔍,client-ui,client,restricted,72845000,3,NU,
jei,Just Enough Items (JEI),client-ui,client,permissive,84344000,3,NU,
journeymap,JourneyMap,client-ui,client,all-rights-reserved,15400000,3,NU,
mekanism,Mekanism,content,deep,permissive,3906000,3,BCDIMNRTVX,
mysticalagriculture,Mystical Agriculture,content,machines,permissive,1303000,3,BDIMNRTV,
refinedstorage,Refined Storage,content,deep,unknown,,3,CDMNRTV,
sophisticatedbackpacks,Sophisticated Backpacks,content,deep,unknown,,3,ADIMNV,
sophisticatedstorage,Sophisticated Storage,content,machines,unknown,,3,DMNTV,
supplementaries,Supplementaries,content,deep,custom,29039000,3,BDHINTX,
twilightforest,The Twilight Forest,content,deep,unknown,,3,BDHIMTVWX,
waystones,Waystones,content,machines,all-rights-reserved,27351000,3,BDEMNT,
actuallyadditions,Actually Additions,content,machines,permissive,246000,2,BCDIMNRTV,
ad_astra,Ad Astra,content,deep,custom,5985000,2,CDEIMNTVWX,
ae2wtlib,Applied Energistics 2 Wireless Terminals,content,machines,permissive,3149000,2,AIMN,ae2
aether,The Aether,content,deep,unknown,,2,BDHIMWX,
allthecompressed,AllTheCompressed,content,data,unknown,,2,J,
allthetweaks,AllTheTweaks,content,logic,unknown,,2,BI,
amendments,Amendments,content,deep,custom,20004000,2,BDHTX,
apothic_enchanting,Apothic-Enchanting,content,machines,permissive,128000,2,EHIMTV,
appleskin,AppleSkin,client-ui,client,permissive,97093000,2,NU,
aquaculture,Aquaculture 2,content,deep,all-rights-reserved,1214000,2,BDIMTVX,
artifacts,Artifacts,content,deep,permissive,14143000,2,ADEIX,
astralsorcery,Astral Sorcery,content,deep,unknown,,2,BDIMNRTVX,
bigreactors,Extreme Reactors,content,machines,unknown,,2,CDMNRTV,
biomeswevegone,Oh The Biomes We've Gone,content,deep,all-rights-reserved,11969000,2,BDWX,
botanypots,Botany Pots,content,machines,copyleft,2167000,2,DMNRTV,
buildinggadgets2,Building Gadgets 2,content,deep,unknown,,2,DIMN,
cataclysm,L_Ender's Cataclysm,content,deep,restricted,19343000,2,BDIWX,
chisel,Chisel Reborn,content,machines,custom,1538000,2,IMNTV,
compactmachines,Compact Machines,content,deep,unknown,,2,CDMNTVW,
computercraft,CC: Tweaked,content,deep,custom,5344000,2,DMNSTUV,
cookingforblockheads,Cooking for Blockheads,content,machines,unknown,,2,BDMNTV,
craftingtweaks,Crafting Tweaks,client-ui,client,all-rights-reserved,10529000,2,NU,
createaddition,Create Crafts & Additions,content,deep,permissive,11314000,2,CDNRTV,create
creeperoverhaul,Creeper Overhaul,content,deep,all-rights-reserved,10880000,2,DX,
deeperdarker,Deeper and Darker,content,deep,copyleft,16319000,2,BDIWX,
draconicevolution,Draconic Evolution,content,deep,custom,926000,2,ACDEIMNTVX,
easy_villagers,Easy Villagers,content,deep,all-rights-reserved,1681000,2,DMNTVX,
elevatorid,ElevatorMod,content,logic,permissive,2338000,2,BMN,
enchdesc,Enchantment Descriptions,client-ui,client,copyleft,39608000,2,U,
enderio,Ender IO,content,deep,permissive,1004000,2,BCDIMNRTV,
endermanoverhaul,Enderman Overhaul,content,deep,all-rights-reserved,8223000,2,DX,
enderstorage,Ender Storage,content,machines,permissive,691000,2,CDMNTV,
explorerscompass,Explorer's Compass,content,machines,restricted,12346000,2,IMN,
extendedae,ExtendedAE,content,machines,unknown,,2,CDMNTV,ae2
fluxnetworks,Flux Networks,content,machines,unknown,,2,CDMNT,
framedblocks,FramedBlocks,content,deep,copyleft,2265000,2,BDNT,
ftbchunks,FTB Chunks,server-only,deep,unknown,,2,ENU,
ftbteams,FTB Teams,server-only,logic,unknown,,2,ENU,
ftbultimine,FTB Ultimine,content,logic,unknown,,2,DEIN,
functionalstorage,Functional Storage,content,machines,unknown,,2,DMNTV,
handcrafted,Handcrafted,content,machines,custom,27534000,2,BDMTVX,
iceandfire,IceAndFire Community Edition,content,deep,copyleft,2208000,2,BDIMTWX,
immersiveengineering,Immersive Engineering,content,deep,custom,3691000,2,BCDIMNRTVX,
industrialforegoing,Industrial Foregoing,content,machines,permissive,978000,2,CDIMNRTVX,
ironfurnaces,Iron Furnaces,content,machines,permissive,10092000,2,MNTV,
ironjetpacks,Iron Jetpacks,content,logic,permissive,641000,2,ADEIN,
justdirethings,Just Dire Things,content,deep,unknown,,2,CDIMNTVX,
laserio,LaserIO,content,machines,unknown,,2,CDMNTV,
lootr,Lootr,content,machines,permissive,23796000,2,DEMNTV,
mcwfurnitures,Macaw's Furniture,content,machines,all-rights-reserved,7371000,2,BMTVX,
mcwwindows,Macaw's Windows,content,logic,all-rights-reserved,11637000,2,B,
mekanismgenerators,Mekanism Generators,content,deep,permissive,2656000,2,CDMNTV,mekanism
minecolonies,MineColonies,content,deep,unknown,,2,BDEIMNTVWX,
mininggadgets,Mining Gadgets,content,deep,unknown,,2,DIMNT,
modern_industrialization,Modern Industrialization,content,deep,permissive,2060000,2,CDMNRTV,
mousetweaks,Mouse Tweaks,client-ui,client,permissive,63400000,2,U,
naturescompass,Nature's Compass,content,machines,restricted,27185000,2,IMN,
notenoughanimations,Not Enough Animations,client-ui,client,custom,93276000,2,DU,
occultism,Occultism,content,deep,permissive,893000,2,DIMNRTVX,
oritech,Oritech,content,deep,permissive,1271000,2,CDMNRTV,
pamhc2crops,Pam's HarvestCraft - Crops,content,data,unknown,,2,J,
pamhc2foodcore,Pam's HarvestCraft - Food Core,content,data,unknown,,2,J,
pipez,Pipez,content,machines,all-rights-reserved,565000,2,CDMNTV,
pneumaticcraft,PneumaticCraft: Repressurized,content,deep,copyleft,811000,2,BCDIMNRTVX,
powah,Powah!,content,machines,copyleft,2573000,2,CDMNTV,
productivebees,Productive Bees,content,deep,unknown,,2,BDMNRTVX,
rechiseled,Rechiseled,content,machines,all-rights-reserved,8991000,2,DIMN,
refurbished_furniture,MrCrayfish's Furniture Mod: Refurbished,content,deep,unknown,,2,BDMNTVX,
relics,Relics,content,deep,all-rights-reserved,3258000,2,ADEIN,
sawmill,Sawmill,content,machines,all-rights-reserved,6447000,2,MNR,
securitycraft,SecurityCraft,content,deep,permissive,5921000,2,BDEMNTVX,
silentgear,Silent Gear,content,deep,permissive,501000,2,DIMNRT,
the_bumblezone,The Bumblezone - NeoForge/Forge,content,deep,all-rights-reserved,4273000,2,BDHIWX,
tiab,Time in a bottle,content,logic,permissive,332000,2,EHI,
tombstone,Corail Tombstone,content,deep,all-rights-reserved,344000,2,DEIMTVX,
toolbelt,Tool Belt,content,machines,unknown,,2,ADIMN,
torchmaster,TorchMaster,content,logic,permissive,3360000,2,BEH,
trashcans,Trash Cans,content,machines,all-rights-reserved,2185000,2,CMNTV,
undergarden,The Undergarden,content,deep,all-rights-reserved,3075000,2,BDIWX,
accelerateddecay,Accelerated Decay,content,logic,copyleft,1187000,1,BH,
ad_astra_giselle_addon,Ad Astra: Giselle Addon,integration,machines,unknown,,1,CMNRTV,ad_astra mekanism
additional_lights,Additional Lights,unknown,none,unknown,,1,B,
advanced_ae,AdvancedAE,content,machines,copyleft,651000,1,MNTV,ae2
advancedperipherals,Advanced Peripherals,integration,machines,unknown,,1,CSTV,computercraft
ae2ct,AE2:Crafting Tree,client-ui,client,unknown,,1,U,ae2
ae2importexportcard,AE2 Import Export Card,content,machines,permissive,320000,1,MNTV,ae2
ae2jeiintegration,AE2 JEI Integration,integration,client,unknown,,1,U,ae2 jei
ae2netanalyser,AE2NetworkAnalyzer,content,machines,unknown,,1,DIN,ae2
aeinfinitybooster,AEInfinityBooster,content,logic,copyleft,220000,1,I,ae2
akashictome,Akashic Tome,content,logic,restricted,454000,1,IN,
allthearcanistgear,All The Arcanist Gear,content,logic,unknown,,1,I,ars_nouveau
allthewizardgear,All the Wizard Gear,content,logic,unknown,,1,I,irons_spellbooks
apothic_spawners,Apothic-Spawners,content,logic,permissive,105000,1,HT,
appflux,AppliedFlux,content,machines,unknown,,1,CMNTV,ae2
appmek,Applied Mekanistics,integration,machines,copyleft,847000,1,CTV,ae2 mekanism
ars_additions,Ars Additions,content,logic,copyleft,388000,1,BI,ars_nouveau
ars_controle,Ars Controle,content,logic,copyleft,92000,1,BI,ars_nouveau
ars_creo,Ars Creo,integration,logic,all-rights-reserved,352000,1,BI,ars_nouveau create
ars_elemancy,Ars Elemancy,content,logic,unknown,,1,BIX,ars_nouveau
ars_elemental,Ars Elemental,content,deep,unknown,,1,BDITX,ars_nouveau
ars_ocultas,Ars Ocultas,content,logic,copyleft,422000,1,BI,ars_nouveau
ars_polymorphia,Ars Polymorphia,content,logic,copyleft,75000,1,BI,ars_nouveau
ars_technica,Ars Technica,content,machines,unknown,,1,CDMNRTV,ars_nouveau create
ars_unification,Ars Unification,integration,data,copyleft,61000,1,J,ars_nouveau
arseng,Ars Énergistique,integration,machines,copyleft,413000,1,CTV,ae2 ars_nouveau
atmcompat,ATM: Compat,integration,data,unknown,,1,J,
auroral,Auroral,content,deep,permissive,9000,1,DIX,
auroras,Auroras,client-ui,client,permissive,262000,1,DU,
bellsandwhistles,Create: Bells & Whistles,content,deep,copyleft,5037000,1,DT,create
betteradvancedtooltips,Better Advanced Tooltips,client-ui,client,permissive,104000,1,U,
betteradvancements,Better Advancements,client-ui,client,custom,27359000,1,U,
bhc,Baubley Heart Canisters,content,logic,unknown,,1,AEI,
bibliobiomes,Bibliobiomes Legacy,integration,data,permissive,69000,1,J,
bibliocraft,Bibliocraft Legacy,content,machines,permissive,132000,1,BDMNTV,
bibliowoods,Bibliowoods Legacy,integration,data,permissive,84000,1,J,
blueflame,Blue Flame Burning,content,logic,all-rights-reserved,69000,1,EH,
botanypotsmystical,Botany Pots - Mystical Agriculture Compat,integration,data,copyleft,64000,1,J,botanypots mysticalagriculture
botanytrees,Botany Trees,integration,data,copyleft,802000,1,J,botanypots
bridgingmod,Bridging Mod,content,logic,permissive,6843000,1,EHN,
bwncr,Bad Wither No Cookie - Reloaded,server-only,logic,permissive,14196000,1,E,
byzantine,Byzantine,unknown,none,unknown,,1,B,
cabletiers,Cable Tiers,content,machines,permissive,206000,1,MNTV,refinedstorage
camol,Camol,unknown,none,unknown,,1,B,
catjammies,CatJammies,unknown,none,unknown,,1,DH,
charginggadgets,Charging Gadgets,content,machines,unknown,,1,CMNTV,
chromacarvings,Chroma Carvings,unknown,none,unknown,,1,J,
cleanswing,Clean Swing,unknown,none,unknown,,1,EH,
cloudglass,Cloud Glass,content,data,unknown,,1,J,
cobblegengalore,Cobblegen Galore,unknown,none,unknown,,1,MTV,
colorfulhearts,Colorful Hearts,client-ui,client,permissive,4640000,1,U,
comforts,Comforts,content,logic,copyleft,24012000,1,BEH,
constructionstick,Construction Sticks,content,logic,unknown,,1,DIN,
controlling,Controlling,client-ui,client,permissive,41821000,1,U,
copycats,Create: Copycats+,content,deep,all-rights-reserved,10946000,1,DT,create
cosmeticarmorreworked,CosmeticArmorReworked,content,machines,unknown,,1,DEMN,
crafting_on_a_stick,Crafting on a Stick,content,logic,all-rights-reserved,416000,1,IM,
crashutilities,Crash Utilities,server-only,logic,unknown,,1,E,
create_aquatic_ambitions,Create: Aquatic Ambitions,content,deep,permissive,620000,1,BDTX,create
create_dragons_plus,Create: Dragons Plus,content,deep,copyleft,3448000,1,DTX,create
create_enchantment_industry,Create: Enchantment Industry,content,deep,copyleft,6160000,1,CDRTV,create
create_hypertube,Create: Hypertubes,content,deep,permissive,1219000,1,DNTX,create
crystalix,Crystalix,content,data,unknown,,1,J,
darkmodeeverywhere,Dark Mode Everywhere,client-ui,client,permissive,375000,1,U,
dimstorage,DimStorage,content,machines,copyleft,105000,1,CMNTV,
disenchanting_table,Dis-Enchanting Table,content,machines,all-rights-reserved,1293000,1,MTV,
domum_ornamentum,Domum Ornamentum,content,deep,unknown,,1,BDMNT,
drippyloadingscreen,Drippy Loading Screen,client-ui,client,custom,23220000,1,U,
dummmmmmy,MmmMmmMmmMmm,content,deep,permissive,13223000,1,DX,
dungeoncrawl,Dungeon Crawl,content,deep,copyleft,664000,1,W,
dyenamics,Dyenamics,content,logic,unknown,,1,BHI,
dyenamicsandfriends,Dyenamics and Friends,integration,data,unknown,,1,J,dyenamics
dysoncubeproject,Dyson Cube Project,content,deep,unknown,,1,CDMNTV,
enderdrives,Ender Drives,content,machines,unknown,,1,MNTV,ae2
energymeter,Energy Meter,content,machines,all-rights-reserved,78000,1,CDMNT,
entangled,Entangled,content,machines,all-rights-reserved,309000,1,CDTV,
eternal_starlight,Eternal Starlight,content,deep,permissive,744000,1,BDIWX,
everythingcopper,Everything is Copper,content,data,unknown,,1,J,
evilcraft,EvilCraft,content,deep,unknown,,1,CDIMNRTVX,
expandedae,Expanded AE,content,machines,unknown,,1,MNTV,ae2
extended_industrialization,Extended Industrialization,content,machines,permissive,562000,1,CDMNRTV,modern_industrialization
extradisks,Extra Disks,content,machines,permissive,312000,1,MNTV,refinedstorage
extrastorage,ExtraStorage,content,machines,copyleft,220000,1,MNTV,refinedstorage
extremesoundmuffler,Extreme sound muffler,client-ui,client,copyleft,5131000,1,U,
factory_blocks,Factory Blocks,content,data,unknown,,1,J,
fancymenu,FancyMenu,client-ui,client,custom,69939000,1,U,
farmingforblockheads,Farming for Blockheads,content,machines,all-rights-reserved,551000,1,MNTV,
fireproofboats,Fireproof Boats,content,deep,unknown,,1,IX,
forbidden_arcanus,Forbidden and Arcanus ,content,deep,all-rights-reserved,541000,1,BDIMRTVX,
ftbessentials,FTB Essentials,server-only,logic,unknown,,1,E,
ftbjeiextras,FTB Jei Extras,integration,client,unknown,,1,U,jei
ftbranks,FTB Ranks,server-only,logic,unknown,,1,E,
ftbxmodcompat,FTB XMod Compat,integration,logic,unknown,,1,E,ftbquests
fuelgoeshere,Fuel Goes Here,content,logic,all-rights-reserved,138000,1,H,
gateways,Gateways To Eternity,content,deep,unknown,,1,DINTX,
generatorgalore,Generator Galore,content,machines,unknown,,1,CMNTV,
gmut,Gravitational Modulating Additional Unit,integration,logic,unknown,,1,EI,mekanism
hardenedarmadillos,Hardened Armadillos,unknown,none,unknown,,1,HX,
herbsandharvest,Mama's Herbs and Harvest,content,data,unknown,,1,J,
heyberryshutup,Hey Berry! SHUT UP,content,logic,all-rights-reserved,108000,1,H,
hostilenetworks,Hostile Neural Networks,content,machines,permissive,55000,1,CDMNTV,
illagerwarship,Illager Warship,content,deep,all-rights-reserved,291000,1,DWX,
imfast,I'm Fast,content,logic,permissive,1436000,1,H,
immeng,Immersive Energistics,integration,machines,copyleft,89000,1,CTV,ae2 immersiveengineering
incontrol,In Control!,server-only,logic,permissive,4035000,1,EH,
industrialforegoingsouls,Industrial Foregoing: Souls,content,machines,permissive,178000,1,CDMNTV,industrialforegoing
industrialization_overdrive,Industrialization Overdrive,content,machines,permissive,70000,1,CDMNRTV,modern_industrialization
integratedcrafting,IntegratedCrafting,content,deep,unknown,,1,CDMNRSTV,integrateddynamics
integrateddynamics,IntegratedDynamics,content,deep,unknown,,1,CDMNRSTV,
integratedscripting,IntegratedScripting,content,deep,unknown,,1,CDMNRSTV,integrateddynamics
integratedterminals,IntegratedTerminals,content,deep,unknown,,1,CDMNRSTV,integrateddynamics
integratedtunnels,IntegratedTunnels,content,deep,unknown,,1,CDMNRSTV,integrateddynamics
interdimensionalwirelesstransmitter,Interdimensional Wireless Transmitter,content,machines,permissive,106000,1,CMNTV,
invtweaks,Inventory Tweaks Refoxed,client-ui,client,unknown,,1,NU,
irons_jewelry,Iron's Gems 'n Jewelry,content,machines,all-rights-reserved,19000,1,AIMTV,irons_spellbooks
itemcollectors,Item Collectors,content,machines,all-rights-reserved,571000,1,DMNTV,
jearchaeology,Just Enough Archaeology,integration,client,unknown,,1,U,jei
jei_mekanism_multiblocks,Just Enough Mekanism Multiblocks,integration,client,unknown,,1,U,jei mekanism
jeistuff,Just Enough Stuff,integration,client,unknown,,1,U,jei
jumpboat,Jumpy Boats,content,logic,all-rights-reserved,179000,1,HX,
justenoughbreeding,Just Enough Breeding (JEBr),integration,client,permissive,8335000,1,U,jei
justenoughprofessions,Just Enough Professions (JEP),integration,client,unknown,,1,U,jei
keybindbundles,KeyBind Bundles,client-ui,client,unknown,,1,U,
keybindspurger,KeybindsPurger,client-ui,client,permissive,159000,1,U,
kotlinforforge-5.12.0-all,,unknown,none,unknown,,1,L,
laserbridges,Laser Bridges & Doors,content,machines,permissive,240000,1,BDMNT,
little_big_redstone,Little Big Redstone,content,deep,permissive,77000,1,DMNT,
livingthings,LivingThings,content,deep,restricted,586000,1,DX,
logisticsnetworks,LogisticsNetworks,unknown,none,unknown,,1,MNTV,
mahoutsukai,Mahou Tsukai,content,deep,unknown,,1,DEINX,
mcwlights,Macaw's Lights and Lamps,content,logic,all-rights-reserved,4128000,1,B,
measurements,Measurements,content,logic,unknown,,1,DIN,
megacells,MEGA Cells,content,machines,unknown,,1,MNTV,ae2
mekanismcovers,Mekanism Covers,integration,machines,copyleft,134000,1,DT,mekanism
mekanisticrouters,Mekanistic Routers,integration,machines,unknown,,1,CTV,mekanism modularrouters
mekmm,Mekanism:More Machine,content,machines,permissive,166000,1,CDMNRTV,mekanism
merequester,ME Requester,content,machines,copyleft,1781000,1,MNTV,ae2
merrymaking,Mama's MerryMaking,content,logic,unknown,,1,BI,
mffs,Modular Force Field System,content,deep,unknown,,1,CDEMNT,
mifa,More Industrial Foregoing Addons (MIFA),content,machines,permissive,98000,1,CDMNTV,industrialforegoing
mob_grinding_utils,Mob Grinding Utils,content,machines,unknown,,1,CDMNTVX,
moderndynamics,Modern Dynamics,content,machines,copyleft,184000,1,CDNTV,
modularbees,ModularBees,content,machines,unknown,,1,MNTV,productivebees
modularrouters,Modular Routers,content,machines,permissive,291000,1,DIMNTV,
moredragoneggs,More Dragon Eggs,content,logic,unknown,,1,B,
moreoverlays,More Overlays Updated,client-ui,client,permissive,2194000,1,U,
morered,More Red,content,logic,unknown,,1,BDT,
moreredxcctcompat,More Red x CC:Tweaked Compat,integration,logic,unknown,,1,T,morered computercraft
multipiston,Multi-Piston,content,logic,unknown,,1,BT,
mysticalagradditions,Mystical Agradditions,content,logic,permissive,707000,1,BI,mysticalagriculture
naturesaura,Nature's Aura,content,deep,permissive,154000,1,BDIRTVX,
neovitae,Neo Vitae,content,deep,permissive,24000,1,DITX,
netherportalfix,NetherPortalFix,server-only,logic,unknown,,1,EH,
not_enough_glyphs,Not Enough Glyphs,content,logic,copyleft,109000,1,BIX,ars_nouveau
novillagerdm,No Villager Death Messages,server-only,logic,permissive,114000,1,E,
overlapless,Overlapless,unknown,none,unknown,,1,U,
overloadedarmorbar,OverloadedArmorBar,client-ui,client,unknown,,1,U,
pamhc2trees,Pam's HarvestCraft - Trees,content,data,unknown,,1,J,
pocketstorage,Pocket Storage,content,machines,unknown,,1,IMN,
polyeng,Polymorphic Energistics,integration,machines,copyleft,2035000,1,MNTV,ae2
polymorph,Polymorph,client-ui,client,copyleft,26223000,1,MNU,
potionsmaster,PotionsMaster,unknown,none,unknown,,1,MRTV,
productivemetalworks,Productive Metalworks,content,machines,unknown,,1,CDMRTV,
productivetrees,Productive Trees,content,logic,unknown,,1,BRT,
pylons,Pylons,content,machines,all-rights-reserved,167000,1,EMNTV,
quarryplus,Additional Enchanted Miner,content,machines,copyleft,538000,1,CDMNTV,
railcraft,Railcraft Reborn,content,deep,custom,165000,1,BCDMNRTVX,
rangedpumps,Ranged Pumps,content,machines,unknown,,1,CMT,
rechiseled_chipped,Rechiseled: Chipped,integration,data,all-rights-reserved,338000,1,J,rechiseled
rechiseledcreate,Rechiseled: Create,integration,data,all-rights-reserved,2978000,1,J,rechiseled create
redstonepen,Redstone Pen,content,logic,permissive,365000,1,BDNT,
refinedfluidsubstitution,Refined Fluid Substitution,integration,logic,permissive,0000,1,T,refinedstorage
refinedstorage_curios_integration,Refined Storage - Curios Integration,integration,machines,unknown,,1,AN,refinedstorage
refinedstorage_jei_integration,Refined Storage - JEI Integration,integration,client,unknown,,1,U,refinedstorage jei
refinedstorage_mekanism_integration,Refined Storage - Mekanism Integration,integration,machines,unknown,,1,CTV,refinedstorage mekanism
refinedstorage_quartz_arsenal,Refined Storage - Quartz Arsenal,content,logic,unknown,,1,I,refinedstorage
refinedtypes,Refined Types,content,machines,permissive,51000,1,MTV,refinedstorage
reliquary,Reliquary Reincarnations,content,deep,unknown,,1,BDEITX,
reliquified_artifacts,Reliquified Artifacts,integration,logic,all-rights-reserved,633000,1,AEI,artifacts relics
repeatable_trial_vaults,Repeatable Trial Vaults,content,logic,permissive,158000,1,H,
restrictions,Restrictions,content,logic,unknown,,1,E,
rftoolsbase,RFTools Base,content,machines,permissive,382000,1,CDMNTV,
rftoolsbuilder,RFTools Builder,content,deep,permissive,309000,1,CDMNTVW,rftoolsbase
rftoolspower,RFTools Power,content,machines,permissive,148000,1,CDMNTV,rftoolsbase
rftoolsstorage,RFTools Storage,content,machines,permissive,91000,1,CDMNTV,rftoolsbase
rftoolsutility,RFTools Utility,content,machines,permissive,327000,1,CDMNTV,rftoolsbase
rootsclassic,Roots Classic,content,deep,unknown,,1,DIMRTVX,
rsinsertexportupgrade,Refined Wireless Upgrades,content,machines,permissive,52000,1,MNTV,refinedstorage
sawmill.zip-(datapack),,other,data,unknown,,1,J,
sfm,Super Factory Manager,content,deep,copyleft,381000,1,DMNSTV,
sgearmetalworks,Silent Gear Metalworks,integration,logic,unknown,,1,I,silentgear
shiny,Shiny!,unknown,none,unknown,,1,DH,
shrink,Shrink,content,deep,unknown,,1,DEHN,
silentgems,Silent's Gems,content,logic,permissive,118000,1,BI,
simple_weather,Simple Weather,unknown,none,unknown,,1,DU,
simplebackups,Simple Backups,server-only,logic,permissive,1023000,1,E,
simplemagnets,Simple Magnets,content,logic,all-rights-reserved,558000,1,EIN,
simplylight,Simply Light,content,data,unknown,,1,J,
smithingtemplateviewer,Smithing Template Viewer,client-ui,client,permissive,120000,1,U,
solcarrot,Spice of Life: Carrot Edition,content,logic,unknown,,1,ENU,
sophisticatedbackpackscreateintegration,Sophisticated Backpacks Create Integration,integration,deep,unknown,,1,TX,sophisticatedbackpacks create
sophisticatedstoragecreateintegration,Sophisticated Storage Create Integration,integration,deep,unknown,,1,TX,sophisticatedstorage create
sophisticatedstorageinmotion,Sophisticated Storage In Motion,integration,deep,unknown,,1,TVX,sophisticatedstorage create
soulplied_energistics,Soulplied Energistics,integration,machines,unknown,,1,MNTV,ae2
starbunclemania,Starbunclemania,content,logic,unknown,,1,X,ars_nouveau
stepcrafter,Step Crafter,content,machines,permissive,9000,1,MNTV,
stevescarts,StevesCarts2,content,deep,unknown,,1,DMNTVX,
storagedelight,Storage Delight,integration,machines,permissive,2117000,1,DMTV,farmersdelight
stylecolonies,Stylecolonies,integration,data,unknown,,1,J,minecolonies
sushigocrafting,Sushi Go Crafting,content,machines,permissive,132000,1,BMTV,
sussysniffers,Sussy Sniffers,unknown,none,unknown,,1,HX,
tempad,Tempad,content,deep,custom,471000,1,DEIMN,
theurgy,Theurgy,content,deep,permissive,224000,1,DRTV,
toastcontrol,Toast Control,client-ui,client,unknown,,1,U,
towntalk,Towntalk,unknown,none,unknown,,1,HX,
transfer_labels,Transfer Labels,content,machines,permissive,69000,1,IT,
trashslot,TrashSlot,client-ui,client,unknown,,1,MNU,
treetap,Tree Tap,content,machines,unknown,,1,TV,
trophymanager,Jonn's Trophies,content,machines,unknown,,1,DNT,
underground_villages,Underground Villages Mod,unknown,none,unknown,,1,WX,
universalgrid,Universal Grid,content,machines,permissive,290000,1,IMN,refinedstorage
uranus,Uranus,unknown,none,unknown,,1,B,
utilitarian,Utilitarian,content,logic,unknown,,1,BIT,
utilityvest,Utility vest,unknown,none,unknown,,1,AIM,
variantsandventures,Variants&Ventures,content,deep,restricted,1788000,1,DX,
wirelesschargers,Wireless Chargers,content,machines,all-rights-reserved,242000,1,CDT,
wits,WITS (What Is This Structure?),server-only,logic,permissive,2090000,1,EN,
wstweaks,Wither Skeleton Tweaks,content,logic,unknown,,1,EH,
xnet,XNet,content,machines,permissive,314000,1,CDMNTV,rftoolsbase
xtonesreworked,XTones Reworked,content,data,unknown,,1,J,
xycraft_machines,XyCraft Machines,content,machines,unknown,,1,CDMNTV,
xycraft_override,XyCraft Override,unknown,none,unknown,,1,H,
xycraft_world,XyCraft World,content,logic,unknown,,1,BDW,
1 mod_id name class tier licence modrinth_downloads notice gaps requires
2 ae2 Applied Energistics 2 content deep custom 6316000 3 CDIMNRTV
3 allthemodium Allthemodium content deep unknown 3 BIWX
4 alltheores AllTheOres content data unknown 3 J
5 apotheosis Apotheosis content deep permissive 373000 3 DEHIMRTX
6 ars_nouveau Ars Nouveau content deep all-rights-reserved 4014000 3 BDHIMNRTVX
7 create Create content deep custom 27867000 3 BCDHINRTVX
8 farmersdelight Farmer's Delight content machines permissive 25505000 3 BDIMRTV
9 ftbquests FTB Quests content machines unknown 3 EIMNU
10 irons_spellbooks Iron's Spells 'n Spellbooks content deep unknown 3 ADEIMNRTX
11 jade Jade 🔍 client-ui client restricted 72845000 3 NU
12 jei Just Enough Items (JEI) client-ui client permissive 84344000 3 NU
13 journeymap JourneyMap client-ui client all-rights-reserved 15400000 3 NU
14 mekanism Mekanism content deep permissive 3906000 3 BCDIMNRTVX
15 mysticalagriculture Mystical Agriculture content machines permissive 1303000 3 BDIMNRTV
16 refinedstorage Refined Storage content deep unknown 3 CDMNRTV
17 sophisticatedbackpacks Sophisticated Backpacks content deep unknown 3 ADIMNV
18 sophisticatedstorage Sophisticated Storage content machines unknown 3 DMNTV
19 supplementaries Supplementaries content deep custom 29039000 3 BDHINTX
20 twilightforest The Twilight Forest content deep unknown 3 BDHIMTVWX
21 waystones Waystones content machines all-rights-reserved 27351000 3 BDEMNT
22 actuallyadditions Actually Additions content machines permissive 246000 2 BCDIMNRTV
23 ad_astra Ad Astra content deep custom 5985000 2 CDEIMNTVWX
24 ae2wtlib Applied Energistics 2 Wireless Terminals content machines permissive 3149000 2 AIMN ae2
25 aether The Aether content deep unknown 2 BDHIMWX
26 allthecompressed AllTheCompressed content data unknown 2 J
27 allthetweaks AllTheTweaks content logic unknown 2 BI
28 amendments Amendments content deep custom 20004000 2 BDHTX
29 apothic_enchanting Apothic-Enchanting content machines permissive 128000 2 EHIMTV
30 appleskin AppleSkin client-ui client permissive 97093000 2 NU
31 aquaculture Aquaculture 2 content deep all-rights-reserved 1214000 2 BDIMTVX
32 artifacts Artifacts content deep permissive 14143000 2 ADEIX
33 astralsorcery Astral Sorcery content deep unknown 2 BDIMNRTVX
34 bigreactors Extreme Reactors content machines unknown 2 CDMNRTV
35 biomeswevegone Oh The Biomes We've Gone content deep all-rights-reserved 11969000 2 BDWX
36 botanypots Botany Pots content machines copyleft 2167000 2 DMNRTV
37 buildinggadgets2 Building Gadgets 2 content deep unknown 2 DIMN
38 cataclysm L_Ender's Cataclysm content deep restricted 19343000 2 BDIWX
39 chisel Chisel Reborn content machines custom 1538000 2 IMNTV
40 compactmachines Compact Machines content deep unknown 2 CDMNTVW
41 computercraft CC: Tweaked content deep custom 5344000 2 DMNSTUV
42 cookingforblockheads Cooking for Blockheads content machines unknown 2 BDMNTV
43 craftingtweaks Crafting Tweaks client-ui client all-rights-reserved 10529000 2 NU
44 createaddition Create Crafts & Additions content deep permissive 11314000 2 CDNRTV create
45 creeperoverhaul Creeper Overhaul content deep all-rights-reserved 10880000 2 DX
46 deeperdarker Deeper and Darker content deep copyleft 16319000 2 BDIWX
47 draconicevolution Draconic Evolution content deep custom 926000 2 ACDEIMNTVX
48 easy_villagers Easy Villagers content deep all-rights-reserved 1681000 2 DMNTVX
49 elevatorid ElevatorMod content logic permissive 2338000 2 BMN
50 enchdesc Enchantment Descriptions client-ui client copyleft 39608000 2 U
51 enderio Ender IO content deep permissive 1004000 2 BCDIMNRTV
52 endermanoverhaul Enderman Overhaul content deep all-rights-reserved 8223000 2 DX
53 enderstorage Ender Storage content machines permissive 691000 2 CDMNTV
54 explorerscompass Explorer's Compass content machines restricted 12346000 2 IMN
55 extendedae ExtendedAE content machines unknown 2 CDMNTV ae2
56 fluxnetworks Flux Networks content machines unknown 2 CDMNT
57 framedblocks FramedBlocks content deep copyleft 2265000 2 BDNT
58 ftbchunks FTB Chunks server-only deep unknown 2 ENU
59 ftbteams FTB Teams server-only logic unknown 2 ENU
60 ftbultimine FTB Ultimine content logic unknown 2 DEIN
61 functionalstorage Functional Storage content machines unknown 2 DMNTV
62 handcrafted Handcrafted content machines custom 27534000 2 BDMTVX
63 iceandfire IceAndFire Community Edition content deep copyleft 2208000 2 BDIMTWX
64 immersiveengineering Immersive Engineering content deep custom 3691000 2 BCDIMNRTVX
65 industrialforegoing Industrial Foregoing content machines permissive 978000 2 CDIMNRTVX
66 ironfurnaces Iron Furnaces content machines permissive 10092000 2 MNTV
67 ironjetpacks Iron Jetpacks content logic permissive 641000 2 ADEIN
68 justdirethings Just Dire Things content deep unknown 2 CDIMNTVX
69 laserio LaserIO content machines unknown 2 CDMNTV
70 lootr Lootr content machines permissive 23796000 2 DEMNTV
71 mcwfurnitures Macaw's Furniture content machines all-rights-reserved 7371000 2 BMTVX
72 mcwwindows Macaw's Windows content logic all-rights-reserved 11637000 2 B
73 mekanismgenerators Mekanism Generators content deep permissive 2656000 2 CDMNTV mekanism
74 minecolonies MineColonies content deep unknown 2 BDEIMNTVWX
75 mininggadgets Mining Gadgets content deep unknown 2 DIMNT
76 modern_industrialization Modern Industrialization content deep permissive 2060000 2 CDMNRTV
77 mousetweaks Mouse Tweaks client-ui client permissive 63400000 2 U
78 naturescompass Nature's Compass content machines restricted 27185000 2 IMN
79 notenoughanimations Not Enough Animations client-ui client custom 93276000 2 DU
80 occultism Occultism content deep permissive 893000 2 DIMNRTVX
81 oritech Oritech content deep permissive 1271000 2 CDMNRTV
82 pamhc2crops Pam's HarvestCraft - Crops content data unknown 2 J
83 pamhc2foodcore Pam's HarvestCraft - Food Core content data unknown 2 J
84 pipez Pipez content machines all-rights-reserved 565000 2 CDMNTV
85 pneumaticcraft PneumaticCraft: Repressurized content deep copyleft 811000 2 BCDIMNRTVX
86 powah Powah! content machines copyleft 2573000 2 CDMNTV
87 productivebees Productive Bees content deep unknown 2 BDMNRTVX
88 rechiseled Rechiseled content machines all-rights-reserved 8991000 2 DIMN
89 refurbished_furniture MrCrayfish's Furniture Mod: Refurbished content deep unknown 2 BDMNTVX
90 relics Relics content deep all-rights-reserved 3258000 2 ADEIN
91 sawmill Sawmill content machines all-rights-reserved 6447000 2 MNR
92 securitycraft SecurityCraft content deep permissive 5921000 2 BDEMNTVX
93 silentgear Silent Gear content deep permissive 501000 2 DIMNRT
94 the_bumblezone The Bumblezone - NeoForge/Forge content deep all-rights-reserved 4273000 2 BDHIWX
95 tiab Time in a bottle content logic permissive 332000 2 EHI
96 tombstone Corail Tombstone content deep all-rights-reserved 344000 2 DEIMTVX
97 toolbelt Tool Belt content machines unknown 2 ADIMN
98 torchmaster TorchMaster content logic permissive 3360000 2 BEH
99 trashcans Trash Cans content machines all-rights-reserved 2185000 2 CMNTV
100 undergarden The Undergarden content deep all-rights-reserved 3075000 2 BDIWX
101 accelerateddecay Accelerated Decay content logic copyleft 1187000 1 BH
102 ad_astra_giselle_addon Ad Astra: Giselle Addon integration machines unknown 1 CMNRTV ad_astra mekanism
103 additional_lights Additional Lights unknown none unknown 1 B
104 advanced_ae AdvancedAE content machines copyleft 651000 1 MNTV ae2
105 advancedperipherals Advanced Peripherals integration machines unknown 1 CSTV computercraft
106 ae2ct AE2:Crafting Tree client-ui client unknown 1 U ae2
107 ae2importexportcard AE2 Import Export Card content machines permissive 320000 1 MNTV ae2
108 ae2jeiintegration AE2 JEI Integration integration client unknown 1 U ae2 jei
109 ae2netanalyser AE2NetworkAnalyzer content machines unknown 1 DIN ae2
110 aeinfinitybooster AEInfinityBooster content logic copyleft 220000 1 I ae2
111 akashictome Akashic Tome content logic restricted 454000 1 IN
112 allthearcanistgear All The Arcanist Gear content logic unknown 1 I ars_nouveau
113 allthewizardgear All the Wizard Gear content logic unknown 1 I irons_spellbooks
114 apothic_spawners Apothic-Spawners content logic permissive 105000 1 HT
115 appflux AppliedFlux content machines unknown 1 CMNTV ae2
116 appmek Applied Mekanistics integration machines copyleft 847000 1 CTV ae2 mekanism
117 ars_additions Ars Additions content logic copyleft 388000 1 BI ars_nouveau
118 ars_controle Ars Controle content logic copyleft 92000 1 BI ars_nouveau
119 ars_creo Ars Creo integration logic all-rights-reserved 352000 1 BI ars_nouveau create
120 ars_elemancy Ars Elemancy content logic unknown 1 BIX ars_nouveau
121 ars_elemental Ars Elemental content deep unknown 1 BDITX ars_nouveau
122 ars_ocultas Ars Ocultas content logic copyleft 422000 1 BI ars_nouveau
123 ars_polymorphia Ars Polymorphia content logic copyleft 75000 1 BI ars_nouveau
124 ars_technica Ars Technica content machines unknown 1 CDMNRTV ars_nouveau create
125 ars_unification Ars Unification integration data copyleft 61000 1 J ars_nouveau
126 arseng Ars Énergistique integration machines copyleft 413000 1 CTV ae2 ars_nouveau
127 atmcompat ATM: Compat integration data unknown 1 J
128 auroral Auroral content deep permissive 9000 1 DIX
129 auroras Auroras client-ui client permissive 262000 1 DU
130 bellsandwhistles Create: Bells & Whistles content deep copyleft 5037000 1 DT create
131 betteradvancedtooltips Better Advanced Tooltips client-ui client permissive 104000 1 U
132 betteradvancements Better Advancements client-ui client custom 27359000 1 U
133 bhc Baubley Heart Canisters content logic unknown 1 AEI
134 bibliobiomes Bibliobiomes Legacy integration data permissive 69000 1 J
135 bibliocraft Bibliocraft Legacy content machines permissive 132000 1 BDMNTV
136 bibliowoods Bibliowoods Legacy integration data permissive 84000 1 J
137 blueflame Blue Flame Burning content logic all-rights-reserved 69000 1 EH
138 botanypotsmystical Botany Pots - Mystical Agriculture Compat integration data copyleft 64000 1 J botanypots mysticalagriculture
139 botanytrees Botany Trees integration data copyleft 802000 1 J botanypots
140 bridgingmod Bridging Mod content logic permissive 6843000 1 EHN
141 bwncr Bad Wither No Cookie - Reloaded server-only logic permissive 14196000 1 E
142 byzantine Byzantine unknown none unknown 1 B
143 cabletiers Cable Tiers content machines permissive 206000 1 MNTV refinedstorage
144 camol Camol unknown none unknown 1 B
145 catjammies CatJammies unknown none unknown 1 DH
146 charginggadgets Charging Gadgets content machines unknown 1 CMNTV
147 chromacarvings Chroma Carvings unknown none unknown 1 J
148 cleanswing Clean Swing unknown none unknown 1 EH
149 cloudglass Cloud Glass content data unknown 1 J
150 cobblegengalore Cobblegen Galore unknown none unknown 1 MTV
151 colorfulhearts Colorful Hearts client-ui client permissive 4640000 1 U
152 comforts Comforts content logic copyleft 24012000 1 BEH
153 constructionstick Construction Sticks content logic unknown 1 DIN
154 controlling Controlling client-ui client permissive 41821000 1 U
155 copycats Create: Copycats+ content deep all-rights-reserved 10946000 1 DT create
156 cosmeticarmorreworked CosmeticArmorReworked content machines unknown 1 DEMN
157 crafting_on_a_stick Crafting on a Stick content logic all-rights-reserved 416000 1 IM
158 crashutilities Crash Utilities server-only logic unknown 1 E
159 create_aquatic_ambitions Create: Aquatic Ambitions content deep permissive 620000 1 BDTX create
160 create_dragons_plus Create: Dragons Plus content deep copyleft 3448000 1 DTX create
161 create_enchantment_industry Create: Enchantment Industry content deep copyleft 6160000 1 CDRTV create
162 create_hypertube Create: Hypertubes content deep permissive 1219000 1 DNTX create
163 crystalix Crystalix content data unknown 1 J
164 darkmodeeverywhere Dark Mode Everywhere client-ui client permissive 375000 1 U
165 dimstorage DimStorage content machines copyleft 105000 1 CMNTV
166 disenchanting_table Dis-Enchanting Table content machines all-rights-reserved 1293000 1 MTV
167 domum_ornamentum Domum Ornamentum content deep unknown 1 BDMNT
168 drippyloadingscreen Drippy Loading Screen client-ui client custom 23220000 1 U
169 dummmmmmy MmmMmmMmmMmm content deep permissive 13223000 1 DX
170 dungeoncrawl Dungeon Crawl content deep copyleft 664000 1 W
171 dyenamics Dyenamics content logic unknown 1 BHI
172 dyenamicsandfriends Dyenamics and Friends integration data unknown 1 J dyenamics
173 dysoncubeproject Dyson Cube Project content deep unknown 1 CDMNTV
174 enderdrives Ender Drives content machines unknown 1 MNTV ae2
175 energymeter Energy Meter content machines all-rights-reserved 78000 1 CDMNT
176 entangled Entangled content machines all-rights-reserved 309000 1 CDTV
177 eternal_starlight Eternal Starlight content deep permissive 744000 1 BDIWX
178 everythingcopper Everything is Copper content data unknown 1 J
179 evilcraft EvilCraft content deep unknown 1 CDIMNRTVX
180 expandedae Expanded AE content machines unknown 1 MNTV ae2
181 extended_industrialization Extended Industrialization content machines permissive 562000 1 CDMNRTV modern_industrialization
182 extradisks Extra Disks content machines permissive 312000 1 MNTV refinedstorage
183 extrastorage ExtraStorage content machines copyleft 220000 1 MNTV refinedstorage
184 extremesoundmuffler Extreme sound muffler client-ui client copyleft 5131000 1 U
185 factory_blocks Factory Blocks content data unknown 1 J
186 fancymenu FancyMenu client-ui client custom 69939000 1 U
187 farmingforblockheads Farming for Blockheads content machines all-rights-reserved 551000 1 MNTV
188 fireproofboats Fireproof Boats content deep unknown 1 IX
189 forbidden_arcanus Forbidden and Arcanus content deep all-rights-reserved 541000 1 BDIMRTVX
190 ftbessentials FTB Essentials server-only logic unknown 1 E
191 ftbjeiextras FTB Jei Extras integration client unknown 1 U jei
192 ftbranks FTB Ranks server-only logic unknown 1 E
193 ftbxmodcompat FTB XMod Compat integration logic unknown 1 E ftbquests
194 fuelgoeshere Fuel Goes Here content logic all-rights-reserved 138000 1 H
195 gateways Gateways To Eternity content deep unknown 1 DINTX
196 generatorgalore Generator Galore content machines unknown 1 CMNTV
197 gmut Gravitational Modulating Additional Unit integration logic unknown 1 EI mekanism
198 hardenedarmadillos Hardened Armadillos unknown none unknown 1 HX
199 herbsandharvest Mama's Herbs and Harvest content data unknown 1 J
200 heyberryshutup Hey Berry! SHUT UP content logic all-rights-reserved 108000 1 H
201 hostilenetworks Hostile Neural Networks content machines permissive 55000 1 CDMNTV
202 illagerwarship Illager Warship content deep all-rights-reserved 291000 1 DWX
203 imfast I'm Fast content logic permissive 1436000 1 H
204 immeng Immersive Energistics integration machines copyleft 89000 1 CTV ae2 immersiveengineering
205 incontrol In Control! server-only logic permissive 4035000 1 EH
206 industrialforegoingsouls Industrial Foregoing: Souls content machines permissive 178000 1 CDMNTV industrialforegoing
207 industrialization_overdrive Industrialization Overdrive content machines permissive 70000 1 CDMNRTV modern_industrialization
208 integratedcrafting IntegratedCrafting content deep unknown 1 CDMNRSTV integrateddynamics
209 integrateddynamics IntegratedDynamics content deep unknown 1 CDMNRSTV
210 integratedscripting IntegratedScripting content deep unknown 1 CDMNRSTV integrateddynamics
211 integratedterminals IntegratedTerminals content deep unknown 1 CDMNRSTV integrateddynamics
212 integratedtunnels IntegratedTunnels content deep unknown 1 CDMNRSTV integrateddynamics
213 interdimensionalwirelesstransmitter Interdimensional Wireless Transmitter content machines permissive 106000 1 CMNTV
214 invtweaks Inventory Tweaks Refoxed client-ui client unknown 1 NU
215 irons_jewelry Iron's Gems 'n Jewelry content machines all-rights-reserved 19000 1 AIMTV irons_spellbooks
216 itemcollectors Item Collectors content machines all-rights-reserved 571000 1 DMNTV
217 jearchaeology Just Enough Archaeology integration client unknown 1 U jei
218 jei_mekanism_multiblocks Just Enough Mekanism Multiblocks integration client unknown 1 U jei mekanism
219 jeistuff Just Enough Stuff integration client unknown 1 U jei
220 jumpboat Jumpy Boats content logic all-rights-reserved 179000 1 HX
221 justenoughbreeding Just Enough Breeding (JEBr) integration client permissive 8335000 1 U jei
222 justenoughprofessions Just Enough Professions (JEP) integration client unknown 1 U jei
223 keybindbundles KeyBind Bundles client-ui client unknown 1 U
224 keybindspurger KeybindsPurger client-ui client permissive 159000 1 U
225 kotlinforforge-5.12.0-all unknown none unknown 1 L
226 laserbridges Laser Bridges & Doors content machines permissive 240000 1 BDMNT
227 little_big_redstone Little Big Redstone content deep permissive 77000 1 DMNT
228 livingthings LivingThings content deep restricted 586000 1 DX
229 logisticsnetworks LogisticsNetworks unknown none unknown 1 MNTV
230 mahoutsukai Mahou Tsukai content deep unknown 1 DEINX
231 mcwlights Macaw's Lights and Lamps content logic all-rights-reserved 4128000 1 B
232 measurements Measurements content logic unknown 1 DIN
233 megacells MEGA Cells content machines unknown 1 MNTV ae2
234 mekanismcovers Mekanism Covers integration machines copyleft 134000 1 DT mekanism
235 mekanisticrouters Mekanistic Routers integration machines unknown 1 CTV mekanism modularrouters
236 mekmm Mekanism:More Machine content machines permissive 166000 1 CDMNRTV mekanism
237 merequester ME Requester content machines copyleft 1781000 1 MNTV ae2
238 merrymaking Mama's MerryMaking content logic unknown 1 BI
239 mffs Modular Force Field System content deep unknown 1 CDEMNT
240 mifa More Industrial Foregoing Addons (MIFA) content machines permissive 98000 1 CDMNTV industrialforegoing
241 mob_grinding_utils Mob Grinding Utils content machines unknown 1 CDMNTVX
242 moderndynamics Modern Dynamics content machines copyleft 184000 1 CDNTV
243 modularbees ModularBees content machines unknown 1 MNTV productivebees
244 modularrouters Modular Routers content machines permissive 291000 1 DIMNTV
245 moredragoneggs More Dragon Eggs content logic unknown 1 B
246 moreoverlays More Overlays Updated client-ui client permissive 2194000 1 U
247 morered More Red content logic unknown 1 BDT
248 moreredxcctcompat More Red x CC:Tweaked Compat integration logic unknown 1 T morered computercraft
249 multipiston Multi-Piston content logic unknown 1 BT
250 mysticalagradditions Mystical Agradditions content logic permissive 707000 1 BI mysticalagriculture
251 naturesaura Nature's Aura content deep permissive 154000 1 BDIRTVX
252 neovitae Neo Vitae content deep permissive 24000 1 DITX
253 netherportalfix NetherPortalFix server-only logic unknown 1 EH
254 not_enough_glyphs Not Enough Glyphs content logic copyleft 109000 1 BIX ars_nouveau
255 novillagerdm No Villager Death Messages server-only logic permissive 114000 1 E
256 overlapless Overlapless unknown none unknown 1 U
257 overloadedarmorbar OverloadedArmorBar client-ui client unknown 1 U
258 pamhc2trees Pam's HarvestCraft - Trees content data unknown 1 J
259 pocketstorage Pocket Storage content machines unknown 1 IMN
260 polyeng Polymorphic Energistics integration machines copyleft 2035000 1 MNTV ae2
261 polymorph Polymorph client-ui client copyleft 26223000 1 MNU
262 potionsmaster PotionsMaster unknown none unknown 1 MRTV
263 productivemetalworks Productive Metalworks content machines unknown 1 CDMRTV
264 productivetrees Productive Trees content logic unknown 1 BRT
265 pylons Pylons content machines all-rights-reserved 167000 1 EMNTV
266 quarryplus Additional Enchanted Miner content machines copyleft 538000 1 CDMNTV
267 railcraft Railcraft Reborn content deep custom 165000 1 BCDMNRTVX
268 rangedpumps Ranged Pumps content machines unknown 1 CMT
269 rechiseled_chipped Rechiseled: Chipped integration data all-rights-reserved 338000 1 J rechiseled
270 rechiseledcreate Rechiseled: Create integration data all-rights-reserved 2978000 1 J rechiseled create
271 redstonepen Redstone Pen content logic permissive 365000 1 BDNT
272 refinedfluidsubstitution Refined Fluid Substitution integration logic permissive 0000 1 T refinedstorage
273 refinedstorage_curios_integration Refined Storage - Curios Integration integration machines unknown 1 AN refinedstorage
274 refinedstorage_jei_integration Refined Storage - JEI Integration integration client unknown 1 U refinedstorage jei
275 refinedstorage_mekanism_integration Refined Storage - Mekanism Integration integration machines unknown 1 CTV refinedstorage mekanism
276 refinedstorage_quartz_arsenal Refined Storage - Quartz Arsenal content logic unknown 1 I refinedstorage
277 refinedtypes Refined Types content machines permissive 51000 1 MTV refinedstorage
278 reliquary Reliquary Reincarnations content deep unknown 1 BDEITX
279 reliquified_artifacts Reliquified Artifacts integration logic all-rights-reserved 633000 1 AEI artifacts relics
280 repeatable_trial_vaults Repeatable Trial Vaults content logic permissive 158000 1 H
281 restrictions Restrictions content logic unknown 1 E
282 rftoolsbase RFTools Base content machines permissive 382000 1 CDMNTV
283 rftoolsbuilder RFTools Builder content deep permissive 309000 1 CDMNTVW rftoolsbase
284 rftoolspower RFTools Power content machines permissive 148000 1 CDMNTV rftoolsbase
285 rftoolsstorage RFTools Storage content machines permissive 91000 1 CDMNTV rftoolsbase
286 rftoolsutility RFTools Utility content machines permissive 327000 1 CDMNTV rftoolsbase
287 rootsclassic Roots Classic content deep unknown 1 DIMRTVX
288 rsinsertexportupgrade Refined Wireless Upgrades content machines permissive 52000 1 MNTV refinedstorage
289 sawmill.zip-(datapack) other data unknown 1 J
290 sfm Super Factory Manager content deep copyleft 381000 1 DMNSTV
291 sgearmetalworks Silent Gear Metalworks integration logic unknown 1 I silentgear
292 shiny Shiny! unknown none unknown 1 DH
293 shrink Shrink content deep unknown 1 DEHN
294 silentgems Silent's Gems content logic permissive 118000 1 BI
295 simple_weather Simple Weather unknown none unknown 1 DU
296 simplebackups Simple Backups server-only logic permissive 1023000 1 E
297 simplemagnets Simple Magnets content logic all-rights-reserved 558000 1 EIN
298 simplylight Simply Light content data unknown 1 J
299 smithingtemplateviewer Smithing Template Viewer client-ui client permissive 120000 1 U
300 solcarrot Spice of Life: Carrot Edition content logic unknown 1 ENU
301 sophisticatedbackpackscreateintegration Sophisticated Backpacks Create Integration integration deep unknown 1 TX sophisticatedbackpacks create
302 sophisticatedstoragecreateintegration Sophisticated Storage Create Integration integration deep unknown 1 TX sophisticatedstorage create
303 sophisticatedstorageinmotion Sophisticated Storage In Motion integration deep unknown 1 TVX sophisticatedstorage create
304 soulplied_energistics Soulplied Energistics integration machines unknown 1 MNTV ae2
305 starbunclemania Starbunclemania content logic unknown 1 X ars_nouveau
306 stepcrafter Step Crafter content machines permissive 9000 1 MNTV
307 stevescarts StevesCarts2 content deep unknown 1 DMNTVX
308 storagedelight Storage Delight integration machines permissive 2117000 1 DMTV farmersdelight
309 stylecolonies Stylecolonies integration data unknown 1 J minecolonies
310 sushigocrafting Sushi Go Crafting content machines permissive 132000 1 BMTV
311 sussysniffers Sussy Sniffers unknown none unknown 1 HX
312 tempad Tempad content deep custom 471000 1 DEIMN
313 theurgy Theurgy content deep permissive 224000 1 DRTV
314 toastcontrol Toast Control client-ui client unknown 1 U
315 towntalk Towntalk unknown none unknown 1 HX
316 transfer_labels Transfer Labels content machines permissive 69000 1 IT
317 trashslot TrashSlot client-ui client unknown 1 MNU
318 treetap Tree Tap content machines unknown 1 TV
319 trophymanager Jonn's Trophies content machines unknown 1 DNT
320 underground_villages Underground Villages Mod unknown none unknown 1 WX
321 universalgrid Universal Grid content machines permissive 290000 1 IMN refinedstorage
322 uranus Uranus unknown none unknown 1 B
323 utilitarian Utilitarian content logic unknown 1 BIT
324 utilityvest Utility vest unknown none unknown 1 AIM
325 variantsandventures Variants&Ventures content deep restricted 1788000 1 DX
326 wirelesschargers Wireless Chargers content machines all-rights-reserved 242000 1 CDT
327 wits WITS (What Is This Structure?) server-only logic permissive 2090000 1 EN
328 wstweaks Wither Skeleton Tweaks content logic unknown 1 EH
329 xnet XNet content machines permissive 314000 1 CDMNTV rftoolsbase
330 xtonesreworked XTones Reworked content data unknown 1 J
331 xycraft_machines XyCraft Machines content machines unknown 1 CDMNTV
332 xycraft_override XyCraft Override unknown none unknown 1 H
333 xycraft_world XyCraft World content logic unknown 1 BDW
+28 -2
View File
@@ -11,7 +11,7 @@ folder, loads no runtime and plays exactly like vanilla 26.3.
| Part | Where | State |
|---|---|---|
| The contract (WIT) | `crates/horizon-mod-sdk/wit/horizon.wit` | 0.1: log, game-read, chat, hud, storage; tick, frame, chat and command hooks |
| The contract (WIT) | `crates/horizon-mod-sdk/wit/horizon.wit` | 0.1: log, game-read, chat, hud, storage; tick, frame, chat and command hooks. 0.4 preview: block entities (runtime only, not in the game yet) |
| SDK for mod authors | `crates/horizon-mod-sdk` | 0.1 |
| Example mod | `crates/horizon-mod-sdk/examples/coords-hud` | coordinates, facing and time HUD, `/where`, `/coords` |
| Runtime | `crates/horizon-mods` | wasmtime 36 LTS, component model |
@@ -356,6 +356,30 @@ none.
"top": "carve", "bottom": "none", "rigid_junctions": true, "inflate": true } }
```
### Block entities (API 0.4, preview)
The first piece of the machines tier ([research/code-mods-plan.md](research/code-mods-plan.md)):
a mod with `api = "0.4"` and capability `block-entities` gives its own blocks
block entities whose code ticks in the sandbox. The runtime and the SDK have
it; the game does not create, tick or save them yet (next step in the plan).
| Part | What |
|---|---|
| export `block-entities` | `types()`: the mod's blocks with a block entity, their slot count and whether they tick; `create(handle, block, saved)`, `tick(handles)`, `save(handle)`, `remove(handle)`, `data(handle)` (the menu's values, vanilla's `ContainerData`) |
| import `machine` | during `tick` only: `get-slot`, `set-slot` and `get-property`, `set-property` (the block state) of the block entities lent to the call; any time: `max-stack-size`, `burn-time`, `remainder`, `cooking(kind, input)` from the game's item, fuel and recipe tables |
- **The game owns the slots and the block state**, so hoppers, menus, drops and
saves see them as they see vanilla's. The mod owns the rest and saves it as
bytes with `save`.
- **One call per mod per tick** for all its ticking block entities, under the
tick's fuel budget. A block entity sees only the handles lent to that call; a
slot write of an unknown item is refused, a stack over the item's maximum is
cut to it, a property value the block does not allow is refused.
- **Types are checked at start:** each for a block in the mod's own namespaces,
at most once, at most 256 slots. A component that exports block entities
without the capability does not start.
- Components built before 0.4 (no `block-entities` export) load as before.
## Versions and compatibility
- The API is the WIT package `horizon:[email protected]`. New functions and
@@ -426,4 +450,6 @@ none.
Modpacks ([modpacks.md](modpacks.md)) move data-driven `registries` first:
API 0.2 is content-only `.hmod`s (blocks, items, recipes, loot, models and
textures), then server hooks (0.3) and block entities, menus, capabilities and
`net` (0.4), in the order the ATM10 inventory needs them.
`net` (0.4), in the order the ATM10 inventory needs them. The ranking of ATM10's
code mods by the gaps they hit, and the first block-entity piece, are in
[research/code-mods-plan.md](research/code-mods-plan.md).
+282
View File
@@ -0,0 +1,282 @@
# Code mods in modpacks: the gaps, the ranking, a first port
ATM10's world now generates well in Horizon (structures, ores, decoration blocks), but **333 of its
496 mods are still "not yet"**. Almost all of them have code: machines, magic, mobs, tools. This page
is the research spike that gives Ludovic the facts to choose the path. It answers four questions:
what the mod API carries today, which ATM10 mods each missing piece unlocks, what a first real code
port looks like, and what porting costs.
The decision from [modpacks.md](../modpacks.md) stands: **porting, not emulation**. Java mods become
native Horizon mods (Rust compiled to WebAssembly, against the Horizon mod API), and each port is
checked against the Java mod running in a dev-only NeoForge oracle. Mod jars and decompiled code stay
on the machine that ports them and never enter this repository.
## 1. What the mod API carries today
| Line | What a mod can do | Where |
|---|---|---|
| API 0.1 (code) | client side only: read the player and the clock, show chat lines and a HUD text panel, keep small files, own chat commands, hooks per tick, frame and chat line | `horizon.wit`, `crates/horizon-mods` |
| API 0.2 (content) | blocks and items declared as data, each copying a vanilla *template* block or item with its numbers changed; recipes, loot, tags, models, textures, lang and worldgen JSON in the mod's namespace | `content.rs`, the content root |
| **API 0.4 preview (this spike)** | block entities for the mod's own blocks: game-owned slots and block state, a batched tick per mod, a saved state, the menu's data values, and the game's item, fuel and cooking-recipe facts | `machine` and `block-entities` in `horizon.wit`, [mods.md](../mods.md#block-entities-api-04-preview) |
Code still cannot touch the world. API 0.1 runs on the client and only reads the player; the runtime
refuses `side = "server"` mods. API 0.2 is pure data: a template gives a mod block every behaviour
Horizon already has for the vanilla block it copies, but nothing of its own.
The sandbox is good and stays: a memory cap, fuel per call (a runaway loop stops the mod, not the
game), no files, sockets or processes, and capabilities the player sees before turning a mod on.
### The gaps for code mods
Each gap is a piece of API plus the game wiring behind it. Codes are used in the ranking below.
| Code | Gap | What it means | State |
|---|---|---|---|
| **T** | ticking block entities with saved state | per-block state, a tick, save and load | **runtime and SDK done here**; the game does not create or save them yet |
| **V** | inventories | slots that hoppers, pipes, menus and drops see; slot rules per side (`WorldlyContainer`) | slots done here (game-owned); slot rules missing |
| **M** | menus and screens | a slot layout, progress bars, buttons, the server-side click logic | data values (`data()`) done here; layout and screen missing |
| **N** | client↔server networking | a mod's own packets between its two halves | missing (`net`, planned in mods.md) |
| **C** | energy, fluid and item transfer between blocks | NeoForge capabilities: FE, fluid tanks, item handlers on neighbours | missing |
| **R** | custom recipe types | machine recipes in the mod's own JSON, and their recipe-viewer layout | missing (vanilla cooking recipes are readable now) |
| **B** | block behaviour hooks | use, place, break, neighbour change, random and scheduled ticks on mod blocks | missing (API 0.3) |
| **I** | item behaviour hooks | use, use on block, inventory tick, attributes, tools and armour of the mod | missing (API 0.3) |
| **E** | server events, player data, config | join, death, spawn, chat, commands with types; data attached to players and chunks; a config schema | missing (API 0.3) |
| **H** | new hooks into vanilla behaviour | what a Mixin does: a named hook added to the engine for everyone | per hook |
| **X** | entities and AI | entity types, attributes, goals, brains, spawning | missing (API 0.5) |
| **D** | custom rendering | block-entity renderers, animated models (GeckoLib), custom baked models, item renderers | missing (API 0.5) |
| **W** | dimensions and worldgen code | dimension types, chunk generators, portals, structure code | worldgen JSON carries; code missing |
| **U** | client UI interfaces | `keys`, `target`, `inventory-read`, `ui`, `recipes` | missing (planned in mods.md) |
| **A** | accessory slots | what Curios does | missing |
| **S** | an embedded scripting VM | CC: Tweaked's Lua computers, Integrated Dynamics, SFM programs | missing |
## 2. ATM10's code mods, ranked by the gaps they hit
**Method.** Each of the 333 not-yet mods in ATM10's coverage report
(`horizon-port-pack report`, 2026-10-19) got the set of gaps above it needs and a "players notice it"
score: 3 for the pack-defining mods (Create, Mekanism, AE2, Refined Storage, Sophisticated Storage and
Backpacks, Waystones, JEI, Jade, JourneyMap, FTB Quests, Ars Nouveau, Iron's Spells, Twilight Forest,
Apotheosis, Farmer's Delight, Mystical Agriculture, Supplementaries, Allthemodium), 2 for mods most
players meet, 1 for the rest. The gap sets are a judgement from what each mod does (its Modrinth
categories, tier and, for the big ones, knowledge of the mod), not from reading 333 jars, so treat any
single row as approximate and the totals as the signal. The table is
[`docs/modpacks/atm10-code-gaps.csv`](../modpacks/atm10-code-gaps.csv) (metadata only: ids, names,
licences, downloads, gaps, required mods).
Of the 333: **24 are data only** (AllTheOres, AllTheCompressed, Pam's crops, XTones...: their jars
were not downloaded yet, no code needed), **1 is a library** (Kotlin for Forge, mislabelled), and
**307 need code**: 221 content mods, 30 integrations, 27 client-UI mods, 18 unclassified and 11
server-only.
### How many mods need each gap
| Gap | Mods that need it | Weighted by "players notice" |
|---|---:|---:|
| T ticking block entities | **162** | 233 |
| N networking | 148 | 231 |
| D custom rendering | 145 | 229 |
| M menus and screens | 140 | 217 |
| V inventories | 124 | 183 |
| I item hooks | 93 | 154 |
| B block hooks | 71 | 115 |
| C energy/fluid/item transfer | 69 | 98 |
| X entities and AI | 69 | 113 |
| E server events, data, config | 47 | 70 |
| R custom recipe types | 44 | 77 |
| U client UI | 42 | 58 |
| H vanilla hooks | 33 | 49 |
| W dimensions | 18 | 32 |
| A accessory slots | 13 | 23 |
| S scripting VM | 8 | 9 |
**Block entities are the most common gap**: 162 of the 307 code mods need them. Of those 162, 123 also
need inventories, 121 menus, 110 custom rendering, 69 transfer between blocks and 43 their own recipe
types. The most common exact sets are "client UI only" (26 mods), **"block entity + inventory + menu +
networking" (17)** and the same plus rendering and transfer (17).
### Unlocked by gap X
A mod is unlocked when every gap it needs is filled *and* the mods it requires are unlocked (AE2's 15
add-ons wait for AE2). Filling the gaps cheapest first, then the machine stack in the order that
unlocks the most at each step:
| Step | Adds | Mods unlocked (total) | Players notice (score 3) |
|---|---|---:|---|
| 1 | U client UI | 17 | |
| 2 | N networking | 28 | JEI, Jade, JourneyMap |
| 3 | B, I, E, H (API 0.3: logic) | 67 | |
| 4 | T, V, M (machines core) | 91 | FTB Quests |
| 5 | D custom rendering | 120 | Sophisticated Storage, Waystones |
| 6 | C transfer | 145 | |
| 7 | R recipe types | 189 | AE2, Refined Storage, Farmer's Delight, Mystical Agriculture |
| 8 | X entities | 264 | Create, Mekanism, Ars Nouveau, Apotheosis, Supplementaries |
| 9 | W dimensions | 283 | Allthemodium, Twilight Forest |
| 10 | A accessory slots | 298 | Iron's Spells, Sophisticated Backpacks |
| 11 | S scripting | 307 | |
What this says:
- **The cheap gaps unlock small mods.** Client UI, networking and the 0.3 logic hooks together unlock
67 mods, but only JEI, Jade and JourneyMap among the ones players notice, and those three are better
built into Horizon (a recipe viewer, a look-at tooltip, a map) than ported.
- **Block entities are the backbone of the big tier.** Machines core (T, V, M) plus rendering (D),
transfer (C) and recipe types (R) take the count from 67 to 189 and bring AE2, Refined Storage,
Sophisticated Storage, Waystones, Farmer's Delight and Mystical Agriculture.
- **Rendering is the hidden gate.** 110 block-entity mods also draw something of their own (a
rotating shaft, items on a pedestal, a fluid level). A data-driven first step (animated models in a
declared format, items and fluids drawn at declared spots) covers most of them without arbitrary
draw code.
- **Entities are the last big step to the flagships.** Create, Mekanism, Ars Nouveau and Apotheosis
all need entities (contraptions, the Robit, familiars, bosses) on top of everything else.
- Licences of the 307: 84 permissive, 33 copyleft, 39 All Rights Reserved, 16 custom, 7 restricted
(non-commercial or no-derivatives) and 128 unknown (CurseForge-only, read from the jar at import).
Ports stay local whatever the licence; only permissive or copyleft ones could ever be published.
## 3. Proof of concept: Iron Furnaces' iron furnace
**The pick.** Iron Furnaces (10.1 million Modrinth downloads, **Apache-2.0**) is the most downloaded
ATM10 mod whose gaps are exactly the most common machine set: block entity, inventory, menu,
networking. Its iron furnace is a ticking block entity with an inventory, a block state that follows
it, fuel and cooking-recipe lookups, and menu data. The slice ported: **the iron furnace in furnace
mode** (no augments, factory or generator modes, redstone settings, auto input/output or experience).
**The API pieces added** (generic, nothing Iron Furnaces-specific; behind tests):
- `horizon.wit`: interface `machine` (capability `block-entities`): slots and block-state properties
of the block entities lent to the current tick; the game's max stack sizes, fuel burn times,
remainders (a bucket from a lava bucket) and cooking recipes. Export `block-entities`: `types`,
`create`, `tick(handles)`, `save`, `remove`, `data`. A second world, so mods built before keep
loading.
- `horizon-mods`: the runtime side. Types checked at start (own namespaces, once each, at most 256
slots); one fuel-budgeted call per mod per tick for all its block entities; slot writes of unknown
items refused, stacks cut to the item's maximum, property values the block does not allow refused;
a mod sees only the handles lent to it. Manifest `api = "0.4"`, capability `block-entities`.
- `horizon-mod-sdk`: the `Mod` trait gains `block_entity_types`, `create_block_entity`,
`tick_block_entities`, `save_block_entity`, `remove_block_entity`, `block_entity_data`, all
defaulted, so existing mods compile unchanged.
- Tests: a test mod with a small smelter block entity (`crates/horizon-mods/tests/machine`) and three
sandbox tests (tick, save and load; only its own handles and known items; refused without the
capability or for a block it does not own).
**The port** is about 200 lines of Rust (local, in the dev-only modports folder; see below). It keeps
the furnace's private state (burn time left, cooking progress and total) by handle, and reads and
writes the game's slots and its `lit` property.
**The harness** runs the same scenarios on both sides and compares every tick:
- Java: `tools/neoforge_oracle.py machines` starts the dev-only NeoForge 21.1.259 server with the
mod's jar and our exporter mod, places the block, fills its container, runs the block entity's own
ticker N times and writes the slots, the block-state properties and the saved tag's fields after
every tick (`MachineTrace.java`).
- Horizon: `crates/horizon-client/examples/mod_machine_probe.rs` loads the `.hmod` through
`horizon-mods` with **Horizon's own** item, fuel and recipe tables (26.3), and writes the same.
- `tools/mod_machine_harness.py` compares the two.
| Scenario | Ticks | Equal |
|---|---:|---:|
| 10 raw iron, 2 coal (one coal smelts 8 items, 160 ticks each) | 1,800 | 1,800 |
| cobblestone on a lava bucket (the bucket stays) | 500 | 500 |
| a stick's fuel runs out mid-item (progress falls back 2 a tick) | 200 | 200 |
| output slot fills up at 64 | 600 | 600 |
| input swapped to kelp, taken out, raw gold put in, fuel taken | 400 | 400 |
| no recipe: the coal is not burnt | 60 | 60 |
| a wet sponge dried over an empty bucket fills it | 200 | 200 |
| **all** | **3,760** | **3,760** |
The harness fails as it should: the same port with the tier speed set to vanilla's 200 instead of
160 differs from the first tick in six of the seven scenarios.
**What the slice does not show yet.** The game does not create, tick or save mod block entities, so
the furnace does not run in a world yet; its block and model come over through the content carry-over
as any other block. That wiring (place and break, chunk save and load, hoppers through the slots,
drops) is the next step and is generic. The menu needs the screen layout (M), and the other furnace
modes need the gaps they name (energy for the generator: C).
**Licence.** Iron Furnaces is Apache-2.0, which allows publishing a derivative with its notice. The
port still stays local (`~/code/horizon-parity/modports/atm10/codemods/ironfurnaces-port`, with a
NOTICE) until Ludovic decides to publish ports and, if he wants, asks the authors (Qelifern and
XenoMustache).
## 4. What porting costs
**This slice, measured** (one agent on the Mac mini, wall clock from its logs; the whole spike,
ranking included, took under two hours, a third of it waiting for release builds):
| Step | Time | Once or per mod |
|---|---|---|
| Gap ranking of 333 mods (a judgement per mod, see section 2) | about 15 min | once |
| API piece (WIT, runtime, SDK, tests) | about 15 min to write, 2 min of tests | once per gap |
| Oracle machine trace, probe and harness | about 20 min, plus a 10 min release build | once |
| Download, decompile, read the mod's furnace path | about 5 min | per mod |
| Write the port (200 lines) | about 5 min | per mod |
| Scenarios (7) | about 5 min | per mod (partly automatable) |
| Harness: oracle 31 s (NeoForge start included), probe 2 s | 1 min | per run |
So for a machine of this size, **once the API exists, a port slice costs well under an hour of agent
time**, and the harness passed on the first run. Small API pieces are cheap to write; what costs is
wiring each into the game and proving it with harnesses, which the estimates below count. A whole small mod (Iron Furnaces with all its tiers,
augments, modes, menus and recipe viewer pages) is an estimated 1–2 agent-days. A big system is a
campaign: Mekanism, AE2 or Create are hundreds of thousands of lines of Java with their own
networks, renderers and entities, so weeks each even with the API complete.
**The API gaps cost more than the ports.** Estimates in agent-days (build, game wiring, harness,
review), from this spike and from comparable vanilla work in the repo:
| Gap | Agent-days | Notes |
|---|---:|---|
| T/V game wiring: create, tick, save, drop, hoppers, slot rules | 3–4 | builds on this PR |
| M menus and screens (declared layouts the game draws, server click logic) | 4–6 | the container screens exist for vanilla |
| N networking (single player is free; LAN and servers need packets) | 2–3 | |
| B, I, E, H logic hooks (0.3) | 4–6, then about 0.5 per new vanilla hook | |
| C capabilities (FE, fluids, item handlers on neighbours) | 3–5 | |
| R custom recipe types and their viewer layout | 3–4 | |
| U client UI interfaces | 5–8 | or build JEI/Jade/map natively |
| D rendering: data-driven animated models first, then a budgeted draw list | 10–15 | the biggest |
| X entities and AI | 10–15 | Horizon's mob AI exists for vanilla |
| W dimensions and worldgen code | 8–12 | |
| A accessory slots | 2–3 | |
**What agents can automate:** inventory and classification (done), fetching the player's jars,
decompiling, carrying data and assets, writing the port from the decompiled reference and the
harness, generating scenarios from the mod's own recipes and blocks, running the oracle and the
harness, and fixing until green. The harness loop is what makes this safe to hand to agents: they
cannot claim a port works without a tick-by-tick match.
**What agents cannot do alone:** decide to publish ports or contact authors (licences; Ludovic's
call), design each new engine hook a Mixin needed (they can draft it, a reviewer must keep the engine
clean and vanilla-exact), judge visual parity of custom renderers (image diffs help, a human looks),
and follow mod updates (each update reruns the harness, and the port is fixed again).
## 5. Options for Ludovic
| Option | Gaps | Unlocks (of 307) | Cost (agent-days, gaps only) |
|---|---|---:|---:|
| **A. Machines first** | the 0.3 logic hooks (B, I, E, H), then T/V wiring, M, N, D, C, R | 148, incl. AE2, Refined Storage, Sophisticated Storage, Waystones, Farmer's Delight, Mystical Agriculture | 30–45 |
| B. Cheap gaps first | U, N, B, I, E, H | 67, mostly small QoL mods (and JEI, Jade, JourneyMap) | 12–20 |
| A and B | all of the above | 189 | 35–50 |
| C. One flagship vertical | everything Create needs (B, C, D, H, I, N, R, T, V, X), then Create | Create + 12 add-ons, and on the way most of A | 35–55, then weeks for the port |
| D. Stop at data | none: ATM10-like packs stay worldgen, decoration and ores | 0 more | 0 |
The machine stack needs the logic hooks: on its own (T, V, M, N, C, R, D without B, I, E, H) it
unlocks only 53 mods, because most machines also have items and blocks that do something when used.
The ports themselves come on top: under an hour per machine slice like the one above, days per small
mod, weeks per big system.
**Recommendation: A, with JEI, Jade and a minimap built into Horizon itself.** Block entities are the
backbone of the tech and magic mods players build their game around, the first piece is in, and its
first port matched the Java mod on the first run. Building the three most-noticed client mods natively
(a recipe viewer, a look-at tooltip, a minimap) gives ATM10 players what they use every minute without
porting them. Rendering (D) comes right after machines because it gates 110 of the 162 block-entity
mods; entities (X) and dimensions (W) come last, and with them Create, Mekanism, Ars Nouveau and
Twilight Forest.
Next steps if Ludovic picks A: wire mod block entities into the game (create, tick, save, hoppers) and
run the iron furnace in a world; then declared menus; then port a second mod of the same set
(Cobblegen Galore, Step Crafter or Treetap) to measure the per-mod cost again.
## Decisions for Ludovic
1. **The path** (A, B, C or D above).
2. **Publishing ports**: whether permissively licensed ports (Iron Furnaces is Apache-2.0) may ever
be published, and whether to ask authors first. Until then every port stays local.
3. **Who runs port agents**: only us (players download published ports) or players too, with their
own AI login.
@@ -52,7 +52,8 @@ import net.neoforged.neoforge.event.server.ServerStartedEvent;
* Dev-only oracle for Horizon's mod ports (docs/modpacks.md): once the server has started,
* writes every block, item, recipe and tag of the namespaces named in {@code
* horizon.oracle.namespaces} (plus the vanilla blocks, so templates can be compared) to {@code
* horizon.oracle.out}, then stops the server. Never shipped to players.
* horizon.oracle.out}, then stops the server. With {@code horizon.oracle.machines} (a scenario
* file) it traces block entities instead ({@link MachineTrace}). Never shipped to players.
*/
@Mod("horizon_oracle")
public final class FactsExporter {
@@ -68,9 +69,14 @@ public final class FactsExporter {
Set<String> namespaces =
new TreeSet<>(Arrays.asList(System.getProperty("horizon.oracle.namespaces", "").split(",")));
namespaces.remove("");
String machines = System.getProperty("horizon.oracle.machines", "");
try (Writer w = Files.newBufferedWriter(out, StandardCharsets.UTF_8);
JsonWriter json = new JsonWriter(w)) {
new Export(server, namespaces, json).run();
if (machines.isEmpty()) {
new Export(server, namespaces, json).run();
} else {
MachineTrace.run(server, Path.of(machines), json);
}
} catch (Throwable e) {
e.printStackTrace();
System.exit(3);
@@ -0,0 +1,148 @@
package dev.horizonlab.neoforge;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.Container;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.properties.Property;
/**
* Dev-only machine oracle for Horizon's code-mod harness (tools/mod_machine_harness.py): places
* each scenario's block, fills its container's slots, runs its block entity ticker N times and
* writes the slots, the asked block-state properties and the asked integer fields of its saved
* tag after every tick. The same scenarios run on the Horizon port through
* crates/horizon-client/examples/mod_machine_probe.rs. Never shipped.
*/
final class MachineTrace {
private static final BlockPos AT = new BlockPos(0, 100, 0);
private MachineTrace() {}
static void run(MinecraftServer server, Path scenarios, JsonWriter json) throws IOException {
ServerLevel level = server.overworld();
JsonArray list = JsonParser.parseString(Files.readString(scenarios)).getAsJsonArray();
json.beginArray();
for (JsonElement e : list) {
scenario(server, level, e.getAsJsonObject(), json);
}
json.endArray();
}
private static ItemStack stack(JsonElement v) {
if (v == null || v.isJsonNull()) {
return ItemStack.EMPTY;
}
JsonArray a = v.getAsJsonArray();
return new ItemStack(BuiltInRegistries.ITEM.get(ResourceLocation.parse(a.get(0).getAsString())),
a.get(1).getAsInt());
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static BlockState with(BlockState state, String name, String value) {
for (Property<?> p : state.getProperties()) {
if (p.getName().equals(name)) {
Property raw = p;
java.util.Optional<Comparable> parsed = raw.getValue(value);
return parsed.isPresent() ? (BlockState) state.setValue(raw, parsed.get()) : state;
}
}
return state;
}
private static String property(BlockState state, String name) {
for (Property<?> p : state.getProperties()) {
if (p.getName().equals(name)) {
return String.valueOf(state.getValue(p));
}
}
return null;
}
@SuppressWarnings("unchecked")
private static void scenario(MinecraftServer server, ServerLevel level, JsonObject s, JsonWriter json)
throws IOException {
Block block = BuiltInRegistries.BLOCK.get(ResourceLocation.parse(s.get("block").getAsString()));
BlockState state = block.defaultBlockState();
JsonObject properties = s.has("properties") ? s.getAsJsonObject("properties") : new JsonObject();
for (Map.Entry<String, JsonElement> p : properties.entrySet()) {
state = with(state, p.getKey(), p.getValue().getAsJsonArray().get(0).getAsString());
}
level.setBlock(AT, state, 3);
BlockEntity be = level.getBlockEntity(AT);
if (!(be instanceof Container container)) {
throw new IllegalStateException(s.get("block") + " has no container block entity");
}
if (s.has("slots")) {
for (Map.Entry<String, JsonElement> slot : s.getAsJsonObject("slots").entrySet()) {
container.setItem(Integer.parseInt(slot.getKey()), stack(slot.getValue()));
}
}
JsonArray fields = s.has("fields") ? s.getAsJsonArray("fields") : new JsonArray();
JsonArray events = s.has("events") ? s.getAsJsonArray("events") : new JsonArray();
json.beginObject();
json.name("name").value(s.get("name").getAsString());
json.name("trace").beginArray();
int ticks = s.get("ticks").getAsInt();
for (int tick = 0; tick < ticks; tick++) {
for (JsonElement e : events) {
JsonObject ev = e.getAsJsonObject();
if (ev.get("tick").getAsInt() == tick) {
container.setItem(ev.get("slot").getAsInt(), stack(ev.get("stack")));
}
}
BlockState now = level.getBlockState(AT);
BlockEntityTicker<BlockEntity> ticker =
now.getTicker(level, (BlockEntityType<BlockEntity>) be.getType());
if (ticker != null) {
ticker.tick(level, AT, now, be);
}
now = level.getBlockState(AT);
CompoundTag tag = be.saveWithoutMetadata(server.registryAccess());
json.beginObject();
json.name("slots").beginObject();
for (int i = 0; i < container.getContainerSize(); i++) {
ItemStack item = container.getItem(i);
if (!item.isEmpty()) {
json.name(Integer.toString(i)).beginArray()
.value(BuiltInRegistries.ITEM.getKey(item.getItem()).toString())
.value(item.getCount()).endArray();
}
}
json.endObject();
json.name("properties").beginObject();
for (String name : properties.keySet()) {
json.name(name).value(property(now, name));
}
json.endObject();
json.name("fields").beginObject();
for (JsonElement f : fields) {
json.name(f.getAsString()).value(tag.getInt(f.getAsString()));
}
json.endObject();
json.endObject();
}
json.endArray();
json.endObject();
container.clearContent();
level.setBlock(AT, Blocks.AIR.defaultBlockState(), 3);
}
}
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Machine harness for code-mod ports (docs/research/code-mods-plan.md): the same scenarios on the
Java mod in the dev-only NeoForge oracle and on its Horizon port, compared tick by tick.
mod_machine_harness.py --jars DIR [--deps DIR] --hmod PORT.hmod --scenarios S.json --work DIR
[--prepared .local/horizon-client/26.3] [--oracle TRACE.json]
A scenario places a block, fills slots, runs N ticks (with slot changes at given ticks) and
records after every tick the slots, the listed block-state properties and the listed integer
fields of the saved block entity (the Java tag's names; the port saves the same names as JSON).
Writes WORK/oracle-trace.json, WORK/horizon-trace.json and WORK/result.json, and exits 1 on a
difference. The jars and the port stay in the work folder: nothing here holds mod code.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def compare(oracle, horizon):
"""Per scenario: ticks compared, ticks equal, the first differences."""
out = []
by_name = {s['name']: s['trace'] for s in horizon}
for scenario in oracle:
name, want = scenario['name'], scenario['trace']
got = by_name.get(name, [])
equal, diffs = 0, []
for tick, (w, g) in enumerate(zip(want, got)):
if w == g:
equal += 1
elif len(diffs) < 5:
keys = sorted({*w, *g})
diffs.append({'tick': tick, **{k: {'java': w.get(k), 'horizon': g.get(k)}
for k in keys if w.get(k) != g.get(k)}})
if len(want) != len(got):
diffs.append({'ticks': {'java': len(want), 'horizon': len(got)}})
out.append({'name': name, 'ticks': len(want), 'equal': equal, 'differences': diffs})
return out
def main():
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
p.add_argument('--jars', help='the Java mod (and its namesakes); not needed with --oracle')
p.add_argument('--deps')
p.add_argument('--hmod', required=True)
p.add_argument('--scenarios', required=True)
p.add_argument('--work', required=True)
p.add_argument('--prepared', default=str(ROOT / '.local' / 'horizon-client' / '26.3'))
p.add_argument('--oracle', help='an oracle trace already written')
args = p.parse_args()
work = Path(args.work)
work.mkdir(parents=True, exist_ok=True)
oracle_path = Path(args.oracle) if args.oracle else work / 'oracle-trace.json'
if not args.oracle:
if not args.jars:
sys.exit('--jars or --oracle')
command = [sys.executable, str(ROOT / 'tools' / 'neoforge_oracle.py'), 'machines', '--jars', args.jars,
'--scenarios', args.scenarios, '--out', str(oracle_path)]
if args.deps:
command += ['--deps', args.deps]
subprocess.run(command, check=True)
horizon_path = work / 'horizon-trace.json'
subprocess.run(['cargo', 'run', '--release', '-q', '-p', 'horizon-client', '--example', 'mod_machine_probe',
'--', args.prepared, args.hmod, args.scenarios, str(horizon_path)], cwd=ROOT, check=True)
result = compare(json.loads(oracle_path.read_text()), json.loads(horizon_path.read_text()))
(work / 'result.json').write_text(json.dumps(result, indent=1))
failed = False
for r in result:
ok = not r['differences']
failed |= not ok
print(f"{'PASS' if ok else 'FAIL'} {r['name']}: {r['equal']}/{r['ticks']} ticks equal")
for d in r['differences'][:3]:
print(' ', json.dumps(d))
sys.exit(1 if failed else 0)
if __name__ == '__main__':
main()
+27 -2
View File
@@ -2,11 +2,14 @@
"""Dev-only NeoForge oracle for Horizon's mod-port harnesses (docs/modpacks.md).
neoforge_oracle.py export --jars DIR [--deps DIR] [--namespaces a,b] --out FACTS.json
neoforge_oracle.py machines --jars DIR [--deps DIR] --scenarios S.json --out TRACE.json
installs a NeoForge server under .local/neoforge-oracle (once), builds the exporter mod in
mods/horizon/src/neoforge-oracle, starts the server headless with the given mod jars, and writes every
block, item, recipe and tag of the mods' namespaces (and the vanilla blocks, for templates) to
FACTS.json. The jars are the user's own; nothing here is shipped or committed.
FACTS.json. `machines` instead places each scenario's block, ticks its block entity and writes its
slots, block state and saved fields per tick (tools/mod_machine_harness.py). The jars are the user's
own; nothing here is shipped or committed.
"""
import argparse
import json
@@ -226,6 +229,23 @@ def export(args):
f'({namespaces}); server ran {seconds:.1f} s')
def machines(args):
jars = sorted(Path(args.jars).glob('*.jar'))
deps = sorted(Path(args.deps).glob('*.jar')) if args.deps else []
home = install()
exporter = build(home)
home, run = instance('machines', jars, [*deps, exporter])
out = Path(args.out).resolve()
out.unlink(missing_ok=True)
seconds, log = start(home, run, {'horizon.oracle.out': out,
'horizon.oracle.machines': Path(args.scenarios).resolve()})
if not out.exists():
tail = '\n'.join(log.splitlines()[-40:])
sys.exit(f'the oracle wrote nothing; the end of {run / "server.log"}:\n{tail}')
traces = json.loads(out.read_text())
print(f'{out}: {len(traces)} scenarios; server ran {seconds:.1f} s')
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest='command', required=True)
@@ -234,8 +254,13 @@ def main():
e.add_argument('--deps', help='their required libraries (loaded, not exported)')
e.add_argument('--namespaces', help='comma-separated; default: the jars\' mod ids')
e.add_argument('--out', required=True)
m = sub.add_parser('machines', help='trace the block entities of the scenarios in --scenarios')
m.add_argument('--jars', required=True, help='the mods')
m.add_argument('--deps', help='their required libraries')
m.add_argument('--scenarios', required=True)
m.add_argument('--out', required=True)
args = parser.parse_args()
{'export': export}[args.command](args)
{'export': export, 'machines': machines}[args.command](args)
if __name__ == '__main__':