Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Native entry point and diagnostics

The generated executable uses the installed game's crates/horizon-client/src/native_main.rs.template. The launcher supplies only ordered literal mod IDs and hook factory paths. The template owns entry construction, the allocator and the canonical game identity. Source without this template is refused before staging trait mods: update the game first.

A mod whose bootstrap cannot complete should call cx.refuse("reason") inside Hooks::register, then return. This refuses only the calling mod's registration: there is no mod ID argument. Its declarations from that registration (before or after the call) are discarded. Later declarations do not add secondary validation errors, and another mod's declarations are unchanged. Native and wasm report the same diagnostic through the normal registration error path. This does not panic or exit the process. The first reason wins, including an empty reason. Reasons are preserved verbatim up to 4096 UTF-8 bytes; longer strings are truncated at the last character boundary within that limit.

A panic in a trait mod's hook adds in native mod <id>, hook <hook> to the panic's message, in the crash report and in the server thread's log line, with the horizon-mod.toml id. Each entry passed to entry::main_with must bind its id and hooks together: ModEntry::new("example", example::hooks()). The ordered list is Mods = Arc<[ModEntry]>; there is no separate naming step or unnamed entry. Before installation, mod_api::mods() still returns an empty list. Parts retain their registering mod's id for startup errors and hook panics. The game stops. A point listener's panic names its mod and method; render hooks are not named yet.

A native runtime that hosts converted packages (Portizon's wasm packages, a Java interpreter's jars) declares the packages it loaded with mod_api::content::crash::ConvertedPackages::new(runtime_id, ids) and marks a guest call with mod_api::content::crash::ConvertedPackage::enter(export, site). The scope nests, restores the previous one on return and while a panic unwinds, and adds in converted package <id>, export <export>, call site <site> below the runtime's in native mod line in the report. A package id the runtime did not declare is refused, as is a scope entered while another mod's call is the current one, so guest code cannot impersonate a mod the runtime did not load.

The guard is thread-affine (not Send or Sync), and attribution ends when the native frame that entered it exits, even if the converted guard is retained. A nested native frame hides the outer frame's package until it returns. Worker threads have no scope unless the runtime enters a native call and package scope there. Registry ids are stored, not interned: entering a package clones an owned id on every call (borrowed static ids do not allocate).

A code mod may listen at the game's injection points as a rare escape hatch, through a crate-root function named by [[inject]].with, taking a mod_points record; the form and its limits are in Injections.