mirror of
https://github.com/horizon-rust/horizon-native-mod-template.git
synced 2026-11-07 10:05:58 -10:00
Package as .hnmod and pin Nightly 2026-10-28
- tools/package.py writes the source-only .hnmod Horizon's registry and launcher take (horizon-native.json + mixins/<id>/), reproducible bytes; based on levzzz5154's Veinminer packager. - The example's id is example_mod: registry ids are lowercase letters, digits and _. - Pinned to Nightly 2026-10-28 (83f15fa), whose source stays downloadable as a nightly-source/ tag; README says how to move to a newer Nightly and publish. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c8bfba72fb
commit
cf1c281e03
7 files changed
+82
-10
No files matched your search
+1
-1
@@ -8,7 +8,7 @@ description = "A minimal native Horizon Mixins mod"
|
||||
|
||||
[lib]
|
||||
# This exact source is also injected as a same-crate module by horizon-mixins.
|
||||
path = "mixins/example-mod/runtime.rs"
|
||||
path = "mixins/example_mod/runtime.rs"
|
||||
|
||||
[workspace]
|
||||
members = ["tools/audit"]
|
||||
|
||||
@@ -48,7 +48,7 @@ a separate clone/worktree and set `HORIZON_MIXINS=/path/to/pinned/horizon-mixins
|
||||
```text
|
||||
Cargo.toml Standalone runtime library + source audit workspace
|
||||
compatibility.json Exact upstream/tool revisions and source hashes
|
||||
mixins/example-mod/
|
||||
mixins/example_mod/
|
||||
horizon-mixin.toml Module and HEAD declaration
|
||||
break.rs Small typed hook adapter
|
||||
runtime.rs Runtime implementation and unit tests (single source)
|
||||
@@ -61,7 +61,7 @@ tools/audit/ Rust AST comparison against the pristine expor
|
||||
To rename the example:
|
||||
|
||||
1. Change Cargo package name/description and the manifest's mod ID/version.
|
||||
2. Rename `mixins/example-mod/` and update the Cargo `[lib].path`.
|
||||
2. Rename `mixins/example_mod/` and update the Cargo `[lib].path`.
|
||||
3. Change the manifest module name `example_mod` and its reference in `break.rs`.
|
||||
4. Change the runtime log prefix, unit-test expectation and the native test filter
|
||||
`game::example_mod::tests` in `tools/horizon.py`.
|
||||
@@ -120,10 +120,30 @@ Horizon version**, and explicitly pass their path:
|
||||
Prepare those resources using the compatible Horizon launcher or
|
||||
`horizon-prepare --prepare`; game resources are not supplied by this repository.
|
||||
Use the binary's `--help` for world/server arguments. Breaking a block should
|
||||
print `[example-mod] Breaking block at ...` to the host's stderr. The HEAD log
|
||||
print `[example_mod] Breaking block at ...` to the host's stderr. The HEAD log
|
||||
reports attempts, including attempts the original game later rejects.
|
||||
Compilation tests do not establish interactive gameplay/resource compatibility.
|
||||
|
||||
## Pin to a Nightly, package, publish
|
||||
|
||||
A native mod builds on one exact Horizon commit, and every mod of a player's build must pin the same one, so
|
||||
pin a **Nightly**: its release notes name the commit, and the tag `nightly-source/<date>-<short>` keeps that
|
||||
commit's source downloadable after the Nightly is replaced. This template pins Nightly 2026-10-28
|
||||
(`83f15fa`). To move to a newer Nightly:
|
||||
|
||||
1. Put its commit in `compatibility.json` (`horizon_revision`) and in the manifest's
|
||||
`[compatibility] horizon_revision`.
|
||||
2. Update `source_sha256` for each target file (`git -C ../horizon show <commit>:<path> | sha256sum`).
|
||||
3. Run the checks below. When a target moved (the game is being split into crates), `plan` names the
|
||||
missing function: find its new path in that commit and fix `target` in the manifest.
|
||||
|
||||
Then `python3 tools/package.py` writes `target/<id>-<version>.hnmod`: `horizon-native.json` plus your
|
||||
`mixins/<id>/` sources, byte-for-byte reproducible. Submit it to
|
||||
[horizon-mods](https://github.com/horizon-rust/horizon-mods) as a `native-mod`. A maintainer reads the
|
||||
source before it is listed, and the Horizon launcher compiles it with its own Rust on the pinned commit, in
|
||||
an instance of its own. The mod id must be lowercase letters, digits and `_` (the registry's rule), and
|
||||
the bundle folder is named after it.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"horizon_revision": "028f60881678a9a0e3e198f693a82858fa475c76",
|
||||
"horizon_revision": "83f15fa276c44711276b6ceb5f0fe0c9c534dc66",
|
||||
"source_sha256": {
|
||||
"crates/horizon-client/src/game.rs": "0dfd3cb9a29a43f7faa3443eb25a88290b6a8e05b18f52c892e8d14e232a2170"
|
||||
"crates/horizon-client/src/game.rs": "6d0497ae51f3f04133bbb40a7b1554ea1303c49525eb1689e758939c35859d20"
|
||||
},
|
||||
"horizon_repository": "https://github.com/horizon-rust/horizon",
|
||||
"horizon_mixins_repository": "https://github.com/horizon-rust/horizon-mixins",
|
||||
|
||||
File renamed without changes.
@@ -1,10 +1,10 @@
|
||||
[mod]
|
||||
id = "example-mod"
|
||||
id = "example_mod"
|
||||
version = "0.1.0"
|
||||
priority = 100
|
||||
|
||||
[compatibility]
|
||||
horizon_revision = "028f60881678a9a0e3e198f693a82858fa475c76"
|
||||
horizon_revision = "83f15fa276c44711276b6ceb5f0fe0c9c534dc66"
|
||||
|
||||
[[module]]
|
||||
id = "runtime"
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
pub fn block_break_message(pos: [i32; 3]) -> String {
|
||||
format!(
|
||||
"[example-mod] Breaking block at {}, {}, {}",
|
||||
"[example_mod] Breaking block at {}, {}, {}",
|
||||
pos[0], pos[1], pos[2]
|
||||
)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ mod tests {
|
||||
fn message_preserves_signed_block_coordinates() {
|
||||
assert_eq!(
|
||||
block_break_message([-3, 64, 12]),
|
||||
"[example-mod] Breaking block at -3, 64, 12"
|
||||
"[example_mod] Breaking block at -3, 64, 12"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Package the mod as a source-only `.hnmod`, the file Horizon's registry and launcher take.
|
||||
|
||||
The archive holds `horizon-native.json` and `mixins/<id>/` (only `.rs` and `.toml`), with fixed dates and
|
||||
modes, so the same sources always give the same bytes and the same SHA-256. Horizon's
|
||||
`horizon_mods::index::native` reads it without running anything; the launcher compiles it on the pinned
|
||||
Horizon revision. Based on levzzz5154's packager for Veinminer.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
GAME = '26.3'
|
||||
|
||||
|
||||
def package():
|
||||
compat = json.loads((ROOT / 'compatibility.json').read_text())
|
||||
manifests = sorted((ROOT / 'mixins').glob('*/horizon-mixin.toml'))
|
||||
if len(manifests) != 1:
|
||||
raise SystemExit('a .hnmod holds one bundle: keep one mixins/<id>/horizon-mixin.toml')
|
||||
bundle = manifests[0].parent
|
||||
mixin = tomllib.loads(manifests[0].read_text())
|
||||
mod = mixin['mod']
|
||||
if bundle.name != mod['id']:
|
||||
raise SystemExit(f"rename mixins/{bundle.name}/ to mixins/{mod['id']}/ (the registry finds it by id)")
|
||||
if not mod['id'].replace('_', '').isalnum() or not mod['id'].islower():
|
||||
raise SystemExit('the registry takes ids of lowercase letters, digits and _ only')
|
||||
if mixin.get('compatibility', {}).get('horizon_revision') != compat['horizon_revision']:
|
||||
raise SystemExit('horizon-mixin.toml and compatibility.json pin different Horizon revisions')
|
||||
manifest = dict(schema=1, id=mod['id'], version=mod['version'], game=GAME,
|
||||
horizon=compat['horizon_revision'], mixins=compat['horizon_mixins_revision'])
|
||||
files = {'horizon-native.json': (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode()}
|
||||
for path in sorted(bundle.iterdir()):
|
||||
if path.is_file() and path.suffix in ('.rs', '.toml'):
|
||||
files[path.relative_to(ROOT).as_posix()] = path.read_bytes()
|
||||
out = ROOT / 'target' / f"{mod['id']}-{mod['version']}.hnmod"
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(out, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for name, content in sorted(files.items()):
|
||||
info = zipfile.ZipInfo(name, (2026, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o100644 << 16
|
||||
archive.writestr(info, content)
|
||||
print(json.dumps(dict(file=str(out), size=out.stat().st_size,
|
||||
sha256=hashlib.sha256(out.read_bytes()).hexdigest()), indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
package()
|
||||
Reference in new issue
Block a user