Merge branch 'main' into submit/viahorizon-0.8.7-dev
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# Native mods on the newest Nightly, as players' launchers build them (scripts/native_compat.py):
|
||||
# - on a pull request, each new native-mod version, applied together with the most downloaded native mods
|
||||
# and compiled (`cargo check`); its support modules' names against every registry mod's;
|
||||
# - every day after the Nightly, every native mod's newest version, applied, then the rest compiled
|
||||
# (`cargo check`): the ones horizon-mixins refuses alone and the ones whose own code doesn't compile go
|
||||
# to the `native-health` release, which publish adds to the index (`broken_on`) so the launcher warns
|
||||
# before an install.
|
||||
# The check compiles the submission, whose crates may have build scripts: it runs with a token that can
|
||||
# only read, and main's script, never the pull request's.
|
||||
name: native-compat
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths: ["mods/*.toml"]
|
||||
schedule:
|
||||
- cron: "40 18 * * *"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
horizon:
|
||||
description: "Horizon commit, or `latest` (the newest Nightly)"
|
||||
default: latest
|
||||
upload:
|
||||
description: "Publish the result (native-health release); `false` for a trial run"
|
||||
default: "true"
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: native-compat-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
# The Rust the launcher builds native mods with (Horizon's launcher/src/rust_toolchain.rs).
|
||||
RUST: "1.99.1"
|
||||
jobs:
|
||||
check:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: head
|
||||
persist-credentials: false
|
||||
- name: Does it change a native mod?
|
||||
id: native
|
||||
run: |
|
||||
if [ -n "$(python3 base/scripts/native_compat.py new base head)" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
run: rustup toolchain install $RUST --profile minimal && rustup default $RUST
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config
|
||||
- if: steps.native.outputs.changed == 'true'
|
||||
name: On the newest Nightly, with the popular native mods
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 base/scripts/native_compat.py check base head | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
health:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
# Each mod horizon-mixins refuses costs one more apply of the whole set.
|
||||
timeout-minutes: 300
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: python3 -m unittest discover -s scripts -p 'test_*.py'
|
||||
- run: rustup toolchain install $RUST --profile minimal && rustup default $RUST
|
||||
- run: sudo apt-get update && sudo apt-get install -y libasound2-dev libudev-dev pkg-config
|
||||
# Yesterday's dependencies, checked, and horizon-mixins: only Horizon's own crates and the mods compile again.
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
native/target/compat/cargo
|
||||
native/target/horizon-mixins
|
||||
key: native-health-${{ env.RUST }}-${{ github.run_id }}
|
||||
restore-keys: native-health-${{ env.RUST }}-
|
||||
- name: Every native mod's newest version on the newest Nightly
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
HORIZON: ${{ inputs.horizon || 'latest' }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 scripts/native_compat.py health --horizon "$HORIZON" --out native-health.json | tee -a "$GITHUB_STEP_SUMMARY"
|
||||
- name: Keep it for publish
|
||||
if: inputs.upload != 'false'
|
||||
run: |
|
||||
gh release view native-health >/dev/null 2>&1 \
|
||||
|| gh release create native-health --prerelease --title "Native mods' health" \
|
||||
--notes "native-health.json: the native mods that don't apply on the newest Nightly (native-compat.yml). publish adds them to the index as broken_on."
|
||||
gh release upload native-health native-health.json --clobber
|
||||
@@ -1,9 +1,9 @@
|
||||
# Merging publishes: each new version's file becomes a release asset
|
||||
# (<id>-<version>/<file>), then index.json and the icons go to GitHub Pages
|
||||
# (https://horizon-rust.github.io/horizon-mods/index.json), which the launcher and
|
||||
# (<id>-<version>/<file>), then index.json, the icons and the screenshots go to GitHub
|
||||
# Pages (https://horizon-rust.github.io/horizon-mods/index.json), which the launcher and
|
||||
# playhorizon.dev/mods read. Nothing is published if any entry fails its checks.
|
||||
# Every hour it runs again to refresh each project's `downloads`: GitHub's
|
||||
# download counts of its release assets.
|
||||
# Every hour it runs again to refresh each project's `downloads` (GitHub's download
|
||||
# counts of its release assets) and `stars` (its source repository's GitHub stars).
|
||||
name: publish
|
||||
on:
|
||||
push:
|
||||
@@ -73,9 +73,48 @@ jobs:
|
||||
| if $n > 0 then .downloads = $n else . end)' site/index.json > index.counted.json
|
||||
mv index.counted.json site/index.json
|
||||
jq -r '.projects[] | "\(.id) \(.downloads // 0)"' site/index.json
|
||||
- name: GitHub stars
|
||||
run: |
|
||||
# A project's stars are its source repository's (owner/repo of a
|
||||
# https://github.com/ source, a folder of it or .git included), asked
|
||||
# once per repository. One that can't be read is left out, not zero.
|
||||
jq -r '.projects[].source // empty' site/index.json \
|
||||
| sed -nE 's#^https://github\.com/([A-Za-z0-9-]+)/([A-Za-z0-9._-]+).*#\1/\2#p' \
|
||||
| sed -E 's/\.git$//' | tr 'A-Z' 'a-z' | sort -u \
|
||||
| while read -r repo; do
|
||||
n="$(gh api "repos/$repo" --jq .stargazers_count 2>/dev/null)" || continue
|
||||
echo "{\"key\": \"$repo\", \"value\": $n}"
|
||||
done | jq -s from_entries > stars.json
|
||||
jq --slurpfile stars stars.json '
|
||||
.projects |= map(
|
||||
(first((.source // "") | ascii_downcase
|
||||
| capture("^https://github[.]com/(?<r>[a-z0-9-]+/[a-z0-9._-]+)").r
|
||||
| sub("[.]git$"; "")) // "") as $r
|
||||
| ($stars[0][$r] // 0) as $n
|
||||
| if $n > 0 then .stars = $n else . end)' site/index.json > index.starred.json
|
||||
mv index.starred.json site/index.json
|
||||
jq -r '.projects[] | "\(.id) \(.stars // 0)"' site/index.json
|
||||
- name: Native mods that don't apply on the newest Nightly
|
||||
run: |
|
||||
# native-compat.yml's daily health: the version each names gets `broken_on`, the Horizon
|
||||
# commit horizon-mixins refused it on and its code, so the launcher warns before an install.
|
||||
if gh release download native-health --pattern native-health.json --dir health 2>/dev/null; then
|
||||
jq --slurpfile h health/native-health.json '
|
||||
$h[0] as $health
|
||||
| .projects |= map(. as $p | .versions |= map(
|
||||
($health.broken[$p.id] // null) as $b
|
||||
| if $b != null and $b.version == .version
|
||||
then .broken_on = [{horizon: $health.horizon, code: $b.code}] else . end))' \
|
||||
site/index.json > index.health.json
|
||||
mv index.health.json site/index.json
|
||||
jq -r '.projects[] | select(.versions[0].broken_on) | "\(.id) \(.versions[0].version): \(.versions[0].broken_on[0].code)"' site/index.json
|
||||
fi
|
||||
- name: The site
|
||||
run: |
|
||||
cp -R icons site/icons
|
||||
# Screenshots are served beside the icons: the index lists them at
|
||||
# <site>screenshots/<id>/<name>.
|
||||
if [ -d screenshots ]; then cp -R screenshots site/screenshots; fi
|
||||
cat > site/index.html <<'HTML'
|
||||
<!doctype html><meta charset="utf-8"><title>Horizon mods</title>
|
||||
<meta http-equiv="refresh" content="0; url=https://playhorizon.dev/mods">
|
||||
|
||||
@@ -11,9 +11,10 @@ with no file, network or clock access, and can only do what their page lists.
|
||||
Native source mods (`.hnmod`, kind `native-mod`) are built into an isolated game by
|
||||
a launcher with native-mod support. They run with the player’s account permissions.
|
||||
Their bundles pin the exact Horizon and Horizon Mixins revisions used for the build.
|
||||
Other revisions have not been validated. Registry checks inspect the source bundle without
|
||||
executing native code; reviewers must also examine the source and pinned-build
|
||||
validation. Publishing these requires the native-mod changes to `cargo-horizon-mod`
|
||||
Other revisions have not been validated. Registry checks inspect the source bundle and compile it
|
||||
on the newest Nightly with the most downloaded native mods (in a job whose token
|
||||
can only read) without running it; reviewers must also examine the source and
|
||||
pinned-build validation. Publishing these requires the native-mod changes to `cargo-horizon-mod`
|
||||
and the launcher to have shipped first.
|
||||
|
||||
**Who is responsible.** Except the projects marked Official (made by the Horizon project), everything here is made and
|
||||
@@ -65,6 +66,7 @@ why.
|
||||
| `.github/workflows/check.yml` | A pull request's checks |
|
||||
| `.github/workflows/publish.yml` | On merge: releases, then `index.json` on Pages |
|
||||
| `.github/workflows/cleanup.yml` | Weekly: staging files nobody needs any more |
|
||||
| `.github/workflows/native-compat.yml` | Native mods on the newest Nightly, as the launcher builds them: a pull request's new version applied and compiled with the most downloaded native mods, its module names against every mod's; daily, every mod's newest version, the ones that don't apply marked `broken_on` in the index (`scripts/native_compat.py`) |
|
||||
| `.github/workflows/native.yml` | Horizon's native mods: each changed one's tests, `.hnmod` and game check, all of them together, and weekly on the newest Nightly |
|
||||
| `native/<name>/` | Horizon's own native mods (Zoom, FPS Counter, Keystrokes, Minimap, Recipes, Coordinates HUD): the source of their `.hnmod` files |
|
||||
| `native/tools/` | Shared by them: apply on Horizon, check, build, package, prove (`native/tools/native.py`) |
|
||||
|
||||
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,59 @@
|
||||
id = "ae2"
|
||||
kind = "native-mod"
|
||||
name = "Applied Energistics 2"
|
||||
summary = "Unofficial port of Applied Energistics 2 15.4.12: ME networks, autocrafting, spatial storage and more."
|
||||
description = """
|
||||
An unofficial port of Applied Energistics 2 15.4.12 to Horizon, not made or endorsed by its authors. Please report problems to this port, not to them.
|
||||
|
||||
Rewritten in Rust: ME networks, storage cells, autocrafting, the cable bus with facades, P2P tunnels, spatial storage, the guide and the Fluix Researcher.
|
||||
|
||||
**One file, no separate content mod.**
|
||||
|
||||
### Optional integrations
|
||||
Each is compiled in only when its mod is in the same build, so adding or removing JEI or Jade takes effect at the next build.
|
||||
- **With JEI:** AE2's recipes and information pages, "+" transfer into terminals, and dragging items onto config slots.
|
||||
- **With Jade:** network state, channels, P2P links, charge, crafting and stored power.
|
||||
|
||||
### Missing or not the same
|
||||
- **No spatial dimension.** Horizon mods can't add one. A player in a spatial cell goes to a walled plot far out in the End, so `/tp` and F3 say `minecraft:the_end`. A swap with a player takes up to about a second.
|
||||
- **No Java addons.** AE2's Java API isn't available, and the integrations work only with the Horizon ports of JEI and Jade.
|
||||
- **Two storage buses on one inventory:** only the first reaches it.
|
||||
- **Moving items** costs power through terminals, interfaces, export buses and wireless terminals. An IO port and the crafting terminal's refills pay nothing yet.
|
||||
- **Power** is judged once a second, so a network with too little goes down at the next look and comes back after about 40 ticks, where AE2's is at once and 32.
|
||||
- **Unformed crafting, quantum and spatial blocks** join a network before they're formed.
|
||||
- **Autocrafting gaps:** a pattern input of more than one is a single template, so some plans report items missing that AE2 would find. Blocking mode looks at all six sides, provider priority is ignored, and a machine's request for more than it can make crafts nothing.
|
||||
- **Other differences:** machines have 6 or 4 orientations rather than 24, menus are in English, fluix tools are diamond tools, and guide scenes have no annotations.
|
||||
- **Crafting ETA is an estimate:** 1.1% off on average on the test rig, 2.0% at worst.
|
||||
- **Nested P2P error** is an approximation.
|
||||
- **World height:** parts on cables assume -64 to 319.
|
||||
|
||||
The Fluix Researcher and its trades rely on mixins, so they are the part most likely to break on a Horizon update.
|
||||
|
||||
### Tested
|
||||
Tested on Horizon commit 6475f62, on Windows, in single player, on LAN with one guest, and on a dedicated server with one client, including attacks by a non-operator with a modified client. This version is built for Horizon commit 66f3981, where AE2 alone compiles but hasn't been run, and the JEI and Jade integrations haven't been compiled. Other platforms, more than one guest, pipes from other mods and speed against the Java mod were not tested.
|
||||
|
||||
### Known issue
|
||||
In some test runs, placing a part into a cable cell did nothing. It isn't one specific part, and it's probably lost test clicks, but that isn't proven.
|
||||
|
||||
Textures and models are CC BY-NC-SA 3.0: noncommercial use only."""
|
||||
authors = ["AlgorithmX2", "TeamAppliedEnergistics", "Joeypc6"]
|
||||
owners = ["Joeypc6"]
|
||||
owner_ids = [35495484]
|
||||
license = "LGPL-3.0-only AND CC-BY-NC-SA-3.0"
|
||||
source = "https://github.com/Joeypc6/AE2-Horizon"
|
||||
icon = "icons/ae2.png"
|
||||
categories = ["technology", "utility", "storage"]
|
||||
tags = ["ae2", "applied-energistics", "storage", "autocrafting", "automation"]
|
||||
|
||||
[[versions]]
|
||||
version = "15.4.14"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "ae2-15.4.14.hnmod"
|
||||
sha256 = "584ec574b2620ccb9810a61f30b8bc4e639b5bf819bef7c3612835efbbdc278f"
|
||||
changelog = """
|
||||
First release on the registry, ported from AE2 15.4.12 (Fabric). 15.4.14 is this port's own version number, not an AE2 release.
|
||||
|
||||
Built for Horizon commit 66f3981. It compiles on that Horizon but wasn't run there; it was last tested on 6475f62.
|
||||
|
||||
One file: the blocks, items and textures, and the JEI and Jade integrations, are all in this mod. This replaces the earlier submissions, and fixes the `ae2:debug` vulnerability reported on them, along with others found by further reviews, and many defects from an outside review (PLAN.md, Rounds 24 to 26)."""
|
||||
@@ -14,7 +14,7 @@ draws the hunger details AppleSkin shows over the food bar:
|
||||
Each part can be turned on or off, and the animation changed, in the mod's settings (Options > Mods > AppleSkin).
|
||||
|
||||
Based on AppleSkin by squeek502. See the LICENSE in the source for the original copyright and licence notices."""
|
||||
authors = ["soufi"]
|
||||
authors = ["sillysoufi"]
|
||||
owners = ["soufied"]
|
||||
owner_ids = [47915535]
|
||||
license = "MIT"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
id = "ayynoxs_map_pack"
|
||||
kind = "native-mod"
|
||||
name = "ayynoxs map pack"
|
||||
summary = "Minimap and full-screen world map with waypoints, cave layers and opt-in map sharing between players."
|
||||
description = """
|
||||
A **minimap** and a **full-screen world map** for Horizon, written from scratch as a native mod. Inspired by the well-known Java map mods, but no code or graphics are taken from them.
|
||||
|
||||
## ayynoxs minimap
|
||||
- Built on Horizon's own minimap renderer, with coordinates and facing direction below it
|
||||
- Square or round, north-up or rotating, 4 sizes, zoom with `I` / `O`
|
||||
- **Waypoints:** `B` opens a window for the name, `Backspace` removes the one you aim at. They show on the minimap, on the world map and in the world with their distance in metres.
|
||||
- Automatic death point
|
||||
- Chat commands: `/wp add|del|rename|share|sync|list|clear|help`
|
||||
|
||||
## ayynoxs world map
|
||||
- Full-screen map on `M`: drag or WASD to pan, wheel to zoom
|
||||
- Drawn like a vanilla map item, one block per cell
|
||||
- Saves every chunk you explore, per world/server, dimension and layer
|
||||
- Same layers as the minimap: the surface outside, your head height in caves and in the Nether
|
||||
|
||||
## Sharing (off by default)
|
||||
- `/wp share <nr> <player>` sends a waypoint as a private message, on any server. Only private messages to you can add waypoints.
|
||||
- Live map sharing is **off by default** and chosen per server with `/wp sync on|off`. When on, explored chunks and new waypoints go to the other players who turned it on, through a Horizon host (co-op/LAN, Horizon servers) or the AyynoxsMapSync plugin on Paper servers.
|
||||
- What arrives is limited: only your current level, within 2048 blocks of you, at most 1000 waypoints per world.
|
||||
|
||||
Languages: English (default), Deutsch, Français, Italiano, Русский.
|
||||
|
||||
## What it does and doesn't
|
||||
- **Hooks:** the native hook points (frame, tick, HUD, keys, commands, minimap, settings, mods list, screen keys and characters, mod messages, player join/leave), plus the Advancements screen's draw, mouse, scroll and close (the world map is drawn in its place), `extras_connecting` (the server address, for the map's folder) and the chat HUD's `add` / `add_signed` (to hide shared-waypoint private messages).
|
||||
- **Files:** writes only under `horizon-mods/.horizon-mod-data/ayynoxs_minimap/` (settings, waypoints) and `ayynoxs_worldmap/` (map tiles).
|
||||
- **Network:** no internet access. It only sends Horizon mod messages on `ayynoxs_map:sync` to the server you play on (shares only after `/wp sync on`), and `/msg` commands you start yourself with `/wp share <nr> <player>`.
|
||||
- Pinned to nightly `6475f62` (2026-11-04)."""
|
||||
authors = ["DenoxyYT (Ayynox)"]
|
||||
owners = ["DenoxyYT"]
|
||||
owner_ids = [94991632]
|
||||
license = "MIT"
|
||||
source = "https://github.com/DenoxyYT/ayynoxs-map-pack"
|
||||
icon = "icons/ayynoxs_map_pack.png"
|
||||
categories = ["utility", "adventure", "social"]
|
||||
tags = ["minimap", "world", "map", "waypoints", "multiplayer"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.4.2"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "ayynoxs_map_pack-0.4.2.hnmod"
|
||||
sha256 = "d5cef20531720a594969b7b7acfd20d2d32d644d7b0ae230dd68a46f98172371"
|
||||
changelog = """
|
||||
Changes from the review of 0.4.1:
|
||||
- Map sharing is off by default and chosen per server: /wp sync on (or the setting) turns it on. When a server can pass shares on, the pack says so once in chat.
|
||||
- Waypoints arrive only through private messages to the player (commands.message.display.incoming); the marker in public chat does nothing.
|
||||
- Shared chunks are taken only for the player's current level, a real layer and within 2048 blocks of the player; shared waypoints stop at 1000 per world.
|
||||
Also: the world map is drawn like a map item again (one block per cell), which is cheaper while dragging.
|
||||
Built for Horizon nightly 6475f62 (2026-11-04)."""
|
||||
@@ -0,0 +1,26 @@
|
||||
id = "chat_heads_rust"
|
||||
kind = "native-mod"
|
||||
name = "Chat Heads Horizon"
|
||||
summary = "Chat Heads displays player heads in the chat before the players name making it easier to see who is writing"
|
||||
description = "Chat Heads displays player heads in the chat before the players name making it easier to see who is writing. with options being to turn off your own head display "
|
||||
authors = ["Demerobit"]
|
||||
owners = ["Demerobit"]
|
||||
owner_ids = [193929759]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Demerobit/Chat-Heads-For-Horizon"
|
||||
icon = "icons/chat_heads_rust.png"
|
||||
categories = ["utility", "optimization", "decoration"]
|
||||
tags = ["optimization", "display"]
|
||||
key = "RWQ8B3u8yfza0a/X/QmiRXnrl8+l/dZFkWlWTSPsfshlLWbzHlb3AjU4"
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.2"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "chat_heads_rust-1.0.2.hnmod"
|
||||
sha256 = "b4dd7f7b8e5689dc0718821d7926ff3e3c9107fc11737903c195aed19c6cb7a1"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key
|
||||
RUQ8B3u8yfza0SgDJ0cjYatSuZ4rK5ILraOltdS3g0iq0usoRVymHortTv33u7Qxer3I0NXrTup0b7DIvbfB3Wa3VP587yFlbQI=
|
||||
trusted comment: timestamp:1793952467\tfile:chat_heads_rust-1.0.2.hnmod\thashed
|
||||
U+qTKpDMR7PFaX5fsvLfLvhVeS23ypKgCBavykz5gTUEPDNzQNzzB9mIOt4DLi0v3Sezb6F06Mhw07EsJmzgDA=="""
|
||||
@@ -12,7 +12,8 @@ Shows the current survival day, an in-game clock and a color-shifting bar that f
|
||||
- Day/night bar with four color themes (Classic, Pastel, Colorblind, Mono), night marks, countdown to night or day, moon phase.
|
||||
- Top or bottom placement, size, opacity, offsets, optional background; vanilla boss bars move below the HUD when it is at the top.
|
||||
- Settings screen opened with H (or from Options > Mods): Elements, Look and Behavior, with tooltips.
|
||||
- English, Italian, German, Spanish, French and Portuguese."""
|
||||
- English, Italian, German, Spanish, French and Portuguese.
|
||||
- Simple mode: only "Day: N", placed anywhere on the screen (drag it with the mouse or set Position X / Y)."""
|
||||
authors = ["SCARABOCCHIO100"]
|
||||
owners = ["sonoFrangu"]
|
||||
owner_ids = [205944444]
|
||||
@@ -21,6 +22,19 @@ source = "https://github.com/sonoFrangu/CustomHUD-Horizon"
|
||||
icon = "icons/customhud.png"
|
||||
categories = ["utility"]
|
||||
tags = ["day-counter", "clock", "hud", "day-night", "moon-phase", "port"]
|
||||
screenshots = ["screenshots/customhud/1.png", "screenshots/customhud/2.png", "screenshots/customhud/3.png", "screenshots/customhud/4.png", "screenshots/customhud/5.png"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.7.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "customhud-1.7.0.hnmod"
|
||||
sha256 = "f83437e2924b57cb442bd585baa46819ecba5b142bdbbfcb34d109a2cff4a144"
|
||||
changelog = """
|
||||
- New **simple mode** (Elements > Simple mode): only "Day: N", anywhere on the screen.
|
||||
- Right next to the toggle: **Move with mouse** to drag it (arrow keys for fine moves), or Position X / Position Y (0-100%).
|
||||
- Size, opacity, background and milestones still apply.
|
||||
- Built on Horizon c9d474d."""
|
||||
|
||||
[[versions]]
|
||||
version = "1.6.0"
|
||||
|
||||
@@ -24,6 +24,7 @@ source = "https://github.com/hildlabs/horizon-shaders"
|
||||
icon = "icons/daybreak.png"
|
||||
categories = ["semi-realistic", "shadows", "medium"]
|
||||
tags = ["shadows", "water", "reflections", "waving-plants", "sun-rays", "bloom", "lightweight"]
|
||||
screenshots = ["screenshots/daybreak/1.jpg", "screenshots/daybreak/2.jpg", "screenshots/daybreak/3.jpg"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -16,6 +16,7 @@ source = "https://github.com/Bioneisme/deepbore-quarry"
|
||||
icon = "icons/deepbore_quarry.png"
|
||||
categories = ["technology", "library"]
|
||||
tags = ["quarry", "mining", "machine", "assets"]
|
||||
screenshots = ["screenshots/deepbore_quarry/1.jpg", "screenshots/deepbore_quarry/2.jpg", "screenshots/deepbore_quarry/3.png"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -26,6 +26,7 @@ source = "https://github.com/Bioneisme/deepbore-quarry"
|
||||
icon = "icons/deepbore_quarry_native.png"
|
||||
categories = ["technology"]
|
||||
tags = ["quarry", "mining", "machine", "automation", "digging"]
|
||||
screenshots = ["screenshots/deepbore_quarry_native/1.jpg", "screenshots/deepbore_quarry_native/2.jpg", "screenshots/deepbore_quarry_native/3.jpg", "screenshots/deepbore_quarry_native/4.png"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -3,14 +3,31 @@ kind = "native-mod"
|
||||
name = "Eating Animation"
|
||||
summary = "While you eat food, or drink a potion or milk, the item in your hand shows its bitten frames."
|
||||
description = """
|
||||
It is a port of [Eating Animation](https://github.com/Theoness1/EatingAnimation) for Horizon.
|
||||
**Port of Eating Animation for Horizon.** Based on [Eating Animation](https://github.com/Theoness1/EatingAnimation) by Theoness1.
|
||||
|
||||
While you eat food, or drink a potion or milk, the item in your hand shows its bitten frames.
|
||||
## What it does
|
||||
|
||||
The frames come from a resource pack you build from your own copy of the EatingAnimation download with `tools/make_frame_pack.py`, and drop into Horizon's `resourcepacks/` folder.
|
||||
While you eat food, or drink a potion or milk, the item in your hand shows its bitten frames as you go.
|
||||
|
||||
~~Or you can just download it from GitHub Releases...~~"""
|
||||
authors = ["soufi"]
|
||||
The frames are built into the mod. No resource pack is needed: the textures and models are compiled into the native
|
||||
build as a content mod (`mixins/eatinganimation/data/eatinganimation-content.hmod`), and loaded with the game's resources.
|
||||
|
||||
To rebuild that content from a frame pack (for example a new version of the Eating Animation textures):
|
||||
|
||||
```sh
|
||||
python3 tools/pack_to_content.py <frame pack (zip or folder)> mixins/eatinganimation/data/eatinganimation-content.hmod
|
||||
```
|
||||
|
||||
The frame pack can be the one `tools/make_frame_pack.py` makes from the original EatingAnimation download.
|
||||
|
||||
## Credits
|
||||
|
||||
- Eating Animation by [Theoness1](https://github.com/Theoness1/EatingAnimation).
|
||||
|
||||
## License
|
||||
|
||||
MIT. See LICENSE."""
|
||||
authors = ["sillysoufi"]
|
||||
owners = ["soufied"]
|
||||
owner_ids = [47915535]
|
||||
license = "MIT"
|
||||
@@ -20,6 +37,21 @@ categories = ["food", "decoration"]
|
||||
tags = ["eating", "animation", "food", "port"]
|
||||
key = "RWT6eNq4e5WfVDA2lzOOi6XCm/+jJgilqT1+HUUM33ie5MCjcRfSg760"
|
||||
|
||||
[[versions]]
|
||||
version = "0.2.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "eatinganimation-0.2.0.hnmod"
|
||||
sha256 = "e9df7c923723b3b047e6442529dac057806521d988ab2694c43fdcc3104ecc5f"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key
|
||||
RUT6eNq4e5WfVLuMgDLyO+no+UCH+2Dee9dcb9thqoPq5LmN+EujX07YcV4lt8b8c32zRRKirAz3Y3NAwYUTCIZXmDKkXfBJ+Q8=
|
||||
trusted comment: timestamp:1793955778\tfile:eatinganimation-0.2.0.hnmod\thashed
|
||||
0bp1WWbSynkXapYDaodH6cDf4MwL46wyPqJ1IEwc0PHvPWhCG4AE2mq9qXespWPDstYWEuM1BRhadKrNk70lAA=="""
|
||||
changelog = """
|
||||
Build-in texture pack.
|
||||
Fix for https://github.com/soufied/EatingAnimation-Horizon/issues/2 | https://github.com/horizon-rust/horizon/pull/10215"""
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.1"
|
||||
published = "2026-10-31"
|
||||
|
||||
@@ -56,8 +56,22 @@ source = "https://github.com/soufied/ElytraTrails-Horizon"
|
||||
icon = "icons/elytratrails.png"
|
||||
categories = ["transportation", "decoration"]
|
||||
tags = ["elytra", "trails", "flying", "cosmetic", "port"]
|
||||
screenshots = ["screenshots/elytratrails/1.png"]
|
||||
key = "RWT6eNq4e5WfVDA2lzOOi6XCm/+jJgilqT1+HUUM33ie5MCjcRfSg760"
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.2"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "elytratrails-0.1.2.hnmod"
|
||||
sha256 = "ad6758f1ff0fa1b50750764bf2eb1f78ea94af8ecc84d75a7cc6ec5dcffb31a6"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key
|
||||
RUT6eNq4e5WfVKcXvbtSBLrKVo7xIwhiDU8HWuu+fwnIUEW2rftVNxgM85wXqFBifP4sLdEwkzPGs3IhqJIt8xrx9V5oyiQGqQE=
|
||||
trusted comment: timestamp:1793888825\tfile:elytratrails-0.1.2.hnmod\thashed
|
||||
+ZO1qtw4aC7wLLskQA9Qg3wYRMqlITduNU194dJ3+2RhGRCgQi4kup09WDL8tlMPtd32Og23omxxx/lBYEtfAg=="""
|
||||
changelog = "Net Update."
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.1"
|
||||
published = "2026-11-04"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
## Horizon registry entry for Fast Trading (port of bendy1234/FastTrading).
|
||||
id = "fast_trading"
|
||||
kind = "native-mod"
|
||||
name = "Fast Trading"
|
||||
summary = "A speed-trade button on the merchant screen repeats the selected trade as fast as the settings allow."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (install it on the launchers Mods page, then press Build there). Native mods are not sandboxed and run with your accounts rights, so read the source before you build it. Pinned to the Horizon tree this was built against.
|
||||
|
||||
- Speed trade button on the merchant screen repeats the selected trade quickly.
|
||||
- Settings control how fast it repeats; a command opens the controls.
|
||||
|
||||
Port of [FastTrading](https://github.com/bendy1234/FastTrading) by bendy1234 (MIT); credited upstream, original license preserved. Feature gaps vs upstream are documented in the port README and tracked on horizon-rust/horizon.
|
||||
"""
|
||||
authors = ["sparticle62ops"]
|
||||
owners = ["sparticle62ops"]
|
||||
owner_ids = [209812745]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Sparticle62ops/horizon-fast-trading"
|
||||
categories = ["utility"]
|
||||
tags = ["trading", "villagers", "gui", "qol"]
|
||||
|
||||
icon = "icons/fast_trading.png"
|
||||
|
||||
[[versions]]
|
||||
published = "2026-11-06"
|
||||
version = "1.0.0"
|
||||
game = "26.3"
|
||||
file = "fast_trading-1.0.0.hnmod"
|
||||
sha256 = "0e215cd7eab59d37d235d8a97dd26608aa8a77c087df703143f0ef3c0bc40938"
|
||||
changelog = "First release: trade-repeat loop on the merchant screen, command + frame + draw hooks wired."
|
||||
@@ -1,6 +1,6 @@
|
||||
id = "flair"
|
||||
kind = "native-mod"
|
||||
name = "flair"
|
||||
name = "Flair"
|
||||
summary = "Custom accessory mod"
|
||||
description = "A cosmetics catalog for Horizon. Open it from the title screen or the pause menu and wear HD capes that move like cloth, animated bandanas with Japanese script, hats, wings, shoulder pets and auras with colour themes. Turn your player in a live preview, hide your armor, and add your own capes and Blockbench cosmetics with animations. Only you see them."
|
||||
authors = ["_idom_"]
|
||||
@@ -13,6 +13,19 @@ categories = ["decoration"]
|
||||
tags = ["cosmetics", "capes", "hats", "wings", "pets", "blockbench"]
|
||||
key = "RWT9Ujo/oCprPGHHiSLHFNOJV1ZzoJdAU6EdZyiQcBwT2iMXfiYms4hD"
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.1"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "flair-1.0.1.hnmod"
|
||||
sha256 = "6f9abce8af586f448c5d450e394fda609ae02d0878851561ea884c0a17c9d76a"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key\r
|
||||
RUT9Ujo/oCprPE95aTdyPbv21Jlzh61FMqzYiFgzQoTWZMN0huMrXPHNZn7wnGToMzY9QM6HrayTdJGzSsmLCJIoA1H5gtI7PAY=\r
|
||||
trusted comment: timestamp:1793995567\tfile:flair-1.0.1.hnmod\thashed\r
|
||||
OSdZh0w/9j58lFtRV4xF+vDBLO2Qn98KebWLLXYXYSGriZiHiTUZ6HcVXO7HIgZRwhHQPXjXyfL1KVrAaHuXCQ=="""
|
||||
changelog = "Works on Nightly 66f3981."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-11-03"
|
||||
|
||||
@@ -16,6 +16,19 @@ categories = ["utility"]
|
||||
tags = ["freecam", "camera", "spectator"]
|
||||
key = "RWT9Ujo/oCprPGHHiSLHFNOJV1ZzoJdAU6EdZyiQcBwT2iMXfiYms4hD"
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.1"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "freecam_plus-1.0.1.hnmod"
|
||||
sha256 = "42687902b5dfe3b5aea2eea59587b5cd62b93da0e623a4ebd0da22cf8a9a20bf"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key\r
|
||||
RUT9Ujo/oCprPBaIHKVmItovhEFGd5pSVv6sva9wzRhBf6+j4URhuZHJoFJHSghQAnJ6A6SIfGTWaz5lLU9EqUmyUy9ijbMxWgs=\r
|
||||
trusted comment: timestamp:1793995562\tfile:freecam_plus-1.0.1.hnmod\thashed\r
|
||||
LYV7ctQMr8YTSAv51H8H3GwMl0chSJi2lzPW1DWcWdbPxuYa4GI06iJKAeKdhTZkeQH+XnmtVkXt2CWIqtj2AA=="""
|
||||
changelog = "Works on Nightly 66f3981."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-10-31"
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
id = "killc"
|
||||
kind = "native-mod"
|
||||
owners = ["Vakarux12"]
|
||||
owner_ids = [167799729]
|
||||
key = "RWS5u/7lEyKbzVHC6uMJRlhM/N7vzKp5fZe2LCDUlHsDK4lvPupxJbqQ"
|
||||
name = "KillC"
|
||||
summary = "/killc, /killc <player> and /suicide, with an optional random respawn near (0, 0)."
|
||||
description = """
|
||||
@@ -18,11 +15,14 @@ Vanilla keeps `/kill`. Runs where the world is hosted: single player and worlds
|
||||
Source: `horizon/` in https://github.com/Vakarux12/KillC, a fork of the original. CC-BY-SA-4.0, the original's licence, with a grant for Horizon's reviewers in `horizon/REVIEW-GRANT.md`.
|
||||
"""
|
||||
authors = ["adasjusk", "Vakarux12"]
|
||||
owners = ["Vakarux12"]
|
||||
owner_ids = [167799729]
|
||||
license = "CC-BY-SA-4.0"
|
||||
source = "https://github.com/Vakarux12/KillC/tree/main/horizon"
|
||||
icon = "icons/killc.png"
|
||||
categories = ["utility", "game-mechanics"]
|
||||
tags = ["commands", "kill", "respawn", "random-spawn", "port"]
|
||||
key = "RWS5u/7lEyKbzVHC6uMJRlhM/N7vzKp5fZe2LCDUlHsDK4lvPupxJbqQ"
|
||||
|
||||
[[versions]]
|
||||
version = "5.1.0"
|
||||
@@ -33,6 +33,6 @@ sha256 = "6c44d0a5e93aef42f2a33df8286980d9957a04526ff2c1884dfe7aa83f837086"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key
|
||||
RUS5u/7lEyKbzbHxeuREFeXgBYM6FjB/wiwBtmdu5na35k0DXINdt3FILWvtW7yDekrFYjn4Xrl5pv9ttijPbocgpNbqZpPqLgY=
|
||||
trusted comment: timestamp:1793694719 file:killc-5.1.0.hnmod hashed
|
||||
trusted comment: timestamp:1793694719\tfile:killc-5.1.0.hnmod\thashed
|
||||
vC2AGwA5JYAw1yjFLScjYyy+ef03gA5lxE3mwpD/D6CqKRblrpIKtHacgjYViC714ob5w1fgXFPya8AobYmVBg=="""
|
||||
changelog = "First Horizon release, ported from adasjusk's KillC."
|
||||
@@ -0,0 +1,34 @@
|
||||
id = "lumen"
|
||||
kind = "shader"
|
||||
name = "Lumen"
|
||||
summary = "Cozy Photon-style look: soft shadows, warm sunsets, volumetric clouds, light shafts, clear water, waving plants."
|
||||
description = """
|
||||
A cozy shader pack in Horizon's own format, inspired by the look of Photon. All code is original, no Minecraft assets.
|
||||
|
||||
- Soft sun and moon shadows
|
||||
- Warm sky with a round sun and golden sunsets
|
||||
- Volumetric clouds with drifting cloud shadows
|
||||
- Light shafts through trees, sun rays, bloom and distance haze
|
||||
- Clear water with waves, refraction, reflections and the sun's glint
|
||||
- Clear turquoise water with caustics when you dive
|
||||
- Waving leaves, grass, flowers and seagrass
|
||||
- Coloured block light and glowing fireflies
|
||||
- Wet ground after rain
|
||||
|
||||
Every effect can be changed in Options > Horizon Settings > Shader Packs > Shader Options."""
|
||||
authors = ["Lutziano888"]
|
||||
owners = ["Lutziano888"]
|
||||
owner_ids = [129792568]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Lutziano888/horizon-lumen"
|
||||
icon = "icons/lumen.png"
|
||||
categories = ["semi-realistic", "atmosphere", "medium"]
|
||||
tags = ["shadows", "clouds", "water", "reflections", "waving-plants", "god-rays", "colored-light"]
|
||||
|
||||
[[versions]]
|
||||
version = "2.1.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "lumen-2.1.0.hshader"
|
||||
sha256 = "3249a3d8706e11c2175a16dbf034b36e41ed47c76c9749e7087ea0aab89a2dce"
|
||||
changelog = "First release. Needs a Horizon build from 2026-11-03 or newer."
|
||||
@@ -0,0 +1,55 @@
|
||||
id = "lumina"
|
||||
kind = "shader"
|
||||
name = "Lumina"
|
||||
summary = "High performance with real-time shadows, physical water, roof overhang occlusion, volumetric 3D clouds, and sharp anti-aliasing"
|
||||
description = """
|
||||
Native shaders designed for high frame rates and clean, balanced visuals without post-processing blur. All options are fully configurable directly in the shader settings menu.
|
||||
|
||||
**Lighting & Overhang Shading**
|
||||
- Balanced daytime lighting with warm sun and clean azure shadows.
|
||||
- Directional sky dome occlusion: accurate contrast between ground, walls, and ceilings.
|
||||
- Non-linear ambient occlusion under house roofs, eaves, porches, and balconies.
|
||||
- Selective midtone vibrance on grass and foliage without clipping bright highlights.
|
||||
- Foliage subsurface scattering and sun-facing rim lighting.
|
||||
|
||||
**Shadows**
|
||||
- Real-time sun and moon shadow maps with distortion-aware filtering.
|
||||
- Acne-free shadows on terrain, blocks, and thin grass at any distance.
|
||||
- Selectable shadow map resolution: 512, 1024, 1536, or 2048.
|
||||
|
||||
**Clarity Anti-Aliasing (FXAA)**
|
||||
- Edge-selective geometric smoothing on block silhouettes, roofs, and stairs.
|
||||
- Flat surfaces, textures, wood, stone, and UI remain 100% sharp with zero blurring.
|
||||
- Modes: Sharp, Smooth, or Off.
|
||||
|
||||
**Water & Underwater**
|
||||
- High-speed Screen Space Reflections (SSR) combined with planar mirror reflections.
|
||||
- Physics-based Beer-Lambert light absorption with depth-dependent color transition.
|
||||
- Snell's window when looking up from underwater with total internal reflection.
|
||||
- Animated seabed caustics, wave refraction, and underwater depth fog.
|
||||
|
||||
**Clouds & Atmosphere**
|
||||
- Raymarched procedural 3D cumulus clouds with Fast, Balanced, and Cinematic presets.
|
||||
- Volumetric god rays through terrain and trees.
|
||||
- Procedural rain puddles on grass and dirt with raindrop impact ripples.
|
||||
- Custom weather rendering for rain and snow particles.
|
||||
- Native block emission and dynamic colored lighting support.
|
||||
|
||||
**Setup Note:**
|
||||
Set Clouds to OFF in your video settings to let the custom 3D clouds render properly."""
|
||||
authors = ["cosmo-jar"]
|
||||
owners = ["cosmo-jar"]
|
||||
owner_ids = [246506206]
|
||||
license = "GPL-3.0"
|
||||
source = "https://github.com/cosmo-jar/Lumina-Shader-Pack-for-Horizon-Minecraft-on-Rust"
|
||||
icon = "icons/lumina.png"
|
||||
categories = ["atmosphere", "shadows", "semi-realistic"]
|
||||
tags = ["shadows", "water", "reflections", "volumetric", "clouds", "atmosphere", "optimized", "lighting", "god", "rays"]
|
||||
screenshots = ["screenshots/lumina/1.jpg", "screenshots/lumina/2.jpg", "screenshots/lumina/3.jpg", "screenshots/lumina/4.jpg", "screenshots/lumina/5.jpg", "screenshots/lumina/6.jpg"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "lumina-1.0.0.hshader"
|
||||
sha256 = "3b81722173fc4c4476def75fbfeff964ccd8a2fde0e31b49507078ea7616dd43"
|
||||
@@ -0,0 +1,60 @@
|
||||
id = "movement_status"
|
||||
kind = "native-mod"
|
||||
name = "Movement Status"
|
||||
summary = "Better toggle Sprint/Sneak display functionality"
|
||||
description = """
|
||||
# Movement Status
|
||||
|
||||
A lightweight native HUD for Horizon that shows your current movement state in the corner of the screen.
|
||||
|
||||
It reads the live player state (sprinting, sneaking, flying, elytra gliding) and the physical vs. logical key state so it can correctly distinguish between a key that is being held and a key that is toggled via vanilla’s Toggle Sprint / Toggle Crouch options.
|
||||
|
||||
## Features
|
||||
|
||||
- Live movement status in the bottom-left corner:
|
||||
- `Sprint [Held]` / `Sprint [Toggled]`
|
||||
- `Sneak [Held]` / `Sneak [Toggled]`
|
||||
- `Flying [Creative]` / `Flying [Elytra]`
|
||||
- Multiple states can be shown at once (sprint + sneak, etc.)
|
||||
- Optional semi-transparent background box
|
||||
- Respects vanilla Toggle Sprint and Toggle Crouch settings
|
||||
- Uses the real physical key state so the Held/Toggled labels stay accurate
|
||||
|
||||
## Requirements
|
||||
|
||||
- Horizon (Minecraft: Java Edition 26.3)
|
||||
- Native mod support enabled in the launcher
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the mod from the launcher’s Mods page (or drop the `.hnmod` into `horizon-native-mods/`).
|
||||
2. Click **Build** and confirm the native-code warning.
|
||||
3. Once built, turn the mod on under Options → Mods.
|
||||
|
||||
## Settings
|
||||
|
||||
A single toggle controls the background box. Open Options → Mods → Movement Status → Settings to turn it on or off.
|
||||
|
||||
## Notes
|
||||
|
||||
This is a status HUD only. It does not change movement, keybinds, or gameplay. It simply reports what the game is already doing.
|
||||
|
||||
The physical key checks currently use the vanilla defaults (Left Control for sprint, Left Shift for sneak). Custom keybinds may show the wrong Held/Toggled label until a future update maps the bound key.
|
||||
|
||||
## License
|
||||
|
||||
MIT"""
|
||||
authors = []
|
||||
owners = ["UniDex101"]
|
||||
owner_ids = [289881197]
|
||||
license = "MIT"
|
||||
source = "https://github.com/UniDex101/movement-status/tree/main"
|
||||
icon = "icons/movement_status.png"
|
||||
categories = ["game-mechanics"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.1.0"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "movement_status-0.1.0.hnmod"
|
||||
sha256 = "1274dab9429a8f5267cabd0137df72c9712c84cb9e5daab99a44dab4fdf21eb6"
|
||||
@@ -0,0 +1,48 @@
|
||||
id = "orbital"
|
||||
kind = "native-mod"
|
||||
name = "Orbital Laser"
|
||||
summary = "Fifteen orbital strikes, from a laser to a Planet Killer: a designator, a wheel, and craters that stay."
|
||||
description = """
|
||||
By Stink-O. Orbital Laser adds an Orbital Laser Designator. Aim it and hold right-click to call down one of fifteen orbital strikes (hold it and press R for the laser wheel, or type `/orbital wheel`), each with its own sky, sound and scar: the Orbital Laser, the Godly Laser, a Black Hole, the Meteor Storm, the Mothership, the Downfall, the Planet Killer, the Crystal Bloom, the Sinkhole, the Payload, the Nuke, the Rod from God, the Supercell, the Hurricane and the Tsunami. The wheel also sets your creative flying speed and the strike's approach, arrival and colours.
|
||||
|
||||
**Read before you fire**
|
||||
|
||||
- **Terrain damage is permanent.** A strike flattens and digs out the land, up to hundreds of blocks across, and kills the mobs and dropped items nearby (not players). Use a throwaway world. `/orbital terrain off` (or the wheel's centre) makes strikes light and sound only.
|
||||
- **Photosensitivity.** The Rod from God has ~0.17 s of stark impact frames as it strikes, and several strikes (the Planet Killer, the Hurricane and the Supercell among them) have lightning and flashes, and the Tsunami's blow, as the water takes you, is one white flash over the whole picture. `/orbital impactframes off` (the wheel's IMPACT FX switch) turns the impact frames off. If flashing light affects you, skip those strikes.
|
||||
- **Big strikes are heavy.** QUALITY FAST (the default) draws the smoke at half size; `/orbital quality full` for the full look.
|
||||
|
||||
**Native mod.** The mod's code is compiled into the game, runs with the access the game has to your computer, and is not sandboxed, so install it only if you trust it. The launcher shows its trust notice and waits for you to press Build; the first native build downloads the launcher's Rust and takes 20 to 30 minutes. Pick **Release build** on the Native build row first: it builds more slowly, but the game runs noticeably faster. It builds on the Horizon revision it was tested on, whatever Nightly you have.
|
||||
|
||||
What each system needs first:
|
||||
|
||||
- **Linux** (x86-64), tested: a C compiler, `pkg-config` and the ALSA and libudev development files (Debian/Ubuntu: `sudo apt install build-essential pkg-config libasound2-dev libudev-dev`); the launcher names what is missing.
|
||||
- **Windows** (x64), build-checked, not yet played: nothing; the launcher brings its own Rust and MinGW-w64.
|
||||
- **macOS** (Apple Silicon), build-checked, not yet played: Apple's Command Line Tools (`xcode-select --install`).
|
||||
- **macOS** (Intel): not available; the launcher can't build native mods there.
|
||||
|
||||
**One file for code and content.** The game reads content mods only from `horizon-mods/`, so this mod carries a small mixin (`content_find`, on `horizon_mods::manifest::find`) that has the game read its content (`horizon-mod.toml`, `assets/`, `data/`) from its own `horizon-native-mods/orbital.hnmod` too. It writes and copies nothing, and steps aside when a content mod `orbital` is already in `horizon-mods/`. It goes away once the game reads content from native files (horizon-rust/horizon#5865).
|
||||
|
||||
Known issues:
|
||||
|
||||
- On Macs the Downfall's effects don't draw: Apple's Metal compiler fails on its pass, so you get its land and sound but not its visuals. Every other strike drew on the Macs it was tried on.
|
||||
- Multiplayer and LAN haven't been tested on the native build; it's made for single player. On a server or LAN host, only a level 4 operator can fire strikes that change the world.
|
||||
- Each new version rebuilds the game (the first build 20 to 30 minutes, later ones quicker), unlike a plain mod.
|
||||
|
||||
Items, models, textures and sounds are the mod's own: nothing from Minecraft is in it. An independent fan project, not affiliated with Mojang, Microsoft or the Horizon project. Source, screenshots, how it works and the full command list: https://github.com/Stink-O/orbital-laser"""
|
||||
authors = ["Stink-O"]
|
||||
owners = ["Stink-O"]
|
||||
owner_ids = [180130752]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Stink-O/orbital-laser"
|
||||
icon = "icons/orbital.png"
|
||||
categories = ["equipment", "game-mechanics"]
|
||||
tags = ["laser", "explosions", "destruction", "shaders"]
|
||||
screenshots = ["screenshots/orbital/1.jpg", "screenshots/orbital/2.jpg", "screenshots/orbital/3.jpg", "screenshots/orbital/4.jpg"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.5.10"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "orbital-0.5.10.hnmod"
|
||||
sha256 = "f860b61f2c54cc4efd6e6373054858768b0fd829afc0b59c60893a556ef4da63"
|
||||
changelog = "Adds the Tsunami, the fifteenth strike. Switching lasers no longer hitches the game, and a long stall no longer ends a strike. World changes from a client now need operator level 4, a guest's /orbital no longer changes the host's game, and it's built on Horizon e57b1d34b, so far terrain shows what strikes did and the hand draws during the Hurricane."
|
||||
@@ -0,0 +1,33 @@
|
||||
id = "pastel_mystic"
|
||||
kind = "shader"
|
||||
name = "Pastel Mystic"
|
||||
summary = "A soft pastel, dreamlike world with auroras, lavender mist and a breathing light from what you hold."
|
||||
description = """
|
||||
A soft pastel, dreamlike shader pack in Horizon's own format, written from scratch in WGSL.
|
||||
|
||||
- **Pastel sky:** periwinkle overhead and blush at the horizon by day, coral and lilac at dawn and dusk.
|
||||
- **Mystic nights:** a deep violet sky with a slowly drifting aurora curtain in mint, lavender and pink, and twinkling stars.
|
||||
- **Light in your hand:** what you hold glows in its own pastel colour (torches peach, soul torches mint, amethyst and ender items lilac, glowstone cream, sea lanterns aqua, redstone rose) and gently breathes and shimmers. Dropped glowing items light up too. Turn on **Dynamic lights** in Options > Extras.
|
||||
- **Soft light:** peach-and-cream sunlight, lavender shade, soft shadows that never go fully black, a violet night instead of a black one.
|
||||
- **Atmosphere:** lavender mist lying low in the valleys (thicker at night and dawn), a pastel haze in the distance and a dreamy glow around light sources.
|
||||
- **Pastel grade:** lifted lavender shadows, blush highlights, softer contrast and a violet vignette.
|
||||
- Plants sway in a slow breeze, water is a clear pastel aqua with gentle waves, fireflies glow lilac.
|
||||
|
||||
Everything is adjustable in Options > Horizon Settings > Shader Packs > Shader Options (Light, Light in hand, Sky, Mist, Dream). Kept light for integrated graphics (tested on Intel Iris Xe): no reflections pass, a small shadow map and quarter-resolution glow."""
|
||||
authors = ["palladium7048"]
|
||||
owners = ["platinum7048"]
|
||||
owner_ids = [328277117]
|
||||
license = "MIT"
|
||||
source = "https://github.com/platinum7048/pastel-mystic"
|
||||
icon = "icons/pastel_mystic.png"
|
||||
categories = ["fantasy", "atmosphere", "low"]
|
||||
tags = ["pastel", "dreamy", "aurora", "mist", "dynamic-lights", "held-light", "fantasy", "soft-shadows"]
|
||||
screenshots = ["screenshots/pastel_mystic/1.jpg", "screenshots/pastel_mystic/2.jpg", "screenshots/pastel_mystic/3.jpg", "screenshots/pastel_mystic/4.jpg", "screenshots/pastel_mystic/5.jpg"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "pastel_mystic-1.0.0.hshader"
|
||||
sha256 = "12df7984da6a1f8f6f06eb74267f83ec4e5abe495e0aa547e5fd09951ecb161e"
|
||||
changelog = "First release."
|
||||
@@ -0,0 +1,34 @@
|
||||
## Horizon registry entry for PVP Music (port of the Modrinth project PvP Music, Tkf0xIjA).
|
||||
id = "pvp_music"
|
||||
kind = "native-mod"
|
||||
name = "PVP Music"
|
||||
summary = "Battle music during combat: fades, death fades and the phased two-half track switch, the original's behavior kept."
|
||||
description = """
|
||||
**A native Horizon mod**: the launcher builds it into the game (install it on the launcher's Mods page, then press Build there). Native mods are not sandboxed and run with your account's rights, so read the source before you build it. Pinned to the Horizon tree this was built against.
|
||||
|
||||
- Mutual-combat detection: attacks on and by players and mobs are remembered for 5 s; a mob we hit that then hits us starts a mob battle.
|
||||
- A landed hit on a player starts a player battle; explosion and bad-respawn-point hurts pull in combat within 12 blocks; a wither within 30 blocks starts a fight.
|
||||
- The battle track fades in and out, death fades run at the configured multiplier, and the phased two-half music switches at low health on a beat. Battles of 30 s or more end with the short end sound, of 60 s or more with the long end sound.
|
||||
- `/pvpmusic` drives every setting the original's config screen kept.
|
||||
|
||||
Sounds: the `.hnmod` carries code only, so the four tracks ship as a companion resource pack on the same release (`pvp-music-sounds-1.2.0.zip`, the original's CC0 assets). Install it as a resource pack and the music plays; without it the mod is silent.
|
||||
|
||||
Port of [PvP Music](https://modrinth.com/mod/Tkf0xIjA) by Countered (CC0-1.0); credited upstream, original license preserved. The sound-asset gap is documented in the port README and tracked on horizon-rust/horizon.
|
||||
"""
|
||||
authors = ["sparticle62ops"]
|
||||
owners = ["sparticle62ops"]
|
||||
owner_ids = [209812745]
|
||||
license = "CC0-1.0"
|
||||
source = "https://github.com/Sparticle62ops/horizon-pvp-music"
|
||||
categories = ["utility"]
|
||||
tags = ["combat", "music", "audio", "qol"]
|
||||
|
||||
icon = "icons/pvp_music.png"
|
||||
|
||||
[[versions]]
|
||||
published = "2026-11-06"
|
||||
version = "1.2.0"
|
||||
game = "26.3"
|
||||
file = "pvp-music-1.2.0.hnmod"
|
||||
sha256 = "21e2e7768a5b4f89bee05453ae83e8130ab8b9019431eb5ecb10b2d7326e5be7"
|
||||
changelog = "First release: battle detection, fades, the phased two-half track switch and /pvpmusic. The four tracks ship as a companion resource pack on the same release (the .hnmod carries code only)."
|
||||
@@ -2,7 +2,7 @@ id = "skins_3d"
|
||||
kind = "native-mod"
|
||||
name = "3D Skins"
|
||||
summary = "The outer layer of every player's skin drawn as real 3D pixels."
|
||||
description = "The outer layer of every player’s skin drawn as real 3D pixels: hat, jacket, sleeves and pants follow the head, arms and legs exactly as the game moves them. Works for everyone you can see, with downloaded, launcher and default skins, classic and slim arms and HD skins, respects Skin Customization, and switches to the flat layer further away. Client side only."
|
||||
description = "The outer layer of every player’s skin drawn as real 3D pixels: hat, jacket, sleeves and pants follow the head, arms and legs exactly as the game moves them. Lighter and cleaner than Horizon’s built-in option: about 4x fewer polygons, closed corners, nothing poking through armor, and 3D sleeves in first person. Works for everyone you can see, respects Skin Customization, and switches to the flat layer further away. Client side only."
|
||||
authors = ["_idom_"]
|
||||
owners = ["umbx-dot"]
|
||||
license = "MIT"
|
||||
@@ -12,6 +12,19 @@ categories = ["decoration"]
|
||||
tags = ["skins", "3d-skins", "players", "cosmetic"]
|
||||
key = "RWT9Ujo/oCprPGHHiSLHFNOJV1ZzoJdAU6EdZyiQcBwT2iMXfiYms4hD"
|
||||
|
||||
[[versions]]
|
||||
version = "1.1.0"
|
||||
published = "2026-11-06"
|
||||
game = "26.3"
|
||||
file = "skins_3d-1.1.0.hnmod"
|
||||
sha256 = "ac9f20ea771f2abb25976c06d320883016b13a984075522ecc07323b48994602"
|
||||
signature = """
|
||||
untrusted comment: signature from minisign secret key\r
|
||||
RUT9Ujo/oCprPF+RAt5OphHM0S5RIZI7zhUJ65ugOkL0YwROHGuVpB9alKJJtoMEp7po82qGwQarg6wZ9tNFkxV6IEUiserpwQQ=\r
|
||||
trusted comment: timestamp:1793995572\tfile:skins_3d-1.1.0.hnmod\thashed\r
|
||||
JiMtdZ3uX7xDgdAzZveNLx5n8yo/qpzBshdHH2cDfkuELSKGQ9mF/78F55V4Ef3gPZMhpbp2X/cvVGlJcmnMBQ=="""
|
||||
changelog = "Works on Nightly 66f3981."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.1"
|
||||
published = "2026-11-04"
|
||||
|
||||
@@ -17,6 +17,14 @@ icon = "icons/wayfarer_stones.png"
|
||||
categories = ["transportation", "library"]
|
||||
tags = ["teleport", "waystones", "fast-travel", "assets"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.6.1"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "wayfarer_stones-0.6.1.hmod"
|
||||
sha256 = "075df3102918ff059abab53392584a58a17b9090a352a7d29018be69fde23034"
|
||||
changelog = "Says in chat, with a link, when the assets mod is missing. Pinned to Nightly 2026-11-04 (67dad0d) and Horizon Mixins e8cd0aa; also checked in the game on Nightly 2026-11-05 (f319701)."
|
||||
|
||||
[[versions]]
|
||||
version = "0.6.0"
|
||||
published = "2026-11-04"
|
||||
|
||||
@@ -27,6 +27,14 @@ icon = "icons/wayfarer_stones_native.png"
|
||||
categories = ["transportation", "adventure"]
|
||||
tags = ["teleport", "waystones", "fast-travel", "villages"]
|
||||
|
||||
[[versions]]
|
||||
version = "0.6.1"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "wayfarer_stones_native-0.6.1.hnmod"
|
||||
sha256 = "5ea5f83595bd7d800d6d79505c8d7f641fd9a963559f9f1f0c8208bb0ec9b416"
|
||||
changelog = "Says in chat, with a link, when the assets mod is missing. Pinned to Nightly 2026-11-04 (67dad0d) and Horizon Mixins e8cd0aa; also checked in the game on Nightly 2026-11-05 (f319701)."
|
||||
|
||||
[[versions]]
|
||||
version = "0.6.0"
|
||||
published = "2026-11-04"
|
||||
|
||||
@@ -8,20 +8,29 @@ Waypoints for Horizon.
|
||||
- **N**: create a waypoint where you stand: name, X/Y/Z, dimension, colour, visible or hidden. Enter saves, Tab moves between fields, Esc cancels.
|
||||
- **M**: the waypoint list, filtered by All / Overworld / Nether / End. Each row shows its colour, name, coordinates, dimension and distance, with Hide/Show, Edit and Delete buttons.
|
||||
- **In the world**: each visible waypoint in your current dimension shows its name, its distance and a marker in its colour.
|
||||
- **Options > Mods > Waypoints**: rebind both keys and turn the in-world labels on or off.
|
||||
- **When you die**: a red `Death 1`, `Death 2`... waypoint marks the block you died on. The newest 5 stay, and the oldest goes when you die again; rename one to keep it for good.
|
||||
- **Options > Mods > Waypoints**: rebind both keys, and turn the in-world labels and the death waypoints on or off.
|
||||
|
||||
**What it does and doesn't do:** it only reads the game's own state (player position, dimension, camera, world folder name, server address) and draws on the HUD and its own screen. It stores waypoints as a text file per world and per server in `horizon-mods/.horizon-mod-data/waypoints/` inside the game folder. It uses only the Rust standard library, opens no network connections, and reads or writes no other files.
|
||||
**What it does and doesn't do:** it only reads the game's own state (player position, dimension, camera, death screen, last death location, world folder name, server address) and draws on the HUD and its own screen. It stores waypoints as a text file per world and per server in `horizon-mods/.horizon-mod-data/waypoints/` inside the game folder. It uses only the Rust standard library, opens no network connections, and reads or writes no other files.
|
||||
|
||||
Pinned to Horizon `c9d474d`, because it reads the world folder, server address and last frame's camera from the game itself."""
|
||||
Pinned to Horizon `c9d474d`, because it reads the world folder, server address, last frame's camera and the player's death from the game itself."""
|
||||
authors = ["Nirokx"]
|
||||
owners = ["Nirokx"]
|
||||
owner_ids = [95123456]
|
||||
license = "MIT"
|
||||
source = "https://github.com/Nirokx/horizon-waypoints"
|
||||
icon = "icons/waypoints.png"
|
||||
categories = ["utility", "adventure"]
|
||||
categories = ["adventure", "utility"]
|
||||
tags = ["waypoints", "navigation", "markers", "hud"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.1.0"
|
||||
published = "2026-11-05"
|
||||
game = "26.3"
|
||||
file = "waypoints-1.1.0.hnmod"
|
||||
sha256 = "8ff61c453204ac26cdbe24588ccaa5660af3ffbc815fb4ad0ef4d7d7e6b1bae9"
|
||||
changelog = "Waypoints on death: dying leaves a red `Death 1`, `Death 2`... waypoint on the block you died on, in the right dimension. Turn it on or off in Options > Mods > Waypoints (on by default). The newest 5 stay, and renaming one keeps it for good."
|
||||
|
||||
[[versions]]
|
||||
version = "1.0.0"
|
||||
published = "2026-11-02"
|
||||
|
||||
@@ -14,6 +14,7 @@ source = "https://github.com/tomasmendes01/wisdom-horizon"
|
||||
icon = "icons/wisdom_horizon.png"
|
||||
categories = ["semi-realistic", "shadows", "high"]
|
||||
tags = ["wisdom", "bloom", "atmosphere", "clouds", "port"]
|
||||
screenshots = ["screenshots/wisdom_horizon/1.png", "screenshots/wisdom_horizon/2.png", "screenshots/wisdom_horizon/3.png"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.3.7"
|
||||
|
||||
@@ -14,6 +14,7 @@ source = "https://github.com/tomasmendes01/wisdom-horizon"
|
||||
icon = "icons/wisdom_horizon_lite.png"
|
||||
categories = ["semi-realistic", "atmosphere", "low"]
|
||||
tags = ["wisdom", "lite", "performance", "port"]
|
||||
screenshots = ["screenshots/wisdom_horizon_lite/1.png", "screenshots/wisdom_horizon_lite/2.png", "screenshots/wisdom_horizon_lite/3.png"]
|
||||
|
||||
[[versions]]
|
||||
version = "1.3.7"
|
||||
|
||||
|
After Width: | Height: | Size: 986 KiB |
|
After Width: | Height: | Size: 953 KiB |
|
After Width: | Height: | Size: 971 KiB |
|
After Width: | Height: | Size: 914 KiB |
|
After Width: | Height: | Size: 994 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 724 KiB |
|
After Width: | Height: | Size: 835 KiB |
|
After Width: | Height: | Size: 996 KiB |
|
After Width: | Height: | Size: 724 KiB |
|
After Width: | Height: | Size: 835 KiB |
|
After Width: | Height: | Size: 996 KiB |
@@ -0,0 +1,413 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Whether native mods build on the newest Nightly, together, as a player's launcher builds them.
|
||||
|
||||
python3 scripts/native_compat.py check BASE HEAD # a pull request's new native versions
|
||||
python3 scripts/native_compat.py health --out health.json # every native mod's newest version
|
||||
|
||||
`check` takes each native-mod version HEAD lists and BASE doesn't, and fails when:
|
||||
- one of its support modules has the name, in the same file, of another registry mod's (the launcher
|
||||
can't build both: E_MODULE_CONFLICT);
|
||||
- horizon-mixins doesn't apply it on the newest Nightly with the most downloaded native mods
|
||||
(`--popular`), as the launcher moves mods on the hook points to the Horizon it builds;
|
||||
- the game doesn't compile (`cargo check`) with it and those mods, when it does without it.
|
||||
Popular mods that don't apply on that Nightly are left out, as the launcher leaves them out, and listed.
|
||||
|
||||
`health` applies every native mod's newest version on the newest Nightly and writes the ones horizon-mixins
|
||||
refuses with only the mods they need; then `cargo check`s the rest together, rounds leaving out the mods the errors
|
||||
are in (scripts/native_trace.py, as the launcher traces them), and writes those too, each with its first error. An
|
||||
error of the kind two mods cause together (a name defined twice...) is checked again with that mod alone. The publish
|
||||
job adds them all to the index (`broken_on`), and the launcher warns before an install.
|
||||
|
||||
horizon-mixins only reads the bundles; `cargo check` runs a mod crate's build script, if it has one, so
|
||||
`check` runs in a job whose token can only read (native-compat.yml). Horizon's source comes from GitHub's archive.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "native" / "tools"))
|
||||
import native # noqa: E402 (native/tools/native.py: source, latest_nightly, get, log)
|
||||
import native_trace # noqa: E402
|
||||
|
||||
INDEX = "https://horizon-rust.github.io/horizon-mods/index.json"
|
||||
REGISTRY = "horizon-rust/horizon-mods"
|
||||
MIXINS_GIT = "https://github.com/horizon-rust/horizon-mixins"
|
||||
WORK = native.TARGET / "compat"
|
||||
|
||||
|
||||
def log(*args):
|
||||
native.log(*args)
|
||||
|
||||
|
||||
class Bundle:
|
||||
"""An unpacked `.hnmod`: its mixin bundle in WORK/mods/<set>/<id>/."""
|
||||
|
||||
def __init__(self, data, origin):
|
||||
archive = zipfile.ZipFile(io.BytesIO(data))
|
||||
meta = json.loads(archive.read("horizon-native.json"))
|
||||
self.id, self.version, self.origin = meta["id"], meta["version"], origin
|
||||
self.mixins = meta.get("mixins")
|
||||
prefix = f"mixins/{self.id}/"
|
||||
self.files = {n[len(prefix):]: archive.read(n) for n in archive.namelist()
|
||||
if n.startswith(prefix) and not n.endswith("/")}
|
||||
manifest = tomllib.loads(self.files["horizon-mixin.toml"].decode())
|
||||
self.modules = {(m["file"], m["name"]) for m in manifest.get("module", [])}
|
||||
self.needs = [d for d, v in manifest.get("dependencies", {}).items()
|
||||
if not (isinstance(v, dict) and v.get("optional"))]
|
||||
# The launcher rewrites these before horizon-mixins and stages API packages for them (native_mods/inject.rs,
|
||||
# api.rs), which this script doesn't: they are applied here, not compiled.
|
||||
self.api = "api" in manifest or any(isinstance(i, dict) and "trait_method" in i
|
||||
for i in manifest.get("injection", []))
|
||||
|
||||
def unpack(self, into, revision):
|
||||
out = into / self.id
|
||||
for name, data in self.files.items():
|
||||
path = out / name
|
||||
if ".." in Path(name).parts:
|
||||
sys.exit(f"{self.id}: {name} leaves its folder")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
# As the launcher does for a mod built on another Horizon than it pins.
|
||||
toml = out / "horizon-mixin.toml"
|
||||
text = re.sub(r'horizon_revision\s*=\s*"[0-9a-f]{40}"', f'horizon_revision = "{revision}"', toml.read_text())
|
||||
toml.write_text(text)
|
||||
|
||||
|
||||
def download(url, sha256):
|
||||
cache = WORK / "files" / sha256
|
||||
if not cache.is_file():
|
||||
data = native.get(url)
|
||||
if hashlib.sha256(data).hexdigest() != sha256:
|
||||
sys.exit(f"{url}: its SHA-256 isn't the index's")
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache.write_bytes(data)
|
||||
return cache.read_bytes()
|
||||
|
||||
|
||||
def published():
|
||||
"""The newest version of every native mod in the published index, most downloaded first."""
|
||||
index = json.loads(native.get(INDEX))
|
||||
revoked = {r.get("sha256") for r in index.get("revoked", [])}
|
||||
found = []
|
||||
for project in sorted(index["projects"], key=lambda p: -p.get("downloads", 0)):
|
||||
if project.get("kind") != "native-mod":
|
||||
continue
|
||||
version = next((v for v in project["versions"] if v["file"]["sha256"] not in revoked), None)
|
||||
if version:
|
||||
found.append(Bundle(download(version["file"]["url"], version["file"]["sha256"]), "registry"))
|
||||
return found
|
||||
|
||||
|
||||
def mixins_tool(bundles):
|
||||
"""horizon-mixins as the launcher picks it: the newest of its own (Horizon main's MIXINS_REV) and the
|
||||
mods' pins, by commit date."""
|
||||
text = native.get(f"https://raw.githubusercontent.com/{native.REPO}/main/launcher/src/native_mods.rs").decode()
|
||||
revs = {re.search(r'pub const MIXINS_REV: &str = "([0-9a-f]{40})"', text).group(1)}
|
||||
revs |= {b.mixins for b in bundles if b.mixins}
|
||||
def date(rev):
|
||||
commit = json.loads(native.get(f"https://api.github.com/repos/horizon-rust/horizon-mixins/commits/{rev}"))
|
||||
return commit["commit"]["committer"]["date"]
|
||||
rev = max(revs, key=date)
|
||||
root = native.TARGET / "horizon-mixins" / rev
|
||||
binary = root / "bin" / "horizon-mixins"
|
||||
if not binary.is_file():
|
||||
log(f"Building horizon-mixins {rev[:7]}")
|
||||
subprocess.run(["cargo", "install", "--locked", "--quiet", "--git", MIXINS_GIT, "--rev", rev,
|
||||
"--root", str(root), "horizon-mixins-cli"], check=True)
|
||||
return binary
|
||||
|
||||
|
||||
def culprit(diagnostic, bundles):
|
||||
"""The bundle a horizon-mixins diagnostic names: by mod id, its manifest's path, its module's name or
|
||||
destination; between two mods' modules, the later one (the launcher's rule)."""
|
||||
by_id = {b.id: b for b in bundles}
|
||||
if diagnostic.get("mod_id") in by_id:
|
||||
return by_id[diagnostic["mod_id"]]
|
||||
file = diagnostic.get("file", "")
|
||||
for b in bundles:
|
||||
if f"/{b.id}/horizon-mixin.toml" in file:
|
||||
return b
|
||||
named = re.search(r"support module '([^']+)'", diagnostic.get("message", ""))
|
||||
declaring = [b for b in bundles if any((named and n == named.group(1)) or f == file for f, n in b.modules)]
|
||||
return declaring[-1] if declaring else None
|
||||
|
||||
|
||||
def apply(tool, tree, bundles, revision, name):
|
||||
"""horizon-mixins on `bundles` together, leaving out each one it refuses (and the ones needing it) until
|
||||
the rest applies: (stage folder or None, {id: diagnostic})."""
|
||||
left, refused = list(bundles), {}
|
||||
while left:
|
||||
mods = WORK / "mods" / name
|
||||
shutil.rmtree(mods, ignore_errors=True)
|
||||
mods.mkdir(parents=True)
|
||||
for b in left:
|
||||
b.unpack(mods, revision)
|
||||
out = WORK / "stage" / name
|
||||
shutil.rmtree(out, ignore_errors=True)
|
||||
run = subprocess.run([str(tool), "apply", "--workspace", str(tree), "--mods", str(mods), "--out", str(out)],
|
||||
capture_output=True, text=True)
|
||||
if run.returncode == 0:
|
||||
return out, refused
|
||||
text = run.stderr + run.stdout
|
||||
try:
|
||||
diagnostic = json.loads(text[text.index("{"):text.rindex("}") + 1])
|
||||
except ValueError:
|
||||
sys.exit(f"horizon-mixins failed without a diagnostic:\n{text[-2000:]}")
|
||||
bad = culprit(diagnostic, left)
|
||||
if bad is None:
|
||||
sys.exit(f"horizon-mixins refused the set, naming no mod: {json.dumps(diagnostic)}")
|
||||
log(f" {bad.id} {bad.version}: {diagnostic.get('code')}: {diagnostic.get('message')}")
|
||||
refused[bad.id] = diagnostic
|
||||
gone = {bad.id}
|
||||
while True:
|
||||
more = [b for b in left if b.id not in gone and any(n in gone for n in b.needs)]
|
||||
if not more:
|
||||
break
|
||||
for b in more:
|
||||
refused[b.id] = {"code": "E_DEPENDENCY", "message": f"needs {', '.join(b.needs)}"}
|
||||
gone.add(b.id)
|
||||
left = [b for b in left if b.id not in gone]
|
||||
return None, refused
|
||||
|
||||
|
||||
# Errors two mods' code can cause together, while each alone compiles.
|
||||
PAIR_CODES = {"E0119", "E0201", "E0252", "E0254", "E0255", "E0260", "E0428", "E0592"}
|
||||
COMPILE_ROUNDS = 6
|
||||
|
||||
|
||||
def check_errors(stage):
|
||||
"""`cargo check` of the game in `stage` as the launcher compiles it (with Horizon's `mods` feature when it has
|
||||
one): None when it compiles, else its errors (native_trace.errors). The workspace is synced into one folder
|
||||
first, so cargo's incremental state survives between rounds."""
|
||||
work = WORK / "check"
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
subprocess.run(["rsync", "-a", "--checksum", "--delete", f"{stage}/", f"{work}/"], check=True)
|
||||
command = ["cargo", "check", "--locked", "--message-format=json", "-p", "horizon-client", "--bin", "horizon-client"]
|
||||
client = tomllib.loads((work / "crates/horizon-client/Cargo.toml").read_text())
|
||||
if "mods" in client.get("features", {}):
|
||||
command += ["--features", "horizon-client/mods"]
|
||||
env = dict(os.environ, CARGO_TARGET_DIR=str(WORK / "cargo"))
|
||||
run = subprocess.run(command, cwd=work, env=env, capture_output=True, text=True)
|
||||
if run.returncode == 0:
|
||||
return None
|
||||
found = native_trace.errors(run.stdout)
|
||||
for e in found[:10]:
|
||||
log(e["rendered"].rstrip())
|
||||
return found or [{"code": "E_CARGO", "message": run.stderr[-2000:], "rendered": run.stderr[-2000:], "spans": []}]
|
||||
|
||||
|
||||
def compiles(stage):
|
||||
return stage is not None and check_errors(stage) is None
|
||||
|
||||
|
||||
def traced(errors, tree, mods):
|
||||
"""{id: its first error} for the errors in `mods`' own code (native_trace)."""
|
||||
tracer = native_trace.Tracer(WORK / "check", tree, {(b.id, b.version) for b in mods})
|
||||
found = {}
|
||||
for e in errors:
|
||||
for span in e["spans"]:
|
||||
owner = tracer.owner(span)
|
||||
if owner and owner not in found:
|
||||
where = f" ({span['file_name']}:{span.get('line_start')})"
|
||||
found[owner] = {"code": e["code"], "message": e["message"] + where}
|
||||
return found
|
||||
|
||||
|
||||
def broken_compiling(tool, tree, mods, revision, needed):
|
||||
"""The mods of `mods` (all applying) whose own code doesn't compile on `revision`: {id: error}."""
|
||||
by_id = {b.id: b for b in mods}
|
||||
left, broken = list(mods), {}
|
||||
for _ in range(COMPILE_ROUNDS):
|
||||
stage, _ = apply(tool, tree, left, revision, "health-compile")
|
||||
if stage is None:
|
||||
break
|
||||
errors = check_errors(stage)
|
||||
if errors is None:
|
||||
log(f" {len(left)} native mods compile together")
|
||||
return broken
|
||||
found = traced(errors, tree, left)
|
||||
if not found:
|
||||
log(" the errors are in no mod's code: stopping the compile check here")
|
||||
break
|
||||
for i, error in sorted(found.items()):
|
||||
if error["code"] in PAIR_CODES:
|
||||
alone = [by_id[n] for n in sorted(needed(by_id[i], set())) if n in by_id] + [by_id[i]]
|
||||
stage, _ = apply(tool, tree, alone, revision, f"health-alone-{i}")
|
||||
if stage is None or i not in traced(check_errors(stage) or [], tree, alone):
|
||||
log(f" {i}: {error['code']} only next to other mods: not broken")
|
||||
continue
|
||||
log(f" {i} {by_id[i].version}: {error['code']}: {error['message']}")
|
||||
broken[i] = error
|
||||
gone = set(found)
|
||||
while True:
|
||||
more = {b.id for b in left if b.id not in gone and any(n in gone for n in b.needs)}
|
||||
if not more:
|
||||
break
|
||||
for i in more:
|
||||
if any(n in broken for n in by_id[i].needs):
|
||||
broken[i] = {"code": "E_DEPENDENCY", "message": f"needs {', '.join(by_id[i].needs)}"}
|
||||
gone |= more
|
||||
left = [b for b in left if b.id not in gone]
|
||||
return broken
|
||||
|
||||
|
||||
def new_entries(base, head):
|
||||
"""(id, version, file, sha256) of each native-mod version HEAD lists and BASE doesn't."""
|
||||
def versions(root):
|
||||
out = {}
|
||||
for path in sorted((Path(root) / "mods").glob("*.toml")):
|
||||
entry = tomllib.loads(path.read_text())
|
||||
if entry.get("kind") == "native-mod":
|
||||
for v in entry.get("versions", []):
|
||||
out[(entry["id"], v["version"])] = (v["file"], v["sha256"])
|
||||
return out
|
||||
before = versions(base)
|
||||
return [(i, v, f, s) for (i, v), (f, s) in versions(head).items() if (i, v) not in before]
|
||||
|
||||
|
||||
def new_versions(base, head):
|
||||
"""The native-mod versions HEAD lists that BASE doesn't: their file from its release once published, else
|
||||
from the `staging` prerelease the submission form uploads it to (scripts/fetch.sh)."""
|
||||
found = []
|
||||
for id, version, file, sha in new_entries(base, head):
|
||||
releases = f"https://github.com/{REGISTRY}/releases/download"
|
||||
try:
|
||||
data = download(f"{releases}/{id}-{version}/{file}", sha)
|
||||
except Exception:
|
||||
data = download(f"{releases}/staging/{sha}.hnmod", sha)
|
||||
found.append(Bundle(data, "submitted"))
|
||||
return found
|
||||
|
||||
|
||||
def check(args):
|
||||
submitted = new_versions(args.base, args.head)
|
||||
if not submitted:
|
||||
print("No new native mod version.")
|
||||
return 0
|
||||
registry = published()
|
||||
revision = native.latest_nightly() if args.horizon == "latest" else args.horizon
|
||||
tree = native.source(revision)
|
||||
failed = []
|
||||
for mod in submitted:
|
||||
others = [b for b in registry if b.id != mod.id]
|
||||
print(f"## {mod.id} {mod.version} on Horizon {revision[:7]} (the newest Nightly)")
|
||||
clashes = sorted({(b.id, f, n) for b in others for f, n in mod.modules & b.modules})
|
||||
for other, file, name in clashes:
|
||||
print(f"- support module `{name}` in `{file}` is also {other}'s: the launcher can't build both."
|
||||
f" Name it after the mod (`{mod.id}_…`).")
|
||||
popular = others[:args.popular]
|
||||
tool = mixins_tool([mod, *popular])
|
||||
stage, refused = apply(tool, tree, [mod, *popular], revision, f"{mod.id}-together")
|
||||
for other, diagnostic in refused.items():
|
||||
if other != mod.id:
|
||||
print(f"- {other} (popular) doesn't apply on this Nightly either: {diagnostic.get('code')}."
|
||||
" The launcher leaves it out.")
|
||||
if mod.id in refused:
|
||||
d = refused[mod.id]
|
||||
print(f"- **doesn't apply**: `{d.get('code')}`: {d.get('message')}"
|
||||
+ (f" (injection `{d['injection_id']}`)" if d.get("injection_id") else ""))
|
||||
failed.append(mod.id)
|
||||
continue
|
||||
if clashes:
|
||||
failed.append(mod.id)
|
||||
if args.no_compile:
|
||||
continue
|
||||
if compiles(stage):
|
||||
print(f"- applies and compiles with {len(popular) - len(refused)} popular mods.")
|
||||
continue
|
||||
alone, _ = apply(tool, tree, [mod], revision, f"{mod.id}-alone")
|
||||
without, _ = apply(tool, tree, [b for b in popular if b.id not in refused], revision, f"{mod.id}-without")
|
||||
if not compiles(alone):
|
||||
print("- **doesn't compile** on this Nightly, even alone (the errors are in the log).")
|
||||
failed.append(mod.id)
|
||||
elif without is None or compiles(without):
|
||||
print("- **doesn't compile together with the popular mods**, while each side compiles alone.")
|
||||
failed.append(mod.id)
|
||||
else:
|
||||
print("- compiles alone; the popular mods already don't compile together on this Nightly.")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def health(args):
|
||||
registry = published()
|
||||
revision = native.latest_nightly() if args.horizon == "latest" else args.horizon
|
||||
tree = native.source(revision)
|
||||
tool = mixins_tool([])
|
||||
_, together = apply(tool, tree, registry, revision, "health")
|
||||
# A mod refused only next to another (both overwrite one function, both name one module) builds for a
|
||||
# player without that other one: broken on this Horizon is what horizon-mixins refuses with only the mods
|
||||
# it needs.
|
||||
by_id = {b.id: b for b in registry}
|
||||
def needed(bundle, seen):
|
||||
for n in bundle.needs:
|
||||
if n in by_id and n not in seen:
|
||||
seen.add(n)
|
||||
needed(by_id[n], seen)
|
||||
return seen
|
||||
refused = {}
|
||||
for i in sorted(together):
|
||||
mods = [by_id[n] for n in sorted(needed(by_id[i], set()))] + [by_id[i]]
|
||||
_, alone = apply(tool, tree, mods, revision, f"health-{i}")
|
||||
if i in alone:
|
||||
refused[i] = alone[i]
|
||||
else:
|
||||
log(f" {i} applies without the others: not broken")
|
||||
# The rest, compiled; without the mods the launcher prepares differently (Bundle.api) and those needing them.
|
||||
skipped = {b.id for b in registry if b.api}
|
||||
while more := {b.id for b in registry if b.id not in skipped and any(n in skipped for n in b.needs)}:
|
||||
skipped |= more
|
||||
if skipped:
|
||||
log(f" not compiled here (an API the launcher prepares): {', '.join(sorted(skipped))}")
|
||||
compiling = [b for b in registry if b.id not in refused and b.id not in skipped]
|
||||
failed = broken_compiling(tool, tree, compiling, revision, needed) if args.compile else {}
|
||||
broken = {i: dict(d, stage="apply") for i, d in refused.items()}
|
||||
broken |= {i: dict(d, stage="compile") for i, d in failed.items()}
|
||||
report = {
|
||||
"horizon": revision,
|
||||
"mixins": tool.parent.parent.name,
|
||||
"broken": {i: {"version": by_id[i].version, "code": d.get("code"), "message": d.get("message"),
|
||||
"stage": d["stage"]}
|
||||
for i, d in sorted(broken.items())},
|
||||
}
|
||||
Path(args.out).write_text(json.dumps(report, indent=2) + "\n")
|
||||
print(f"{len(broken)} of {len(registry)} native mods don't build on Horizon {revision[:7]}: "
|
||||
f"{len(refused)} don't apply ({', '.join(sorted(refused))}), {len(failed)} don't compile "
|
||||
f"({', '.join(sorted(failed))}).")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = parser.add_subparsers(dest="action", required=True)
|
||||
c = sub.add_parser("check")
|
||||
c.add_argument("base")
|
||||
c.add_argument("head")
|
||||
c.add_argument("--popular", type=int, default=20)
|
||||
c.add_argument("--no-compile", action="store_true")
|
||||
n = sub.add_parser("new", help="list the new native-mod versions, one per line")
|
||||
n.add_argument("base")
|
||||
n.add_argument("head")
|
||||
h = sub.add_parser("health")
|
||||
h.add_argument("--out", required=True)
|
||||
h.add_argument("--no-compile", dest="compile", action="store_false")
|
||||
for p in (c, h):
|
||||
p.add_argument("--horizon", default="latest", help="a Horizon commit, or `latest` (the newest Nightly)")
|
||||
args = parser.parse_args()
|
||||
if args.action == "new":
|
||||
for id, version, _, _ in new_entries(args.base, args.head):
|
||||
print(id, version)
|
||||
return
|
||||
sys.exit(check(args) if args.action == "check" else health(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,191 @@
|
||||
"""Which native mod a compiler error is in, as the launcher traces it (launcher/src/native_mods/diagnostics.rs).
|
||||
|
||||
An error is a mod's when its primary span lies inside what that mod brought into the staged game: one of its
|
||||
helpers (the function of the helper's name, absent from the pristine source) or one of its support modules (the
|
||||
inline module of its name, absent from the pristine source). An error that more than one mod's code holds, or none,
|
||||
names no mod.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
PLAN = ".horizon-mixins-stage.json"
|
||||
|
||||
|
||||
def masked(src):
|
||||
"""`src` (bytes) with comments, strings and char literals blanked, so braces and keywords are code's."""
|
||||
out = bytearray(src)
|
||||
i, n = 0, len(src)
|
||||
|
||||
def blank(a, b):
|
||||
for k in range(a, min(b, n)):
|
||||
if out[k] != ord("\n"):
|
||||
out[k] = ord(" ")
|
||||
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if src.startswith(b"//", i):
|
||||
end = src.find(b"\n", i)
|
||||
end = n if end < 0 else end
|
||||
blank(i, end)
|
||||
i = end
|
||||
elif src.startswith(b"/*", i):
|
||||
depth, k = 1, i + 2
|
||||
while k < n and depth:
|
||||
if src.startswith(b"/*", k):
|
||||
depth, k = depth + 1, k + 2
|
||||
elif src.startswith(b"*/", k):
|
||||
depth, k = depth - 1, k + 2
|
||||
else:
|
||||
k += 1
|
||||
blank(i, k)
|
||||
i = k
|
||||
elif c in b"rb" and (i == 0 or not (chr(src[i - 1]).isalnum() or src[i - 1] == ord("_"))) and \
|
||||
re.match(rb"(?:br|r)(#*)\"", src[i:i + 260]):
|
||||
hashes = re.match(rb"(?:br|r)(#*)\"", src[i:i + 260])
|
||||
close = b'"' + hashes.group(1)
|
||||
end = src.find(close, i + hashes.end())
|
||||
end = n if end < 0 else end + len(close)
|
||||
blank(i, end)
|
||||
i = end
|
||||
elif c == ord('"'):
|
||||
k = i + 1
|
||||
while k < n and src[k] != ord('"'):
|
||||
k += 2 if src[k] == ord("\\") else 1
|
||||
blank(i, k + 1)
|
||||
i = k + 1
|
||||
elif c == ord("'"):
|
||||
# A char literal ('x', '\n', '\u{1F600}'), else a lifetime ('a).
|
||||
if i + 1 < n and src[i + 1] == ord("\\"):
|
||||
end = src.find(b"'", i + 2)
|
||||
end = n if end < 0 else end + 1
|
||||
blank(i, end)
|
||||
i = end
|
||||
elif i + 2 < n and src[i + 2] == ord("'"):
|
||||
blank(i, i + 3)
|
||||
i += 3
|
||||
else:
|
||||
m = re.match(rb"'([^\x00-\x7f]+)'", src[i:i + 8])
|
||||
if m:
|
||||
blank(i, i + m.end())
|
||||
i += m.end()
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def item_ranges(code, keyword, name):
|
||||
"""Byte ranges of the `fn NAME` or `mod NAME { … }` items in masked source `code`, through their closing brace."""
|
||||
pattern = rb"\b%s\s+%s\b" % (keyword.encode(), re.escape(name.encode()))
|
||||
ranges = []
|
||||
for m in re.finditer(pattern, code):
|
||||
# The body's `{`, unless a `;` ends the item first; either outside the signature's () and [] (`[f64; 3]`).
|
||||
open_at, nested, k = -1, 0, m.end()
|
||||
while k < len(code):
|
||||
c = code[k]
|
||||
if c in b"([":
|
||||
nested += 1
|
||||
elif c in b")]":
|
||||
nested -= 1
|
||||
elif nested <= 0 and c in b"{;":
|
||||
open_at = k if c == ord("{") else -1
|
||||
break
|
||||
k += 1
|
||||
if open_at < 0:
|
||||
continue # a declaration without a body
|
||||
if keyword == "mod" and code[m.end():open_at].strip():
|
||||
continue
|
||||
depth, k = 0, open_at
|
||||
while k < len(code):
|
||||
if code[k] == ord("{"):
|
||||
depth += 1
|
||||
elif code[k] == ord("}"):
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
ranges.append(_with_call(code, name, m.start(), k + 1) if keyword == "fn" else (m.start(), k + 1))
|
||||
break
|
||||
k += 1
|
||||
return ranges
|
||||
|
||||
|
||||
def _with_call(code, name, start, end):
|
||||
"""The block horizon-mixins wraps a helper in, `{ fn NAME(…) { … } NAME(…) }`, when that is all the block holds:
|
||||
an error in the generated call (an argument the helper's signature no longer takes) is the helper's too."""
|
||||
before = re.search(rb"\{\s*(?:#\[[^\]]*\]\s*)*\Z", code[max(0, start - 400):start])
|
||||
after = re.match(rb"\s*%s\s*\([^{}]*\)\s*;?\s*\}" % re.escape(name.encode()), code[end:])
|
||||
if before and after:
|
||||
return start - (len(before.group(0))), end + after.end()
|
||||
return start, end
|
||||
|
||||
|
||||
def helper_name(source):
|
||||
m = re.search(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)", source or "")
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
class Tracer:
|
||||
"""Traces errors in the staged workspace `stage` (horizon-mixins' output, holding its plan) whose pristine source
|
||||
is `pristine`, to the mods in `mods` ({(id, version)})."""
|
||||
|
||||
def __init__(self, stage, pristine, mods):
|
||||
self.stage, self.pristine = Path(stage).resolve(), Path(pristine)
|
||||
plan = json.loads((self.stage / PLAN).read_text())
|
||||
self.entries = [] # (id, version, file, keyword, name)
|
||||
for e in plan.get("injections", []):
|
||||
name = helper_name(e.get("helper_source"))
|
||||
if name:
|
||||
self.entries.append((e.get("mod_id"), e.get("mod_version"), e.get("file"), "fn", name))
|
||||
for e in plan.get("modules", []):
|
||||
self.entries.append((e.get("mod_id"), e.get("mod_version"), e.get("file"), "mod", e.get("name")))
|
||||
self.entries = [e for e in self.entries if (e[0], e[1]) in mods and e[2] and e[4]]
|
||||
self.cache, self.code = {}, {}
|
||||
|
||||
def _masked(self, path):
|
||||
if path not in self.code:
|
||||
self.code[path] = masked(path.read_bytes()) if path.is_file() else b""
|
||||
return self.code[path]
|
||||
|
||||
def _ranges(self, rel, keyword, name):
|
||||
key = (rel, keyword, name)
|
||||
if key not in self.cache:
|
||||
found = []
|
||||
if (self.stage / rel).is_file() and not item_ranges(self._masked(self.pristine / rel), keyword, name):
|
||||
found = item_ranges(self._masked(self.stage / rel), keyword, name)
|
||||
self.cache[key] = found
|
||||
return self.cache[key]
|
||||
|
||||
def owner(self, span):
|
||||
"""The mod (id) whose code holds a cargo JSON span, or None."""
|
||||
try:
|
||||
path = (self.stage / span["file_name"]).resolve()
|
||||
rel = path.relative_to(self.stage).as_posix()
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
start, end = span.get("byte_start", 0), span.get("byte_end", 0)
|
||||
owners = {i for i, _, file, keyword, name in self.entries
|
||||
if file == rel and any(a <= start and end <= b for a, b in self._ranges(rel, keyword, name))}
|
||||
return owners.pop() if len(owners) == 1 else None
|
||||
|
||||
|
||||
def errors(cargo_stdout):
|
||||
"""The errors in `cargo --message-format=json` output: {code, message, rendered, spans (primary)}."""
|
||||
found = []
|
||||
for line in cargo_stdout.splitlines():
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
if value.get("reason") != "compiler-message":
|
||||
continue
|
||||
message = value.get("message") or {}
|
||||
if message.get("level") != "error":
|
||||
continue
|
||||
found.append({
|
||||
"code": (message.get("code") or {}).get("code") or "E_COMPILE",
|
||||
"message": message.get("message", ""),
|
||||
"rendered": message.get("rendered") or "",
|
||||
"spans": [s for s in message.get("spans", []) if s.get("is_primary")],
|
||||
})
|
||||
return found
|
||||
@@ -0,0 +1,110 @@
|
||||
"""python3 -m unittest discover -s scripts -p 'test_*.py'"""
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from native_trace import Tracer, errors, item_ranges, masked
|
||||
|
||||
PRISTINE = """struct MeshTables { base: u8, layers: u8 }
|
||||
pub fn resolve(x: u8) -> u8 {
|
||||
x
|
||||
}
|
||||
"""
|
||||
# One mod's helper inside a target (formatted with a trailing comma, an array type's `;` in its signature), another's
|
||||
# support module; braces in strings, chars and comments must not end them early.
|
||||
STAGED = """struct MeshTables { base: u8, layers: u8 }
|
||||
pub fn resolve(x: u8) -> u8 {
|
||||
if let Some(v) = {
|
||||
fn fast_mesh_resolve(x: u8, _at: [f64; 3],) -> Option<u8> {
|
||||
let _ = "}"; let _ = '}'; // }
|
||||
let _ = r#"}"#;
|
||||
Some(MeshTables { base: x }.base)
|
||||
}
|
||||
fast_mesh_resolve(x, [0.0; 3])
|
||||
} {
|
||||
return v;
|
||||
}
|
||||
x
|
||||
}
|
||||
mod carry_tables {
|
||||
pub fn lift<'a>(t: &'a super::MeshTables) -> u8 { t.missing }
|
||||
}
|
||||
fn untouched() -> u8 { MeshTables { base: 1 }.base }
|
||||
"""
|
||||
|
||||
|
||||
def span(text, needle, file="src/main.rs"):
|
||||
start = STAGED.encode().index(needle.encode())
|
||||
return {"file_name": file, "byte_start": start, "byte_end": start + len(needle), "is_primary": True}
|
||||
|
||||
|
||||
class Trace(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dir = Path(tempfile.mkdtemp())
|
||||
(self.dir / "stage/src").mkdir(parents=True)
|
||||
(self.dir / "pristine/src").mkdir(parents=True)
|
||||
(self.dir / "stage/src/main.rs").write_text(STAGED)
|
||||
(self.dir / "pristine/src/main.rs").write_text(PRISTINE)
|
||||
self.plan([
|
||||
{"mod_id": "fast_mesh", "mod_version": "0.1.0", "file": "src/main.rs",
|
||||
"helper_source": "fn fast_mesh_resolve(x: u8) -> Option<u8> { Some(MeshTables { base: x }.base) }"},
|
||||
], [{"mod_id": "carryblock", "mod_version": "0.1.2", "file": "src/main.rs", "name": "carry_tables"}])
|
||||
self.mods = {("fast_mesh", "0.1.0"), ("carryblock", "0.1.2")}
|
||||
|
||||
def plan(self, injections, modules):
|
||||
(self.dir / "stage/.horizon-mixins-stage.json").write_text(
|
||||
json.dumps({"schema": 6, "injections": injections, "modules": modules}))
|
||||
|
||||
def tracer(self, mods=None):
|
||||
return Tracer(self.dir / "stage", self.dir / "pristine", mods or self.mods)
|
||||
|
||||
def test_an_error_in_a_helper_names_its_mod_though_formatting_changed_it(self):
|
||||
self.assertEqual(self.tracer().owner(span(STAGED, "MeshTables { base: x }")), "fast_mesh")
|
||||
|
||||
def test_an_error_in_a_support_module_names_its_mod(self):
|
||||
self.assertEqual(self.tracer().owner(span(STAGED, "t.missing")), "carryblock")
|
||||
|
||||
def test_an_error_in_the_generated_call_to_a_helper_names_its_mod(self):
|
||||
self.assertEqual(self.tracer().owner(span(STAGED, "[0.0; 3]")), "fast_mesh")
|
||||
|
||||
def test_an_error_in_horizons_own_code_names_no_mod(self):
|
||||
self.assertIsNone(self.tracer().owner(span(STAGED, "MeshTables { base: 1 }")))
|
||||
self.assertIsNone(self.tracer().owner(span(STAGED, "return v")))
|
||||
|
||||
def test_a_path_through_another_crate_folder_resolves(self):
|
||||
(self.dir / "stage/lib").mkdir()
|
||||
s = span(STAGED, "t.missing", file="lib/../src/main.rs")
|
||||
self.assertEqual(self.tracer().owner(s), "carryblock")
|
||||
|
||||
def test_a_function_the_pristine_source_has_is_not_a_mods(self):
|
||||
(self.dir / "pristine/src/main.rs").write_text(PRISTINE + "fn fast_mesh_resolve() {}\n")
|
||||
self.assertIsNone(self.tracer().owner(span(STAGED, "MeshTables { base: x }")))
|
||||
|
||||
def test_a_mod_not_built_or_two_owners_name_no_mod(self):
|
||||
self.assertIsNone(self.tracer({("carryblock", "0.1.2")}).owner(span(STAGED, "MeshTables { base: x }")))
|
||||
self.plan([{"mod_id": m, "mod_version": "1", "file": "src/main.rs",
|
||||
"helper_source": "fn fast_mesh_resolve() {}"} for m in ("a", "b")], [])
|
||||
self.assertIsNone(self.tracer({("a", "1"), ("b", "1")}).owner(span(STAGED, "MeshTables { base: x }")))
|
||||
|
||||
def test_braces_in_literals_and_comments_are_not_code(self):
|
||||
code = masked(STAGED.encode())
|
||||
(start, end), = item_ranges(code, "fn", "fast_mesh_resolve")
|
||||
self.assertTrue(STAGED.encode()[start:end].endswith(b"}.base)\n }\n fast_mesh_resolve(x, [0.0; 3])\n }"))
|
||||
self.assertEqual(len(code), len(STAGED.encode()))
|
||||
|
||||
def test_cargo_json_errors(self):
|
||||
out = "\n".join([
|
||||
"not json",
|
||||
json.dumps({"reason": "compiler-artifact"}),
|
||||
json.dumps({"reason": "compiler-message", "message": {"level": "warning", "message": "w", "spans": []}}),
|
||||
json.dumps({"reason": "compiler-message", "message": {
|
||||
"level": "error", "message": "missing fields", "code": {"code": "E0063"}, "rendered": "error[E0063]",
|
||||
"spans": [{"file_name": "a.rs", "is_primary": True}, {"file_name": "b.rs", "is_primary": False}]}}),
|
||||
])
|
||||
(only,) = errors(out)
|
||||
self.assertEqual((only["code"], [s["file_name"] for s in only["spans"]]), ("E0063", ["a.rs"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||