Клиент: начальный импорт кодовой базы
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# Horizon APIs
|
||||
|
||||
Horizon has six surfaces that other programs and people build on. Each one has an **API** (the contract: what you
|
||||
may call, read or write, and what Horizon promises about it) and an **SDK** (the tools that help you use it). This
|
||||
section has one page per surface, plus the rules every surface follows:
|
||||
[stability and versioning](stability.md).
|
||||
|
||||
| Surface | For | API today | Stability | Reference |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [Mods](mods.md) | mod authors | WIT `horizon:mods@0.1.0`, manifest `api` 0.1 (code) and 0.2 (content) | preview | [mods-reference.md](mods-reference.md) (generated) |
|
||||
| [Shaders](shaders.md) | shader authors | none yet: packs were removed on 2026-10-24 | planned | none |
|
||||
| [Agent control](agent-control.md) | agents, scripts, tests | `horizon-ctl`, its MCP server, the game's control socket | preview | [horizon-ctl-commands.md](../horizon-ctl-commands.md) (generated) |
|
||||
| [Servers](server.md) | hosts, panels, bots | `horizon-server` commands, `server.toml`, control API `/api/v1` | stable | [server-reference.md](server-reference.md) (generated) |
|
||||
| [Launcher](launcher.md) | players' agents, pack makers | `horizon-launcher` commands and its MCP server | preview | [launcher-commands.md](../launcher-commands.md) (generated) |
|
||||
| [Data formats](data-formats.md) | tools that read Horizon's files | benchmark reports, telemetry, settings files, signed checksums | stable or preview, per format | on that page |
|
||||
|
||||
## How these APIs are designed
|
||||
|
||||
- **From the people who use them.** Each API starts from what a mod author, a pack author, a server host or an agent
|
||||
needs to do, written down as tasks ("a host backs up the world from a script"), and from how Horizon is built:
|
||||
Rust, wgpu, WebAssembly components and a server thread that runs every world. A function exists because a task
|
||||
needs it.
|
||||
- **Our own design.** Horizon's APIs are not modelled on any other project's. Contributors may know other mod
|
||||
loaders, renderers and server platforms exist, but their source and API references are never read, copied or
|
||||
paraphrased here, and our names, shapes and docs are not taken from theirs. Vanilla Minecraft 26.3 is the reference for game
|
||||
concepts: ids, commands, `server.properties`, data packs and resource packs.
|
||||
- **Safe by default.** Mods run in a sandbox and can only use the capabilities their manifest declares; the control
|
||||
sockets listen on this computer only, behind a token; nothing here is on by default where it would change vanilla's
|
||||
look or behaviour.
|
||||
- **Free when unused.** An API nobody uses costs nothing per tick or per frame: no engine, no thread, no allocation.
|
||||
- **One contract for every mode.** In-frame single player, the server thread, LAN and the dedicated server run the
|
||||
same gameplay code ([AGENTS.md](../../AGENTS.md)), so a server-side API serves all of them.
|
||||
|
||||
## References are generated
|
||||
|
||||
A reference page is written by code from the source it describes, and a test fails when the two differ:
|
||||
|
||||
| Page | Generated from | Update it with |
|
||||
| --- | --- | --- |
|
||||
| [mods-reference.md](mods-reference.md) | `crates/horizon-mod-sdk/wit/horizon.wit`, the manifest's capabilities | `HORIZON_WRITE_DOCS=1 cargo test -p horizon-mods --features pack --test api_reference` |
|
||||
| [server-reference.md](server-reference.md) | `horizon-server`'s help, `config::TEMPLATE`, `api::ROUTES` | `HORIZON_WRITE_DOCS=1 cargo test -p horizon-server reference` |
|
||||
| [horizon-ctl-commands.md](../horizon-ctl-commands.md) | the command table in `crates/horizon-client/src/bin/horizon-ctl/commands.rs` | `horizon-ctl help --markdown > docs/horizon-ctl-commands.md` |
|
||||
| [launcher-commands.md](../launcher-commands.md) | `launcher/src/cli.rs` | `HORIZON_WRITE_DOCS=1 cargo test --manifest-path launcher/Cargo.toml the_launcher_reference` |
|
||||
|
||||
So a pull request that changes an API also changes its reference, and the reviewer sees the API's diff in the
|
||||
reference's diff. The guides (overview, how-to, examples) are written by hand.
|
||||
|
||||
## Each surface's page
|
||||
|
||||
Every page has the same parts: **Overview** (what you can build), **Guide** (where to start), **Reference**,
|
||||
**Examples**, **Stability** (the level of each part, per [stability.md](stability.md)), **Changelog** and
|
||||
**Not there yet** (what is planned, so nobody builds on a guess).
|
||||
|
||||
The website shows these pages at [playhorizon.dev/docs/api](https://playhorizon.dev/docs/api), built from this
|
||||
folder, and a summary for players and authors at [playhorizon.dev/docs/apis](https://playhorizon.dev/docs/apis)
|
||||
(in the website's repository, horizon-rust/horizon-site).
|
||||
@@ -0,0 +1,55 @@
|
||||
# Agent control API
|
||||
|
||||
How a program drives a running game: an AI agent through MCP, a script through `horizon-ctl`, a test through the
|
||||
control socket. Design and inventory: [agent-control.md](../agent-control.md); for players:
|
||||
[playhorizon.dev/docs/agents](https://playhorizon.dev/docs/agents).
|
||||
|
||||
## Overview
|
||||
|
||||
The game listens on a local control socket (a Unix socket, or localhost TCP with a token on Windows), takes one
|
||||
command per text line and answers one JSON line. `horizon-ctl` wraps it: it starts the game, sends commands and
|
||||
serves the same commands as MCP tools. The launcher's MCP server offers them too, as `game_<tool>`.
|
||||
|
||||
| | Count |
|
||||
| --- | --- |
|
||||
| `horizon-ctl` commands | 96, in 8 groups (session, observe, move and look, act, screens and chat, multiplayer, world and cheats, scripting and benchmarks) |
|
||||
| MCP tools | 88 (8 commands are CLI only) |
|
||||
| Destructive commands (need `--confirm`) | 3 |
|
||||
| Command names the game's socket answers | about 160, test hooks included (`horizon-ctl help --game`) |
|
||||
|
||||
## Guide
|
||||
|
||||
1. Build `horizon-ctl` (`cargo build --profile fast -p horizon-client`), then
|
||||
`horizon-ctl --socket $PWD/ctl.sock launch --world W --creative --wait --resources <prepared files>`.
|
||||
2. `horizon-ctl --socket … state`, `keys`, `turn`, `command`, `screenshot`, `quit`.
|
||||
3. For an agent: `horizon-ctl mcp`, or the launcher's `horizon-launcher mcp` with `agent-config` to connect a client
|
||||
([launcher-commands.md](../launcher-commands.md)).
|
||||
|
||||
## Reference
|
||||
|
||||
[horizon-ctl-commands.md](../horizon-ctl-commands.md), generated from the command table in
|
||||
`crates/horizon-client/src/bin/horizon-ctl/commands.rs`: every command, its MCP tool and its arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
- `tools/agent_control_e2e.py`: starts a game and drives it end to end through `horizon-ctl` and MCP.
|
||||
- `tools/lan_e2e.py`: two games over LAN, both driven through their sockets.
|
||||
|
||||
## Stability
|
||||
|
||||
Preview ([stability.md](stability.md)). The commands in `horizon-ctl`'s table and their MCP tool names are what the
|
||||
API offers; the other commands the game's socket answers are test hooks and experimental.
|
||||
|
||||
## Not there yet
|
||||
|
||||
- **A version.** A `hello` command that answers the protocol version, the game's release and the features it has;
|
||||
MCP `serverInfo` carries the same version; `horizon-ctl` refuses a game whose major it doesn't speak.
|
||||
- **Reply schemas.** Each command's reply described in the table (fields, types, units), so the reference documents
|
||||
replies as well as arguments and MCP tools declare an output schema.
|
||||
- **Events as a stream.** `events --since` polls today; a subscription that pushes events as they happen.
|
||||
- **Test hooks apart.** The hooks only harnesses use move under their own prefix, out of the agent's tool list.
|
||||
- **Compatibility tests.** A transcript per protocol version: commands and the reply keys each must keep.
|
||||
|
||||
## Changelog
|
||||
|
||||
- Unversioned so far: the command table and the generated reference are the contract.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Data formats
|
||||
|
||||
The files Horizon writes that other tools may read: benchmark reports, telemetry, settings, mods, release checksums
|
||||
and worlds. Each entry names its version field, its documentation and its level ([stability.md](stability.md)).
|
||||
|
||||
## Overview
|
||||
|
||||
| Format | Where | Version | Level | Documented in |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Benchmark report | the benchmark's output, the local log, uploads | `"format": "horizon-benchmark-report"`, `"version": 1` | stable | [benchmark-report.md](../benchmark-report.md) |
|
||||
| Telemetry batch | `POST /v1/batch` from the launcher | `"schema": 1` | stable | [telemetry.md](../telemetry.md) |
|
||||
| Telemetry consent | `telemetry/consent.json` in the game folder | `"version": 2` | stable | [telemetry.md](../telemetry.md) |
|
||||
| Mod package | `.hmod`: a zip with `horizon-mod.toml`, `mod.wasm`, `assets/`, `data/` | the manifest's `api` | preview | [mods.md](../mods.md#the-manifest), [mods-reference.md](mods-reference.md) |
|
||||
| Mods turned on | `horizon-mods.txt` | none | preview | [mods.md](../mods.md#playing-with-mods) |
|
||||
| Horizon settings | `horizon-graphics.txt`, `horizon-extras.txt` (`key:value`, as `options.txt`) | none: unknown keys fall back to defaults | preview | [graphics.md](../graphics.md), [extras.md](../extras.md#options-storage) |
|
||||
| Server settings | `server.toml` | its keys | stable | [server-reference.md](server-reference.md) |
|
||||
| Control endpoints | the game's socket file (TCP form), `.horizon/api.json` | none | preview | [agent-control.md](agent-control.md), [server-reference.md](server-reference.md) |
|
||||
| Release checksums | `SHA256SUMS`, `SHA256SUMS.sig` on releases since signing began (#1011) | the signed text names the tag | stable | [security.md](../security.md) |
|
||||
| Worlds | the world folder | vanilla 26.3's (`DataVersion`) | vanilla's | [world-data.md](../world-data.md) |
|
||||
|
||||
Worlds are vanilla's format, so any tool that reads a 26.3 world reads a Horizon world; Horizon adds itself to
|
||||
`level.dat`'s `ServerBrands` as vanilla's server does.
|
||||
|
||||
## Rules for every format
|
||||
|
||||
- A reader ignores fields it doesn't know, and refuses a version newer than it knows with a clear message.
|
||||
- An optional field may be added in the same version; renaming, removing or changing the meaning of a field makes the
|
||||
next version, and Horizon's own readers keep reading the older ones for the deprecation window.
|
||||
- Each versioned format has a sample file of every version its reader accepts, read by a test.
|
||||
|
||||
## Not there yet
|
||||
|
||||
- Versions for the settings files and the control endpoint files (a `version` line or field).
|
||||
- Sample files and reader tests for every versioned format.
|
||||
- A generated schema for the benchmark report and the telemetry batch from their Rust types, checked by a test like
|
||||
the references.
|
||||
|
||||
## Changelog
|
||||
|
||||
- Benchmark report version 1; telemetry schema 1, consent version 2.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Launcher API
|
||||
|
||||
What a player's agent, a script or a pack maker can do with `horizon-launcher`: install and run versions, manage
|
||||
accounts, settings, mods, modpacks and hosted servers, from a terminal or over MCP. The launcher itself:
|
||||
[launcher/README.md](../../launcher/README.md).
|
||||
|
||||
## Overview
|
||||
|
||||
Every page of the launcher has a command, and every command but three has an MCP tool with the same arguments; a tool
|
||||
runs the command with `--json`. Tools that change the player's things for good need `confirm: true`; with No AI on,
|
||||
the MCP server is off.
|
||||
|
||||
| | Count |
|
||||
| --- | --- |
|
||||
| Commands | 46: versions, installs, play, the server, notes, links, accounts, settings, logs, updates, data collection, modpacks, mods, hosted servers, MCP, benchmark |
|
||||
| MCP tools | 43, plus the game's tools as `game_<tool>` ([agent-control.md](agent-control.md)) |
|
||||
| Modpack formats it imports | 3: a Modrinth `.mrpack`, a CurseForge zip, an instance folder |
|
||||
|
||||
## Guide
|
||||
|
||||
1. `horizon-launcher versions`, `horizon-launcher install latest`, `horizon-launcher play`.
|
||||
2. Modpacks: `horizon-launcher modpack import <file>`, then `modpack port` and `modpack play`
|
||||
([modpacks.md](../modpacks.md)).
|
||||
3. For an agent: `horizon-launcher agent-config` prints how to connect a client to `horizon-launcher mcp`.
|
||||
|
||||
## Reference
|
||||
|
||||
[launcher-commands.md](../launcher-commands.md), generated from `launcher/src/cli.rs`: every command, its arguments
|
||||
and its MCP tool.
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
horizon-launcher versions --installed --json
|
||||
horizon-launcher mods search minimap --kind mod --json
|
||||
horizon-launcher servers create --name "Weekend" --player Alex --json
|
||||
```
|
||||
|
||||
## Stability
|
||||
|
||||
Preview ([stability.md](stability.md)): command names, arguments and MCP tool names are the contract; `--json`
|
||||
output shapes are not yet described in the reference.
|
||||
|
||||
## Not there yet
|
||||
|
||||
- **A Horizon modpack manifest.** A pack made for Horizon (not a port of a Java pack) needs its own file: the pack's
|
||||
id, name and version; the Horizon API lines and the release line it needs; its `.hmod` mods (id, version, SHA-256,
|
||||
where to download it, client, server or both); resource packs; default settings; and whether it has a server side.
|
||||
One manifest serves the launcher, `horizon-server` (a server installs the server side of the same pack) and the mod
|
||||
browser. Designed from what a pack maker does, versioned per [stability.md](stability.md), with a schema test.
|
||||
- **JSON output schemas** for every command, in the generated reference, so MCP tools declare their output.
|
||||
- **A version** in MCP `serverInfo` and `--json` output, as for agent control.
|
||||
- **Compatibility tests**: golden `--json` output per command.
|
||||
|
||||
## Changelog
|
||||
|
||||
- Unversioned so far: the command table and the generated reference are the contract.
|
||||
@@ -0,0 +1,174 @@
|
||||
# The registry index and `.hpack`
|
||||
|
||||
Horizon's registry lists what is made for Horizon and reviewed: mods (`.hmod`), modpacks (`.hpack`), resource packs
|
||||
and Horizon shader packs (`.hshader`, [shaders.md](../shaders.md)). Submissions are pull requests in
|
||||
[horizon-rust/horizon-mods](https://github.com/horizon-rust/horizon-mods); merging one publishes it. The launcher and the
|
||||
website read one file, `index.json`, which that repository's CI builds. The Rust types and every check below are
|
||||
`horizon_mods::index` and `horizon_mods::modpack` (feature `index`); the sample is
|
||||
`crates/horizon-mods/tests/index/index-v1.json`, read by the tests with the files it lists.
|
||||
|
||||
Level: preview ([stability.md](stability.md)). Schema 1.
|
||||
|
||||
## `index.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": 1,
|
||||
"generated": "2026-10-24T18:00:00Z",
|
||||
"projects": [{
|
||||
"id": "coords_hud", "kind": "mod", "name": "Coordinates HUD", "summary": "One line.",
|
||||
"description": "Markdown.", "authors": ["Horizon"], "owners": ["ludovic111"],
|
||||
"license": "MIT", "source": "https://github.com/...", "icon": "https://...png", "key": "RWT...", "downloads": 1234,
|
||||
"versions": [{
|
||||
"version": "0.1.0", "published": "2026-10-24", "changelog": "",
|
||||
"api": "0.1", "side": "client", "capabilities": ["game-read", "hud", "chat"], "api_level": 0, "game": "26.3",
|
||||
"file": { "name": "coords_hud-0.1.0.hmod", "url": "https://github.com/horizon-rust/horizon-mods/releases/download/...",
|
||||
"size": 438, "sha256": "e113...", "signature": "untrusted comment: ...\n...\ntrusted comment: ...\n...\n" }
|
||||
}]
|
||||
}],
|
||||
"revoked": [{ "id": "bad_mod", "sha256": "1111...", "reason": "Sent chat messages it didn't declare.", "date": "2026-10-24" }]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `schema` | 1. A reader refuses a higher number with "update Horizon" rather than misread it. |
|
||||
| `projects[].id` | `[a-z0-9_]{1,64}`, unique; a mod's is its manifest's `id`. |
|
||||
| `kind` | `mod`, `modpack`, `resourcepack`, `shader` (Horizon's own shader packs, not Iris or OptiFine packs). Other kinds (data packs, worlds, themes) may come without a new schema: a reader keeps a kind it doesn't know and doesn't offer it. |
|
||||
| `summary`, `description` | One line for lists; Markdown for the project's page. |
|
||||
| `owners` | GitHub logins allowed to publish updates. |
|
||||
| `license`, `source` | An SPDX expression and an `https://` address of the source code: both required. |
|
||||
| `icon` | `https://`, optional. |
|
||||
| `key` | The author's [minisign](https://jedisct1.github.io/minisign/) public key. When set, every version must be signed with it; an update signed with another key is refused. Absent: the project is unsigned and the launcher says so. |
|
||||
| `versions` | Newest first, at least one. |
|
||||
| `downloads` | How many times its files were downloaded, every version together: GitHub's count of each release asset, summed by the publish job, which runs every hour. Absent until a file is downloaded. Read-only: never in an entry. |
|
||||
| `api` | A mod's API line (its manifest's `api`); a modpack's manifest format (`1`). |
|
||||
| `side`, `capabilities` | A mod's, read from its manifest by CI, never typed by hand. |
|
||||
| `api_level` | The mod API level a mod's code needs ([stability.md](stability.md#mod-api-levels)): its newest imported function's, read from the file by CI. Absent for content-only mods and in indexes built before levels. Installers take the newest version whose level the running game serves. |
|
||||
| `game` | The Minecraft version it is for. |
|
||||
| `requires` | A modpack's mods: `{ id, version, sha256 }`, each a version in this index with that hash. |
|
||||
| `file` | A plain name with the kind's extension (`.hmod`, `.hpack`, `.zip`), an `https://` URL, the size (at most 64 MiB), the lowercase hex SHA-256, and the `.minisig` text when the project has a key. |
|
||||
| `revoked` | Files taken down: one file by `sha256`, or every version of `id` without it. The launcher refuses to install them and turns installed copies off with the reason. |
|
||||
|
||||
Readers ignore fields they don't know, so an optional field can be added to schema 1. Renaming or removing a field, or
|
||||
changing what one means, makes schema 2.
|
||||
|
||||
## Before a file is installed
|
||||
|
||||
`index::check_file` runs on the downloaded file, before it reaches the game folder:
|
||||
|
||||
1. Its size and SHA-256 are the ones the entry lists, and it isn't revoked.
|
||||
2. With a project key: its signature verifies with that key (minisign's prehashed and legacy signatures both).
|
||||
3. What the file says matches the entry: a mod's `horizon-mod.toml` (id, version, API, side, capabilities), its API
|
||||
level when the entry lists one, and its
|
||||
content's fence ([mods.md](../mods.md#content-mods-api-02)); a modpack's manifest (id, version, format, game,
|
||||
mods); a resource pack's `pack.mcmeta`; a shader pack's `horizon_shader_api::check` and manifest (id, version,
|
||||
shader API).
|
||||
4. A kind this build can't install is refused with "this Horizon can't install a … yet".
|
||||
|
||||
The game still checks a mod's component imports against its capabilities when it starts it, as for any mod.
|
||||
|
||||
## `.hpack`
|
||||
|
||||
A zip holding `horizon-modpack.toml` and, optionally, `overrides/`. The mods are not inside: the launcher downloads
|
||||
each from the index and checks it as above.
|
||||
|
||||
```toml
|
||||
format = 1
|
||||
id = "cozy" # [a-z0-9_]{1,64}: the profile's folder
|
||||
name = "Cozy"
|
||||
version = "1.0.0"
|
||||
description = "Coordinates and a calm render distance."
|
||||
authors = ["Horizon"]
|
||||
game = "26.3"
|
||||
|
||||
[[mods]]
|
||||
id = "coords_hud"
|
||||
version = "0.1.0"
|
||||
sha256 = "e1135345692f6dd8fd4c51920151944b82333e17049c9bb7d756f27b8deefc53"
|
||||
on = true # optional, default true: turned on in the profile's horizon-mods.txt
|
||||
```
|
||||
|
||||
Unknown keys are refused. `overrides/` may hold only `options.txt`, `horizon-graphics.txt`, `horizon-extras.txt`,
|
||||
`resourcepacks/<name>.zip` and `horizon-mods/.horizon-mod-data/<a listed mod>/...` (a mod's own saved settings,
|
||||
capability `storage`), each at most 64 MiB and 256 MiB in all; anything else refuses the pack. The launcher copies
|
||||
them into the profile's game folder.
|
||||
|
||||
## The registry repository
|
||||
|
||||
[horizon-rust/horizon-mods](https://github.com/horizon-rust/horizon-mods) holds one `mods/<id>.toml` per project, written by
|
||||
the submission form or by hand, and `revoked.toml`. What an entry can't vouch for is read from its file by the kind's
|
||||
own check (`index::inspect`): a mod's API, side and capabilities, a modpack's mods, the size. Types and rules:
|
||||
`horizon_mods::index::registry`.
|
||||
|
||||
```toml
|
||||
id = "coords_hud" # the file's name, and the manifest's id
|
||||
kind = "mod" # mod | modpack | resourcepack | shader
|
||||
name = "Coordinates HUD"
|
||||
summary = "One line, at most 160 characters."
|
||||
description = """Markdown.""" # optional
|
||||
authors = ["Horizon"]
|
||||
owners = ["ludovic111"] # GitHub logins who may publish updates
|
||||
license = "MIT"
|
||||
source = "https://github.com/..."
|
||||
icon = "icons/coords_hud.png" # optional: a PNG of at most 256 KiB in the repository, or https://
|
||||
key = "RWT..." # optional: the minisign public key every version is signed with
|
||||
|
||||
[[versions]] # newest first
|
||||
version = "0.1.0" # letters, digits, '.', '-', '_': the release tag is <id>-<version>
|
||||
published = "2026-10-24"
|
||||
game = "26.3"
|
||||
file = "coords_hud-0.1.0.hmod" # the name players download
|
||||
sha256 = "e113..." # pins the reviewed file
|
||||
signature = """untrusted comment: ...""" # when the project has a key
|
||||
changelog = "" # optional
|
||||
```
|
||||
|
||||
```toml
|
||||
# revoked.toml
|
||||
[[revoked]]
|
||||
id = "bad_mod"
|
||||
sha256 = "1111..." # optional: without it, every version
|
||||
reason = "Sent chat messages it didn't declare."
|
||||
date = "2026-10-24"
|
||||
```
|
||||
|
||||
The submission form's service (`crates/horizon-mod-registry`) writes these entries: it signs the creator in with GitHub, runs the file's checks, uploads the file to the repository's `staging` release as `<sha256>.<extension>` and opens the pull request. Only an entry's `owners` publish updates through it.
|
||||
|
||||
Unknown keys are refused. Its CI runs `cargo-horizon-mod` (`crates/cargo-horizon-mod`):
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `cargo horizon-mod check <file> [--json]` | A submission's checks: its kind's (above), its signature with `--key` and `--signature`, and for a mod a trial run in the sandbox (40 ticks, 120 frames, a chat line, each command). |
|
||||
| `cargo horizon-mod registry check <base> <head> --files <dir>` | `check` on each file `head` lists and `base` doesn't: a pull request's new files. |
|
||||
| `cargo horizon-mod registry list <checkout>` | `<sha256> <id> <version> <file>` per version: the files to download. |
|
||||
| `cargo horizon-mod registry build <checkout> --files <dir> --out index.json` | Reads every entry and its file (`<dir>/<sha256>`), lists every problem, and writes the index with URLs `<release><id>-<version>/<file>` (`--release`, ending in `/`, defaults to the repository's releases). |
|
||||
| `cargo horizon-mod registry review <base> <head> --author <login>` | A pull request's changes: a published file never changes and is never removed (take it down in `revoked.toml`), a kind never changes; changes by a non-owner and to owners or keys are pointed out for the reviewers. |
|
||||
|
||||
How the repository runs (its workflows are in `.github/workflows/`):
|
||||
|
||||
- **A pull request** (`check.yml`) runs `registry review`, downloads each new file from the `staging` prerelease, runs
|
||||
`registry check` on it and builds the index it would publish; the report is the run's summary. The tool is
|
||||
`cargo-horizon-mod` from Horizon at the commit in the repository variable `HORIZON_REV`. The form's pull requests
|
||||
are opened with the token of the account in the variable `REGISTRY_BOT`; for those, the author is the login in the
|
||||
body's `<!-- submitted-by: … -->` line.
|
||||
- **Merging** needs the `check` run and a CODEOWNER's review. `publish.yml` then makes each new version a release
|
||||
`<id>-<version>` and deploys `index.json` and `icons/` to GitHub Pages:
|
||||
`https://horizon-rust.github.io/horizon-mods/index.json`, served with `Access-Control-Allow-Origin: *`.
|
||||
- **Weekly**, `cleanup.yml` deletes staged copies of published files after 7 days and of files no open pull request
|
||||
lists after 30.
|
||||
- **Taking a file down** is a pull request adding it to `revoked.toml`. Removing an entry outright (a test, a project
|
||||
nobody installed) fails `registry review` on purpose and needs an administrator's merge.
|
||||
|
||||
Kinds open to submissions: `mod`, `modpack`, `resourcepack`, `shader`. A shader pack (`.hshader`) passes
|
||||
`horizon_shader_api::check`, the check the game runs before it loads one (`horizon-shader check`, [shaders.md](../shaders.md)):
|
||||
every stage validated by naga and translated for Vulkan, Metal and DirectX 12; its manifest's id, version and shader
|
||||
API line are its facts. Iris and OptiFine packs are for the Modrinth browser. A new kind gets its own check before it
|
||||
opens.
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-10-26: `projects[].downloads`, GitHub's download counts of a project's files, refreshed every hour.
|
||||
- 2026-10-26: the launcher installs `shader` files into `shaderpacks/`; the sample index lists one (`glow`).
|
||||
- 2026-10-25: `shader` opens, checked by `horizon_shader_api::check`; its files are `.hshader`.
|
||||
- Schema 1 (2026-10-24): kinds `mod`, `modpack`, `resourcepack`, `shader`; `.hpack` format 1.
|
||||
@@ -0,0 +1,770 @@
|
||||
<!-- Generated from crates/horizon-mod-sdk/wit/horizon.wit and the manifest's capabilities by crates/horizon-mods/tests/api_reference.rs; a test fails when this file is out of date. Update it with: HORIZON_WRITE_DOCS=1 cargo test -p horizon-mods --features pack --test api_reference -->
|
||||
# Mod API reference
|
||||
|
||||
The contract between Horizon and its mods: the WIT package `horizon:mods@0.1.0`, every interface, type, function and hook in it, and the capabilities a manifest may list. The guide is [mods.md](../mods.md); stability and versions: [stability.md](stability.md). Manifest `api` lines this game serves: `0.1`, `0.2`, `0.3`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
| In `capabilities` | Grants | The player reads "May …" |
|
||||
| --- | --- | --- |
|
||||
| `game-read` | [`game`](#game) | see your position, health and the time |
|
||||
| `chat` | [`chat`](#chat) | show you chat messages |
|
||||
| `hud` | [`hud`](#hud) | draw text on your screen |
|
||||
| `storage` | [`storage`](#storage) | save its own settings to disk |
|
||||
| `command` | [`command`](#command) | run commands as you, with your permissions |
|
||||
| `registries` | data files (`assets/`, `data/`), no interface | add blocks and items to your worlds |
|
||||
| `ui` | [`ui`](#ui) | show its own screens and a settings page |
|
||||
| `render` | [`render`](#render) | draw on your screen, shake and zoom the camera |
|
||||
| `input` | [`input`](#input) | see the keys you hold and your clicks, take over the use button on its own items and the mouse wheel, and add keys |
|
||||
| `players` | [`players`](#players) | see who is on the server and message them |
|
||||
| `world` | [`world`](#world) | read and change the world's blocks |
|
||||
| `recipes` | [`recipes`](#recipes) | read the game's recipes and items and which recipes you know, place recipes in your crafting grid and, in creative mode, give you items |
|
||||
|
||||
## World `horizon-mod`
|
||||
|
||||
A client mod: runs in the player's game.
|
||||
|
||||
- Imports (what the game offers): `log`, `game`, `chat`, `hud`, `storage`, `command`
|
||||
- Exports (what the mod provides): `events`
|
||||
|
||||
## World `horizon-server-mod`
|
||||
|
||||
A server mod (manifest `side = "server"`, `api = "0.3"`, experimental): runs where the world is simulated, on the dedicated server, a LAN host and single player's own server.
|
||||
|
||||
- Imports (what the game offers): `log`, `storage`, `players`, `world`
|
||||
- Exports (what the mod provides): `server-events`
|
||||
|
||||
## World `horizon-ui-mod`
|
||||
|
||||
A mod with widgets (capability `ui`): the SDK builds every mod against this world. Mods built for `horizon-mod` alone (no `ui-events` export) still load and simply hear nothing of their widgets.
|
||||
|
||||
- Imports (what the game offers): `ui`, `render`, `input`, `recipes`, `browser`, `log`, `game`, `chat`, `hud`, `storage`, `command`
|
||||
- Exports (what the mod provides): `ui-events`, `events`
|
||||
|
||||
## World `ui-host`
|
||||
|
||||
The game's side of `horizon-ui-mod` beyond `horizon-mod`.
|
||||
|
||||
- Imports (what the game offers): `ui`
|
||||
- Exports (what the mod provides): `ui-events`
|
||||
|
||||
## World `render-host`
|
||||
|
||||
The game's side of capability `render` (imports only).
|
||||
|
||||
- Imports (what the game offers): `render`, `input`
|
||||
- Exports (what the mod provides):
|
||||
|
||||
## World `recipes-host`
|
||||
|
||||
The game's side of capability `recipes` (imports only).
|
||||
|
||||
- Imports (what the game offers): `recipes`, `browser`
|
||||
- Exports (what the mod provides):
|
||||
|
||||
## `log`
|
||||
|
||||
Access: always granted.
|
||||
|
||||
The mod's log, in the game's log under the mod's id. Always granted.
|
||||
|
||||
### enum `level`
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `debug` | |
|
||||
| `info` | |
|
||||
| `warn` | |
|
||||
| `error` | |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `write` | `func(level: level, message: string)` | One line in the game's log as `[mod <id>] message`, at that level. |
|
||||
|
||||
## `game`
|
||||
|
||||
Access: capability `game-read`.
|
||||
|
||||
Capability `game-read`: the local player, the world clock and the block the player looks at.
|
||||
|
||||
### record `player`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `f64` | Feet position, in blocks. |
|
||||
| `y` | `f64` | |
|
||||
| `z` | `f64` | |
|
||||
| `yaw` | `f32` | Degrees, as F3 shows them. |
|
||||
| `pitch` | `f32` | Degrees: -90 looks straight up, 90 straight down. |
|
||||
| `health` | `f32` | Hit points as vanilla counts them: 20 is full health without effects. |
|
||||
| `food` | `u32` | 0 to 20, as the hunger bar. |
|
||||
| `dimension` | `string` | `minecraft:overworld`, `minecraft:the_nether`, ... |
|
||||
|
||||
### record `clock`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `game-time` | `u64` | Ticks since the world was created. |
|
||||
| `day-time` | `u64` | 24000 ticks per day; 0 is 6:00 on the first day. |
|
||||
|
||||
### record `hit`
|
||||
|
||||
A block along the player's view.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `block-x` | `s32` | The block hit. |
|
||||
| `block-y` | `s32` | |
|
||||
| `block-z` | `s32` | |
|
||||
| `x` | `f64` | The point on its face where the view meets it. |
|
||||
| `y` | `f64` | |
|
||||
| `z` | `f64` | |
|
||||
| `block` | `string` | `minecraft:stone`, ... |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `get-player` | `func() -> option<player>` | None on the title screen. |
|
||||
| `get-clock` | `func() -> option<clock>` | None on the title screen. |
|
||||
| `raycast` | `func(distance: f32) -> option<hit>` | The first block along the player's view (fluids pass), up to `distance` blocks (at most 1024; past the loaded chunks, across the distant terrain when LOD Distance is on), as of this tick. None when nothing is hit, and on the title screen. |
|
||||
|
||||
## `chat`
|
||||
|
||||
Access: capability `chat`.
|
||||
|
||||
Capability `chat`: messages only the local player sees.
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `show` | `func(text: string)` | Shown in chat after the mod's name, like a system message. |
|
||||
|
||||
## `hud`
|
||||
|
||||
Access: capability `hud`.
|
||||
|
||||
Capability `hud`: one panel of text lines per mod, drawn by the game every frame until the mod changes or clears it (no call per frame).
|
||||
|
||||
### enum `corner`
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `top-left` | |
|
||||
| `top-right` | |
|
||||
| `bottom-left` | |
|
||||
| `bottom-right` | |
|
||||
|
||||
### record `line`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `text` | `string` | Plain text, up to 256 characters. |
|
||||
| `color` | `u32` | 0xRRGGBB. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `set-panel` | `func(corner: corner, lines: list<line>)` | Replaces the mod's panel: up to 16 lines in that corner of the screen. |
|
||||
| `clear` | `func()` | Removes the mod's panel. |
|
||||
|
||||
## `storage`
|
||||
|
||||
Access: capability `storage`.
|
||||
|
||||
Capability `storage`: a per-mod folder of small key/value files, quota'd (docs/mods.md). Keys are `[a-z0-9_.-]{1,64}`, one path segment.
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `get` | `func(key: string) -> option<list<u8>>` | None if `key` has never been `set`, or the key is not well-formed. |
|
||||
| `set` | `func(key: string, value: list<u8>) -> result<_, string>` | Errs on a malformed key or over quota (bytes for the whole mod). |
|
||||
| `delete` | `func(key: string)` | No error if `key` was already absent. |
|
||||
|
||||
## `command`
|
||||
|
||||
Access: capability `command`.
|
||||
|
||||
Capability `command`: vanilla commands run as the player, with the player's permissions, after the hook call returns (8 per call). A mod's own commands are not run this way.
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `run` | `func(line: string)` | `line` without its slash; its feedback goes to the chat as if typed. |
|
||||
| `run-quiet` | `func(lines: list<string>)` | `lines` (each without its slash, at most 256 a call) as `run` does, but with nothing in the chat: no feedback and no failure. In single player and on Horizon's servers, each runs exactly as the player typing it would (the same rights), in order, 64 a tick, and the server logs one line a batch; one that carries a chat message (`say`, `msg`) is refused. On any other server they go as chat commands, and there their feedback shows: a mod running many should say so. |
|
||||
|
||||
## `ui`
|
||||
|
||||
Access: capability `ui`.
|
||||
|
||||
Capability `ui`: widgets the game draws with its own menus' widgets, for a screen the mod owns and a settings page in Options > Mods. Layout and drawing are the game's: a mod says what its widgets hold, never where they sit or how they look, and the game clips everything it is handed.
|
||||
|
||||
### record `text-widget`
|
||||
|
||||
A line of text.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | The mod's own name for the widget; `ui-events` calls echo it. |
|
||||
| `text` | `string` | Up to 256 characters. |
|
||||
| `color` | `u32` | 0xRRGGBB; 0 is the menus' white. |
|
||||
|
||||
### record `item-widget`
|
||||
|
||||
An item's icon and a name beside it, as recipe viewers show them.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | The mod's own name for the widget. |
|
||||
| `item` | `string` | `minecraft:diamond`, `<mod>:<path>`; an unknown id draws a "?". |
|
||||
| `name` | `string` | The name shown beside the icon (empty: the item's own). |
|
||||
|
||||
### record `list-widget`
|
||||
|
||||
Rows the player picks one of, as an options page lists choices: a pick is `ui-events::select`, `selected` is the row to mark now.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | The mod's own name for the widget; `ui-events::select` echoes it. |
|
||||
| `rows` | `list<string>` | The rows' texts, at most 512, each up to 256 characters. |
|
||||
| `selected` | `option<u32>` | The row to mark as the current choice, if any. |
|
||||
|
||||
### record `button-widget`
|
||||
|
||||
A thing on a mod's screen or settings page. A button as the options pages have them (200 × 20): a click is `ui-events::select(id, 0)`. Mods cycle its label for a choice ("Corner: Top Left").
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `u32` | The mod's own name for the widget. |
|
||||
| `label` | `string` | Up to 256 characters. |
|
||||
|
||||
### variant `widget`
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `text` | `text-widget` | Text the player reads. |
|
||||
| `button` | `button-widget` | A button. |
|
||||
| `item` | `item-widget` | An item's icon and name. |
|
||||
| `list` | `list-widget` | Rows the player picks from. |
|
||||
|
||||
### record `screen`
|
||||
|
||||
A screen the mod owns: a title over widgets stacked down the middle, the Done button the game adds, and Esc to leave it.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `title` | `string` | Its title, up to 256 characters. |
|
||||
| `widgets` | `list<widget>` | Its widgets, in order, at most 64. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `open-screen` | `func(screen: screen)` | Show `screen` over the game; a second call while it is open replaces it. Only the last call of one hook counts. |
|
||||
| `close-screen` | `func()` | Close the mod's screen when it is open (the mod hears no `closed` for this: it was its own doing). |
|
||||
| `set-settings` | `func(widgets: list<widget>)` | The mod's page in Options > Mods, reached from a settings button on its row: the widgets it shows, an empty list for no page. The player opening the page is `ui-events::settings-opened`: push fresh widgets then; later calls update the page while it shows. |
|
||||
|
||||
## `ui-events`
|
||||
|
||||
Access: exported by the mod: the game calls it.
|
||||
|
||||
What the game calls on a mod that shows widgets (capability `ui`): what the player did with them.
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `select` | `func(id: u32, row: u32)` | A row of one of the mod's `list` widgets was picked: the widget's `id` and the row, on the screen or the settings page that showed it. |
|
||||
| `closed` | `func()` | The player left the mod's screen (Esc or Done); not for a screen the mod closed itself or one that went away with the mod. |
|
||||
| `settings-opened` | `func()` | The player opened the mod's settings page in Options > Mods: call `ui::set-settings` now to fill it with fresh widgets. |
|
||||
|
||||
## `input`
|
||||
|
||||
Access: capability `input`.
|
||||
|
||||
Capability `input` (experimental: the manifest needs `experimental = ["input"]`): the use button on items the mod claims, and the mod's own keys. Events wait for `poll`, at most 256.
|
||||
|
||||
### record `held-item`
|
||||
|
||||
The main hand's stack.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `string` | `minecraft:spyglass`, `<mod>:<path>`. |
|
||||
| `count` | `u32` | |
|
||||
| `slot` | `u32` | The hotbar slot, 0 to 8. |
|
||||
|
||||
### record `item-use`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `item` | `held-item` | |
|
||||
| `ticks` | `u32` | Game ticks the button was held (0 on `use-start`). |
|
||||
|
||||
### variant `event`
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `use-start` | `item-use` | The use button went down with a claimed item in the main hand. The game does nothing else with the click: no placing, no eating, no swing. |
|
||||
| `use-stop` | `item-use` | It came up again, or the item left the hand, or a screen opened. |
|
||||
| `key-down` | `string` | A key `register-key` named went down or up while playing (no screen open). The game's own mapping on that key still runs. A screen opening lets every held key up. |
|
||||
| `key-up` | `string` | |
|
||||
| `wheel` | `f32` | Mouse wheel steps while the mod holds the wheel (`claim-wheel`): positive away from the player, as the hotbar reads them. |
|
||||
|
||||
### record `mapping-state`
|
||||
|
||||
A mapping as the player has bound it.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `label` | `string` | The bound key's name as Controls shows it: `W`, `Space`, `LMB`. |
|
||||
| `down` | `bool` | Held down now (as of this frame for a mod with a `frame` hook, else this tick). |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `claim-items` | `func(ids: list<string>)` | The items whose use the mod handles instead of the game, by id (at most 64). Replaces the previous list. |
|
||||
| `register-key` | `func(name: string, label: string, key: string) -> result<_, string>` | A key of the mod's: `name` comes back in `key-down` and `key-up`, `label` says what it does, `key` is the key as options.txt writes it (`key.keyboard.r`), the default: the player can bind another one on the mod's page in Options > Mods, and that one wins. At most 16; an unknown key is an error. |
|
||||
| `held` | `func() -> option<held-item>` | The main hand's stack, as of this tick; none when it is empty. |
|
||||
| `poll` | `func() -> list<event>` | The events since the last call, oldest first. |
|
||||
| `claim-wheel` | `func(on: bool)` | Takes the mouse wheel while playing (true) or gives it back: while taken, its steps are the mod's `wheel` events and the hotbar doesn't move. The game gives it back when the mod stops. |
|
||||
| `mapping` | `func(name: string) -> option<mapping-state>` | A movement or click mapping as the player has bound it: `key.forward`, `key.left`, `key.back`, `key.right`, `key.jump`, `key.sneak`, `key.sprint`, `key.attack` or `key.use`. None for another name. |
|
||||
| `clicks-per-second` | `func() -> tuple<u32, u32>` | Clicks of the attack and use buttons in the last second. |
|
||||
|
||||
## `render`
|
||||
|
||||
Access: capability `render`.
|
||||
|
||||
Capability `render` (experimental: the manifest needs `experimental = ["render"]`): shapes the game draws for the mod with its own renderer, kept until the mod replaces them (no call per frame). A mod never supplies shader code.
|
||||
|
||||
### enum `corner`
|
||||
|
||||
A corner of the screen, as `hud`'s panels use them.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `top-left` | |
|
||||
| `top-right` | |
|
||||
| `bottom-left` | |
|
||||
| `bottom-right` | |
|
||||
|
||||
### record `rect`
|
||||
|
||||
A rectangle in GUI pixels: the top left corner and the size.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `f32` | |
|
||||
| `y` | `f32` | |
|
||||
| `w` | `f32` | |
|
||||
| `h` | `f32` | |
|
||||
|
||||
### record `point`
|
||||
|
||||
A point in GUI pixels from the screen's top left.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `f32` | |
|
||||
| `y` | `f32` | |
|
||||
|
||||
### record `fill-shape`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `rect` | `rect` | |
|
||||
| `color` | `u32` | 0xAARRGGBB. |
|
||||
|
||||
### record `quad-shape`
|
||||
|
||||
Any four-cornered shape (a wheel's segment, an arrow): the corners in order round it.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `corners` | `tuple<point, point, point, point>` | |
|
||||
| `color` | `u32` | 0xAARRGGBB. |
|
||||
|
||||
### record `sprite-shape`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `sprite` | `string` | One of the game's GUI sprites, as `gui/sprites/` names them (`hud/heart/full`, `widget/button`), the resource packs' versions included. An unknown one draws nothing. |
|
||||
| `rect` | `rect` | |
|
||||
| `color` | `u32` | A tint, 0xAARRGGBB: 0xFFFFFFFF draws it as it is. |
|
||||
|
||||
### record `item-shape`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `item` | `string` | `minecraft:diamond`, `<mod>:<path>`; an unknown one draws nothing. |
|
||||
| `at` | `point` | The icon's top left. |
|
||||
| `scale` | `f32` | 1.0 is the hotbar's 16 × 16. |
|
||||
|
||||
### record `text-shape`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `text` | `string` | Up to 256 characters, one line. |
|
||||
| `at` | `point` | |
|
||||
| `color` | `u32` | 0xRRGGBB, as `hud`'s lines. |
|
||||
| `centered` | `bool` | Centred on `at.x` instead of starting there. |
|
||||
| `shadow` | `bool` | |
|
||||
|
||||
### variant `shape`
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `fill` | `fill-shape` | |
|
||||
| `quad` | `quad-shape` | |
|
||||
| `sprite` | `sprite-shape` | |
|
||||
| `item` | `item-shape` | |
|
||||
| `text` | `text-shape` | |
|
||||
|
||||
### record `frame-rate`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `fps` | `u32` | Frames drawn in the last second. |
|
||||
| `min` | `u32` | The slowest frame's rate, and the 1% low (the 99th percentile frame time's rate), over the last 5 seconds. |
|
||||
| `low` | `u32` | |
|
||||
|
||||
### record `zoom`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `factor` | `f32` | The field of view is divided by it: 1 is none, at most 10. |
|
||||
| `smooth` | `bool` | Ease in and out through the game's field-of-view easing, as the spyglass does, instead of at once. |
|
||||
| `turn` | `f32` | Mouse turning is multiplied by it while zoomed: 1 keeps the player's sensitivity, at least 0.05. |
|
||||
|
||||
### record `minimap`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `corner` | `corner` | |
|
||||
| `size` | `f32` | Its side in GUI pixels, 16 to 256. |
|
||||
| `blocks-per-pixel` | `f32` | World blocks per map pixel, 0.5 to 4; the map is 128 × 128 pixels. |
|
||||
| `rotate` | `bool` | The map turns with the player, who faces up, and an N on its rim marks north. Otherwise north is up and the player's arrow turns. |
|
||||
| `round` | `bool` | Round, the corners left clear; otherwise square. |
|
||||
|
||||
### record `far-terrain`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `distance` | `u32` | How far the distant terrain reaches, in chunks: 64 to 1024. On integrated graphics other than Apple's the game caps it lower. |
|
||||
| `reduced` | `bool` | Coarser far tiles, as the Far Detail setting's Reduced: 2 to 4 ms less GPU time per frame on integrated graphics. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `set-hud` | `func(shapes: list<shape>)` | Replaces the mod's HUD shapes: up to 1024, drawn every frame in order over the HUD (after the hotbar, under chat and screens), hidden with the HUD (F1). An empty list removes them. |
|
||||
| `gui-size` | `func() -> tuple<f32, f32>` | The screen's size in GUI pixels (width, height), as of this tick. |
|
||||
| `shake` | `func(strength: f32, seconds: f32)` | Shakes the camera: a jolt of `strength` (0 to 1, 1 the hardest) that fades out over `seconds` (at most 10). Jolts add up to 1. The view turns, the player doesn't, and the player's Distortion Effects option scales it (0 turns it off). |
|
||||
| `set-corner` | `func(corner: corner, width: f32, height: f32, shapes: list<shape>)` | Replaces the mod's corner panel: shapes in GUI pixels from the top left of a `width` × `height` box that the game places in `corner`, stacked with the game's overlays and the other mods' panels (under the status effects at the top right), as `hud`'s panels are. |
|
||||
| `clear-corner` | `func()` | Removes the mod's corner panel. |
|
||||
| `text-width` | `func(text: string) -> f32` | The width of one line of text in GUI pixels, in the HUD's font: exact for Latin letters (U+0000 to U+024F), 6 per other character. |
|
||||
| `frame-stats` | `func() -> frame-rate` | The game's frame counter, recomputed twice a second. |
|
||||
| `set-zoom` | `func(zoom: zoom)` | Zooms the camera until the next call (`factor` 1 ends it), only while playing: a screen opening or the spyglass's own view (which wins) leave the camera vanilla's. With several mods, the strongest zoom counts. |
|
||||
| `set-minimap` | `func(minimap: option<minimap>)` | Shows a map of the world around the player in `corner`, stacked with the game's overlays and the mods' panels, until the next call (none hides it). It looks like a map item: vanilla's map colours with their height shading and water depth, and the player's arrow. Underground, under a roof and in the Nether it maps the floor around the player's height. The game keeps the map's tiles and refreshes them within its own frame budget; hidden, they are dropped. One map shows at a time: the first loaded mod's. |
|
||||
| `set-far-terrain` | `func(far-terrain: option<far-terrain>)` | Draws the world beyond the render distance as distant terrain, out to `distance` chunks, until the next call (none ends it). The game builds it on its own workers, from the chunks already loaded or saved and, where nobody has been, from the world's generator; the simulation distance doesn't change. Only worlds whose generator the game has (single player and a LAN world you host) show it. The player's own LOD Distance setting, when on, wins. One far terrain shows at a time: the first loaded mod's. |
|
||||
|
||||
## `events`
|
||||
|
||||
Access: exported by the mod: the game calls it.
|
||||
|
||||
What the game calls. Only the hooks `init` asks for are ever called.
|
||||
|
||||
### record `registration`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `commands` | `list<string>` | Chat commands, without the slash. A name vanilla already uses is refused (vanilla's command runs). |
|
||||
| `tick` | `bool` | Call `tick` every game tick (20 per second while playing). |
|
||||
| `frame` | `bool` | Call `frame` every rendered frame. Budget: 0.1 ms. |
|
||||
| `chat` | `bool` | Call `chat` for each chat line the player receives. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `init` | `func() -> result<registration, string>` | Once, after loading. An error keeps the mod off and is shown. |
|
||||
| `tick` | `func(tick: u64)` | Each game tick while in a world, with the game's tick counter. |
|
||||
| `frame` | `func(seconds: f32)` | Each rendered frame, with the seconds since the last one. |
|
||||
| `command` | `func(name: string, args: string) -> result<option<string>, string>` | `/name args`: ok(some(text)) is shown as feedback, err(text) in red. |
|
||||
| `chat` | `func(text: string)` | Each chat line the player receives, as plain text. |
|
||||
|
||||
## `recipes`
|
||||
|
||||
Access: capability `recipes`.
|
||||
|
||||
Capability `recipes` (experimental): the recipes and items the game knows, for recipe viewers. In single player and on Horizon's servers they are the world's; on any other server they are vanilla 26.3's (a client is sent no others), while `known` follows that server's book.
|
||||
|
||||
### record `stack`
|
||||
|
||||
An item of `items`, by its index there, with a count and the components it has beyond the item's own, in vanilla's item syntax (`[potion_contents={potion:"minecraft:swiftness"}]`), or "".
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `item` | `u32` | |
|
||||
| `count` | `u32` | |
|
||||
| `components` | `string` | |
|
||||
|
||||
### enum `kind`
|
||||
|
||||
The station a recipe is made at.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `crafting-shaped` | |
|
||||
| `crafting-shapeless` | |
|
||||
| `smelting` | |
|
||||
| `blasting` | |
|
||||
| `smoking` | |
|
||||
| `campfire` | |
|
||||
| `stonecutting` | |
|
||||
| `smithing` | |
|
||||
| `brewing` | |
|
||||
|
||||
### record `recipe`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `string` | `minecraft:oak_planks`. |
|
||||
| `kind` | `kind` | |
|
||||
| `category` | `string` | The recipe book's tab (`crafting_building_blocks`, `furnace_food`), or "". |
|
||||
| `group` | `string` | Recipes the book shows on one button share a group, or "". |
|
||||
| `width` | `u32` | A shaped recipe's grid: `slots` holds its cells row by row. 0 for the other kinds. |
|
||||
| `height` | `u32` | |
|
||||
| `slots` | `list<list<stack>>` | Each slot's stacks, any one of which fits; an empty list is an empty cell. Cooking and stonecutting: the input. Smithing: the template, the base, the addition. Brewing: the bottle, the reagent. |
|
||||
| `results` | `list<stack>` | What it makes; several when that varies (a trim on any armour). |
|
||||
| `cooking-time` | `u32` | Cooking: game ticks and experience; 0 for the other kinds. |
|
||||
| `experience` | `f32` | |
|
||||
| `book-id` | `s32` | The recipe book's id for it (one per way the book shows it), as `known` lists them; -1 for the kinds the book doesn't show (campfire, stonecutting, smithing, brewing). |
|
||||
|
||||
### record `item`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `string` | `minecraft:diamond`, `<mod>:<path>`. |
|
||||
| `name` | `string` | In the player's language. |
|
||||
| `tags` | `list<string>` | Its item tags, without the `#`. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `items` | `func() -> list<item>` | Every item, in the registry's order: `stack.item` indexes this list. |
|
||||
| `all` | `func() -> list<recipe>` | Every recipe: the book's in its order, then campfire, stonecutting, smithing and brewing. They don't change while a world is open. |
|
||||
| `known` | `func() -> list<s32>` | The `book-id`s of the recipes the player's recipe book holds now. |
|
||||
| `place` | `func(book-id: s32, all: bool) -> result<_, string>` | Places a recipe in the open screen's grid as a click in the recipe book does (`ServerboundPlaceRecipePacket`): the inventory's grid or a crafting table. `all` places as many as the items allow, as with Shift. Refused while no such screen is open, for a recipe the player's book doesn't hold and for one that can't be placed; one missing items shows as a ghost in the grid, as in the book. |
|
||||
| `give` | `func(stack: stack) -> result<_, string>` | Creative mode only: gives the player the stack, as taking it from the creative inventory does (`ServerboundSetCreativeModeSlot`): the selected slot first, then the hotbar, then the inventory. The count is cut to one stack of the item (64, 16 or 1). |
|
||||
|
||||
## `browser`
|
||||
|
||||
Access: not granted by any capability.
|
||||
|
||||
Capability `ui` (experimental): a recipe viewer's panel beside the game's inventory and container screens (not the creative inventory), on the right of the screen. The game draws it with its own widgets and slots, pages it and keeps its search box; the mod says what it holds. The first loaded mod with a panel shows.
|
||||
|
||||
### record `stack`
|
||||
|
||||
An item to show: its id, a count (1 draws none) and its components beyond the item's own, in vanilla's item syntax, or "".
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `item` | `string` | |
|
||||
| `count` | `u32` | |
|
||||
| `components` | `string` | |
|
||||
|
||||
### record `click`
|
||||
|
||||
A click on an item of the panel, by its index in `set-items`'s list.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `index` | `u32` | |
|
||||
| `right` | `bool` | |
|
||||
|
||||
### record `item-key`
|
||||
|
||||
A key the mod registered (`input.register-key`) went down while the pointer was on an item: a slot of the screen's, or the panel's.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `key` | `string` | |
|
||||
| `stack` | `stack` | |
|
||||
|
||||
### record `slot-click`
|
||||
|
||||
A click on a slot of a recipe the panel shows (`set-recipes`).
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `recipe` | `u32` | The recipe's index in `set-recipes`'s list and the slot's in it; the results come after the slots. |
|
||||
| `slot` | `u32` | |
|
||||
| `stack` | `stack` | The stack the slot showed at that moment. |
|
||||
| `right` | `bool` | |
|
||||
|
||||
### variant `event`
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `clicked` | `click` | |
|
||||
| `search` | `string` | The search box's text, each time the player changes it. |
|
||||
| `key` | `item-key` | |
|
||||
| `slot-clicked` | `slot-click` | |
|
||||
| `action` | `u32` | The action button of the recipe at this index in `set-recipes`'s list. |
|
||||
| `page` | `u32` | The player turned the recipes' page (`set-recipes`): the index of the first recipe now shown. |
|
||||
| `back` | | The back button of `set-recipes`. |
|
||||
| `tab` | `u32` | The player picked the tab at this index of `set-recipes`' tabs. |
|
||||
|
||||
### enum `layout`
|
||||
|
||||
How the game lays out a recipe of `set-recipes`.
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `grid` | A crafting grid of `width` x `height` slots, an arrow and the result. |
|
||||
| `cooking` | One input over a flame, an arrow and the result (furnaces, smokers, campfires). |
|
||||
| `single` | One input, an arrow and the result (the stonecutter). |
|
||||
| `smithing` | Template, base and addition, an arrow and the result. |
|
||||
| `brewing` | A potion and an ingredient, an arrow and the result. |
|
||||
|
||||
### record `recipe-view`
|
||||
|
||||
A recipe to show. Each slot, and the result, cycles through its stacks, one a second; an empty slot shows nothing.
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `layout` | `layout` | |
|
||||
| `width` | `u32` | The grid's size (`grid` only), at most 3 x 3. |
|
||||
| `height` | `u32` | |
|
||||
| `slots` | `list<list<stack>>` | |
|
||||
| `results` | `list<stack>` | |
|
||||
| `note` | `string` | A short line under the recipe (a cooking time), or "". |
|
||||
| `action` | `option<bool>` | The action button beside it: none, enabled, or shown greyed. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `set-items` | `func(items: list<stack>)` | The panel's items, at most 8192 (an empty list hides the panel). |
|
||||
| `set-recipes` | `func(title: string, tabs: list<stack>, tab: u32, recipes: list<recipe-view>)` | Shows these recipes in the panel instead of its items, as many as fit a page, under `title` and a back button, with a row of `tabs` (each an icon; `tab` is the selected one) between them; an empty list of recipes shows the items again. At most 256 recipes and 16 tabs. |
|
||||
| `poll` | `func() -> list<event>` | What the player did on the panel since the last poll, at most 256 kept. |
|
||||
|
||||
## `players`
|
||||
|
||||
Access: capability `players`.
|
||||
|
||||
Capability `players`, server mods only (manifest `api = "0.3"`, experimental): the players on the server, and messages to them.
|
||||
|
||||
### enum `game-mode`
|
||||
|
||||
| Case | |
|
||||
| --- | --- |
|
||||
| `survival` | |
|
||||
| `creative` | |
|
||||
| `adventure` | |
|
||||
| `spectator` | |
|
||||
|
||||
### record `player`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `id` | `string` | The player's UUID, `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. |
|
||||
| `name` | `string` | |
|
||||
| `x` | `f64` | Feet position, in blocks. |
|
||||
| `y` | `f64` | |
|
||||
| `z` | `f64` | |
|
||||
| `dimension` | `string` | `minecraft:overworld`, `minecraft:the_nether`, ... |
|
||||
| `game-mode` | `game-mode` | |
|
||||
| `health` | `f32` | Hit points as vanilla counts them: 20 is full health without effects. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `online` | `func() -> list<player>` | The players online, in the order they joined. |
|
||||
| `tell` | `func(player: string, text: string) -> bool` | A system message to one player, after the mod's name; false when no player with that id is online. Up to 8 messages per call. |
|
||||
| `broadcast` | `func(text: string)` | A system message to every player and the server's log, after the mod's name. Counts toward the same 8 per call. |
|
||||
|
||||
## `world`
|
||||
|
||||
Access: capability `world`.
|
||||
|
||||
Capability `world`, server mods only (manifest `api = "0.3"`, experimental): the blocks of the server's loaded chunks, and changes to them through the server's own block updates.
|
||||
|
||||
### record `block-change`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `s32` | |
|
||||
| `y` | `s32` | |
|
||||
| `z` | `s32` | |
|
||||
| `state` | `string` | `minecraft:oak_log[axis=y]`; properties left out take the block's defaults, `minecraft:stone` alone is the default state. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `get-block` | `func(dimension: string, x: s32, y: s32, z: s32) -> option<string>` | The block state at a position in a dimension (`minecraft:overworld`, ...), as `minecraft:oak_log[axis=y]`, as of this call: none where the server has no chunk loaded there. |
|
||||
| `get-biome` | `func(dimension: string, x: s32, y: s32, z: s32) -> option<string>` | The biome at a position (`minecraft:plains`, ...), as the server stores it (one per 4 x 4 x 4 cell): none where no chunk is loaded. |
|
||||
| `set-blocks` | `func(dimension: string, blocks: list<block-change>) -> result<_, string>` | Sets blocks as `/setblock` does: the old block entity removed, neighbours and shapes updated, light, and every player told. Every state is checked now (an error queues nothing); the changes are made in order after this call returns, before that dimension's next tick; one in a chunk not loaded then is skipped. At most 4096 per call. |
|
||||
|
||||
## `server-events`
|
||||
|
||||
Access: exported by the mod: the game calls it.
|
||||
|
||||
What the server calls in a server mod (manifest `api = "0.3"`, experimental). Only the hooks `init` asks for are ever called. The player in each event is as the server saw them when it happened.
|
||||
|
||||
### type `player`
|
||||
|
||||
`type player = player`
|
||||
|
||||
### record `block-pos`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `x` | `s32` | |
|
||||
| `y` | `s32` | |
|
||||
| `z` | `s32` | |
|
||||
|
||||
### variant `argument-kind`
|
||||
|
||||
What an argument of a command takes. The server sends each command and its arguments to players' games, whose chat box completes and colours them as vanilla's; the server parses them before the mod's `command` hook gets their values.
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `word` | | One word: letters, digits and `_ - . +`. |
|
||||
| `text` | | The rest of the line, spaces included: only last. |
|
||||
| `integer` | `tuple<option<s32>, option<s32>>` | A whole number, within the bounds given. |
|
||||
| `number` | | A number with or without decimals. |
|
||||
| `boolean` | | `true` or `false`. |
|
||||
| `player` | | One online player: a name, or `@s` or `@p` (whoever typed it, as vanilla picks for a player). |
|
||||
| `block-pos` | | A block position: three whole numbers, each one also `~` or `~n`, relative to whoever typed it. |
|
||||
|
||||
### record `argument`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `name` | `string` | Shown in the chat box's hint (`<height>`) and the key of its value. |
|
||||
| `kind` | `argument-kind` | |
|
||||
| `optional` | `bool` | Optional arguments come after every required one. |
|
||||
|
||||
### record `server-command`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `name` | `string` | Without the slash. A name vanilla already uses is refused (vanilla's command runs). |
|
||||
| `arguments` | `list<argument>` | In order; none: the command takes nothing after its name. Free text is one optional `text` argument. |
|
||||
|
||||
### variant `argument-value`
|
||||
|
||||
An argument's value, parsed as its kind says.
|
||||
|
||||
| Case | Payload | |
|
||||
| --- | --- | --- |
|
||||
| `word` | `string` | |
|
||||
| `text` | `string` | |
|
||||
| `integer` | `s32` | |
|
||||
| `number` | `f64` | |
|
||||
| `boolean` | `bool` | |
|
||||
| `player` | `player` | |
|
||||
| `block-pos` | `block-pos` | |
|
||||
|
||||
### record `registration`
|
||||
|
||||
| Field | Type | |
|
||||
| --- | --- | --- |
|
||||
| `commands` | `list<server-command>` | Commands players may type. |
|
||||
| `tick` | `bool` | Call `tick` every server tick (20 per second). |
|
||||
| `joins` | `bool` | Call `player-joined` and `player-left`. |
|
||||
| `chat` | `bool` | Call `chat` for each chat message a player sends. |
|
||||
| `blocks` | `bool` | Call `block-broken` and `block-placed`. |
|
||||
|
||||
| Function | Signature | What it does |
|
||||
| --- | --- | --- |
|
||||
| `init` | `func() -> result<registration, string>` | Once, after loading. An error keeps the mod off and is logged. |
|
||||
| `tick` | `func(tick: u64)` | Each server tick, with the server's tick counter. |
|
||||
| `player-joined` | `func(player: player)` | A player joined the server; they are in `players.online` already. |
|
||||
| `player-left` | `func(player: player)` | A player left the server; they are no longer in `players.online`. |
|
||||
| `chat` | `func(player: player, text: string)` | A chat message a player sent, as plain text. |
|
||||
| `command` | `func(player: player, name: string, args: string, values: list<tuple<string, argument-value>>) -> result<option<string>, string>` | `/name args` typed by `player`: ok(some(text)) is their feedback, err(text) their error in red. `values` holds each argument given, by name, in order (an optional one left out isn't there); a line that doesn't parse never reaches the mod: the player gets vanilla's error. |
|
||||
| `block-broken` | `func(player: player, pos: block-pos, state: string)` | A player broke the block at `pos` in their dimension: `state` is what was there, as `minecraft:oak_log[axis=y]`. |
|
||||
| `block-placed` | `func(player: player, pos: block-pos, state: string)` | A player placed `state` at `pos` in their dimension. |
|
||||
@@ -0,0 +1,125 @@
|
||||
# Mod API
|
||||
|
||||
Mods are Rust crates compiled to sandboxed WebAssembly components. Client mods run in the player's game; server mods
|
||||
(experimental) run wherever a world is simulated: single player, a LAN host, the dedicated server. Design and the runtime: [mods.md](../mods.md).
|
||||
|
||||
## Overview
|
||||
|
||||
A mod today can read the local player and the world clock, show chat lines only that player sees, draw a text panel
|
||||
on the HUD, keep small files of its own, register chat commands, and run on every tick, frame or chat line it asks
|
||||
for. A content mod adds blocks and items from JSON in its `.hmod`, copying vanilla templates, plus tags, recipes,
|
||||
loot, models, textures and structures in its own namespaces. Every capability is declared in the manifest, shown to
|
||||
the player before the mod is turned on, and checked against the component's real imports.
|
||||
|
||||
| | Count |
|
||||
| --- | --- |
|
||||
| Interfaces the game offers | 6: `log`, `game`, `chat`, `hud`, `storage`; `players` for server mods |
|
||||
| Functions in them | 12 |
|
||||
| Hooks a mod exports | 5: `init`, `tick`, `frame`, `command`, `chat`; a server mod 8: `init`, `tick`, `player-joined`, `player-left`, `chat`, `command`, `block-broken`, `block-placed` |
|
||||
| Capabilities | 6: `game-read`, `chat`, `hud`, `storage`, `registries`, `players` |
|
||||
| Manifest `api` lines served | 3: `0.1` (code), `0.2` (content), `0.3` (server mods, experimental) |
|
||||
| SDK | the `Mod` trait (5 methods), `Registration` (4 builders), `export_mod!`, log helpers; `server::ServerMod` (8 methods), `server::Registration` (6 builders), `server::Argument`, `server::Args`, `export_server_mod!` |
|
||||
| `cargo horizon-mod` | `new` (templates `code` and `content`), `build`, `run`, `test`, `check` |
|
||||
| Example mods | `coords-hud` (code), `ruby-content` (content), `welcome` (server) |
|
||||
|
||||
## Guide
|
||||
|
||||
The step-by-step guide, from installing the tools to publishing on the registry, is
|
||||
[Make a mod](https://playhorizon.dev/docs/make-a-mod) on the website. In short:
|
||||
|
||||
1. `rustup target add wasm32-unknown-unknown`, `cargo install --git https://github.com/horizon-rust/horizon
|
||||
cargo-horizon-mod`, then `cargo horizon-mod new my-mod` ([Making a mod](../mods.md#making-a-mod)).
|
||||
2. Implement `Mod` for your type, list the capabilities you import in `horizon-mod.toml`
|
||||
([The manifest](../mods.md#the-manifest)).
|
||||
3. `cargo horizon-mod run` builds the `.hmod` and starts the game with it as a dev mod; after an edit,
|
||||
`cargo horizon-mod build` and the running game reloads it.
|
||||
4. `cargo horizon-mod test` runs the mod's `tests/*.toml` scripts in the sandbox, with no game: ticks, commands,
|
||||
chat lines and the player's position in, chat, feedback and the HUD panel checked
|
||||
([Testing a mod](../mods.md#testing-a-mod)).
|
||||
5. Content mods: [Content mods (API 0.2)](../mods.md#content-mods-api-02).
|
||||
|
||||
## Reference
|
||||
|
||||
[mods-reference.md](mods-reference.md), generated from `crates/horizon-mod-sdk/wit/horizon.wit`: every capability,
|
||||
interface, type, function and hook, with the limits the guide lists (16 HUD lines of 256 characters, 8 chat lines per
|
||||
call, 4 MiB of storage, fuel per call).
|
||||
|
||||
## Examples
|
||||
|
||||
- `crates/horizon-mod-sdk/examples/coords-hud`: coordinates, facing and time on the HUD; `/where`, `/coords`; storage.
|
||||
- `crates/horizon-mod-sdk/examples/ruby-content`: no code; a gem and a block of ruby from vanilla templates, with
|
||||
their recipes, loot, tags, models, textures and names.
|
||||
- `crates/horizon-mod-sdk/examples/welcome`: a server mod; greets players who join, announces newcomers (kept in
|
||||
storage), `/online` and `/mined`.
|
||||
- `crates/horizon-mods/tests/guest`: a mod that misbehaves on command (spins, panics, grows its memory), for the
|
||||
sandbox tests.
|
||||
|
||||
## Ways in
|
||||
|
||||
One API (the WIT world) and one sandbox (WebAssembly), whatever the mod is written in. Every way in produces a
|
||||
component for the same world, checked against the same capabilities and given the same limits; there is no second
|
||||
scripting runtime in the game.
|
||||
|
||||
| Way in | For | Status | Template and example |
|
||||
| --- | --- | --- | --- |
|
||||
| Rust | most mods; the fastest | first-class: the SDK, `cargo horizon-mod` | `cargo horizon-mod new`, `coords-hud` |
|
||||
| Data only | blocks and items with no code | first-class: content mods (API 0.2) | `cargo horizon-mod new --template content`, `ruby-content` |
|
||||
| Lua scripts | people who don't want a compiler | planned: a script mod whose code is a `.lua` file, run by Horizon's own interpreter mod | planned, with the same example as Rust |
|
||||
| C, C++, Zig | authors of those languages | to evaluate: bindings generated from the WIT, built without WASI | one each if it passes |
|
||||
| Go (TinyGo), AssemblyScript, JavaScript / TypeScript | authors of those languages | to evaluate: each needs its toolchain to emit a component without WASI imports, or an interpreter mod as for Lua | one each if it passes |
|
||||
|
||||
- **Scripts run inside the sandbox.** The Lua interpreter is itself a WebAssembly mod, built in Rust with the SDK and
|
||||
shipped by Horizon. A script mod's manifest names the interpreter and its own capabilities; the game links the
|
||||
interpreter with only those, so a script can do exactly what a compiled mod with the same manifest can.
|
||||
- **Hot reload.** The game keeps the interpreter compiled and loads the script into a fresh instance when the file
|
||||
changes, without restarting the game; what it saved with `storage` survives, as for dev mods.
|
||||
- **Measured, not guessed.** Each way in is listed with its measured cost on the same tick-heavy example (time and
|
||||
fuel per tick, memory, load time) against Rust, and a language is supported only with a template, an example and a
|
||||
test that builds and loads it in CI.
|
||||
|
||||
## Stability
|
||||
|
||||
Preview ([stability.md](stability.md)): API 0.1 and content 0.2 are served together; a mod built for 0.1.0 keeps
|
||||
loading on every 0.1.x game. Server mods are their own line, `api = "0.3"`, experimental: they may change in any
|
||||
release.
|
||||
|
||||
## Not there yet
|
||||
|
||||
What real mods need next, in the order modpacks and servers need it:
|
||||
|
||||
- **More for server mods** (`api = "0.3"`, [Server mods](../mods.md#server-mods-api-03-experimental)): they run
|
||||
on the server thread, a LAN host and the dedicated server today, with joins, chat, typed commands in players'
|
||||
command trees, players' block breaking and placing, and `world` (read blocks and biomes, set blocks as `/setblock`
|
||||
does); next entity events, game rules, entity, item and block-state arguments.
|
||||
- **World and blocks**: blocks, block states and biomes are there for server mods (`world`); next block entities,
|
||||
and `world` for client mods (raycasts, single player and LAN hosts).
|
||||
- **Items and inventories**: the player's inventory, held item and open container; item stacks with components.
|
||||
- **Entities**: query and change entities near a position; spawn from a mod's own entity types (data first).
|
||||
- **Block entities and menus**: ticking machines with slots and their screens.
|
||||
- **Screens and UI**: widgets built from vanilla's, a screen the mod owns, a settings page under Options > Mods.
|
||||
- **Networking**: a channel between a mod's two halves, client and server, typed by the mod's own WIT.
|
||||
- **Config**: typed settings the game shows and stores for the mod, on the server too.
|
||||
- **Keys and the crosshair target**: keybinds in Controls; what the player is looking at.
|
||||
- **Script mods and other languages**: see [Ways in](#ways-in).
|
||||
- **SDK**: templates per kind of mod, typed wrappers in the SDK for every interface. Dev mods already reload when
|
||||
rebuilt, and `cargo horizon-mod test` runs a mod headless.
|
||||
|
||||
## Changelog
|
||||
|
||||
- API 0.3: typed server commands: `registration.commands` lists each command's arguments (`argument-kind`: word,
|
||||
text, integer, number, boolean, player, block-pos), sent in players' command trees and parsed by the server;
|
||||
`command` gets their values. SDK `Registration::command_with`, `Argument`, `Args`.
|
||||
- API 0.3: capability `world` (`get-block`, `get-biome`, `set-blocks`); example `beacons`; test scripts' `blocks` and
|
||||
`expect.blocks`.
|
||||
- Server mods run in the game: single player (server thread and in-frame), LAN hosts, the dedicated server.
|
||||
- API 0.3 (server mods, experimental): world `horizon-server-mod`, capability `players`, the `server-events`
|
||||
hooks; SDK `server::ServerMod`; example `welcome`.
|
||||
- SDK: `cargo horizon-mod test` and `horizon_mods::testing`, test scripts in TOML; the template has one.
|
||||
- Experimental: `ui` button widget; `render.set-corner`, `text-width`, `frame-stats`; `input.mapping`,
|
||||
`clicks-per-second` (what the official FPS, keystrokes and zoom mods need).
|
||||
- Experimental `input` (`experimental = ["input"]`): `claim-items`, `register-key`, `held`, `poll` (use-start,
|
||||
use-stop, key-down, key-up).
|
||||
- Experimental `render` (`experimental = ["render"]`): HUD shapes (fill, quad, sprite, item, text), `gui-size`, camera
|
||||
`shake`, drawn by the game's own renderer.
|
||||
- API 0.2 (content): data-driven blocks and items, tags, `horizon:add_to`, terrain adaptation.
|
||||
- API 0.1.0: `log`, `game`, `chat`, `hud`, `storage`; `init`, `tick`, `frame`, `command`, `chat`.
|
||||
@@ -0,0 +1,137 @@
|
||||
<!-- Generated from crates/horizon-server/src (the help text, config::TEMPLATE, api::ROUTES) by reference.rs; a test fails when this file is out of date. Update it with: HORIZON_WRITE_DOCS=1 cargo test -p horizon-server reference -->
|
||||
# Server reference
|
||||
|
||||
Everything `horizon-server` offers a host or a tool: its commands, every `server.toml` key and the control API. How to host: [hosting.md](../hosting.md); the design: [server.md](../server.md); stability and versions: [stability.md](stability.md).
|
||||
|
||||
## Commands
|
||||
|
||||
`horizon-server help` prints:
|
||||
|
||||
```text
|
||||
horizon-server: host a Horizon (Minecraft Java Edition 26.3) server
|
||||
|
||||
Usage: horizon-server [--dir DIR] <command>
|
||||
|
||||
init Set up a server here: the EULA, the world, the port, who may join (asks, or takes flags:
|
||||
--eula --world NAME --seed SEED --port N --whitelist/--no-whitelist --player NAME... --op NAME
|
||||
--offline --yes)
|
||||
run Run the server in the foreground; type console commands, Ctrl-C stops it cleanly. It
|
||||
restarts the game if it crashes.
|
||||
start Run the server in the background (its output goes to logs/supervisor.log)
|
||||
stop, restart
|
||||
status [--json]
|
||||
Up or not, TPS, MSPT, players, RAM and CPU
|
||||
cmd COMMAND...
|
||||
Run a console command on the running server and print what it answers
|
||||
logs [-f] [-n N]
|
||||
The last N lines of the log (default 50); -f follows it
|
||||
backup [NAME]
|
||||
Zip the world and the settings into backups/ (a running server saves first and stays up)
|
||||
backups [--json]
|
||||
List the backups
|
||||
restore BACKUP [--yes]
|
||||
Put a backup's world in place (the current one is kept in backups/); a running
|
||||
server restarts with it
|
||||
service install|uninstall|print [--system]
|
||||
Start the server at boot or login: a systemd unit on Linux (--system: for every user,
|
||||
with sudo), a launchd agent on macOS, a task at logon on Windows
|
||||
panel A one-time login link for the web panel ([panel] in server.toml; 127.0.0.1:8130 by
|
||||
default)
|
||||
update [--check [--json]] [--version TAG] [--timeout SECONDS]
|
||||
Install the newest release (checked against GitHub's SHA-256), back up the world and
|
||||
restart on it; goes back by itself if the game isn't up within the timeout (300 s).
|
||||
--archive FILE --version NAME installs an archive you have
|
||||
rollback Back to the release used before the update
|
||||
config [--json]
|
||||
The settings in server.toml
|
||||
config set KEY=VALUE... [--json]
|
||||
Change settings (for example world.difficulty=hard server.max_players=10), checked first;
|
||||
a running server takes the difficulty, default game mode and whitelist at once, the rest
|
||||
at its next start
|
||||
import FILE Write server.toml from a vanilla server.properties
|
||||
op NAME, deop NAME
|
||||
whitelist on|off|list, whitelist add|remove NAME
|
||||
Who may join (on the running server, or in the files when it is stopped)
|
||||
|
||||
--dir DIR The server folder (default: the current folder)
|
||||
|
||||
Settings live in server.toml; the server writes server.properties from it before every start.
|
||||
```
|
||||
|
||||
## `server.toml`
|
||||
|
||||
Horizon server settings (docs/server.md). The server writes them into server.properties before every start: edit this file, not that one. Unknown keys are refused. `horizon-server init` writes this file with these defaults; `horizon-server config set SECTION.KEY=VALUE` changes it.
|
||||
|
||||
### `[server]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `motd` | `"A Minecraft Server"` | `motd` | the line under the server's name in the server list |
|
||||
| `port` | `25565` | `server-port` | the port players connect to |
|
||||
| `bind` | `""` | `server-ip` | listen on every address, or only on this one (server-ip) |
|
||||
| `max_players` | `20` | `max-players` | how many players can be online at once |
|
||||
| `online_mode` | `true` | `online-mode` | check players' Minecraft accounts (false: anyone joins under any name) |
|
||||
| `whitelist` | `true` | `white-list` | only whitelisted players and operators can join |
|
||||
| `enforce_whitelist` | `false` | `enforce-whitelist` | kick online players taken off the whitelist |
|
||||
| `view_distance` | `10` | `view-distance` | chunks sent around each player |
|
||||
| `simulation_distance` | `10` | `simulation-distance` | chunks around each player where mobs, crops and redstone tick |
|
||||
|
||||
### `[world]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `name` | `"world"` | `level-name` | the world folder |
|
||||
| `seed` | `""` | `level-seed` | empty: a random seed on the first start |
|
||||
| `game_mode` | `"survival"` | `gamemode` | survival, creative, adventure or spectator |
|
||||
| `difficulty` | `"easy"` | `difficulty` | peaceful, easy, normal or hard |
|
||||
| `hardcore` | `false` | `hardcore` | one life: a player who dies becomes a spectator |
|
||||
|
||||
### `[properties]`
|
||||
|
||||
Any other server.properties key, spelled as vanilla spells it, for example: spawn-protection = 16
|
||||
|
||||
### `[engine]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `path` | `""` | | the game program; empty: the one installed beside horizon-server |
|
||||
| `resources` | `""` | | the prepared game files; empty: resources/ here, else the usual places |
|
||||
| `restart_on_crash` | `true` | | start the game again when it crashes |
|
||||
|
||||
### `[backups]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `every` | `"6h"` | | automatic backups while the server runs: 30m, 6h, 1d...; "0" turns them off |
|
||||
| `keep` | `12` | | automatic backups kept (backups made by hand are never deleted) |
|
||||
|
||||
### `[updates]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `keep` | `1` | | other releases kept by updates, besides the one in use and the one rollback goes to |
|
||||
|
||||
### `[panel]`
|
||||
|
||||
| Key | Default | In `server.properties` | What it does |
|
||||
| --- | --- | --- | --- |
|
||||
| `bind` | `"127.0.0.1:8130"` | | the web panel: this computer only, or a tailnet address (100.x); "" turns it off |
|
||||
| `allow_public` | `false` | | true lets bind be any address; never put the panel on the open internet |
|
||||
|
||||
## Control API (`/api/v1`)
|
||||
|
||||
HTTP and JSON on 127.0.0.1. A running server writes its address and a random token to `.horizon/api.json`, readable only by its own user; every request sends `Authorization: Bearer <token>` and a `Host` that is that address. Errors answer `{"error": "..."}` with 400, 401, 403, 404, 405 or 409. The web panel uses the same routes with its session.
|
||||
|
||||
| Method | Path | Takes | Answers | What it does |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `GET` | `/api/v1/status` | nothing | `phase`, `version`, `pid`, `uptime_seconds`, `engine_pid`, `engine_uptime_seconds`, `crashes`, `last_crash`, `sample` (TPS, MSPT, players, RAM, CPU), `update` | How the server does |
|
||||
| `GET` | `/api/v1/metrics` | `?since=T` (seconds since 1970) | `{"samples": [...]}`: one a second, the last 10 minutes | The samples taken after T, for graphs |
|
||||
| `GET` | `/api/v1/logs` | `?since=N` | `{"next": N, "lines": [...]}` | The log lines after the Nth (the last 2000 are kept) |
|
||||
| `POST` | `/api/v1/command` | `{"line": "<one command>"}` | the lines the command logged | Run a console command on the running game |
|
||||
| `POST` | `/api/v1/stop` | nothing | `{"ok": true}` | Stop the game and the supervisor |
|
||||
| `POST` | `/api/v1/restart` | nothing | `{"ok": true}` | Restart the game |
|
||||
| `GET` | `/api/v1/backups` | nothing | `{"backups": [{"name", "bytes"}]}` | List the backups |
|
||||
| `POST` | `/api/v1/backups` | `{"name": "..."}` (optional) | `{"name": "<file>"}` | Back up now (a running game saves first and stays up) |
|
||||
| `POST` | `/api/v1/restore` | `{"name": "<backup>"}` | `{"name": "<backup>"}` | Restart the game with that backup's world (the current one is kept in backups/) |
|
||||
| `POST` | `/api/v1/update` | `{"tag": "...", "timeout": seconds}` (both optional) | `{"ok": true}`; `status` says how it went (`update`) | Restart on that installed release, or download the newest first; goes back if the game isn't up within the timeout |
|
||||
| `POST` | `/api/v1/panel/login` | nothing | `{"url": "..."}` | A one-time login link for the web panel (with the token only, not from the panel) |
|
||||
@@ -0,0 +1,60 @@
|
||||
# Server API
|
||||
|
||||
What a host, a panel or a bot can do with a Horizon server: the `horizon-server` commands, `server.toml` and the
|
||||
control API. How to host: [hosting.md](../hosting.md); the design: [server.md](../server.md).
|
||||
|
||||
## Overview
|
||||
|
||||
`horizon-server` sets up a server folder, runs the game as a dedicated server under a supervisor (restarts on a crash,
|
||||
backups on a schedule, updates with a rollback), and lets other programs manage it through a local HTTP API with a
|
||||
token. The web panel uses that API.
|
||||
|
||||
| | Count |
|
||||
| --- | --- |
|
||||
| Commands | 22 (`init`, `run`, `start`, `stop`, `restart`, `status`, `cmd`, `logs`, `backup`, `backups`, `restore`, `service`, `panel`, `update`, `rollback`, `config`, `config set`, `import`, `op`, `deop`, `whitelist`, `version`) |
|
||||
| `server.toml` keys | 22 in 6 sections, plus `[properties]` for any other vanilla key |
|
||||
| Control API endpoints | 11 under `/api/v1` |
|
||||
|
||||
## Guide
|
||||
|
||||
1. `horizon-server init` in an empty folder, then `horizon-server start` ([hosting.md](../hosting.md)).
|
||||
2. From a script: `horizon-server status --json`, `horizon-server cmd say hi`, `horizon-server backup`.
|
||||
3. From a program: read `.horizon/api.json` (address and token), then `GET /api/v1/status` with
|
||||
`Authorization: Bearer <token>`.
|
||||
|
||||
## Reference
|
||||
|
||||
[server-reference.md](server-reference.md), generated from `horizon-server`'s source: the help text, every
|
||||
`server.toml` key with its default and the `server.properties` key it writes, and every endpoint.
|
||||
|
||||
## Examples
|
||||
|
||||
```sh
|
||||
token=$(jq -r .token .horizon/api.json); address=$(jq -r .address .horizon/api.json)
|
||||
curl -s -H "Authorization: Bearer $token" "http://$address/api/v1/status"
|
||||
curl -s -H "Authorization: Bearer $token" -d '{"line": "list"}' "http://$address/api/v1/command"
|
||||
```
|
||||
|
||||
## Stability
|
||||
|
||||
Stable ([stability.md](stability.md)): the commands, `server.toml`'s keys and `/api/v1`. A new key or endpoint is an
|
||||
addition; a change to one starts `/api/v2`, served beside `/api/v1`.
|
||||
|
||||
## Not there yet
|
||||
|
||||
- **Players**: list with ping and position, kick, ban and pardon, op and deop, the whitelist, as endpoints (today only
|
||||
through `cmd` and the files).
|
||||
- **Console stream**: the log as a stream (server-sent events) rather than `logs?since=N` polling; chat, joins, leaves,
|
||||
deaths and crashes as typed events.
|
||||
- **Metrics**: a plain-text metrics endpoint any monitoring tool can scrape (TPS, MSPT percentiles, players, chunks,
|
||||
entities, RAM, CPU), from the samples `status` already takes.
|
||||
- **Backups**: delete and download one; restore a single dimension.
|
||||
- **Server mods**: list, turn on and off, and their settings, once the mod API runs on the server
|
||||
([mods.md](mods.md)).
|
||||
- **An MCP server** for the same endpoints, so a host's agent manages the server as it manages the game.
|
||||
- **Compatibility tests**: golden replies per endpoint, and a `server.toml` from each release line.
|
||||
|
||||
## Changelog
|
||||
|
||||
- `api::ROUTES` lists the endpoints; the reference is generated from it.
|
||||
- `/api/v1`: status, metrics, logs, command, stop, restart, backups, restore, update, panel login.
|
||||
@@ -0,0 +1,162 @@
|
||||
# Shader API reference (0.1)
|
||||
|
||||
Generated from `crates/horizon-shader-api` (`HORIZON_WRITE_DOCS=1 cargo test -p horizon-shader-api --test reference`
|
||||
rewrites it; the test fails when it drifts). The API's design and pack format: [shaders.md](shaders.md).
|
||||
|
||||
A stage is a function your WGSL defines, with the signature below; Horizon puts the prelude (these structs, the
|
||||
resources and helpers) before your files and calls the function from its own pass. Your files declare no
|
||||
bindings and no entry points, and no names starting with `horizon_`.
|
||||
|
||||
## Stages
|
||||
|
||||
| Stage | In `horizon-shaders.toml` | Your function |
|
||||
| --- | --- | --- |
|
||||
| sky | `[sky]` | `fn sky(input: SkyInput) -> vec4<f32>` |
|
||||
| material | `[material]` | `fn material(input: MaterialInput) -> Surface` |
|
||||
| post | `[[post]]` | `fn post(input: PostInput) -> vec4<f32>` |
|
||||
|
||||
## sky
|
||||
|
||||
`fn sky(input: SkyInput) -> vec4<f32>`
|
||||
|
||||
### `SkyInput`
|
||||
|
||||
One direction of the sky, for `fn sky(input: SkyInput) -> vec4<f32>`. The sun, moon, stars and clouds are drawn over the result.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `direction` | `vec3<f32>` | Unit view ray, camera-relative world axes. |
|
||||
| `color` | `vec4<f32>` | Vanilla's sky colour along the ray (sky, fog and sunrise). |
|
||||
|
||||
Resources (bind group 3):
|
||||
|
||||
| Binding | Declaration | Meaning |
|
||||
| --- | --- | --- |
|
||||
| 0 | `var<uniform> frame: Frame` | The frame (`Frame`). |
|
||||
|
||||
## material
|
||||
|
||||
`fn material(input: MaterialInput) -> Surface`
|
||||
|
||||
### `MaterialInput`
|
||||
|
||||
One point of a block's surface, for `fn material(input: MaterialInput) -> Surface`.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `albedo` | `vec4<f32>` | The block texture's colour here (alpha 0..1). |
|
||||
| `tint` | `vec4<f32>` | The vertex colour: biome tint, face shade and ambient occlusion. |
|
||||
| `light` | `vec2<f32>` | Block and sky light, 0..1, smoothed over the face. |
|
||||
| `lightmap` | `vec3<f32>` | Vanilla's light colour for that light this frame. |
|
||||
| `position` | `vec3<f32>` | The point, camera-relative, in blocks. |
|
||||
| `uv` | `vec2<f32>` | The texture coordinate inside the block atlas. |
|
||||
| `translucent` | `bool` | Drawn in the translucent pass (water, stained glass, ice). |
|
||||
|
||||
### `Surface`
|
||||
|
||||
What `material` returns. Vanilla's own is `albedo * tint * vec4(lightmap, 1.0)`; Horizon then applies the alpha test (cutout and translucent) and fog.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `color` | `vec4<f32>` | The lit colour, 0..1, with its alpha. |
|
||||
|
||||
Resources (bind group 3):
|
||||
|
||||
| Binding | Declaration | Meaning |
|
||||
| --- | --- | --- |
|
||||
| 0 | `var<uniform> frame: Frame` | The frame (`Frame`). |
|
||||
|
||||
## post
|
||||
|
||||
`fn post(input: PostInput) -> vec4<f32>`
|
||||
|
||||
### `PostInput`
|
||||
|
||||
One pixel of the picture, for `fn post(input: PostInput) -> vec4<f32>`.
|
||||
|
||||
| Field | Type | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `uv` | `vec2<f32>` | Where the pixel is, 0..1 from the top left. |
|
||||
| `pixel` | `vec2<f32>` | The pixel's centre, in pixels from the top left of what the pass draws (the world's size; the window's for the last pass). |
|
||||
| `color` | `vec4<f32>` | The previous pass's colour here (the world's picture for the first pass). |
|
||||
| `depth` | `f32` | The world's depth here, reversed: 0 is the sky. |
|
||||
|
||||
Resources (bind group 3):
|
||||
|
||||
| Binding | Declaration | Meaning |
|
||||
| --- | --- | --- |
|
||||
| 0 | `var<uniform> frame: Frame` | The frame (`Frame`). |
|
||||
| 1 | `var scene_color: texture_2d<f32>` | The world's picture before any pass. |
|
||||
| 2 | `var scene_depth: texture_depth_2d` | The world's depth, reversed (1 near, 0 far and sky). |
|
||||
| 3 | `var previous: texture_2d<f32>` | The previous pass's output (`scene_color` for the first pass). |
|
||||
| 4 | `var linear_sampler: sampler` | Bilinear filtering, clamped to the edges. |
|
||||
| 5 | `var nearest_sampler: sampler` | Nearest texel, clamped to the edges. |
|
||||
|
||||
Helpers:
|
||||
|
||||
- `fn depth_at(uv: vec2<f32>) -> f32`: the world's depth at `uv`.
|
||||
- `fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32>`: the camera-relative position, in blocks, of
|
||||
the point at `uv` and `depth`.
|
||||
|
||||
## The frame
|
||||
|
||||
### `Frame`
|
||||
|
||||
The frame, as `frame` in every stage. Positions and directions are in world axes relative to the camera (x east, y up, z south), so they stay precise far from the world's origin; depth is reversed (1 near, 0 at the far plane and in the sky).
|
||||
|
||||
| Field | Type | Offset | Meaning |
|
||||
| --- | --- | --- | --- |
|
||||
| `view` | `mat4x4<f32>` | 0 | Camera-relative world to view space: the camera's rotation. |
|
||||
| `projection` | `mat4x4<f32>` | 64 | View to clip space (reversed depth, wgpu's clip space: z 0..1). |
|
||||
| `view_inverse` | `mat4x4<f32>` | 128 | View to camera-relative world space. |
|
||||
| `projection_inverse` | `mat4x4<f32>` | 192 | Clip to view space. |
|
||||
| `camera_position` | `vec3<f32>` | 256 | The camera's world position, in blocks (about a millimetre of precision 10 000 blocks out). |
|
||||
| `time` | `f32` | 268 | Seconds since the pack was loaded, wrapping to 0 every hour. |
|
||||
| `sun_direction` | `vec3<f32>` | 272 | Unit vector toward the sun (below the horizon at night). |
|
||||
| `day_time` | `f32` | 284 | The time of day, 0..1: 0 sunrise, 0.25 noon, 0.5 sunset, 0.75 midnight. |
|
||||
| `moon_direction` | `vec3<f32>` | 288 | Unit vector toward the moon. |
|
||||
| `game_time` | `f32` | 300 | Vanilla's GameTime: (game time % 24000 + partial tick) / 24000, 0..1. |
|
||||
| `sky_color` | `vec3<f32>` | 304 | Vanilla's sky colour this frame, 0..1. |
|
||||
| `rain` | `f32` | 316 | Rain strength, 0..1. |
|
||||
| `fog_color` | `vec3<f32>` | 320 | Vanilla's fog colour this frame (also the clear colour), 0..1. |
|
||||
| `thunder` | `f32` | 332 | Thunder strength, 0..1. |
|
||||
| `resolution` | `vec2<f32>` | 336 | The size of the world's picture, in pixels (the resolution scale applied). |
|
||||
| `eye_light` | `vec2<f32>` | 344 | Block and sky light at the camera, 0..1 (level / 15). |
|
||||
| `fog_start` | `f32` | 352 | Where vanilla's fog starts, in blocks from the camera. |
|
||||
| `fog_end` | `f32` | 356 | Where vanilla's fog is full, in blocks. |
|
||||
| `render_distance` | `f32` | 360 | The render distance, in blocks. |
|
||||
| `far` | `f32` | 364 | The far plane, in blocks. |
|
||||
| `night_vision` | `f32` | 368 | Night vision strength, 0..1. |
|
||||
| `blindness` | `f32` | 372 | Blindness strength, 0..1. |
|
||||
| `darkness` | `f32` | 376 | The darkness effect's strength, 0..1. |
|
||||
| `brightness` | `f32` | 380 | The Brightness option, 0..1 (Moody to Bright). |
|
||||
| `eye_in` | `u32` | 384 | What the camera is in: 0 air, 1 water, 2 lava, 3 powder snow. |
|
||||
| `dimension` | `i32` | 388 | 0 the Overworld, -1 the Nether, 1 the End, 2 any other dimension. |
|
||||
| `frame_index` | `u32` | 392 | Frames drawn since the pack was loaded (wraps). |
|
||||
| `hide_gui` | `u32` | 396 | 1 while F1 hides the HUD, else 0. |
|
||||
|
||||
## Options
|
||||
|
||||
Each `[[options]]` entry sets the WGSL `override` of the same name when the pipelines are built:
|
||||
|
||||
| `type` | Keys | The `override` |
|
||||
| --- | --- | --- |
|
||||
| `bool` | `default` | `override <id>: bool` |
|
||||
| `float` | `default`, `min`, `max`, `step` | `override <id>: f32` |
|
||||
| `int` | `default`, `min`, `max` | `override <id>: i32` |
|
||||
| `choice` | `values`, `default` (one of them) | `override <id>: u32`, the value's index |
|
||||
|
||||
## Limits
|
||||
|
||||
- 8 post passes, 64 options, `budget.gpu_ms` up to 16.
|
||||
- 256 KiB per WGSL file, 8 MiB per pack.
|
||||
- Per stage: 20000 naga expressions over all its functions, 64 texture reads (samples,
|
||||
gathers and loads, the prelude's included).
|
||||
|
||||
## The check
|
||||
|
||||
A pack runs only after it passes `horizon_shader_api::check` (the game when it loads a pack, the launcher before it
|
||||
installs one, the registry's CI): each stage is parsed and validated by naga, checked against its signature and
|
||||
the rules above, and translated for Vulkan (SPIR-V), Metal and DirectX 12 (HLSL) with its options at their
|
||||
defaults, minimums and maximums. Every option must be an `override` of its type in some stage; an `override`
|
||||
that is no option needs a default.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Shader API
|
||||
|
||||
Horizon has no shader packs today: the earlier implementation was removed on 2026-10-24. Two things will replace it,
|
||||
designed separately: a **Horizon shader API**, described here, and a clean-room loader for the existing pack format
|
||||
(its own spec; it is not part of this API). Both sit on the same renderer hooks, agreed between the two.
|
||||
|
||||
## Overview: what a shader author will be able to do
|
||||
|
||||
Write WGSL against documented stages of Horizon's renderer, with typed inputs, options the player can change, a
|
||||
reload as soon as a file is saved and an overlay that shows what each pass costs and draws. The renderer keeps
|
||||
everything that makes it fast (culling, batching, LOD, the mesher); a pack changes how surfaces and light look, not
|
||||
how the world is fed to the GPU.
|
||||
|
||||
## The design
|
||||
|
||||
- **A pack** is a folder or a zip with `horizon-shaders.toml` (id, name, version, the `api` line, the stages it
|
||||
uses, its options) and WGSL files. No preprocessor and no other shading language: WGSL is what wgpu validates and
|
||||
compiles for Vulkan, Metal and DirectX 12.
|
||||
- **Stages, not pipelines.** A stage is a function with a fixed signature that the renderer calls inside its own pass,
|
||||
for example a terrain material (`fn material(input: TerrainInput) -> Surface`), the lighting of the visible
|
||||
surfaces, the sky, an optional shadow pass, and a chain of full-screen post passes ending in the frame. A pack
|
||||
that leaves a stage out gets vanilla's look for it.
|
||||
- **Typed inputs.** Each stage receives structs (the frame: time, camera, sun and moon, weather, fog; the surface:
|
||||
position, normal, block and its light) whose WGSL is generated from the Rust types the renderer uploads. The
|
||||
generated reference lists every field with its unit and range, and a test fails when it drifts.
|
||||
- **Options are WGSL `override` constants.** The manifest declares each option (on/off, a number in a range, a choice)
|
||||
with its default; the game shows them on the pack's settings screen and sets them when it builds the pipeline. No
|
||||
text substitution, and every option combination is validated the same way.
|
||||
- **Hot reload.** The game watches the pack's folder. A saved file is validated with naga and the stage rebuilt; on an
|
||||
error the last good version keeps drawing and the error, with its file and line, is shown in the overlay.
|
||||
- **Debug overlay.** GPU time per stage and pass (timestamp queries), the targets each pass writes (show any of them
|
||||
full-screen), the pack's options and its compile errors.
|
||||
- **Budgets.** A pack declares the targets and passes it needs; the renderer refuses more than its limits, and the
|
||||
benchmark ([benchmark-report.md](../benchmark-report.md)) records the pack and its cost, so "fast" stays measured.
|
||||
- **Off by default.** Without a pack the renderer is vanilla's look, and no pack code runs.
|
||||
|
||||
## The pack format (schema 1)
|
||||
|
||||
A pack is a folder, or the same files zipped as a `.hshader`, with `horizon-shaders.toml` at its root
|
||||
(crate `horizon-shader-api`; the sample with every key is
|
||||
`crates/horizon-shader-api/tests/samples/schema-1/`):
|
||||
|
||||
```toml
|
||||
schema = 1 # the manifest's format; a reader refuses schemas newer than it knows
|
||||
id = "soft_light" # [a-z0-9_]{1,64}: the settings key and the registry id
|
||||
name = "Soft Light"
|
||||
version = "1.0.0"
|
||||
api = "0.1" # the shader API line the WGSL is written for
|
||||
description = "..." # optional, like authors and license
|
||||
shared = ["common.wgsl"] # optional: WGSL put before every stage's file, in order
|
||||
|
||||
[sky] # each stage is optional, but a pack has at least one
|
||||
file = "sky.wgsl"
|
||||
|
||||
[material]
|
||||
file = "material.wgsl"
|
||||
|
||||
[[post]] # full-screen passes, in the order they run (8 at most)
|
||||
file = "post/grade.wgsl"
|
||||
name = "Colour grade" # optional: the GPU time overlay's label
|
||||
|
||||
[[options]] # 64 at most; `id` is the WGSL `override` it sets
|
||||
id = "saturation"
|
||||
name = "Saturation"
|
||||
type = "float" # bool, float (min, max, step), int (min, max), choice (values)
|
||||
default = 1.1
|
||||
min = 0.0
|
||||
max = 2.0
|
||||
step = 0.05
|
||||
|
||||
[budget]
|
||||
gpu_ms = 1.5 # optional: the GPU time the pack aims to add (0 to 16 ms)
|
||||
```
|
||||
|
||||
An option's `override` has the type of its kind: `bool`, `f32`, `i32`, or `u32` for a choice (the index of the
|
||||
value). The pack holds only the manifest, the `.wgsl` files it names (each used, UTF-8, 256 KiB at most),
|
||||
`README.md`, `LICENSE` (`.md`, `.txt`) and `icon.png`, under plain paths, 8 MiB in all. A `.hshader` is built
|
||||
the same way every time (files in path order, fixed dates), so a pack's SHA-256 depends only on its files.
|
||||
|
||||
## Guide, reference and examples
|
||||
|
||||
- [shaders-reference.md](shaders-reference.md): every stage's signature, input and output structs, resources and
|
||||
helpers, the frame's fields with their units, option types and limits, generated from `crates/horizon-shader-api`.
|
||||
- [The guide](../shaders.md): making a pack with `horizon-shader new`, `check` and `pack`; the templates and the
|
||||
example pack (`crates/horizon-shader-api/packs/golden_hour`).
|
||||
|
||||
## Stability
|
||||
|
||||
Not started. The first stages will be experimental (opt-in), then preview once a reference, an example and a test
|
||||
pack exist ([stability.md](stability.md)).
|
||||
|
||||
## Not there yet
|
||||
|
||||
Option values other than the defaults (the three stages run, with hot reload), then the overlay, lighting and shadow
|
||||
stages.
|
||||
|
||||
## Changelog
|
||||
|
||||
- 2026-10-26: the material stage in the game: terrain's surfaces through the pack's `material`.
|
||||
- 2026-10-26: the sky stage in the game: the pack's `sky` gets vanilla's colour along each ray.
|
||||
- 2026-10-26: the post stage in the game (`HORIZON_SHADER_PACK`, hot reload with the last good pack kept).
|
||||
- 2026-10-25: `horizon-shader` (`new`, `check`, `pack`), three templates, the example pack and the guide.
|
||||
- 2026-10-25: the check (naga validation, signatures, options, budgets, SPIR-V/Metal/HLSL translation).
|
||||
- 2026-10-25: the stages of API 0.1 (sky, material, post), their inputs and the generated reference.
|
||||
- 2026-10-25: the pack format, schema 1: `horizon-shaders.toml` and the `.hshader` package.
|
||||
- 2026-10-24: the earlier shader pack code was removed; this API is designed from scratch.
|
||||
@@ -0,0 +1,141 @@
|
||||
# API stability and versioning
|
||||
|
||||
The rules every Horizon API follows ([the APIs](README.md)): how each one is versioned, what the stability levels
|
||||
promise, how something is deprecated and removed, and the tests that keep old mods, scripts and tools working.
|
||||
|
||||
## Versions
|
||||
|
||||
- **Each API has its own version**, separate from the game's release (`v0.2.14`, [versioning.md](../versioning.md))
|
||||
and from Minecraft's. A game release says which API versions it serves.
|
||||
- **Semantic versioning**, `MAJOR.MINOR.PATCH`. A **line** is what stays compatible: `0.1`, `0.2`, … before 1.0,
|
||||
then `1`, `2`, ….
|
||||
- Before 1.0: an addition makes the next patch (`0.1.0` → `0.1.1`); a breaking change starts the next line
|
||||
(`0.2.0`).
|
||||
- From 1.0: an addition makes the next minor (`1.0` → `1.1`); a breaking change starts the next major (`2.0`).
|
||||
- **Where the version lives:**
|
||||
|
||||
| API | The version | A user names the line they need |
|
||||
| --- | --- | --- |
|
||||
| Mods | the WIT package (`horizon:mods@0.1.0`) | `api = "0.1"` in `horizon-mod.toml` |
|
||||
| Agent control | the control protocol's version (planned: a `hello` reply and MCP `serverInfo`) | `horizon-ctl --api 1` (planned) |
|
||||
| Server control API | the path (`/api/v1/`) | the path |
|
||||
| `server.toml` | the file's keys; a key is never renamed in place | nothing: old files keep loading |
|
||||
| Launcher commands | the command names and their `--json` output | nothing |
|
||||
| Data formats | a `version` or `schema` field in the file | the reader rejects versions newer than it knows |
|
||||
|
||||
## What counts as a breaking change
|
||||
|
||||
Breaking: removing or renaming anything; changing a type, a default, a unit or a meaning; making something optional
|
||||
required; refusing input that was accepted; any change to a WIT type a function uses, a new `enum` or `variant` case
|
||||
included, in either direction (the runtime links a component only when each imported function's types match exactly:
|
||||
a mod built before the change no longer loads); a JSON field that changes type.
|
||||
|
||||
Not breaking: a new function, interface, capability, command, endpoint, tool or `server.toml` key; a new optional
|
||||
argument or JSON field (readers must ignore fields they don't know); a new WIT function or type (mods built earlier
|
||||
don't import it); faster, or fixing behaviour to what the reference already says.
|
||||
|
||||
### Mod API levels
|
||||
|
||||
A new WIT function doesn't break older mods, but a mod that calls it can't run on a Horizon from before it. So every
|
||||
function the game offers mods has an **API level**, the one it arrived at (`crates/horizon-mods/src/api_level.rs`):
|
||||
level 0 is the contract of Horizon 0.2.67 to 0.2.69, level 1 adds `render.set-minimap` and server mods' `players`, level 2 `game.raycast`, level 3 `command.run-quiet`, level 4 server mods' `world`, level 5 `recipes` (`items`, `all`, `known`), level 6 `browser` (`set-items`, `poll`, `set-recipes`) and `recipes.place` and `recipes.give`, level 7 `render.set-far-terrain`. A mod needs the level of
|
||||
the newest function it imports (a component imports only the functions it calls). The level is read from the file,
|
||||
never declared:
|
||||
|
||||
- the game refuses a mod that needs a function it doesn't have, with "made for a newer Horizon (it uses
|
||||
render.set-minimap): update Horizon to run it", instead of a linking error;
|
||||
- the registry's CI writes each mod version's level into the index (`api_level`), and installers take the newest
|
||||
version the running game can run.
|
||||
|
||||
A pull request that adds a function lists it in `api_level.rs` at the next level and raises `LEVEL`; the
|
||||
`api_reference` test fails until it does.
|
||||
|
||||
## Stability levels
|
||||
|
||||
Every part of an API has one level, shown in its reference and its surface page.
|
||||
|
||||
| | Experimental | Preview | Stable |
|
||||
| --- | --- | --- | --- |
|
||||
| Who may use it | opt-in only (a flag or a manifest field naming it); never in a release default | anyone; marked "preview" where players see it (a mod's row on the Mods page) | anyone |
|
||||
| May change | in any release, without notice | only at a new line, after a deprecation | only at a new major, after a deprecation |
|
||||
| Deprecation window | none | at least 30 days, with one release line serving old and new | at least 180 days, with the old major served beside the new one |
|
||||
| Needs | a design note | a generated reference, a guide section, an example, compatibility tests | the same, plus one line of real use without a break |
|
||||
|
||||
- **Promotion.** Experimental → preview when it has what the table asks. Preview → stable when real users (a published
|
||||
mod, a host's script, an agent's tool) used it for a whole line and nothing broke. The maintainers' orchestrator
|
||||
proposes the promotion with the evidence (the compatibility tests, the complete docs, that line of use) and Ludovic
|
||||
approves it.
|
||||
- **Horizon-only by design.** An API is stable only for what Horizon itself does; a vanilla concept it exposes (a block
|
||||
id, a command, a `server.properties` key) changes when vanilla changes it, with a version port
|
||||
([version-ports.md](../version-ports.md)), and the changelog says so.
|
||||
|
||||
## Deprecation and removal
|
||||
|
||||
1. The pull request that deprecates marks it where it is defined (a `/// Deprecated: use X; goes in 0.3.` doc comment,
|
||||
WIT's `@deprecated`, a `deprecated` field in a tool's schema) and adds the replacement in the same release.
|
||||
2. The reference shows it as deprecated, and the surface page's changelog says what replaces it and when it goes.
|
||||
3. Using it logs one warning per mod, session or client, naming the replacement; it keeps working.
|
||||
4. It is removed only after the window, and only at the next line (preview) or major (stable).
|
||||
5. A line or major that is no longer served is refused with a clear message (`needs API 0.1; this Horizon serves 0.3
|
||||
and 0.4`), never loaded half-way.
|
||||
|
||||
## Compatibility tests
|
||||
|
||||
Every preview and stable API has tests that run old users against the current code, in CI:
|
||||
|
||||
| API | The test |
|
||||
| --- | --- |
|
||||
| Mods | a test mod per served line, built from source against that line's WIT and SDK, loaded by today's runtime with expected output (`crates/horizon-mods/tests/`); a line is served as long as its test mod passes |
|
||||
| Agent control | a transcript per protocol version: commands and the reply keys each must have |
|
||||
| Server | `api::ROUTES` against `route()` (generated reference); a `server.toml` from each release line parses to the same settings; golden replies per endpoint |
|
||||
| Launcher | the generated reference; golden `--json` output per command |
|
||||
| Data formats | a sample file of every version a reader accepts, read by today's reader |
|
||||
|
||||
The generated references are the API's own diff: a pull request that changes an API changes its reference, and the
|
||||
reviewer applies the rules above to that diff.
|
||||
|
||||
## Changelog
|
||||
|
||||
Each surface page ends with a changelog: one line per change, newest first, with the API version and the pull request.
|
||||
Deprecations and breaking changes are in bold. The release notes of the game release that ships a change link it.
|
||||
|
||||
## Today
|
||||
|
||||
| Part | Level | Version |
|
||||
| --- | --- | --- |
|
||||
| Mod API: `log`, `game`, `chat`, `hud`, `storage`, `command`, `events` | preview | 0.1.0 |
|
||||
| Mod API: `ui`, `ui-events` (a mod's screens and settings page) | experimental | manifest `experimental = ["ui"]` |
|
||||
| Mod API: `render` (HUD shapes, camera shake and zoom; drawn by the game, no mod shaders) | experimental | manifest `experimental = ["render"]` |
|
||||
| Mod API: `input` (claimed items' use, the mod's keys, the mouse wheel) | experimental | manifest `experimental = ["input"]` |
|
||||
| Content mods (`registries`: data-driven blocks and items) | preview | manifest `api = "0.2"` |
|
||||
| Server mods: world `horizon-server-mod`, `players`, `server-events`, `world`, typed `server-command`s | experimental | manifest `api = "0.3"` |
|
||||
| Script mods (Lua run by an interpreter mod, [mods.md](mods.md#ways-in)) | not started | |
|
||||
| `cargo horizon-mod` (`new`, `build`, `run`), `horizon-mod-sdk` | preview | 0.1.0 |
|
||||
| Shader API | not started | |
|
||||
| `horizon-ctl` commands and MCP tools | preview | none yet (unversioned) |
|
||||
| The game's own control-socket commands outside `horizon-ctl`'s table (test hooks) | experimental | |
|
||||
| `horizon-server` commands, `server.toml` | stable | `horizon-server --version` |
|
||||
| Server control API | stable | `/api/v1` |
|
||||
| `horizon-launcher` commands and MCP tools | preview | none yet (unversioned) |
|
||||
| Benchmark report | stable | `"version": 1` |
|
||||
| Telemetry batch and consent | stable | `"schema": 1`, consent `2` |
|
||||
| `horizon-graphics.txt`, `horizon-extras.txt`, `horizon-mods.txt` | preview | none |
|
||||
| `SHA256SUMS` and `SHA256SUMS.sig` | stable | |
|
||||
|
||||
### The experimental mod APIs' choices
|
||||
|
||||
- **What a mod may take from the game.** `input.claim-wheel` is the one input a mod consumes: while it holds the
|
||||
wheel, the steps are its `wheel` events and the hotbar doesn't move. `claim-items` takes the use button only on the
|
||||
items it names. A mod's keys (`register-key`) never consume: vanilla's mapping on the same key still runs. Whether
|
||||
keys may consume too is decided before `input` becomes preview.
|
||||
- **`input`'s `wheel` event** is a new case in a variant the game sends, so it breaks mods built before it (allowed
|
||||
because `input` is experimental): an `input` mod that calls `poll` needs a rebuild against the new WIT.
|
||||
- **`render.set-minimap`** draws a map the game makes, not one a mod composes: reading the blocks around the player
|
||||
for a map image costs too much through hook calls, so the game keeps the tiles (vanilla's map colours) and the mod
|
||||
picks the corner, side, scale and shape. One map shows at a time, the first loaded mod's. It is a new function and a
|
||||
new type, so mods built before it keep loading.
|
||||
- **`render.set-far-terrain`** turns on the game's own distant terrain, not one a mod builds: sampling the generator
|
||||
and meshing thousands of far tiles runs on the game's workers within its frame and upload budgets, which no hook
|
||||
call could match. The mod picks the distance (64 to 1024 chunks, capped lower on integrated graphics as LOD Distance
|
||||
is) and Far Detail's Reduced. The player's own LOD Distance, when on, wins; one far terrain shows at a time, the
|
||||
first loaded mod's.
|
||||
Reference in New Issue
Block a user