Stand alone: read the game's files from its main, docs and setup for this repo

The site moves out of ludovic111/horizon (website/) into this repository, history kept.

- build.mjs reads the README, launcher/links.json, docs/api/, the design tokens and the roadmap from the game
  repository's main on GitHub, all from one commit (raw files of a branch are cached for minutes), or from a
  local checkout with HORIZON_CHECKOUT. The tokens and the roadmap try their new paths in the game repository
  first (crates/horizon-theme/tokens.css, roadmap/roadmap.json), then the old website/ ones.
- The local copies of tokens.css and roadmap.json go; the roadmap stays as a test fixture.
- Docs pages of this (private) repo lose "Edit this page on GitHub"; the API pages keep theirs.
- The benchmark fixtures are a copy of the telemetry server's; a test checks they still match.
- railway.json: no watch patterns, every push deploys. README and CLAUDE.md for people and agents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Ludovic MarieandClaude Opus 5.5 committed 2026-10-26 16:08:44 +01:00
1 parent 6a20f759dc
commit 6d7848a1b1
15 files changed
+195 -220

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
# horizon-site: playhorizon.dev
The website of Horizon (Minecraft: Java Edition 26.3 rewritten in Rust). Private. The game, the launcher and their
developer docs are in [ludovic111/horizon](https://github.com/ludovic111/horizon) (public), called "the game's
repository" below. People edit it too (not only agents): [README.md](README.md) has "Working on it", the layout, the docs
rules and the benchmarks; this page is the short map for agents.
## Build and test
```sh
node build.mjs && node serve.mjs # dist/, served on http://localhost:4173
node --test src/ scripts/ # all tests (npm test); no npm install needed
HORIZON_CHECKOUT=~/code/horizon node build.mjs # read the game's files from a local checkout instead of GitHub
```
`build.mjs` has no dependencies and fails on a broken docs link, a missing image or a README without its parity
table. `npm install` only for `npm run assets` (fonts, screenshots, pixel art from the game's repository) and
`npm run shots` (Playwright screenshots at every width).
## Where the data comes from
- **The game's `main` on GitHub**, all files from one commit (`horizonRef` in build.mjs): `README.md` (parity
table, harness count), `launcher/links.json` (Discord, Sponsor), `docs/api/*.md` (the `/docs/api` pages),
`crates/horizon-theme/tokens.css` (design tokens, generated by `cargo run -p xtask -- tokens` there),
`roadmap/roadmap.json` (the hub, and `/data/roadmap.json`, which Kira reads; generated by `tools/roadmap.py build`
there). Change those in the game's repository, never here.
- **GitHub's API at build time**: releases, contributors, the roadmap's issues and pull requests.
- **Other services**: the mod registry's index (ludovic111/horizon-mods), Kira's contributors list, the telemetry
server (players' benchmarks, the hub's forms; `telemetry.playhorizon.dev`).
- **This repository**: the pages (`src/`), the player docs (`docs/`), our benchmark runs (`data/benchmarks/`,
`public/benchmarks.json`), assets (`public/`).
## Content-Security-Policy
`Caddyfile` serves a strict CSP. Inline `<script>`/`<style>` blocks are allowed by the hashes the build writes
(`.csp-script`, `.csp-style`); inline handlers, `style="…"` attributes and `javascript:` URLs are blocked. A new
origin (fetch, image, font) needs an entry in the Caddyfile's CSP, and third-party scripts, fonts and trackers are not
allowed at all.
## Deploy
Railway project `horizon-site`, service `site`, environment `production`: every push to `main` here goes live within
minutes (Dockerfile: Node builds, Caddy serves). When the files above change on the game's `main`, the site is
redeployed from source: for now by `~/code/horizon-parity/tools/site_redeploy_watch.sh` on the Mac mini (every 10
minutes), later by the game repository's `site-redeploy.yml` workflow. A failed build keeps the previous deployment live. After a deploy, check
https://playhorizon.dev (/, /hub, /mods, /docs/, /releases, /benchmarks, /data/roadmap.json).
## Rules
- Branch work in a worktree under `.claude/worktrees/<name>`; keep the main checkout on `main`.
- The site's rules (disclaimer, no Minecraft logo, no trackers, reduced motion): README "Rules the site keeps".
- Agents' PRs start with `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, comments with
`🤖 Agent:`; commits end with a `Co-Authored-By:` line for the agent.
- Never commit Mojang's files, secrets, or `dist/` and `shots/`.
+2 -2
View File
@@ -1,5 +1,5 @@
# Railway builds this with the service's root directory set to `website`.
# Stage 1 renders dist/ from src/ and the README on main (no npm dependencies needed).
# Railway builds this repository with the Dockerfile at its root.
# Stage 1 renders dist/ from src/ and the game repository's files on its main (no npm dependencies needed).
FROM node:22-alpine AS build
WORKDIR /site
COPY build.mjs ./
+50 -18
View File
@@ -1,9 +1,11 @@
# Horizon's website
Live at **https://playhorizon.dev**.
Live at **https://playhorizon.dev**. The game, the launcher and their developer docs are in
[ludovic111/horizon](https://github.com/ludovic111/horizon) (public); this repository (private) is only the site.
A landing page, a Releases page, a Benchmarks page and a documentation section in Horizon's design language (`docs/design-system.md`), with the tokens of
`crates/horizon-theme` (generated into `src/tokens.css` by `cargo run -p xtask -- tokens`):
`crates/horizon-theme` (generated into the game repository's `crates/horizon-theme/tokens.css` by
`cargo run -p xtask -- tokens`, and read from there at build time):
the Blocks look: Pixelify Sans headings over Geist text, Horizon's purple in a light and a dark
appearance (the system's, or the toggle's), square panels with an outline, a bevel and a lip, a
stone texture and a pixel landscape drawn in code (`scripts/art.mjs`), and short, eased motion. A headline and the download, then a scroll through what it is, the screenshots, the parity grid, the launcher, the newest
@@ -13,6 +15,32 @@ no runtime dependencies.
Home, Releases, Benchmarks and every Docs page share the header and phone menu rendered by
`build.mjs`, with the same links, theme switch and Download button.
## Working on it
You need Node 22 or newer; nothing to install for the site itself.
```sh
git clone https://github.com/ludovic111/horizon-site && cd horizon-site
node build.mjs && node serve.mjs # build dist/ and open http://localhost:4173
node --test src/ scripts/ # the tests; run them before you push
```
- **A push to `main` goes live.** Railway rebuilds and deploys https://playhorizon.dev within a few minutes of every
push to `main`. If the build fails (a broken docs link, a missing image, an error in `build.mjs`), the previous
version stays online and nothing breaks; fix it and push again. For anything bigger than a typo, push a branch and
open a pull request first.
- **Some content isn't in this repository.** The parity table and harness count (the game's `README.md`), the
Discord and Sponsor links (`launcher/links.json`), the API pages under `/docs/api` (`docs/api/`), the colours and
spacing (`crates/horizon-theme/tokens.css`) and the hub's roadmap (`roadmap/roadmap.json`) come from
[ludovic111/horizon](https://github.com/ludovic111/horizon) at build time. Change them there; the site redeploys
by itself when they change on its `main`.
- **Content-Security-Policy.** The site is served with a strict CSP (`Caddyfile`). Inline `<script>` and `<style>`
blocks in the page templates are fine: the build hashes each one and the server allows exactly those. Inline event
handlers (`onclick="…"`), `style="…"` attributes and `javascript:` links are blocked: use a class and the
stylesheet, or a listener in the page's script. Every script, style, font and image comes from this site; the only
other origins allowed are GitHub (API, raw files, avatars), the mod registry's index and the telemetry server. A new
origin needs a change to the CSP in the `Caddyfile`, and the site loads no third-party scripts, fonts or trackers.
## Layout
```
@@ -28,7 +56,6 @@ src/benchmarks.html Horizon against vanilla 26.3 on our machines, the hardware p
src/hub.html the hub (/hub, /roadmap redirects to its Roadmap tab): Overview, Roadmap, Kanban, Journal, Contribute, a Simple / Technical switch
src/mods.html the registry's list (/mods) and a page per project (/mods/<id>), one template
src/docs.html the template of every documentation page (/docs/...), with its left navigation
src/tokens.css the design tokens, generated from crates/horizon-theme (do not edit)
src/style.css every style, on top of the tokens
src/main.js the home page: download buttons, recent releases, links, contributors, nav, tabs, lightbox, motion
src/chrome.js the light/dark switch, the phone menu and GitHub fetching, shared by the pages
@@ -42,9 +69,9 @@ src/estimate.js estimates for unmeasured hardware from the nearest measured m
src/markdown.js a small Markdown renderer (escapes everything), shared by build and page
src/releases.js the home page's "Recently added" and the Releases page's markup, shared by build and page
src/contributors.js the contributors' filtering and markup, shared by build and page
src/hub.js the hub's roadmap, board and journal from data/roadmap.json, GitHub's issues and the releases, shared by build and page; /data/journal.json
src/hub.js the hub's roadmap, board and journal from the game repository's roadmap/roadmap.json, GitHub's issues and the releases, shared by build and page; /data/journal.json
src/hub-page.js the hub page: tabs by #hash, the Simple / Technical switch (localStorage), the area filter, live states
data/roadmap.json the roadmap, generated by tools/roadmap.py from roadmap/roadmap.toml (do not edit); served at /data/roadmap.json
src/fixtures/roadmap.json a snapshot of the roadmap for the hub's tests (the build reads the live one)
public/ fonts, screenshots, icons, og.png, robots.txt, benchmarks.json: copied as is (sitemap.xml is written by the build)
data/benchmarks/ our raw benchmark reports (tools/perf_bench.py) and fleet.json, the list scripts/benchmarks.mjs reads
scripts/benchmarks.mjs benchmark reports -> public/benchmarks.json (ours, and players' groups of 3+ installs)
@@ -60,11 +87,12 @@ shots/ (gitignored) full-page screenshots at 360–1920 px, a 1250×
- **Built in** (`build.mjs`): Horizon's registry (`/mods`, one page per project) from the index ludovic111/horizon-mods
publishes (`HORIZON_MOD_INDEX=<file>` builds from a test index; docs/api/mod-index.md); the parity table, its score, harness count and audit date,
all from `README.md`; the documentation pages (`/docs`) from `website/docs/`; the Discord and Sponsor links from
`launcher/links.json`; the latest releases and the contributors (`/repos/ludovic111/horizon/contributors`,
all from the game's `README.md`; the documentation pages (`/docs`) from `docs/` here; the Discord and Sponsor links
from the game's `launcher/links.json`; the design tokens from its `crates/horizon-theme/tokens.css`; the hub's roadmap
from its `roadmap/roadmap.json` (also served as `/data/roadmap.json`, which Kira reads); the latest releases and the contributors (`/repos/ludovic111/horizon/contributors`,
bots left out, the owner first), when GitHub answers; without an answer the build still succeeds and
the Contributors block and its menu entries stay hidden until the page loads them. Locally these come from
the checkout; on Railway, which builds only this folder, from `main` on GitHub. The build
the Contributors block and its menu entries stay hidden until the page loads them. The game's files come from its `main` on GitHub, all from
the same commit (`HORIZON_CHECKOUT=<path>` reads a local checkout instead). The build
fails if the README has no parity table, so a broken README never deploys.
- **Fetched by the page**: the registry's index (a project merged after the build shows at once), the releases (the download button picks the newest
`launcher-v*` release's asset for the visitor's OS, the home page lists the newest game releases, and
@@ -84,7 +112,7 @@ between pages and every image, and fails with the list when one is broken. `/ser
redirects to `/docs/hosting`.
The API portal is the repository's `docs/api/`, not a copy: `nav.json` lists its README as `api` (`/docs/api`) and
its pages as `api/<page>` (`/docs/api/<page>`), read from the checkout or from `main` on GitHub like the README.
its pages as `api/<page>` (`/docs/api/<page>`), read from the game's `main` on GitHub like the README.
Links between its pages become the site's, every other relative link goes to the file on GitHub, and the same link
check applies. Markdown tables render as tables. The other developer docs in `docs/` stay on GitHub and are linked
from the Contributing page.
@@ -92,8 +120,9 @@ from the Contributing page.
## Run it locally
```sh
cd website
node build.mjs && node serve.mjs # http://localhost:4173
node --test src/ scripts/ # the tests (npm test)
HORIZON_CHECKOUT=../horizon node build.mjs # against a local checkout of the game (unmerged README, docs/api, roadmap)
```
Asset and screenshot scripts need the dev dependencies (sharp, wawoff2, Playwright):
@@ -101,7 +130,7 @@ Asset and screenshot scripts need the dev dependencies (sharp, wawoff2, Playwrig
```sh
npm install
GITHUB_TOKEN=$(gh auth token) node build.mjs # optional token: bakes the releases past the API rate limit
npm run assets # after a font, screenshot or logo changes in the repo
npm run assets # after a font, screenshot or logo changes in the game's repo (../horizon or HORIZON_CHECKOUT)
npm run shots # shots/*.png (gitignored; SHOTS=dir to write elsewhere) and public/og.png
```
@@ -112,20 +141,23 @@ and characters per line, for 360 to 1920 px wide and a 1250×700 window.
## Railway
The service deploys `main` (Railway's GitHub integration; there is no `website` branch any more) when `/website/**`, `README.md` or `launcher/links.json` change, and answers on https://playhorizon.dev. Its
Railway project `horizon-site`, service `site`, environment `production`. It deploys `main` of this repository on
every push (Railway's GitHub integration, root directory `/`), and answers on https://playhorizon.dev. Its
`*.up.railway.app` address redirects there with a 301 (the Caddyfile), so search engines index
one URL; the page also names it as canonical, in its Open Graph tags and in `sitemap.xml`.
Railway's healthcheck reaches `$PORT` under its own host name and is served normally.
Create a service from the GitHub repo with **root directory `website`**. `railway.json`
selects the Dockerfile: a Node stage runs `build.mjs` (reading the README from `main`), and
`railway.json` selects the Dockerfile: a Node stage runs `build.mjs` (reading the game's files from its `main`), and
a Caddy stage serves `dist/` on `$PORT` with compression, cache headers, a strict
Content-Security-Policy and the 404 page. Its watch patterns also redeploy when the README
or `launcher/links.json` change on main. No variables are needed; an optional
Content-Security-Policy and the 404 page. When the files the site reads change on the game's `main` (`README.md`,
`launcher/links.json`, `docs/api/`, `roadmap/roadmap.json`, `crates/horizon-theme/tokens.css`), a watcher runs
`railway redeploy --from-source`, so the site rebuilds from the new files (for now a loop on the maintainer's machine
every 10 minutes; later the game repository's `site-redeploy.yml` workflow, with a project token scoped to this
environment). No variables are needed; an optional
`GITHUB_TOKEN` (read-only, public repos) lets the build bake the releases even when
GitHub's anonymous rate limit is hit. `REGISTRY_UPSTREAM`
(`horizon-mod-registry.railway.internal:8080`) sends `/api/mods/*` to the registry's submission
service (`crates/horizon-mod-registry`); unset, `/mods/submit` says the form isn't open.
service (`crates/horizon-mod-registry` in the game's repository); unset, `/mods/submit` says the form isn't open.
## Rules the site keeps
+49 -19
View File
@@ -2,10 +2,11 @@
// the community links and contributors baked in, the documentation (docs/*.md, one page each, listed in
// docs/nav.json), plus public/ and fingerprinted CSS and JS. No dependencies.
//
// Railway builds only website/, so the README and links.json come from the repo when they
// are next to it (a local checkout) and from main on GitHub otherwise. The documentation lives in website/, except the
// API portal (docs/api/), read the same way as the README. The latest releases and
// the contributors are baked in when GitHub answers, so the page reads well before (or without) JavaScript.
// Horizon's own files (the README, launcher/links.json, the API portal in docs/api/, the design tokens and the roadmap)
// live in the game's repository, ludovic111/horizon, which is public: they come from its main on GitHub, pinned to one
// commit so every file is from the same one, or from a local checkout with HORIZON_CHECKOUT=<path>. The rest of the
// documentation lives here. The latest releases and the contributors are baked in when GitHub answers, so the page
// reads well before (or without) JavaScript.
import { readFile, writeFile, mkdir, rm, cp } from "node:fs/promises";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
@@ -23,15 +24,40 @@ import { INDEX_URL, KINDS, SORTS, SUBMIT, listHtml, projectHtml, readIndex, sort
const site = dirname(fileURLToPath(import.meta.url));
const dist = join(site, "dist");
const REPO = "ludovic111/horizon";
const RAW = `https://raw.githubusercontent.com/${REPO}/main/`;
const DEFAULT_LINKS = { discord: "https://discord.gg/HcTeMZxe5E", sponsor: "https://github.com/sponsors/ludovic111" };
async function repoFile(path) {
const local = join(site, "..", path);
if (existsSync(local)) return [await readFile(local, "utf8"), "the checkout"];
const res = await fetch(RAW + path, { signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`${path}: ${res.status} from GitHub`);
return [await res.text(), "main on GitHub"];
const CHECKOUT = process.env.HORIZON_CHECKOUT;
if (CHECKOUT && !existsSync(join(CHECKOUT, "README.md"))) throw new Error(`HORIZON_CHECKOUT=${CHECKOUT}: no README.md there`);
// main's commit: raw.githubusercontent.com caches a branch's files for minutes, a commit's are fixed. Without an
// answer from the API, main itself.
let horizonRef = "main";
if (!CHECKOUT) {
try {
const res = await fetch(`https://api.github.com/repos/${REPO}/commits/main`, {
headers: { Accept: "application/vnd.github.sha", ...(process.env.GITHUB_TOKEN ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}) },
signal: AbortSignal.timeout(10000),
});
if (!res.ok) throw new Error(`${res.status}`);
horizonRef = (await res.text()).trim();
} catch (e) {
console.warn(`main's commit: ${e.message}; reading main`);
}
}
// A file of the game's repository: the first of `paths` that exists (a file that moved there lists its new path
// first), with where it came from.
async function repoFile(...paths) {
for (const path of paths) {
if (CHECKOUT) {
if (existsSync(join(CHECKOUT, path))) return [await readFile(join(CHECKOUT, path), "utf8"), `the checkout ${CHECKOUT}`];
continue;
}
const res = await fetch(`https://raw.githubusercontent.com/${REPO}/${horizonRef}/${path}`, { signal: AbortSignal.timeout(15000) });
if (res.ok) return [await res.text(), horizonRef === "main" ? "main on GitHub" : `main on GitHub (${horizonRef.slice(0, 9)})`];
if (res.status !== 404) throw new Error(`${path}: ${res.status} from GitHub`);
}
throw new Error(`${paths.join(" or ")}: not in ${CHECKOUT || `${REPO}@${horizonRef}`}`);
}
// The README: required, a site with a stale or missing parity table is worse than no deploy.
@@ -95,10 +121,12 @@ try {
console.warn(`contributors: ${e.message}; the page will load them itself`);
}
// The hub's roadmap (data/roadmap.json, generated from roadmap/roadmap.toml by tools/roadmap.py) and the live state
// The hub's roadmap (roadmap/roadmap.json in the game's repository, generated from roadmap/roadmap.toml by
// tools/roadmap.py) and the live state
// of the issues and pull requests its items link: the open ones and those closed lately, then (with a token) the
// older ones in one GraphQL request. Without GitHub the hub shows the links without their state.
const roadmap = JSON.parse(await readFile(join(site, "data", "roadmap.json"), "utf8"));
const [roadmapText] = await repoFile("roadmap/roadmap.json", "website/data/roadmap.json");
const roadmap = JSON.parse(roadmapText);
const linked = new Set(roadmap.items.flatMap((i) => (i.links || []).map((l) => Number((l.match(/^#(\d+)$/) || [])[1])).filter(Boolean)));
let hubLive = [];
try {
@@ -248,9 +276,10 @@ for (const file of modules) {
await writeFile(join(dist, "assets", names[file]), code);
}
const jsName = names["main.js"];
// The design tokens come first: src/tokens.css is generated from crates/horizon-theme
// The design tokens come first: the game's repository generates them from crates/horizon-theme
// (`cargo run -p xtask -- tokens`), so the page shares the launcher's and the game's values.
const css = (await readFile(join(site, "src/tokens.css"), "utf8")) + "\n" + (await readFile(join(site, "src/style.css"), "utf8"));
const [tokens] = await repoFile("crates/horizon-theme/tokens.css", "website/src/tokens.css");
const css = tokens + "\n" + (await readFile(join(site, "src/style.css"), "utf8"));
// The stylesheet is inlined into the page, so the first paint needs no second request.
const cssName = `style.${fingerprint(css)}.css`;
await writeFile(join(dist, "assets", cssName), css);
@@ -369,7 +398,7 @@ const values = {
};
// --- The documentation ---------------------------------------------------------------------
// website/docs/<slug>.md: "# Title", a one-paragraph lede, then the body. docs/nav.json orders them in groups.
// docs/<slug>.md: "# Title", a one-paragraph lede, then the body. docs/nav.json orders them in groups.
// The API portal is the repository's docs/api/ (the pages and their generated references): nav.json lists it as
// `api` (its README, /docs/api) and `api/<page>` (/docs/api/<page>). Its links are written for GitHub, so links
// between its pages become the site's and every other relative link goes to the file on GitHub.
@@ -393,7 +422,7 @@ const docs = [];
for (const { group, pages: slugs } of docsNav) {
for (const slug of slugs) {
const api = slug === "api" || slug.startsWith("api/");
const file = api ? apiSource(slug) : `website/docs/${slug}.md`;
const file = api ? apiSource(slug) : `docs/${slug}.md`;
let text = (api ? (await repoFile(file))[0] : await readFile(join(site, "docs", `${slug}.md`), "utf8")).replace(/\r\n?/g, "\n");
if (api) text = apiLinks(text.replace(/^<!--[\s\S]*?-->\n/, ""), file);
const m = text.match(/^# (.+)\n\n((?:[^\n]+\n)+)\n/);
@@ -454,7 +483,8 @@ const docPages = renderedDocs.map((d, i) => {
DOC_TOC: toc.length >= 4 ? ` <nav class="docs-toc" aria-label="On this page"><p>On this page</p><ul>${toc.map((h) => `<li><a href="#${h.id}">${escape(h.text)}</a></li>`).join("")}</ul></nav>` : "",
DOC_BODY: d.html,
DOC_PAGER: pagerLink(renderedDocs[i - 1], "prev") + pagerLink(renderedDocs[i + 1], "next"),
DOC_FILE: d.file,
// The API portal is the game's public repository; the other pages are this private one's, so no edit link.
DOC_EDIT: d.slug === "api" || d.slug.startsWith("api/") ? `<a href="https://github.com/${REPO}/blob/main/${d.file}">Edit this page on GitHub</a> · ` : "",
SITE_NAV: siteNav("/docs/"),
},
};
@@ -531,7 +561,7 @@ for (const { file, values: own } of docPages) {
// The roadmap for other readers (Kira, scripts): /data/roadmap.json, as tools/roadmap.py wrote it.
await mkdir(join(dist, "data"), { recursive: true });
await cp(join(site, "data", "roadmap.json"), join(dist, "data", "roadmap.json"));
await writeFile(join(dist, "data", "roadmap.json"), roadmapText);
// The journal for Kira's #updates and its X posts: /data/journal.json, rebuilt with every deploy.
await writeFile(join(dist, "data", "journal.json"), JSON.stringify(journalJson(releases, hubLive, roadmap), null, 1) + "\n");
+1 -2
View File
@@ -6,11 +6,10 @@ Questions and ideas are welcome on [Discord](https://discord.gg/HcTeMZxe5E). Bug
## The repository
[github.com/ludovic111/horizon](https://github.com/ludovic111/horizon) holds the game, the launcher and this website, plus the tools, tests and docs that build, check and release them.
[github.com/ludovic111/horizon](https://github.com/ludovic111/horizon) holds the game and the launcher, plus the tools, tests and docs that build, check and release them. This website is built in a separate repository; its API pages come from the game's `docs/api/`.
- `crates/` is the game, as Rust crates: the client, the server, the world and protocol code, the mod system, the design tokens and more.
- `launcher/` is the launcher, a GPUI app with its own Cargo workspace.
- `website/` is this site and its documentation, in `website/docs/`.
- `tools/` holds the parity harness drivers, the parity runner, the release tools and the self-check.
- `tests/` holds the Python tests, `benchmarks/` the published results and `packaging/` the installers.
- `docs/` holds the developer documentation, [indexed here](https://github.com/ludovic111/horizon/blob/main/docs/README.md): the native client, the design system, world generation, the parity workflow, the release flow and more.
+1 -2
View File
@@ -2,8 +2,7 @@
"$schema": "https://railway.com/railway.schema.json",
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile",
"watchPatterns": ["/website/**", "/README.md", "/launcher/links.json", "/docs/api/**"]
"dockerfilePath": "Dockerfile"
},
"deploy": {
"healthcheckPath": "/",
+2 -1
View File
@@ -8,7 +8,8 @@ import { fileURLToPath } from "node:url";
import sharp from "sharp";
const site = join(dirname(fileURLToPath(import.meta.url)), "..");
const repo = join(site, "..");
// The game's repository, for the design tokens and the fonts: HORIZON_CHECKOUT, or ../horizon beside this one.
export const repo = process.env.HORIZON_CHECKOUT || join(site, "..", "horizon");
// The same small fixed random sequence as art.rs (xorshift), so the art is the same every time.
function rng(seed) {
+3 -3
View File
@@ -1,15 +1,15 @@
// Copies the launcher's fonts and the repo's screenshots into public/, draws the pixel art,
// converted for the web (woff2, webp in two widths). Run it after any of them change:
// `npm run assets`. Its output is committed, because Railway builds only website/.
// `npm run assets`, with the game's repository beside this one (or HORIZON_CHECKOUT=<path>). Its output is
// committed, because Railway builds only this repository.
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import sharp from "sharp";
import wawoff2 from "wawoff2";
import { writeArt } from "./art.mjs";
import { repo, writeArt } from "./art.mjs";
const site = join(dirname(fileURLToPath(import.meta.url)), "..");
const repo = join(site, "..");
const pub = join(site, "public");
const fonts = [
+27 -1
View File
@@ -1,4 +1,5 @@
// node --test (npm test in website/). The fixtures are shared with the telemetry server's summary test.
// node --test (npm test). The fixtures are the telemetry server's (crates/horizon-telemetry-server/tests/fixtures/
// benchmarks/ in the game's repository); the copy here must stay the same, which the last test checks when online.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
@@ -89,3 +90,28 @@ test("a display cap on our run is kept, and the comparison says at least", async
assert.equal(compareRows(fps, h, { medians: { avg_fps: 120 } }).text, "at least 1.2x faster");
assert.equal(compareRows(fps, h, { medians: { avg_fps: 200 } }).wins, "none");
});
// The server's copy on the game's main: both sides must make the same groups from the same items. Skipped offline.
test("the fixtures are the telemetry server's", async (t) => {
const repo = process.env.HORIZON_CHECKOUT;
for (const name of ["items.jsonl", "expected.json"]) {
const ours = readFileSync(fixture(name), "utf8");
let theirs = null;
if (repo) {
for (const dir of ["crates/horizon-telemetry-server/tests/fixtures/benchmarks", "website/scripts/fixtures/benchmarks"]) {
try { theirs = readFileSync(`${repo}/${dir}/${name}`, "utf8"); break; } catch {}
}
} else {
for (const dir of ["crates/horizon-telemetry-server/tests/fixtures/benchmarks", "website/scripts/fixtures/benchmarks"]) {
try {
const res = await fetch(`https://raw.githubusercontent.com/ludovic111/horizon/main/${dir}/${name}`, { signal: AbortSignal.timeout(10000) });
if (res.ok) { theirs = await res.text(); break; }
} catch {
return t.skip("GitHub unreachable");
}
}
}
assert.ok(theirs !== null, `${name}: not in the game's repository`);
assert.equal(ours, theirs, `${name} differs from the telemetry server's: copy it from the game's repository`);
}
});
+3 -2
View File
@@ -1,7 +1,8 @@
# Benchmark summary fixtures
Shared by the website's aggregator test (`website/scripts/benchmarks.test.mjs`) and the telemetry server's
`GET /v1/benchmarks/summary` test: the same `benchmark` items in, the same groups out.
A copy of the telemetry server's (`crates/horizon-telemetry-server/tests/fixtures/benchmarks/` in the game's
repository), used by `scripts/benchmarks.test.mjs`: the same `benchmark` items in, the same groups out as its
`GET /v1/benchmarks/summary` test. The test checks that the copy matches the game's main.
- `items.jsonl`: one stored item per line (`install_id`, `day`, `version`, `data`: a benchmark report). Group
RTX 4080 + i7-13700KF at render distance 12 has three installs (one with two runs); the same install at 32,
+1 -1
View File
@@ -46,7 +46,7 @@
<nav class="docs-pager" aria-label="Next and previous pages">
{{DOC_PAGER}}
</nav>
<p class="docs-edit"><a href="https://github.com/ludovic111/horizon/blob/main/{{DOC_FILE}}">Edit this page on GitHub</a> · Spotted something wrong? <a href="https://github.com/ludovic111/horizon/issues">Open an issue</a> or ask on <a href="{{DISCORD}}">Discord</a>.</p>
<p class="docs-edit">{{DOC_EDIT}}Spotted something wrong? <a href="https://github.com/ludovic111/horizon/issues">Open an issue</a> or ask on <a href="{{DISCORD}}">Discord</a>.</p>
<p class="disclaimer">Not an official Minecraft product. Not approved by or associated with Mojang or Microsoft.</p>
</main>
</div>
File renamed without changes.
+1 -1
View File
@@ -1,4 +1,4 @@
// The hub (/hub): the roadmap from website/data/roadmap.json (generated from roadmap/roadmap.toml by
// The hub (/hub): the roadmap from the game repository's roadmap/roadmap.json (generated from roadmap/roadmap.toml by
// tools/roadmap.py), with each item's issues and pull requests in their live GitHub state. Shared by build.mjs
// (baked into the page) and hub-page.js (refreshed from GitHub in the browser). Every item says the same thing
// twice: `plain` (.hub-plain) for players and `dev` (.hub-dev) for contributors; the page's Simple / Technical
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { boardFrom, boardHtml, creditFrom, creditHtml, fixesIn, itemOf, journalFrom, journalHtml, journalJson, overviewHtml, progress, roadmapHtml, slimIssue, suggestItemsHtml } from "./hub.js";
const roadmap = JSON.parse(readFileSync(new URL("../data/roadmap.json", import.meta.url), "utf8"));
const roadmap = JSON.parse(readFileSync(new URL("./fixtures/roadmap.json", import.meta.url), "utf8"));
const pr = (number, extra = {}) => ({ number, title: `PR ${number}`, state: "open", pull_request: { merged_at: null }, user: { login: "a" }, labels: [], assignees: [], body: "", ...extra });
const issue = (number, extra = {}) => ({ number, title: `Issue ${number}`, state: "open", user: { login: "b" }, labels: [{ name: "bug" }], assignees: [], ...extra });
-167
View File
@@ -1,167 +0,0 @@
/* Generated from crates/horizon-theme by `cargo run -p xtask -- tokens`; do not edit.
docs/design-system.md describes the tokens. */
:root {
--page: #ece7f7;
--plate-soft: #e9e3f8;
--plate: #ddd4f3;
--surface: #f8f6fd;
--rule: #e4ddf4;
--ink: #1b1530;
--ink-soft: #3b3354;
--muted: #585071;
--accent: #5333c9;
--on-accent: #ffffff;
--danger: #a3301d;
--success: #136336;
--warn: #7a4f00;
--side: #e2dbf2;
--panel: #f8f6fd;
--raised: #e9e3f8;
--button: #ddd4f3;
--button-bevel: #f4f0fe;
--well: #f3f0fb;
--bevel: #ffffff;
--lip: #c3b8e2;
--outline: #2b2244;
--face: #5b3fd6;
--face-bevel: #8069ef;
--face-lip: #3a2694;
--on-face: #ffffff;
color-scheme: light;
--font: "Geist", ui-sans-serif, system-ui, sans-serif;
--mono: "Geist Mono", ui-monospace, monospace;
--pixel: "Pixelify Sans", "Geist", ui-sans-serif, sans-serif;
--block-outline: 2px;
--block-bevel: 2px;
--block-lip: 4px;
--text-micro: 11px;
--text-micro-line: 1.5;
--text-micro-tracking: 0.04em;
--text-micro-weight: 400;
--text-caption: 12px;
--text-caption-line: 1.5;
--text-caption-tracking: 0em;
--text-caption-weight: 400;
--text-label: 13px;
--text-label-line: 1.5;
--text-label-tracking: 0em;
--text-label-weight: 400;
--text-ui: 14px;
--text-ui-line: 1.5;
--text-ui-tracking: 0em;
--text-ui-weight: 400;
--text-body: clamp(15px, calc(15px + 2 * (100vw - 375px) / 1065), 17px);
--text-body-line: 1.6;
--text-body-tracking: 0em;
--text-body-weight: 400;
--text-heading: clamp(20px, calc(20px + 4 * (100vw - 375px) / 1065), 24px);
--text-heading-line: 1.3;
--text-heading-tracking: -0.01em;
--text-heading-weight: 500;
--text-title: clamp(32px, calc(32px + 16 * (100vw - 375px) / 1065), 48px);
--text-title-line: 1.05;
--text-title-tracking: -0.02em;
--text-title-weight: 500;
--text-display: clamp(34px, calc(34px + 30 * (100vw - 375px) / 1065), 64px);
--text-display-line: 1.05;
--text-display-tracking: -0.025em;
--text-display-weight: 500;
--text-wordmark: 18vw;
--text-wordmark-line: 0.8;
--text-wordmark-tracking: -0.04em;
--text-wordmark-weight: 500;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
--space-9: 96px;
--gutter: 64px;
--gutter-narrow: 16px;
--radius: 8px;
--radius-plate: 12px;
--radius-pill: 999px;
--rule-width: 1px;
--control: 32px;
--control-large: 40px;
--segment: 28px;
--field: 36px;
--row: 86px;
--index: 40px;
--plate-bleed: 16px;
--rise: 8px;
--measure: 56ch;
--quick: 150ms;
--ease-quick: cubic-bezier(0.4, 0, 0.2, 1);
--move: 240ms;
--ease-move: cubic-bezier(0.23, 1, 0.32, 1);
--enter: 600ms;
--ease-enter: cubic-bezier(0.23, 1, 0.32, 1);
--stagger: 40ms;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--page: #0f0d17;
--plate-soft: #221e34;
--plate: #2b2641;
--surface: #1a1727;
--rule: #120f1c;
--ink: #ecedf3;
--ink-soft: #c5c7d1;
--muted: #a29cb8;
--accent: #9a86ff;
--on-accent: #ffffff;
--danger: #f07a68;
--success: #45c27a;
--warn: #f0b84f;
--side: #0b0a11;
--panel: #1a1727;
--raised: #221e34;
--button: #2b2641;
--button-bevel: #3d3560;
--well: #0c0b12;
--bevel: #342d52;
--lip: #07060b;
--outline: #05040a;
--face: #5b3fd6;
--face-bevel: #8a74ff;
--face-lip: #3a2694;
--on-face: #ffffff;
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--page: #0f0d17;
--plate-soft: #221e34;
--plate: #2b2641;
--surface: #1a1727;
--rule: #120f1c;
--ink: #ecedf3;
--ink-soft: #c5c7d1;
--muted: #a29cb8;
--accent: #9a86ff;
--on-accent: #ffffff;
--danger: #f07a68;
--success: #45c27a;
--warn: #f0b84f;
--side: #0b0a11;
--panel: #1a1727;
--raised: #221e34;
--button: #2b2641;
--button-bevel: #3d3560;
--well: #0c0b12;
--bevel: #342d52;
--lip: #07060b;
--outline: #05040a;
--face: #5b3fd6;
--face-bevel: #8a74ff;
--face-lip: #3a2694;
--on-face: #ffffff;
color-scheme: dark;
}