diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3738138 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# Every change needs a review from one of these before it merges (branch protection: +# "Require review from Code Owners"). Add trusted contributors to the first line. +* @ludovic111 @rdbtCVS @levzzz5154 @socksprox +/.github/ @ludovic111 +/revoked.toml @ludovic111 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ccfe4e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Submit a project + url: https://playhorizon.dev/mods/submit + about: Mods, modpacks and resource packs for Horizon. Each submission is reviewed. + - name: Horizon itself + url: https://github.com/ludovic111/horizon/issues + about: Bugs in the game or the launcher. diff --git a/.github/ISSUE_TEMPLATE/report.yml b/.github/ISSUE_TEMPLATE/report.yml new file mode 100644 index 0000000..f8dbfc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report.yml @@ -0,0 +1,43 @@ +name: Report a project +description: Something in the registry does harm, breaks the game, isn't what it says, or isn't its author's to publish. +title: "Report: " +labels: [report] +body: + - type: markdown + attributes: + value: | + Thanks. Reviewers read every report. If it is dangerous (it deletes files, steals + accounts, spreads), say so in the first line: it can be taken down within hours, and + the launcher then turns installed copies off. Bugs in a mod go to its source repository. + - type: input + id: project + attributes: + label: Project id + description: The last part of its page's address, playhorizon.dev/mods/ + placeholder: coords_hud + validations: + required: true + - type: input + id: version + attributes: + label: Version + placeholder: 0.1.0, or "all" + - type: dropdown + id: why + attributes: + label: What is wrong + options: + - It does harm (files, accounts, chat it shouldn't send, anything malicious) + - It isn't what its page says + - It breaks the game or other mods + - It isn't the submitter's to publish (copyright, licence, stolen) + - Something else + validations: + required: true + - type: textarea + id: details + attributes: + label: What happened + description: Steps, what you saw, and logs if you have them (the game's log is latest.log in its folder). + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e89fe9e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ + + +**Project:** + +- [ ] I made it, or I am allowed to publish it, under the licence the entry names. +- [ ] The `source` link has the code this file was built from. +- [ ] I ran `cargo horizon-mod check ` and it passed. + +The file goes in the `staging` release as `.` (the form uploads it; by hand, ask a reviewer). diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..91da193 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,82 @@ +# A pull request's checks: what it changes compared with main, each new file's +# own check (its kind's, its signature, a trial run of a mod in the sandbox), and +# the index it would publish. Runs with a read-only token and no secrets; the tool +# comes from Horizon, never from the pull request, and a submission's code only +# runs inside Horizon's mod sandbox. +name: check +on: + pull_request: + branches: [main] +permissions: + contents: read +concurrency: + group: check-${{ github.event.pull_request.number }} + cancel-in-progress: true +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + HORIZON_REV: ${{ vars.HORIZON_REV || 'main' }} + 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 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin + ~/.cargo/horizon-rev + key: cargo-horizon-mod-${{ env.HORIZON_REV }} + # main's scripts, not the pull request's. + - run: base/scripts/install-tool.sh + - name: Who submitted it + id: author + env: + LOGIN: ${{ github.event.pull_request.user.login }} + BODY: ${{ github.event.pull_request.body }} + BOT: ${{ vars.REGISTRY_BOT || 'horizon-mods-bot' }} + run: | + # The submission form opens pull requests as the bot and names the + # GitHub account that signed in to it. + author="$LOGIN" + if [ "$LOGIN" = "$BOT" ] || [ "$LOGIN" = "${BOT}[bot]" ]; then + author="$(printf '%s' "$BODY" | sed -n 's/.*.*/\1/p' | head -1)" + fi + echo "login=${author:-$LOGIN}" >> "$GITHUB_OUTPUT" + - name: What it changes + run: | + set -o pipefail + { echo "## Changes"; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + status=0 + cargo horizon-mod registry review base head --author "${{ steps.author.outputs.login }}" 2>&1 \ + | tee -a "$GITHUB_STEP_SUMMARY" || status=1 + echo '```' >> "$GITHUB_STEP_SUMMARY" + exit $status + - name: Download its files + run: base/scripts/fetch.sh head + - name: Check each new file + run: | + set -o pipefail + { echo "## New files"; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + status=0 + cargo horizon-mod registry check base head --files files 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" || status=1 + echo '```' >> "$GITHUB_STEP_SUMMARY" + exit $status + - name: The index it would publish + run: | + cargo horizon-mod registry build head --files files --out index.json --generated "$(date -u +%FT%TZ)" + python3 - >> "$GITHUB_STEP_SUMMARY" <<'PY' + import json + index = json.load(open("index.json")) + print("## index.json") + for p in index["projects"]: + print(f"- {p['kind']} `{p['id']}` {p['versions'][0]['version']}") + PY diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml new file mode 100644 index 0000000..226a19d --- /dev/null +++ b/.github/workflows/cleanup.yml @@ -0,0 +1,43 @@ +# The submission form uploads files to the `staging` prerelease as +# .. Once a version is published in its own release, or when +# no open pull request lists a file any more, its staging copy goes: +# published ones after 7 days, abandoned ones after 30. +name: cleanup +on: + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: +permissions: + contents: write + pull-requests: read +jobs: + cleanup: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/checkout@v4 + - name: Delete staging files nobody needs + run: | + set -euo pipefail + # Hashes main lists (published), and those open pull requests add. + grep -ho 'sha256 = "[0-9a-f]\{64\}"' mods/*.toml | cut -d'"' -f2 | sort -u > published.txt || true + : > pending.txt + for pr in $(gh pr list --state open --json number --jq '.[].number'); do + gh pr diff "$pr" | grep -o '^+sha256 = "[0-9a-f]\{64\}"' | cut -d'"' -f2 >> pending.txt || true + done + now=$(date +%s) + gh api --paginate "repos/$GH_REPO/releases/tags/staging" --jq '.assets[] | [.id, .name, .created_at] | @tsv' | + while IFS=$'\t' read -r id name created; do + sha="${name%%.*}" + age=$(( (now - $(date -d "$created" +%s)) / 86400 )) + grep -qx "$sha" pending.txt && continue + if grep -qx "$sha" published.txt; then + [ "$age" -ge 7 ] || continue + else + [ "$age" -ge 30 ] || continue + fi + echo "Deleting $name ($age days)" + gh api -X DELETE "repos/$GH_REPO/releases/assets/$id" + done diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..0889cda --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,65 @@ +# Merging publishes: each new version's file becomes a release asset +# (-/), then index.json and the icons go to GitHub Pages +# (https://ludovic111.github.io/horizon-mods/index.json), which the launcher and +# playhorizon.dev/mods read. Nothing is published if any entry fails its checks. +name: publish +on: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: write + pages: write + id-token: write +concurrency: + group: publish + cancel-in-progress: false +jobs: + publish: + runs-on: ubuntu-latest + timeout-minutes: 30 + environment: + name: github-pages + url: ${{ steps.pages.outputs.page_url }} + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + HORIZON_REV: ${{ vars.HORIZON_REV || 'main' }} + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin + ~/.cargo/horizon-rev + key: cargo-horizon-mod-${{ env.HORIZON_REV }} + - run: scripts/install-tool.sh + - run: scripts/fetch.sh . + - name: Build the index + run: | + mkdir -p site + cargo horizon-mod registry build . --files files --out site/index.json --generated "$(date -u +%FT%TZ)" + - name: Publish new files as releases + run: | + cargo horizon-mod registry list . | while read -r sha id version file; do + tag="$id-$version" + gh release view "$tag" >/dev/null 2>&1 && continue + dir="$(mktemp -d)" + cp "files/$sha" "$dir/$file" + gh release create "$tag" "$dir/$file" --title "$id $version" --latest=false \ + --notes "Reviewed in the registry: mods/$id.toml. SHA-256 $sha." + done + - name: The site + run: | + cp -R icons site/icons + cat > site/index.html <<'HTML' + Horizon mods + +

playhorizon.dev/mods ยท index.json

+ HTML + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: pages + uses: actions/deploy-pages@v4 diff --git a/icons/coords_hud.png b/icons/coords_hud.png new file mode 100644 index 0000000..8f95203 Binary files /dev/null and b/icons/coords_hud.png differ diff --git a/mods/coords_hud.toml b/mods/coords_hud.toml new file mode 100644 index 0000000..00b8e9e --- /dev/null +++ b/mods/coords_hud.toml @@ -0,0 +1,26 @@ +id = "coords_hud" +kind = "mod" +name = "Coordinates HUD" +summary = "Your coordinates, facing and the time of day in a corner of the screen." +description = """ +The SDK's example mod. It draws your block position, the direction you face and the time of day +in the top-left corner, every frame. + +- `/where` puts your coordinates and dimension in chat. +- `/coords` hides or shows the panel. + +It reads the game (position, health, time), draws on the HUD and writes to chat: nothing else. +""" +authors = ["Horizon"] +owners = ["ludovic111"] +license = "MIT" +source = "https://github.com/ludovic111/horizon/tree/main/crates/horizon-mod-sdk/examples/coords-hud" +icon = "icons/coords_hud.png" + +[[versions]] +version = "0.1.0" +published = "2026-10-24" +game = "26.3" +file = "coords_hud-0.1.0.hmod" +sha256 = "612335e4e00d638f8b97b6c2f5c718c069c7a13811ba922557c27cb2612daf59" +changelog = "First release: the panel, `/where` and `/coords`." diff --git a/revoked.toml b/revoked.toml new file mode 100644 index 0000000..50dbc69 --- /dev/null +++ b/revoked.toml @@ -0,0 +1,8 @@ +# Files taken down. The launcher refuses to install them and turns installed copies off, +# showing the reason. One file by its sha256, or every version of an id without one. +# +# [[revoked]] +# id = "some_mod" +# sha256 = "..." +# reason = "What it did, in one sentence players will read." +# date = "2026-10-24" diff --git a/scripts/fetch.sh b/scripts/fetch.sh new file mode 100755 index 0000000..960283a --- /dev/null +++ b/scripts/fetch.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Downloads every file the entries list into files/: from its own release +# (-) once published, else from the `staging` prerelease, where the +# submission form uploads .. Usage: scripts/fetch.sh [checkout] +# Needs `gh` (GH_TOKEN) and cargo-horizon-mod. FILES_FROM= reads / +# instead, for a local trial. +set -euo pipefail +checkout="${1:-.}" +mkdir -p files +cargo horizon-mod registry list "$checkout" | while read -r sha id version file; do + [ -f "files/$sha" ] && continue + ext="${file##*.}" + tmp="$(mktemp -d)" + if [ -n "${FILES_FROM:-}" ]; then + cp "$FILES_FROM/$file" "$tmp/$file" + elif gh release download "$id-$version" --pattern "$file" --dir "$tmp" 2>/dev/null; then + : + elif gh release download staging --pattern "$sha.$ext" --dir "$tmp"; then + mv "$tmp/$sha.$ext" "$tmp/$file" + else + echo "::error::$id $version: $file is neither in release $id-$version nor in staging as $sha.$ext" + exit 1 + fi + mv "$tmp/$file" "files/$sha" + rmdir "$tmp" +done diff --git a/scripts/install-tool.sh b/scripts/install-tool.sh new file mode 100755 index 0000000..e938025 --- /dev/null +++ b/scripts/install-tool.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Installs cargo-horizon-mod from Horizon at $HORIZON_REV (a commit; the repository +# variable HORIZON_REV, else main). The registry's checks are that tool's. +set -euo pipefail +rev="${HORIZON_REV:-main}" +if command -v cargo-horizon-mod >/dev/null && [ "$(cat ~/.cargo/horizon-rev 2>/dev/null)" = "$rev" ]; then + exit 0 +fi +cargo install --locked --git https://github.com/ludovic111/horizon --rev "$rev" cargo-horizon-mod 2>/dev/null \ + || cargo install --locked --git https://github.com/ludovic111/horizon --branch "$rev" cargo-horizon-mod +echo "$rev" > ~/.cargo/horizon-rev