// Single source of truth for the landing page's *non-translatable* config: // domains, URLs, theme, launcher files, and the structural shape of the // stat/feature lists. Translatable copy lives in src/i18n/ui.ts. // BASE_DOMAIN is read at build time; falls back to ulicraft.local. // @ts-ignore — node builtins (no @types/node in this Astro SSG; same reason // `process` below is untyped). Used only at build time to read the manifest. import { readFileSync } from "node:fs"; const BASE_DOMAIN = process.env.BASE_DOMAIN ?? "ulicraft.local"; // NMSR render mode for avatar tiles. AVATAR_MODE is the ServerCard online row + // account preview (heads); ROSTER_AVATAR_MODE is the Members grid (full body by // default). Read at build time, same as BASE_DOMAIN. e.g. "headiso" | "head" | // "fullbody" | "fullbodyiso". // @ts-ignore — `process` is untyped here (no @types/node), same as BASE_DOMAIN. const AVATAR_MODE = process.env.AVATAR_MODE ?? "headiso"; // @ts-ignore — `process` is untyped here (no @types/node). const ROSTER_AVATAR_MODE = process.env.ROSTER_AVATAR_MODE ?? "fullbodyiso"; // UlicraftLauncher download list. Source of truth is the custom-launcher repo's // build output `launcher.json` (shape: productName/version/baseUrl/files[]), // published to https://distribution.${BASE}/launcher/launcher.json. The release // flow syncs that file into this dir as `launcher.json` (gitignored, generated) // — same pattern as sync-server-mods.sh. It MAY NOT EXIST at build time, so we // read it at runtime with fs (a static `import` would hard-fail the build when // absent). Fall back to the committed `launcher.example.json` (the documented // shape); if even that is unreadable, expose an empty file list so the homepage // simply renders no download buttons. The raw launcher.json is normalized into // the internal shape the page renders against (builds[]). type LauncherBuild = { os: string; // "windows" | "macos" | "linux" arch: string; // "x64" | "arm64" | "universal" format: string; // file extension: "exe" | "dmg" | "AppImage" | "deb" | "tar.gz" | … filename: string; url: string; size?: number; }; type LauncherFile = { filename: string; url: string; size?: number }; type LauncherManifest = { product: string; version: string; builds: LauncherBuild[]; // Not present in launcher.json; the Fjord page degrades gracefully when absent. fjordPack?: LauncherFile; }; // Raw shape as produced by the custom-launcher build (UlicraftLauncher/dist/launcher.json). type RawLauncherJson = { productName?: string; version?: string; baseUrl?: string; files?: Array<{ name: string; os: string; arch: string; ext: string; url: string; size?: number }>; }; function normalizeLauncher(raw: RawLauncherJson): LauncherManifest { return { product: raw.productName ?? "UlicraftLauncher", version: raw.version ?? "0.0.0", builds: (raw.files ?? []).map((f) => ({ os: f.os, arch: f.arch, format: f.ext, filename: f.name, url: f.url, size: f.size, })), }; } function loadLauncherManifest(): LauncherManifest { const empty: LauncherManifest = { product: "UlicraftLauncher", version: "0.0.0", builds: [] }; // Resolve relative to this module first, then fall back to a cwd-relative path: // Astro bundles this module so import.meta.url may not point at src/data/ at // build time, but the build runs from the landing root, so /src/data/* // resolves. Prefer the synced launcher.json, then the committed example. // @ts-ignore — `process` is untyped here (no @types/node). const cwd: string = typeof process !== "undefined" ? process.cwd() : "."; const candidates: Array = []; for (const name of ["launcher.json", "launcher.example.json"]) { try { candidates.push(new URL(name, import.meta.url)); } catch { // import.meta.url unavailable — skip the module-relative candidate. } candidates.push(`${cwd}/src/data/${name}`); } for (const c of candidates) { try { const raw = JSON.parse(readFileSync(c, "utf8")) as RawLauncherJson; if (raw && Array.isArray(raw.files)) return normalizeLauncher(raw); } catch { // try the next candidate (synced file missing → fall back to example) } } return empty; } const LAUNCHER_MANIFEST = loadLauncherManifest(); // REGISTRATION_MODE (invite|open) is read at build time, same .env as Drasl's // render-config.sh. "invite" => the register form must collect an invite code. const REGISTRATION_MODE = process.env.REGISTRATION_MODE ?? "invite"; // REGISTRATION_SHOW_INVITE (true|false, default false=hidden) ONLY toggles the // visibility of the invite-code field on the register form. The backend invite // gate stays on Drasl's RequireInvite (driven by REGISTRATION_MODE). Decoupled on // purpose: invites are usually admin-minted and the field is hidden by default. // CAVEAT: if REGISTRATION_MODE=invite (backend requires a code) but this is // false, public self-registration can't supply one — set both together. // @ts-ignore — `process` is untyped here (no @types/node). const REGISTRATION_SHOW_INVITE = process.env.REGISTRATION_SHOW_INVITE ?? "false"; // Minecraft / pack facts, surfaced in a few places. const MC_VERSION = "1.21.1"; export const site = { name: "Ulicraft", baseDomain: BASE_DOMAIN, mcVersion: MC_VERSION, // Register form behaviour, driven by REGISTRATION_MODE in .env. // true => Drasl RequireInvite = true (backend gate) // false => open self-registration registrationRequiresInvite: REGISTRATION_MODE === "invite", // Whether the invite-code FIELD is shown on the register form (default false). // Independent of the backend gate above — see REGISTRATION_SHOW_INVITE note. registrationShowInvite: REGISTRATION_SHOW_INVITE === "true", // Design knobs (replace the design's in-browser TweaksPanel). Baked at build. // mood: "grass" | "nether" | "end" // hero: "centered" | "split" | "spotlight" // headFont: "blockblueprint" | "pixelify" | "8bit" | "silkscreen" // dust: floating pixel particles in the hero theme: { mood: "grass", hero: "centered", headFont: "blockblueprint", dust: true, }, // Connect target shown to guests. No port needed (defaults to 25565). serverAddress: `${BASE_DOMAIN}`, // Drasl auth web UI + authlib-injector API root (public HTTPS). authUrl: `https://auth.${BASE_DOMAIN}`, authlibUrl: `https://auth.${BASE_DOMAIN}/authlib-injector`, // Drasl REST API v2 root — the /register form calls this from the browser. // Requires CORSAllowOrigins to include the apex in drasl config.toml. draslApiUrl: `https://auth.${BASE_DOMAIN}/drasl/api/v2`, // NMSR avatar renderer root. Player heads come from OUR Drasl skins, not // Mojang/Crafatar — head URL is `${avatarUrl}/headiso/?size=64`. avatarUrl: `https://avatar.${BASE_DOMAIN}`, // NMSR render mode for avatar tiles. Avatar URL = `${avatarUrl}//?size=N`. // avatarMode → ServerCard online row + account preview (heads) // rosterAvatarMode → PlayerRoster members grid (full body) avatarMode: AVATAR_MODE, rosterAvatarMode: ROSTER_AVATAR_MODE, // Uptime Kuma status page (external link in footer). statusUrl: `https://status.${BASE_DOMAIN}`, // Live server card (ServerCard.astro). `slots` is the SSG fallback cap shown // before/without JS; live data comes from our SELF-HOSTED mc-status service // (docker/mc-status, mcutil wrapper) reverse-proxied same-origin at the apex // /api/mcstatus/* path — no CORS, no third-party calls. It emits mcstatus.io // v2 JSON, so the URL stays drop-in compatible. Uptime Kuma stays the history // + alerting backend (plan/10). status: { slots: 10, statusApi: `/api/mcstatus/v2/status/java/${BASE_DOMAIN}`, // Registered-players roster (PlayerRoster.astro). Same mc-status service, // same-origin via caddy /api/mcstatus/* → internal /players. Returns a // sanitized [{uuid,name}] of every Drasl player (admin login is server-side // in mc-status; no admin token in the page). rosterApi: `/api/mcstatus/players`, }, // Feature cards. glyph ∈ shield|diamond|orb|heart; gold = gold pixel fill. // Text (h/p) comes from ui.ts (same order). features: [ { glyph: "diamond", gold: true }, { glyph: "shield", gold: false }, { glyph: "orb", gold: false }, { glyph: "heart", gold: true }, ], // UlicraftLauncher download manifest — feeds the homepage join step 2 // per-OS buttons from `builds[]` (and the Fjord page's .mrpack via // `fjordPack`). Synced from the custom-launcher repo; see loader above. launcherManifest: LAUNCHER_MANIFEST, // Fjord Launcher downloads (the alternative manual path on /fjord, OFF the // homepage flow). Stable filenames are symlinks created by // tooling/fetch-launcher.sh — keep these in sync with that script. OS names // and hints are technical, kept language-neutral here. fjord: { name: "Fjord Launcher", base: "/launcher/latest", downloads: [ { os: "Windows", file: "fjord-windows-setup.exe", hint: "x64 installer" }, { os: "macOS", file: "fjord-macos.dmg", hint: "Apple Silicon / Intel" }, { os: "Linux", file: "fjord-linux-x86_64.AppImage", hint: "x86_64 AppImage" }, ], }, } as const; // Product / in-game UI literals — never translated (they match what guests see // on screen or type verbatim). export const LITERALS = { authlib: "authlib-injector", multiplayer: "Multiplayer", addServer: "Add Server", } as const; // Head-font CSS stacks, keyed by theme.headFont. export const HEAD_FONTS: Record = { blockblueprint: "'Block Blueprint', system-ui, sans-serif", pixelify: "'Pixelify Sans', system-ui, sans-serif", "8bit": "'Press Start 2P', monospace", silkscreen: "'Silkscreen', system-ui, sans-serif", };