diff --git a/docs/games/gtnh-daily.md b/docs/games/gtnh-daily.md new file mode 100644 index 00000000..4f388dc1 --- /dev/null +++ b/docs/games/gtnh-daily.md @@ -0,0 +1,631 @@ +# GT New Horizons Daily administration guide + +This is the long-form administration and historical session record for the isolated GT New Horizons Daily setup. For the clearer from-scratch guide, start with `docs/gtnh-daily/README.md`; for unfamiliar terms, use `docs/gtnh-daily/glossary.md`. + +This guide documents the isolated GT New Horizons Daily setup managed by this repo. It covers day-to-day administration, bootstrap/recovery commands, client/server synchronization, and implementation details from the initial setup session. + +## Overview + +The Daily environment is intentionally separate from the stable GTNH server: + +| Concern | Stable | Daily | +| --- | --- | --- | +| Server directory | `/srv/gtnh` | `/var/lib/gtnh-daily/server` | +| Main service | `gtnh-server.service` | `gtnh-daily-server.service` | +| Update service | existing stable flow | `gtnh-daily-update.service` | +| Backups | existing stable flow | `/var/lib/gtnh-daily/backups` | +| Minecraft port | `25565` | `25566` | +| User | existing stable user | `gtnh-daily` | + +Daily builds come from the GTNH Dev Builds pipeline in `GTNewHorizons/DreamAssemblerXXL`, workflow `daily-modpack-build.yml`. The server update service publishes the exact manifest it applied, and the Prism client sync command pulls that manifest over SSH so the client updates to the same Daily build as the server. + +## Important paths and units + +Server-side paths: + +```text +/var/lib/gtnh-daily +/var/lib/gtnh-daily/server +/var/lib/gtnh-daily/backups +/var/lib/gtnh-daily/cache +/var/lib/gtnh-daily/config +/var/lib/gtnh-daily/current-manifest.json +/var/lib/gtnh-daily/current-manifest.sha256 +``` + +Client-side paths: + +```text +~/.local/share/PrismLauncher/instances/GT New Horizons (Daily) +~/.local/share/PrismLauncher/instances/GT New Horizons (Daily)/.gtnh-daily-updater.json +~/.local/share/PrismLauncher/backups +~/.cache/gtnh-daily-client-sync/current-manifest.json +~/.cache/gtnh-daily-client-sync/current-manifest.sha256 +``` + +System units: + +```text +gtnh-daily-server.service +gtnh-daily-server.socket +gtnh-daily-update.service +gtnh-daily-update.timer +``` + +User units: + +```text +gtnh-daily-client-sync.service +gtnh-daily-client-sync.timer +``` + +Installed commands: + +```text +gtnh-daily-updater +gtnh-daily-rollback +gtnh-daily-client-sync +``` + +## Daily server operations + +Check the stable and Daily servers: + +```sh +systemctl is-active gtnh-server.service gtnh-daily-server.service +``` + +Start the Daily server: + +```sh +sudo systemctl start gtnh-daily-server.service +``` + +Stop the Daily server: + +```sh +sudo systemctl stop gtnh-daily-server.service +``` + +Follow server logs: + +```sh +sudo journalctl -u gtnh-daily-server.service -f +``` + +Verify the Daily server port: + +```sh +sudo grep -n '^server-port=' /var/lib/gtnh-daily/server/server.properties +``` + +The service rewrites `server-port=25566` in `server.properties` before every start to avoid conflict with the stable server. + +Send a Minecraft console command through the service FIFO: + +```sh +printf 'say hello from systemd\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +``` + +Whitelist and op a player: + +```sh +printf 'whitelist add wagoqi\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +printf 'op wagoqi\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +``` + +Verify whitelist/op state: + +```sh +sudo grep -R 'wagoqi' \ + /var/lib/gtnh-daily/server/whitelist.json \ + /var/lib/gtnh-daily/server/ops.json +``` + +## Server updates and backups + +Run a Daily update immediately: + +```sh +sudo systemctl start gtnh-daily-update.service +``` + +Check update logs: + +```sh +sudo journalctl -u gtnh-daily-update.service -n 120 --no-pager +``` + +Check the timer: + +```sh +systemctl list-timers --all 'gtnh-daily-update.timer' --no-pager +``` + +The timer runs daily around 05:00 with a randomized delay: + +```text +OnCalendar=*-*-* 05:00:00 +RandomizedDelaySec=30m +Persistent=true +``` + +A server update does the following: + +1. Acquires `/run/gtnh-daily-update.lock` with `flock` so updates cannot overlap. +2. Downloads the current Daily manifest from `https://raw.githubusercontent.com/GTNewHorizons/DreamAssemblerXXL/master/releases/manifests/daily.json` into a temporary file. +3. Records whether `gtnh-daily-server.service` was active before the update. +4. Stops only `gtnh-daily-server.service`; the stable `gtnh-server.service` is untouched. +5. Creates a pre-update backup under `/var/lib/gtnh-daily/backups`. +6. Runs `gtnh-daily-updater update --manifest-file --instance-dir /var/lib/gtnh-daily/server` as user `gtnh-daily`. +7. Publishes the exact applied manifest and SHA-256 hash: + - `/var/lib/gtnh-daily/current-manifest.json` + - `/var/lib/gtnh-daily/current-manifest.sha256` +8. Restarts the Daily server only if it was active before the update. + +List backups: + +```sh +sudo ls -lh /var/lib/gtnh-daily/backups +``` + +The latest pre-update backup is also symlinked as: + +```text +/var/lib/gtnh-daily/backups/latest.tar.zst +``` + +## Server rollback + +Rollback to the latest pre-update backup: + +```sh +sudo gtnh-daily-rollback +``` + +Rollback to a specific backup: + +```sh +sudo gtnh-daily-rollback /var/lib/gtnh-daily/backups/pre-update-YYYYmmdd-HHMMSS.tar.zst +``` + +Rollback behavior: + +1. Stops only `gtnh-daily-server.service`. +2. Moves the current server directory to `/var/lib/gtnh-daily/server.rollback-`. +3. Extracts the selected backup into `/var/lib/gtnh-daily/server`. +4. Fixes ownership to `gtnh-daily:gtnh-daily`. +5. Starts `gtnh-daily-server.service`. + +## Prism client operations + +The Daily Prism instance is named exactly: + +```text +GT New Horizons (Daily) +``` + +The desktop entry runs: + +```sh +prismlauncher --launch "GT New Horizons (Daily)" --server localhost:25566 +``` + +Run a manual client sync: + +```sh +gtnh-daily-client-sync +``` + +Use a different SSH server host for one run: + +```sh +GTNH_DAILY_SERVER=nemesis gtnh-daily-client-sync +``` + +Check the user timer: + +```sh +systemctl --user list-timers --all 'gtnh-daily-client-sync.timer' --no-pager +``` + +Start the sync user service manually: + +```sh +systemctl --user start gtnh-daily-client-sync.service +``` + +Read sync logs: + +```sh +journalctl --user -u gtnh-daily-client-sync.service -n 120 --no-pager +``` + +The client sync command: + +1. Refuses to run if Prism or Minecraft appears to be running. +2. Runs `gtnh-daily-client-bootstrap`, which creates/initializes the Prism instance if needed and downloads/verifies declared resource/shader packs. +3. Uses SSH to read the server's `/var/lib/gtnh-daily/current-manifest.sha256` from `nemesis` by default. +4. Exits if the local applied hash in `~/.cache/gtnh-daily-client-sync/current-manifest.sha256` matches the server hash. +5. Copies `/var/lib/gtnh-daily/current-manifest.json` from the server with `scp`. +6. Verifies the copied manifest against the server-published hash. +7. Creates a full client instance backup under `~/.local/share/PrismLauncher/backups`. +8. Runs: + + ```sh + gtnh-daily-updater update \ + --instance-dir "$HOME/.local/share/PrismLauncher/instances/GT New Horizons (Daily)" \ + --manifest-file "$HOME/.cache/gtnh-daily-client-sync/current-manifest.json" + ``` + +9. Writes the applied server hash to `~/.cache/gtnh-daily-client-sync/current-manifest.sha256`. + +The client sync timer is hourly with a 15 minute randomized delay and `Persistent=true`. + +## Initial client updater state + +Manual initialization is no longer needed in the normal path: `gtnh-daily-client-sync` invokes `gtnh-daily-client-bootstrap`, and the bootstrap command initializes updater state if `.gtnh-daily-updater.json` is missing. + +The historical manual initialization flow was: + +```sh +config=$(ssh nemesis "python3 - <<'PY' +import json +with open('/var/lib/gtnh-daily/current-manifest.json') as f: + print(json.load(f)['config']) +PY +") +gtnh-daily-updater init \ + --instance-dir "$HOME/.local/share/PrismLauncher/instances/GT New Horizons (Daily)" \ + --side client \ + --config "$config" +``` + +If Python is unavailable on the server, copy the manifest locally and inspect it with any JSON tool: + +```sh +scp nemesis:/var/lib/gtnh-daily/current-manifest.json /tmp/gtnh-daily-current-manifest.json +jq -r .config /tmp/gtnh-daily-current-manifest.json +``` + +## Bootstrap from GitHub Actions artifacts + +Normal updates do not need GitHub Actions artifact downloads; they use `gtnh-daily-updater` and the published Daily manifest. Artifact downloads are only needed for first bootstrap or full manual re-seeding. + +The NixOS/Home Manager configuration now performs this bootstrap idempotently: + +- `gtnh-daily-bootstrap.service` creates `/var/lib/gtnh-daily`, downloads a non-expired Daily server artifact when the server files are missing, initializes `.gtnh-daily-updater.json`, accepts the EULA, and reconciles declared server extras/excludes before the server starts. +- `gtnh-daily-client-bootstrap` downloads the server-published manifest, creates the Prism instance from a Daily `mmcprism-java17-25` artifact if missing, initializes client updater state, reconciles declared client extras, installs declared resource/shader packs, and enables the selected resource packs in `options.txt`. +- `gtnh-daily-client-sync` runs the client bootstrap first, then performs the pinned-manifest update. + +GitHub artifact downloads may require `GITHUB_TOKEN` in the service/user environment if GitHub refuses unauthenticated artifact API downloads. + +List recent successful Daily workflow runs: + +```sh +gh run list \ + -R GTNewHorizons/DreamAssemblerXXL \ + -w daily-modpack-build.yml \ + --limit 10 +``` + +Inspect a run: + +```sh +gh run view -R GTNewHorizons/DreamAssemblerXXL +gh api repos/GTNewHorizons/DreamAssemblerXXL/actions/runs//artifacts \ + --jq '.artifacts[] | [.name,.size_in_bytes,.expired] | @tsv' +``` + +Expected artifact names include: + +```text +GTNH-daily-YYYY-MM-DD+NNN-mmcprism-java17-25.zip +GTNH-daily-YYYY-MM-DD+NNN-server-java17-25.zip +gtnh-daily-YYYY-MM-DD+NNN-manifest.json +daily-build-bundle +``` + +Download a server artifact and manifest: + +```sh +gh run download \ + -R GTNewHorizons/DreamAssemblerXXL \ + -n GTNH-daily-YYYY-MM-DD+NNN-server-java17-25.zip \ + -n gtnh-daily-YYYY-MM-DD+NNN-manifest.json \ + --dir /tmp/gtnh-daily-bootstrap +``` + +`gh run download` extracts each artifact into a directory named after the artifact. Seed the server with: + +```sh +sudo cp -a /tmp/gtnh-daily-bootstrap/GTNH-daily-YYYY-MM-DD+NNN-server-java17-25.zip/. \ + /var/lib/gtnh-daily/server/ +sudo chown -R gtnh-daily:gtnh-daily /var/lib/gtnh-daily +``` + +Extract the config version from the downloaded manifest. It looks like: + +```json +{ "config": "2.9.0-nightly-2026-06-12" } +``` + +Initialize server updater state: + +```sh +sudo -u gtnh-daily env HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + PATH=/run/current-system/sw/bin \ + gtnh-daily-updater init \ + --instance-dir /var/lib/gtnh-daily/server \ + --side server \ + --config +``` + +Accept the Minecraft EULA before starting the server: + +```sh +sudo sed -i 's/^eula=false/eula=true/' /var/lib/gtnh-daily/server/eula.txt +``` + +### Daily updater-managed extras: JourneyMap Unlimited, GTNH-Web-Map, MineMenu + +Extra/excluded Daily mods are stored by `gtnh-daily-updater` in each instance's `.gtnh-daily-updater.json`; this repo now treats the desired entries as declarative and reconciles that updater state during server/client bootstrap. + +The GTNH JourneyMap wiki says the pack server uses JourneyMap FairPlay by default. For a private server with JourneyMap Unlimited, remove the server-side JourneyMap FairPlay jar and replace the client-side FairPlay jar with JourneyMap Unlimited. `gtnh-daily-updater` supports this directly: a same-name extra overrides the manifest entry, so client `JourneyMap` is an extra sourced from TeamJM's legacy releases with the `unlimited.jar` asset selected. + +Current client extras: + +```sh +gtnh-daily-updater extra add JourneyMap \ + --instance-dir "$HOME/.local/share/PrismLauncher/instances/GT New Horizons (Daily)" \ + --side client \ + --source github:TeamJM/journeymap-legacy \ + --match 'unlimited\.jar$' + +gtnh-daily-updater extra add MineMenu \ + --instance-dir "$HOME/.local/share/PrismLauncher/instances/GT New Horizons (Daily)" \ + --side client \ + --source modrinth:mine-menu/HNivj4HD +``` + +Current server exclude/extra state: + +```sh +sudo -u gtnh-daily env \ + HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + gtnh-daily-updater exclude add \ + --instance-dir /var/lib/gtnh-daily/server \ + "JourneyMap Server" + +sudo -u gtnh-daily env \ + HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + gtnh-daily-updater extra add GTNH-Web-Map \ + --instance-dir /var/lib/gtnh-daily/server \ + --side server \ + --source github:GTNewHorizons/GTNH-Web-Map \ + --match '^gtnh-web-map-.*[0-9]\.jar$' + +sudo -u gtnh-daily env \ + HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + gtnh-daily-updater extra add MineMenu \ + --instance-dir /var/lib/gtnh-daily/server \ + --side server \ + --source modrinth:mine-menu/HNivj4HD +``` + +Verify state with: + +```sh +gtnh-daily-updater extra list \ + --instance-dir "$HOME/.local/share/PrismLauncher/instances/GT New Horizons (Daily)" + +sudo -u gtnh-daily env HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + gtnh-daily-updater exclude list --instance-dir /var/lib/gtnh-daily/server + +sudo -u gtnh-daily env HOME=/var/lib/gtnh-daily \ + XDG_CACHE_HOME=/var/lib/gtnh-daily/cache \ + XDG_CONFIG_HOME=/var/lib/gtnh-daily/config \ + gtnh-daily-updater extra list --instance-dir /var/lib/gtnh-daily/server +``` + +Enable ServerUtilities chunk claiming/loading and ranks in `/var/lib/gtnh-daily/server/serverutilities/serverutilities.cfg`: + +```cfg +ranks { + B:enabled=true +} + +world { + B:chunk_claiming=true + B:chunk_loading=true +} +``` + +The next `gtnh-daily-updater update` / `gtnh-daily-client-sync` run downloads and applies changed extras/excludes. Restart the server after changing server-side mods/config. GTNH-Web-Map listens on TCP `8123` by default, so expose that port where appropriate. + +### 2026-06-13 world migration from stable, player data stripped + +The Daily server was migrated to a copy of the stable `/srv/gtnh` world while stripping player state. `/srv/gtnh` itself was only stopped/read/backed up/restarted; its files were not otherwise modified. + +Backups created before the migration: + +```text +/var/lib/gtnh-daily/backups/source-srv-gtnh-20260613-105333.tar.zst +/var/lib/gtnh-daily/backups/pre-stripped-world-20260613-105333.tar.zst +``` + +Procedure performed: + +1. Stopped `gtnh-server.service` and `gtnh-daily-server.service` for a consistent copy. +2. Archived `/srv/gtnh` into the Daily backup directory. +3. Archived the previous Daily server directory. +4. Copied `/srv/gtnh/world` into the Daily server. +5. Corrected the copied directory to the active Daily level name, `World`, because Daily has `level-name=World` in `server.properties`. +6. Removed old Daily in-instance `server/backups`; the canonical migration backups are in `/var/lib/gtnh-daily/backups`. +7. Stripped player data from the copied world: + - `World/playerdata` + - `World/stats` + - BetterQuesting `QuestProgress`, `NameCache`, `LifeDatabase`, `QuestingParties` + - ServerUtilities world players/teams/claims + - backpack player data + - NEI player data + - Baubles and Thaumcraft username files + - OpenBlocks grave/death inventory backups + - Forestry player trackers + - POBox player data + - KubaTech player data + - mobsinfo player data + - LootBags player-ish data + - ThaumicExploration player warp queue + - nicknames + - LogisticPipes player/name info + - Daily sidecar `journeymap` and `visualprospecting` +8. Reset root identity files: `usercache.json`, `whitelist.json`, `ops.json`, `banned-players.json`, and removed `usernamecache.json`. +9. Restarted both servers. +10. Accepted the expected FML/ChunkAPI migration prompts with `/fml confirm`; this was expected because the stable source world was GTNH 2.8.4 and Daily has newer mod IDs/managers. +11. Verified Daily reached `Done`, listened on `25566`, and GTNH-Web-Map served on `8123`. +12. Re-added `wagoqi` to the Daily whitelist and ops: + +```sh +printf 'whitelist add wagoqi\nop wagoqi\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +``` + +Post-checks: + +- `gtnh-server.service`: active +- `gtnh-daily-server.service`: active +- Daily map: `http://nemesis:8123/` +- No old `wagoqi`, `22c6c7d9-3c10-41a0-9e9d-759f2aeec75b`, or `e43c1aea-e5b8-3c4f-95b0-ef6163be3402` references remained in active Daily server data after excluding logs/mods/libraries/dynmap/backups. +- New `wagoqi` whitelist/op entries were regenerated with UUID `22c6c7d9-3c10-41a0-9e9d-759f2aeec75b`. + +## Implementation notes + +### Nix package + +The repo packages `Caedis/gtnh-daily-updater` with `pkgs.buildGoModule` from a flake input: + +```nix +gtnh-daily-updater = { + url = "github:Caedis/gtnh-daily-updater"; + flake = false; +}; +``` + +The package uses the upstream source as `src`, sets a fixed Go `vendorHash`, and includes `git` in `nativeCheckInputs` because upstream tests invoke `git`. + +A local Nix patch adds `--manifest-file` support to the updater. The patch is applied with the package's `patches` attribute, so no fork is needed: + +```nix +patches = [ ./patches/gtnh-daily-updater-manifest-file.patch ]; +``` + +The patch is intentionally small: it adds a CLI flag, expands `~` in that flag, adds `ManifestFile` to updater options, loads that JSON manifest when present, and otherwise preserves upstream behavior. + +### Why pinned manifests are needed + +Upstream `gtnh-daily-updater update` normally fetches the latest manifest at runtime. That is fine for a single machine, but it is not sufficient for exact client/server sync. If the server updates at 05:00 and the client syncs after a newer Daily appears, an unpinned client update could jump ahead of the server. + +The patched `--manifest-file` mode lets the server publish the exact manifest it used and lets every client update against that same manifest. The assets database is still fetched normally by the updater to resolve downloads, but mod/config selection is pinned to the server-applied Daily manifest. + +### Server module + +`nix/modules/gtnh-daily-server.nix` defines one isolated NixOS module. It creates the `gtnh-daily` system user/group, tmpfiles directories, firewall openings for TCP/UDP `25566`, a socket-backed stdin FIFO, the server service, the update service/timer, and the rollback command. + +The server uses `jdk25_headless` and starts with: + +```text +-Xms6G +-Xmx10G +-XX:+UseZGC +-Dfml.readTimeout=180 +@java9args.txt +-jar lwjgl3ify-forgePatches.jar +nogui +``` + +`gtnh-daily-server.socket` creates `/run/gtnh-daily-server.stdin`, which systemd connects to the Java process as standard input. This allows admin commands such as `stop`, `whitelist add`, and `op` to be sent without attaching to a terminal. + +`ExecStop` sends `stop` through the FIFO, waits up to 120 seconds, then sends `SIGTERM` if the process is still alive. `SuccessExitStatus = "0 143"` treats a final SIGTERM exit as successful. + +The update service intentionally only stops/restarts the Daily server. It records whether the Daily server was active before updating and restarts it only in that case, so a manually stopped Daily server stays stopped across scheduled updates. + +### Client module + +`nix/modules/prismlauncher.nix` manages Prism Launcher with JDK 25, the stable GTNH desktop entry, the Daily desktop entry, and the Linux-only client sync helper/timer. The client sync package and user systemd units are only enabled when Home Manager is evaluated for Linux, so the shared Prism module still evaluates on Darwin. + +`gtnh-daily-client-sync` defaults to `nemesis` as the SSH source host. Override per invocation with: + +```sh +GTNH_DAILY_SERVER= gtnh-daily-client-sync +``` + +The command is installed only on Linux Home Manager systems because it depends on Linux-oriented tools and user systemd integration. + +### Filesystem permissions + +`/var/lib/gtnh-daily` is mode `0755` so SSH users can traverse it and read the published manifest files. This is enforced both by the tmpfiles rule and by `users.users.gtnh-daily.homeMode = "0755"`, so rebuild/user activation does not reset the home directory back to a private mode. Sensitive subdirectories remain private: + +```text +/var/lib/gtnh-daily/server 0750 gtnh-daily:gtnh-daily +/var/lib/gtnh-daily/backups 0750 gtnh-daily:gtnh-daily +/var/lib/gtnh-daily/cache 0750 gtnh-daily:gtnh-daily +/var/lib/gtnh-daily/config 0750 gtnh-daily:gtnh-daily +``` + +The published files are mode `0644`: + +```text +/var/lib/gtnh-daily/current-manifest.json +/var/lib/gtnh-daily/current-manifest.sha256 +``` + +## Session record + +The setup was built in stages: + +1. Added `Caedis/gtnh-daily-updater` as a `flake = false` input and packaged it with `pkgs.buildGoModule`. +2. Added the package to Home Manager development packages. +3. Verified no existing nixpkgs package for `gtnh-daily-updater` was available. +4. Resolved and documented the Go `vendorHash`. Upstream does not commit a `vendor/` directory, so `vendorHash = null` is not appropriate. +5. Added `nativeCheckInputs = [ pkgs.git ]` because upstream tests call `git`. +6. Inspected the GTNH Dev Builds documentation and confirmed Daily builds come from `GTNewHorizons/DreamAssemblerXXL` workflow `daily-modpack-build.yml`, not from `GT-New-Horizons-Modpack` releases. +7. Downloaded authenticated GitHub Actions artifacts with `gh run download`. +8. Bootstrapped `/var/lib/gtnh-daily/server` from `GTNH-daily-2026-06-12+569-server-java17-25.zip`. +9. Initialized server updater state with config version `2.9.0-nightly-2026-06-12`. +10. Accepted the Minecraft EULA in `/var/lib/gtnh-daily/server/eula.txt`. +11. Added `gtnh-daily-server.service` and verified it can run alongside the stable server. +12. Added scheduled pre-update backups and rollback via `gtnh-daily-rollback`. +13. Verified `gtnh-server.service` stayed active while the Daily server and updater were tested. +14. Added a Daily Prism desktop entry for the already-created Prism instance. +15. Added a local patch to `gtnh-daily-updater` for `--manifest-file` because exact client/server sync requires pinning the manifest. +16. Changed the server update service to publish the exact applied manifest and hash. +17. Initialized the local Daily Prism instance updater state. +18. Added `gtnh-daily-client-sync` and an hourly user timer. +19. Tested client sync over SSH against `nemesis`; it verified the hash, created a client backup, and reported the client already up to date. +20. Reviewed the full diff since `prime`, made the client sync package Linux-only for shared Home Manager evaluation, and set `gtnh-daily.homeMode = "0755"` so the published manifest remains reachable over SSH after rebuilds. + +Validation performed during the session included: + +```sh +nix build .#gtnh-daily-updater --no-link +nix develop -c just check-nix +nix build .#nixosConfigurations.nemesis.config.system.build.toplevel --no-link +nix develop -c just rb +sudo systemctl start gtnh-daily-update.service +gtnh-daily-client-sync +systemctl is-active gtnh-server.service gtnh-daily-server.service +systemctl list-timers --all 'gtnh-daily-update.timer' --no-pager +systemctl --user list-timers --all 'gtnh-daily-client-sync.timer' --no-pager +``` + +At the end of setup, both stable and Daily servers were active, the Daily server was on port `25566`, the server update timer was enabled, and the client sync timer was enabled. diff --git a/docs/gtnh-daily/README.md b/docs/gtnh-daily/README.md new file mode 100644 index 00000000..264a41c7 --- /dev/null +++ b/docs/gtnh-daily/README.md @@ -0,0 +1,43 @@ +# GTNH Daily reproducible setup + +This folder is the from-scratch reference for our GT New Horizons Daily deployment. + +In plain language: **GTNH Daily** is a frequently rebuilt test/development version of the GT New Horizons Minecraft modpack. This repo uses Nix/NixOS/Home Manager to recreate the Daily server and the matching Prism Launcher client after a rebuild or disk wipe. It recreates the pack files, updater state, services, timers, launchers, resource packs, and shader packs. It does **not** recreate the Minecraft world unless you restore a separate world backup. + +The main goal is: after a drive wipe and rebuild, mutable world data aside, the Daily server and Prism client can be recreated, updated to the same exact server-published manifest, and used to join the server once downloads complete. + +## If you just want to play + +1. Make sure the Daily server is running or reachable through an SSH tunnel. +2. Close Prism/Minecraft. +3. Run `gtnh-daily-client-sync` as your normal user, not with `sudo`. +4. Launch the `GT New Horizons (Daily)` desktop entry. + +If the server is remote and the desktop entry expects `localhost:25566`, run this in another terminal first: + +```sh +ssh -N -L 25566:localhost:25566 nemesis +``` + +## If you administer the server + +1. Check `gtnh-daily-bootstrap.service` after rebuild; it creates missing server files. +2. Check `gtnh-daily-server.service`; it runs the Minecraft server on port `25566`. +3. Check `gtnh-daily-update.timer`; it schedules Daily updates. +4. Keep separate backups of `/var/lib/gtnh-daily/server/World` if the world must survive disk loss. + +## Read in this order + +1. [glossary.md](./glossary.md) — plain-language definitions for Minecraft, Linux, systemd, Nix, and updater terms. +2. [concepts.md](./concepts.md) — Minecraft/Forge/GTNH concepts and file layout. +3. [daily-builds.md](./daily-builds.md) — GTNH Daily artifacts, manifests, and why client/server pinning matters. +4. [gtnh-daily-updater.md](./gtnh-daily-updater.md) — updater behavior, state, extras/excludes, config git repo, and our patch. +5. [server.md](./server.md) — NixOS server user, paths, services, bootstrap, update, rollback, firewall, and world-data boundary. +6. [client.md](./client.md) — Prism Launcher instance, bootstrap, client sync, resource packs, shader packs, desktop entries, and user timer. +7. [state-and-drift.md](./state-and-drift.md) — what we found on the live server/client and how it maps to declarative config. +8. [recovery.md](./recovery.md) — wipe/rebuild flow and operational recovery notes. +9. [implementation-notes.md](./implementation-notes.md) — guide for reading the Nix modules and generated shell/Python scripts. +10. [research-server-management.md](./research-server-management.md) — research note comparing itzg Docker GTNH, nixpkgs, and Minecraft server flakes. +11. [research-server-management.html](./research-server-management.html) — exhaustive plain HTML walkthrough of the same research, with each implementation separated into sections. + +`docs/games/gtnh-daily.md` is the older long-form administration/history record. Start here instead unless you need historical migration notes. diff --git a/docs/gtnh-daily/client.md b/docs/gtnh-daily/client.md new file mode 100644 index 00000000..b413664c --- /dev/null +++ b/docs/gtnh-daily/client.md @@ -0,0 +1,107 @@ +# Client and Prism Launcher + +For definitions of Prism, Home Manager, SSH, resource packs, shader packs, and other terms, see [glossary.md](./glossary.md). For implementation details, see [implementation-notes.md](./implementation-notes.md). + +## Prism Launcher + +Prism Launcher manages Minecraft instances: separate installed copies of Minecraft/modpacks. Our Home Manager module installs Prism with JDK 25 available because GTNH Daily Java 17-25 builds are intended for modern Java/LWJGL3ify use. In practice that means Prism can launch this old Minecraft pack using a modern Java runtime. + +The Daily instance name is exactly this. The name matters because the desktop entry and sync scripts use it in commands and paths: + +```text +GT New Horizons (Daily) +``` + +The instance directory is: + +```text +~/.local/share/PrismLauncher/instances/GT New Horizons (Daily) +``` + +The game directory is: + +```text +~/.local/share/PrismLauncher/instances/GT New Horizons (Daily)/.minecraft +``` + +## Desktop entries + +A desktop entry is the launcher icon/menu item shown by a desktop environment. Home Manager declares: + +- a stable GTNH desktop entry for `GT_New_Horizons_2.8.4_Java_17-25`; +- a Daily desktop entry that launches `GT New Horizons (Daily)` and passes `--server localhost:25566`. + +`--server localhost:25566` asks Prism/Minecraft to connect to a server on this machine at port `25566` after launch. If an SSH tunnel is running, that local port is forwarded to the remote server. + +The Daily entry assumes the server is reachable locally or through whatever networking/SSH forwarding makes `localhost:25566` valid. For a remote server named `nemesis`, one simple tunnel is `ssh -N -L 25566:localhost:25566 nemesis`. + +## Client bootstrap + +`gtnh-daily-client-bootstrap` is installed on Linux Home Manager systems. Close Prism/Minecraft before running it; the command refuses to mutate the instance while the game appears active. It is idempotent and does the following: + +1. reads `/var/lib/gtnh-daily/current-manifest.sha256` from the server over SSH, so passwordless SSH to the server host must work; +2. copies `/var/lib/gtnh-daily/current-manifest.json` with `scp`; +3. verifies the copied manifest against the server hash; +4. creates the Prism instance from a recent non-expired `mmcprism-java17-25` artifact if `.minecraft` is missing; +5. extracts the manifest config version; +6. runs `gtnh-daily-updater init --side client` if updater state is missing; +7. reconciles declared client extra mods into `.gtnh-daily-updater.json`; +8. installs declared resource pack and shader pack files; +9. unpacks the resource packs whose active names are directories; +10. writes the active `resourcePacks:` line in `options.txt` when the file exists. + +If SSH is not configured, bootstrap fails before changing the instance. If `.minecraft` already exists but is broken or incomplete, bootstrap does not overwrite it; move the broken instance aside after backing up anything important, then rerun bootstrap/sync. + +The bootstrap defaults to SSH host `nemesis`, the server hostname used by this repo. Override with: + +```sh +GTNH_DAILY_SERVER= gtnh-daily-client-bootstrap +``` + +## Client sync + +`gtnh-daily-client-sync` is the normal update command. It: + +1. takes a user lock under `~/.cache/gtnh-daily-client-sync`; +2. refuses to run if Prism/Minecraft appears active; +3. runs the client bootstrap first, so a missing client can be created; +4. exits early if the local applied hash equals the server hash; +5. copies and verifies the server manifest; +6. creates a full instance backup under `~/.local/share/PrismLauncher/backups`; +7. runs `gtnh-daily-updater update --manifest-file `; +8. records the server hash locally. + +The user timer runs hourly with a 15 minute randomized delay and `Persistent=true`. If the timer fires while Prism/Minecraft is open, the sync command refuses to change files and exits. Logs are visible with `journalctl --user -u gtnh-daily-client-sync.service`. + +Backups are full directory copies under `~/.local/share/PrismLauncher/backups`. They can be large, and this script does not prune old backups automatically. + +## Resource packs + +A resource pack changes client-side textures, models, sounds, or UI. It does not change server gameplay. The live client had these selected resource packs: + +```text +AE2-Dark-Mode.v.1.18 +shadowui +Modernity-GTNH-main +``` + +The repo does not store these zip files because large binary assets make git history heavy and are harder to review. Instead, `nix/modules/prismlauncher.nix` declares each download URL and SHA-256 hash. Bootstrap downloads the files into `.minecraft/resourcepacks` when missing or when the local copy has the wrong hash. It then unpacks the zips into the directory names used by `options.txt`, because Minecraft enables these packs by directory name. + +Declared resource pack downloads: + +- `AE2-Dark-Mode.v.1.18.zip` from `Ranzuu/AE2-Dark-Mode`; +- `Shadow.UI.v5.30-Modernity.version.zip` from `Ranzuu/Shadow-UI`; +- `Modernity-GTNH-main.zip` from a pinned `ModernityGTNH/Modernity-GTNH` commit archive. + +The GTNH-provided resource pack zips that come with the pack also remain present after artifact/bootstrap/update; our declared packs add the current custom selection on top. + +## Shader packs + +A shader pack changes client-side graphics such as lighting, shadows, and color. Shader packs are handled the same way as resource packs: URLs and hashes are declared in Nix, and bootstrap downloads them into `.minecraft/shaderpacks`. + +Declared shader pack downloads: + +- `ComplementaryReimagined_r5.8.1.zip` from Modrinth; +- `ComplementaryUnbound_r5.8.1.zip` from Modrinth. + +Bootstrap also writes the small `ComplementaryUnbound_r5.8.1.zip.txt` option sidecar because it is plain text, not a large binary asset. The sidecar stores Complementary Unbound settings such as light color multipliers/night brightness. Shader selection itself is normally local/client preference; the files are what matters for being able to select/use the same shaders after rebuild. diff --git a/docs/gtnh-daily/concepts.md b/docs/gtnh-daily/concepts.md new file mode 100644 index 00000000..39469bc6 --- /dev/null +++ b/docs/gtnh-daily/concepts.md @@ -0,0 +1,67 @@ +# Concepts: GTNH and modded Minecraft + +If any term here is unfamiliar, check [glossary.md](./glossary.md). This page gives the short conceptual overview; the glossary gives one-line definitions. + +## GT New Horizons + +GT New Horizons (GTNH) is a large Minecraft 1.7.10 expert modpack. It combines Forge, GregTech, questing, many technology/magic mods, custom configs, scripts, resources, and launcher/server packaging. A working instance is not just a list of mod jars: it is the combination of jars, configuration trees, scripts, resources, Java arguments, launcher metadata, and runtime state. + +## Client vs server sides + +Many mods are side-specific: + +- **Server-required mods** run in the dedicated server JVM and define gameplay, world generation, blocks, items, recipes, networking, permissions, and backend integrations. +- **Client-required mods** run in the player JVM and provide rendering, UI, keybinds, minimaps, shader support, resource loading, and client-only helpers. +- **Both-side mods** must exist on both sides with compatible versions so Forge networking and registries line up. + +A modpack update must keep the client and server on compatible builds. For Daily builds, compatibility means both sides should use the same Daily manifest or at least the same coherent build set. + +## Forge 1.7.10 layout + +A dedicated server directory contains items such as: + +- `mods/` — Forge mod jars loaded by the server. +- `config/` — mod configuration files. +- `scripts/` — CraftTweaker/pack scripts. +- `libraries/` — Forge/Minecraft libraries. +- `java9args.txt` — Java 9+ / modern Java argument file used by GTNH packaging. +- `lwjgl3ify-forgePatches.jar` — patched launch jar used by modern Java/LWJGL3ify setups. +- `server.properties` — vanilla server settings such as port and level name. +- `eula.txt` — Mojang EULA acceptance; must contain `eula=true` before starting. +- `ops.json`, `whitelist.json`, ban lists, `usercache.json` — server identity/admin state. +- `World/` — mutable world data. This is intentionally outside our reproducibility guarantee. + +A Prism/MultiMC client instance directory contains: + +- `.minecraft/mods/` — client mod jars. +- `.minecraft/config/` — client mod config. +- `.minecraft/resourcepacks/` — resource packs and unpacked resource pack directories. +- `.minecraft/shaderpacks/` — shader zip files and sidecar config files. +- `.minecraft/options.txt` — selected resource packs and many local client options. +- launcher metadata at the instance root, plus `.gtnh-daily-updater.json` at the instance root. + +`gtnh-daily-updater` deliberately treats Prism/MultiMC clients differently from servers: if `/.minecraft` exists, the game directory is `.minecraft`; otherwise the game directory is the instance directory itself. + +## Mutable vs declarative data + +We reproduce pack and environment state, not world history. “Declarative” means the desired state is written in repo config so it can be recreated. “Mutable” means the data changes during play and must be backed up separately if it matters. + +Declarative state includes: + +- chosen Daily artifact/update mechanism; +- updater extras/excludes; +- server port and service launch policy; +- EULA acceptance; +- client bootstrap/sync commands; +- current resource/shader pack files that are not generated by play; +- timers and firewall ports. + +Mutable data includes: + +- `World/` and all dimensions/chunk/player/world mod data; +- logs; +- backups; +- generated map tiles/cache (`dynmap`, web-map output); +- download caches; +- transient runtime files; +- local player-only preferences that are not required to join, such as many video/keybind/UI preferences. The docs only force the resource-pack selection needed to restore the intended visual setup. diff --git a/docs/gtnh-daily/daily-builds.md b/docs/gtnh-daily/daily-builds.md new file mode 100644 index 00000000..f75ce80d --- /dev/null +++ b/docs/gtnh-daily/daily-builds.md @@ -0,0 +1,45 @@ +# GTNH Daily builds, artifacts, and manifests + +For definitions of GitHub Actions, artifacts, manifests, SHA-256, and tokens, see [glossary.md](./glossary.md). + +## Source of Daily builds + +GTNH Daily builds are produced by the `GTNewHorizons/DreamAssemblerXXL` GitHub Actions workflow `daily-modpack-build.yml`. GitHub Actions is GitHub's build automation; this workflow assembles GTNH Daily packages. They are not normal stable GitHub releases from `GT-New-Horizons-Modpack`. + +The workflow publishes artifacts whose names follow this pattern: + +- `GTNH-daily-YYYY-MM-DD+NNN-server-java17-25.zip` — dedicated server bootstrap files. +- `GTNH-daily-YYYY-MM-DD+NNN-mmcprism-java17-25.zip` — Prism/MultiMC client bootstrap files. +- `gtnh-daily-YYYY-MM-DD+NNN-manifest.json` — build manifest; includes the config version. +- `daily-build-bundle` — convenience bundle. + +Artifacts can expire, meaning GitHub may delete old downloadable build files. Our bootstrap scripts therefore search recent successful workflow runs and choose a run with non-expired required artifacts. If GitHub requires authenticated artifact download or rate-limit help, provide `GITHUB_TOKEN` in the service/user environment. Recovery examples show how to set it temporarily. + +## Published latest manifest + +Normal updates use the published Daily manifest URL: + +```text +https://raw.githubusercontent.com/GTNewHorizons/DreamAssemblerXXL/master/releases/manifests/daily.json +``` + +This manifest is the current latest Daily metadata. It records pack version information, the config version, timestamp, and selected mods. “Selected mods” means the exact mod entries the Daily build expects for that side/version. + +## Why the server publishes its applied manifest + +If the server updated directly from the latest remote manifest and the client later independently updated from the latest remote manifest, the client could accidentally advance beyond the server if a newer Daily appeared between the two operations. “Advance beyond the server” means the client installs a newer/different mod set than the server is running, which can cause join failures or mod mismatch errors. + +To prevent this, our server update flow: + +1. downloads the current Daily manifest into a temporary file; +2. updates the server using that exact local file; +3. publishes that exact file to `/var/lib/gtnh-daily/current-manifest.json`; +4. writes `/var/lib/gtnh-daily/current-manifest.sha256`. + +The client sync flow then fetches those two files from the server over SSH and updates against the same pinned manifest. This keeps client and server coherent: both sides use the same intended mod/config metadata. + +## Bootstrap versus update + +Initial bootstrap requires a full artifact because `gtnh-daily-updater init` needs an existing instance to scan. The updater is not a from-nothing installer; it records what is already present, then can update it. After that, updates can use `gtnh-daily-updater update` plus the manifest. + +Our server bootstrap creates the initial server files from a server artifact only when the expected launch files are missing. Our client bootstrap creates the Prism instance from a client artifact only when `.minecraft` is missing. Re-running bootstrap is therefore idempotent and does not clobber an already-created instance. diff --git a/docs/gtnh-daily/glossary.md b/docs/gtnh-daily/glossary.md new file mode 100644 index 00000000..c8261d0c --- /dev/null +++ b/docs/gtnh-daily/glossary.md @@ -0,0 +1,233 @@ +# Plain-language glossary + +This page defines the words used by the GTNH Daily docs and Nix code. It is intentionally basic; it assumes no Linux, Nix, Minecraft modding, or server-admin background. + +## Project and game terms + +- **Minecraft 1.7.10** — an old Minecraft version from 2014. GTNH is built on it because many of its mods target that version. +- **GT New Horizons / GTNH** — a very large modded Minecraft pack centered on GregTech progression. It adds hundreds of machines, quests, recipes, dimensions, and support mods. +- **Expert modpack** — a modpack where recipes and progression are intentionally complex and long. It expects planning and many hours of play. +- **Daily** — a frequently rebuilt development/testing version of GTNH. It can be newer than the stable release and may change often. +- **Stable GTNH server** — the separate normal server on this machine. It uses `/srv/gtnh`, service `gtnh-server.service`, and port `25565`. +- **Daily server** — the separate testing/development server managed here. It uses `/var/lib/gtnh-daily`, service `gtnh-daily-server.service`, and port `25566`. +- **GregTech** — the main technology/progression mod in GTNH. +- **Questing** — in-game quest books/tasks that guide progression. +- **Mod** — code that changes Minecraft. Mod files are usually Java `.jar` files. +- **Mod jar** — a `.jar` file in `mods/` that Forge loads. +- **Forge** — the old Minecraft mod loader used by GTNH. It starts Minecraft and loads mods. +- **Forge networking** — the messages exchanged between client and server for modded blocks/items/actions. +- **Registry** — Forge's internal name table for blocks, items, entities, and other modded content. Client and server must agree on these names/IDs. +- **Client** — the Minecraft program running on the player's computer. +- **Server** — the dedicated Minecraft process that stores the world and accepts player connections. +- **Dedicated server JVM** — the Java process running the Minecraft server. +- **Player JVM** — the Java process running the player's Minecraft client. +- **Side** — where a mod belongs: `client`, `server`, or both. Client-only mods may render UI; server-only mods may manage permissions or maps. +- **Both-side mod** — a mod that must exist on both the server and client at compatible versions. +- **Coherent build set** — client and server files that were built to work together. In this setup, the server publishes the exact manifest it used so the client can match it. +- **Runtime state** — files created while the program runs, such as logs, caches, crash reports, player files, generated map data, and world changes. + +## Minecraft file layout terms + +- **Instance** — one installed copy of a Minecraft pack. In Prism, an instance has launcher metadata plus a `.minecraft` game folder. +- **Instance directory** — the top-level folder for one Prism instance, for example `~/.local/share/PrismLauncher/instances/GT New Horizons (Daily)`. +- **Game directory** — the folder Minecraft/Forge actually runs from. For Prism clients this is `/.minecraft`; for the server it is `/var/lib/gtnh-daily/server`. +- **Prism Launcher** — a Minecraft launcher that manages multiple Minecraft instances. It understands MultiMC-style instance packages. +- **MultiMC** — another launcher whose instance format Prism can use. GTNH publishes Daily client artifacts in this format. +- **`.minecraft`** — the actual game folder inside a Prism instance. +- **`mods/`** — mod jar files loaded by Forge. +- **`config/`** — mod configuration files. +- **`scripts/`** — CraftTweaker and pack scripts that change recipes or behavior. +- **CraftTweaker** — a mod used by packs to script recipes and other tweaks. +- **`libraries/`** — downloaded Java libraries used by Minecraft/Forge. +- **`server.properties`** — vanilla Minecraft server settings, including the port and world name. +- **`eula.txt`** — Mojang's required EULA acceptance file. The server refuses to start until it contains `eula=true`. +- **Mojang EULA** — Minecraft's End User License Agreement. This setup writes `eula=true` because the server is intended to run; the human operator is responsible for accepting the terms. +- **`java9args.txt`** — a GTNH-provided file of Java arguments needed by the modern-Java launch path. +- **LWJGL3ify** — GTNH's compatibility layer for running old Minecraft 1.7.10 on modern Java and LWJGL 3. +- **`lwjgl3ify-forgePatches.jar`** — the patched launch jar used by GTNH's modern Java server/client packages. +- **`nogui`** — tells the Minecraft server not to open a graphical window. +- **`ops.json`** — server operator/admin list. +- **`whitelist.json`** — players allowed to join when whitelist is enabled. +- **Ban lists** — server files that record banned players or IPs. +- **`usercache.json`** — Minecraft's cache of player names and UUIDs. +- **UUID** — Minecraft account identifier. Names can change; UUIDs identify accounts. +- **`World/`** — the Daily server world folder. It contains builds, inventories, dimensions, chunks, player data, and mod world data. Nix does not recreate it. +- **Dimension** — a separate world space, such as Overworld, Nether, End, or modded worlds. +- **Chunk** — a 16×16 block column of world data. +- **Resource pack** — client-side textures/models/sounds/UI assets. It changes how the game looks/sounds, not server gameplay. +- **Shader pack** — client-side graphics effects, usually for lighting/shadows. +- **`options.txt`** — Minecraft client options, including selected resource packs and many personal preferences. +- **Sidecar config file** — a small text file next to another file that stores options for it, such as `ComplementaryUnbound_r5.8.1.zip.txt`. + +## Daily build and updater terms + +- **GitHub Actions** — GitHub's automated build system. GTNH uses it to build Daily packages. +- **Workflow** — a GitHub Actions recipe. GTNH Daily uses `daily-modpack-build.yml`. +- **DreamAssemblerXXL** — the GTNH repository that assembles/builds modpack artifacts. +- **Artifact** — a downloadable file produced by a GitHub Actions run. For Daily this includes server zips, Prism client zips, and manifest JSON files. +- **Run ID** — GitHub's numeric identifier for one workflow run. +- **Artifact expiry** — GitHub Actions artifacts are temporary; old ones can disappear. Bootstrap searches recent successful runs for files that still exist. +- **`server-java17-25` artifact** — the server bootstrap zip intended for Java 17 through Java 25. +- **`mmcprism-java17-25` artifact** — the Prism/MultiMC client bootstrap zip intended for Java 17 through Java 25. +- **Java 17-25** — the supported modern Java range for these GTNH packages. This repo installs JDK 25. +- **JDK** — Java Development Kit; includes the Java runtime used to run Minecraft. +- **Headless JDK** — Java runtime without desktop GUI components, suitable for servers. +- **Manifest** — a JSON file describing one GTNH Daily build: version metadata, config version, selected mods, and timestamps. +- **Pinned manifest** — a saved exact manifest file. The server publishes the one it actually used, and the client updates from that same file. +- **Latest manifest** — GTNH's moving `daily.json` URL. It can change whenever a new Daily build is published. +- **Config version** — the GTNH pack configuration version used by `gtnh-daily-updater` to merge config files correctly. +- **SHA-256 hash** — a fingerprint of file contents. If a downloaded file's hash matches the expected hash, the file is almost certainly exactly the intended file. +- **Download URL** — the web address used to fetch a file. +- **`GITHUB_TOKEN`** — a GitHub authentication token. Public artifact downloads sometimes still need API authentication/rate-limit help. Do not commit tokens to git. +- **`gtnh-daily-updater`** — the updater tool for existing GTNH Daily/Experimental instances. It scans an existing instance, records state, and applies manifest-driven updates. +- **Experimental instance** — another non-stable GTNH update mode supported by the updater. This repo uses Daily mode. +- **Init** — `gtnh-daily-updater init`; scans an existing instance and creates `.gtnh-daily-updater.json`. +- **Update** — `gtnh-daily-updater update`; applies manifest changes to an initialized instance. +- **`.gtnh-daily-updater.json`** — the updater's local state file at the instance root. +- **`.gtnh-configs`** — the updater's local git repository used to track/merge GTNH config changes. +- **GTNH assets database** — GTNH metadata that maps mod names to downloadable files. +- **CurseForge / Modrinth** — common Minecraft mod hosting sites. +- **GitHub release asset** — a file attached to a GitHub release. +- **Direct URL source** — an extra mod downloaded from a literal web URL. +- **Download cache** — stored downloaded files so repeated runs do not download everything again. +- **Profile-based batch updates** — an updater feature for updating groups of mods by profile; this setup does not rely on it. +- **Extra mod** — a mod added by local policy, outside or overriding the manifest. +- **Excluded mod** — a manifest mod intentionally skipped by local policy. +- **Same-name override** — an extra with the same name as a manifest mod replaces the manifest choice. We use this for JourneyMap on the client. +- **Regex / regular expression** — a pattern used to select filenames. Example: `unlimited\.jar$` means “a name ending in `unlimited.jar`”. +- **JourneyMap FairPlay** — JourneyMap variant with server-enforced restrictions. +- **JourneyMap Unlimited** — JourneyMap variant without those FairPlay restrictions. This private server uses it on clients. +- **GTNH-Web-Map** — a server mod that can expose a browser map on TCP port `8123`. +- **MineMenu** — a mod providing an in-game radial/menu UI. It is declared on both sides because the chosen version/source is part of the desired pack policy. +- **Modrinth version id** — a Modrinth identifier for an exact mod file version, such as `HNivj4HD`. +- **Upstream** — the original project before local patches. +- **Patch** — a local code change applied to an upstream source package. +- **CLI flag** — a command-line option such as `--manifest-file`. +- **Expand `~`** — replace `~` with the user's home directory. + +## Linux, systemd, and command terms + +- **Linux user/group** — operating-system identity used for file ownership and permissions. +- **`gtnh-daily:gtnh-daily`** — user `gtnh-daily` and group `gtnh-daily`. +- **System user** — a non-login account used to run a service. +- **Home directory** — a user's main directory. For `gtnh-daily` it is `/var/lib/gtnh-daily`; for the normal user it is usually `/home/rafiq`. +- **`~`** — shorthand for the current user's home directory. +- **`/var/lib`** — conventional Linux location for persistent service data. +- **`/run`** — temporary runtime files that disappear on reboot. +- **`~/.cache`** — per-user cache directory. +- **`~/.local/share`** — per-user application data directory. +- **File mode `0755`** — owner can read/write/enter; everyone can read/enter. For directories, “enter” means traverse. +- **File mode `0750`** — owner can read/write/enter; group can read/enter; everyone else cannot. +- **File mode `0644`** — owner can read/write; everyone else can read. +- **Traverse a directory** — permission to pass through it to reach a child path. +- **Sensitive subdirectory** — a directory containing mutable private state such as world files, caches, configs, or backups. +- **Port** — a numbered network endpoint. Minecraft normally uses `25565`; Daily uses `25566` so stable and Daily can run side by side. +- **TCP / UDP** — two common network protocols. Minecraft can use TCP for normal server traffic and UDP for query/status-related traffic, so this module opens both on `25566`. +- **Firewall** — rules controlling what network ports other machines can reach. +- **SSH** — secure remote shell protocol. +- **`scp`** — copy files over SSH. +- **SSH forwarding / tunnel** — make a remote port appear locally. `ssh -N -L 25566:localhost:25566 nemesis` means “connect to host `nemesis`; do not run a shell; forward my local port 25566 to port 25566 as seen from `nemesis`.” +- **`nemesis`** — this repo's default hostname for the Daily server machine. Override with `GTNH_DAILY_SERVER=` if needed. +- **`localhost`** — this machine itself. With SSH forwarding, `localhost:25566` on the client can point to the remote server. +- **`sudo`** — run a command with administrator/root privileges. +- **root** — the administrator account. +- **systemd** — Linux service manager. +- **Unit** — a systemd object, such as a service, socket, or timer. +- **System service** — a systemd service managed by the OS, usually controlled with `sudo systemctl`. +- **User service** — a systemd service managed for one login user, controlled with `systemctl --user`. +- **Service** — a program managed by systemd. +- **Oneshot service** — a service that runs a task and exits instead of staying running. +- **Timer** — systemd scheduler, like cron, that starts a service at intervals. +- **Socket unit** — systemd object that creates/listens on a socket or FIFO for a service. +- **FIFO** — a named pipe file. Writing text to it sends that text to the process reading it. +- **stdin** — standard input. The server reads console commands from stdin. +- **`/run/gtnh-daily-server.stdin`** — FIFO used to send commands to the running Daily server. +- **`printf 'stop\n' | sudo tee ...`** — write the text `stop` into the FIFO as root. +- **`ExecStop`** — systemd command run when stopping a service. +- **SIGTERM / TERM** — polite process termination signal. +- **Exit status 143** — common exit code when a process ends because of SIGTERM. It is accepted as successful here. +- **`network-online.target`** — systemd target indicating the network should be up. +- **`multi-user.target`** — normal non-graphical multi-user boot state. Wanting a service by this target starts it during boot. +- **`timers.target`** — user/systemd target that enables timers. +- **`wantedBy` / `WantedBy`** — systemd installation setting saying “start/enable this as part of that target.” +- **`After` / ordered before** — systemd ordering rules; they control startup order, not whether a service is enabled. +- **`ConditionPathExists`** — systemd only starts the service if a path exists. +- **`Restart=on-failure`** — systemd restarts the service if it crashes unexpectedly. +- **Sandbox** — systemd restrictions that limit what a service can access. +- **Writable path** — a path a sandboxed service may modify. +- **`ProtectSystem=strict`** — make most system paths read-only to the service. +- **`ProtectHome=true`** — hide/protect normal home directories from the service. +- **`PrivateTmp=true`** — give the service a private temporary directory. +- **`NoNewPrivileges=true`** — prevent the service from gaining extra privileges. +- **`UMask=0027`** — default permission mask for newly-created files, keeping them private-ish. +- **journald** — systemd's log storage. +- **`journalctl`** — view systemd logs. +- **`journalctl --user`** — view the current user's user-service logs. +- **`systemctl status`** — show service state and recent logs. +- **`systemctl is-active`** — print whether a service is currently active. +- **`flock`** — file lock tool. It prevents overlapping updates. +- **Lock** — a marker ensuring only one copy of a script runs at a time. +- **`tar.zst` backup** — a tar archive compressed with zstd. +- **zstd** — fast compression algorithm. +- **Symlink** — a pointer file. `latest.tar.zst` points at the newest backup. +- **`chown -R`** — recursively change file owner/group. +- **`cp -a`** — copy while preserving attributes such as permissions and timestamps. +- **`sed -i`** — edit a file in place. +- **`grep -n`** — search text and print matching line numbers. +- **`jq`** — command-line JSON tool. +- **Python snippet** — a short Python program embedded in a shell command. +- **`gh`** — GitHub CLI. +- **`gh api`** — call the GitHub API using the GitHub CLI. +- **`gh run list` / `gh run download`** — list/download GitHub Actions workflow runs/artifacts. + +## Nix and repo terms + +- **Nix** — package/configuration system used to build software and NixOS configurations reproducibly. +- **NixOS** — Linux distribution configured by Nix. +- **Home Manager** — Nix tool for user-level packages, files, desktop entries, and user services. +- **Darwin module** — Nix module for macOS (`nix-darwin`). This file shares Prism package configuration with macOS, but the Daily systemd scripts are Linux-only. +- **Nix module** — a Nix file that declares part of the system/home configuration. +- **Flake** — Nix project format with pinned inputs and outputs. +- **Flake input** — dependency of the flake, such as upstream source code. +- **`flake = false`** — tells Nix an input is plain source, not another flake. +- **Derivation** — Nix build recipe/result. +- **Toplevel build** — the complete NixOS system closure for one host. +- **`--no-link`** — build without creating the default `result` symlink. +- **`pkgs`** — the Nixpkgs package set. +- **`config.flake` / `cfg`** — this repo's flake-parts configuration namespace. +- **`let ... in`** — Nix expression form for local variables. +- **`${...}`** — Nix string interpolation; insert a Nix value into a string. +- **`builtins.toJSON`** — Nix function converting a Nix value to JSON text. +- **`pkgs.writeText`** — create a file in the Nix store containing given text. +- **`pkgs.writeShellScriptBin`** — create an executable shell script package. +- **`pkgs.buildGoModule`** — Nix helper for building Go programs. +- **Go** — programming language used by `gtnh-daily-updater`. +- **`vendorHash`** — Nix hash of Go dependencies. It makes Go dependency fetching reproducible. +- **`vendorHash = null`** — temporary bootstrap setting to discover the real hash; not suitable for committed reproducible packages. +- **`hostPlatform.system`** — Nix's current platform string, such as `x86_64-linux`. +- **`environment.systemPackages`** — programs installed system-wide. +- **`home.packages`** — programs installed for one user by Home Manager. +- **`xdg.desktopEntries`** — Home Manager declarations for desktop launcher icons/menu entries. +- **`home-manager.sharedModules`** — NixOS/nix-darwin integration: apply these Home Manager modules to users on that host. +- **tmpfiles** — systemd mechanism to create directories/files with fixed ownership and permissions at boot/activation. +- **`just`** — command runner used by this repo. +- **`just check-nix`** — repo command that formats/checks Nix code. +- **`just rb`** — repo rebuild command. It may restart changed services. +- **`nix build`** — build a Nix output. +- **`nix develop`** — enter the repo development shell. +- **`prime`** — one of this repo's host/branch names referenced in historical notes. + +## Policy words used in these docs + +- **Declarative** — described in code/config so it can be recreated automatically. +- **Mutable** — expected to change during normal use and not fully controlled by Nix. +- **Reproducibility boundary** — the line between what Nix recreates and what remains external. This setup recreates pack/service/client environment, not the Minecraft world. +- **Idempotent** — safe to run repeatedly; if the desired state already exists, it does little or nothing. +- **Clobber** — overwrite/delete existing data unexpectedly. +- **Reconcile** — set a small controlled part of live state to match the declared config. +- **Drift** — live state differs from declared/documented state. +- **Source of truth** — the place treated as authoritative. For extras/excludes, Nix declarations are the source of truth; for the world, backups/live world storage are the source of truth. +- **Bootstrap** — create missing initial files so the normal updater can work. +- **Rollback** — restore from a previous backup. +- **Port policy** — which network port the service must listen on. +- **Jar/config replacement race** — risk of changing files while Minecraft is running and reading them. diff --git a/docs/gtnh-daily/gtnh-daily-updater.md b/docs/gtnh-daily/gtnh-daily-updater.md new file mode 100644 index 00000000..15c5fcc7 --- /dev/null +++ b/docs/gtnh-daily/gtnh-daily-updater.md @@ -0,0 +1,103 @@ +# `gtnh-daily-updater` + +For definitions of updater, init, update, extras, excludes, regexes, config git repos, and source types, see [glossary.md](./glossary.md). + +## Purpose + +`gtnh-daily-updater` is the tool that updates an existing GTNH Daily or Experimental instance. “Experimental” is another non-stable GTNH update mode; this repo uses Daily. The updater is not a general installer for a missing instance: it expects server/client files to already exist, then initializes state by scanning those files. + +It handles: + +- manifest-based mod additions/removals/updates; +- excluded manifest mods; +- extra mods from the GTNH assets database, GitHub releases, CurseForge, Modrinth, or direct URLs; +- Prism/MultiMC versus server directory layout; +- config updates through a local git repository; +- download caching; +- optional profile-based batch updates. + +## Local state + +Each instance has: + +```text +/.gtnh-daily-updater.json +``` + +Important fields: + +- `side` — `client` or `server`. +- `mode` — `daily` or `experimental`. +- `manifest_date` — applied manifest timestamp; empty after init so the first update runs. +- `config_version` — config version the updater believes is applied. +- `mods` — tracked mod names and installed filenames/versions. +- `exclude_mods` — manifest mod names skipped during updates. +- `extra_mods` — additional mod specs or same-name manifest overrides. + +The updater preserves extras/excludes across `init`, so our bootstrap can initialize first and then reconcile the declarative extras/excludes. In this repo, Nix is the source of truth for those two fields; manual changes to them can be overwritten by the next bootstrap/update. + +## Init + +`init` requires an existing instance and a correct config version: + +```sh +gtnh-daily-updater init --instance-dir --side client|server --config +``` + +During init it: + +1. fetches the GTNH assets database; +2. resolves the game directory (`.minecraft` for Prism/MultiMC clients, root for servers); +3. backs up `mods/` to `.gtnh-mods-backup-YYYY-MM-DD`; +4. scans installed jars and matches them to assets/manifest entries; +5. removes jars for already-declared excludes; +6. initializes `.gtnh-configs` if git is available; +7. writes `.gtnh-daily-updater.json`. + +## Update + +`update` loads local state, resolves the manifest/assets database, canonicalizes mod names, computes a mod diff, downloads needed jars, removes obsolete jars, updates LWJGL3ify if needed, merges config updates, and persists the new state. + +Config merge behavior uses `/.gtnh-configs` on the `local` branch. That directory is a local git repository used by the updater to remember pack config versions and merge new pack config changes. Pack config versions are represented as git refs/tags. Updates merge with `-X theirs`, meaning pack changes win on direct conflicts while local edits are generally preserved when they do not conflict. + +## Extras + +Extras are declared by name. Sources can be: + +- empty source: GTNH assets database lookup; +- `github:Owner/Repo` plus optional `--match` regex for selecting a release asset; +- `curseforge:project` or `curseforge:project/file`; +- `modrinth:project` or `modrinth:project/version`; +- direct `http(s)` URL. + +A same-name extra overrides a manifest entry. We use this for client JourneyMap: the manifest provides FairPlay JourneyMap, while our client extra named `JourneyMap` selects the unlimited jar from `github:TeamJM/journeymap-legacy`. The `--match` regex chooses the release asset whose filename ends with `unlimited.jar`. + +## Excludes + +Excludes are manifest mod names skipped during updates. We exclude `JourneyMap Server` on the server because the private server uses JourneyMap Unlimited on clients and does not need the server-side FairPlay component. If it were not excluded, the updater could reinstall the server-side FairPlay jar during updates. + +## Our desired updater state + +Server: + +- exclude `JourneyMap Server`; +- extra `GTNH-Web-Map` from `github:GTNewHorizons/GTNH-Web-Map`, matched by `^gtnh-web-map-.*[0-9]\.jar$`; +- extra `MineMenu` from `modrinth:mine-menu/HNivj4HD`, where `HNivj4HD` is the exact Modrinth version/file id. + +Client: + +- extra `JourneyMap` from `github:TeamJM/journeymap-legacy`, matched by `unlimited\.jar$`; +- extra `MineMenu` from `modrinth:mine-menu/HNivj4HD`, where `HNivj4HD` is the exact Modrinth version/file id. + +## Local patch + +Upstream normally fetches the latest manifest at update time. We patch the tool with `--manifest-file` because exact client/server matching needs a saved manifest file, not “whatever is latest right now.” The server can update against a downloaded manifest file and clients can update against the exact server-published manifest. + +The patch: + +- adds the CLI flag; +- expands `~` in the flag; +- adds `ManifestFile` to updater options; +- loads JSON from that file when present; +- still fetches the assets database normally; +- preserves upstream behavior when the flag is absent. diff --git a/docs/gtnh-daily/implementation-notes.md b/docs/gtnh-daily/implementation-notes.md new file mode 100644 index 00000000..7b24135e --- /dev/null +++ b/docs/gtnh-daily/implementation-notes.md @@ -0,0 +1,226 @@ +# Implementation notes for reading the Nix code + +This page explains why `nix/modules/gtnh-daily-server.nix` and `nix/modules/prismlauncher.nix` look the way they do. It is a companion to the inline comments in those files. + +## Why the code mixes Nix, shell, and Python + +- **Nix** declares packages, services, timers, files, and generated scripts. +- **Shell** is used for service actions: create directories, download files, stop/start services, call the updater, and run `systemctl`. +- **Python** is used only where structured JSON/file manipulation is clearer and safer than shell string editing. + +The generated shell scripts are still reproducible because Nix writes their exact text into the Nix store. + +## Shared code-reading assumptions + +- `pkgs` is the package set. Paths such as `${pkgs.curl}/bin/curl` point at an exact Nix-built program, not whatever happens to be on `PATH`. +- `${...}` inside a Nix string inserts a Nix value into the generated file/script. +- Shell snippets use `set -euo pipefail` so unexpected errors abort instead of continuing with a half-written instance. +- Most scripts create directories every run. That is deliberate: directory creation with fixed ownership/permissions is idempotent. +- State files are edited narrowly. The code reconciles only the fields it owns, such as updater extras/excludes, instead of rewriting unrelated updater state. + +## `nix/modules/gtnh-daily-server.nix` + +### Overall shape + +The file defines one NixOS module named `gtnh-daily-server`. A NixOS module is a reusable piece of system configuration. This one declares: + +- the `gtnh-daily` system user and group; +- persistent directories under `/var/lib/gtnh-daily`; +- the bootstrap, server, update, rollback, socket, and timer units; +- the firewall opening for Minecraft port `25566`; +- helper scripts installed into the system. + +The top-level `{ inputs, ... }:` receives flake inputs, including this repo's packaged `gtnh-daily-updater`. + +### Important paths and variables + +- `user`, `group`, and `unit` are separate so service names, ownership, and paths stay consistent without repeating strings. +- `rootDir = /var/lib/gtnh-daily` follows Linux convention: long-lived service data belongs under `/var/lib`. +- `serverDir = /var/lib/gtnh-daily/server` is the actual Minecraft server directory. +- `stdin = /run/gtnh-daily-server.stdin` is a temporary FIFO path used to send console commands. +- `lockFile = /run/gtnh-daily-update.lock` prevents overlapping server updates. +- `manifestUrl` is GTNH's moving latest Daily manifest URL. The server downloads it into a local temp file, then publishes the exact file it used. +- `port = 25566` avoids conflict with the stable server on the normal Minecraft port `25565`. +- `pkgs.jdk25_headless` is Java 25 without desktop pieces; it is enough for the server. +- The `tar` command includes zstd compression and preserves extended attributes/ACLs where possible. +- `updaterEnv` sets `HOME`, `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, and `PATH` so the updater stores cache/config under `/var/lib/gtnh-daily` and can find required tools such as git. + +### Declared extras/excludes + +Nix stores the desired server updater policy as JSON: + +- exclude `JourneyMap Server`; +- add/override `GTNH-Web-Map` from GitHub; +- add `MineMenu` from one exact Modrinth version. + +`builtins.toJSON` renders the Nix data as JSON. `pkgs.writeText` writes that JSON into an immutable Nix store file that the script can read. + +### `reconcileState` + +This generated helper updates `.gtnh-daily-updater.json` after init/bootstrap. + +- Python is used because JSON editing in shell is fragile. +- Invalid JSON fails the script, intentionally: continuing would risk corrupting updater state. +- The code replaces `exclude_mods` and `extra_mods` exactly because those fields are owned by Nix policy. Manual additions to those fields will be removed on the next bootstrap/update; add wanted entries to Nix instead. +- Ownership is reset to `gtnh-daily:gtnh-daily` because services run as that user. + +### Bootstrap script + +Bootstrap creates the minimum required server files when a server does not already exist. + +- `install -d` creates directories with owner/group/mode in one command. +- The script fetches the latest manifest only if no published manifest exists yet. +- `mktemp` creates safe temporary files/directories. +- The manifest hash file stores the SHA-256 plus filename, the normal `sha256sum` format. Consumers compare the first field for the actual hash. +- GitHub Actions artifacts do not have one stable URL, so the script asks the GitHub API for recent successful workflow runs and their artifact URLs. +- `GITHUB_TOKEN`, when present, is sent as an API authorization header. The `auth=()` array is a shell array; the unusual quoting is how Nix emits shell array syntax without treating it as Nix interpolation. +- Searching the 20 most recent successful runs is a practical balance: enough history to skip expired/incomplete runs without spending too long. +- Embedded Python parses GitHub JSON responses safely and prints shell assignments for the selected artifact URLs. +- `eval` is used only on output produced by our embedded Python from GitHub JSON values that are shell-quoted by `shlex.quote`. +- The script requires both a server artifact and its manifest artifact for artifact bootstrap, so the initial files and manifest config version come from the same Daily run. +- The manifest artifact is zipped by GitHub even though it contains a JSON file, so the script unzips it and requires exactly one JSON file. +- `cp -a "$tmp/server/."` copies the artifact contents while preserving attributes. +- The artifact is extracted only when launch files are missing. If `java9args.txt` exists but another required launch file is missing, systemd's `ConditionPathExists` prevents server start; repair by deleting/recreating the incomplete instance or inspecting manually. +- Updater init runs only after files exist because it scans the actual jars/configs. +- `runuser -u gtnh-daily` runs the updater as the service user so generated files have normal ownership. +- `eula.txt` is written/edited automatically because the server cannot start without it. The operator is responsible for agreeing to Mojang's terms. + +### Stop script + +The stop helper receives the Java process ID from systemd as `$1`. + +1. It writes `stop` to the FIFO so Minecraft can save and shut down cleanly. +2. It waits up to 120 seconds. +3. If Java is still running, it sends SIGTERM. + +If writing to the FIFO fails, the script still eventually uses SIGTERM. If Java ignores SIGTERM, systemd's normal service stop timeout/kill behavior applies. + +### Backup script + +The backup covers `/var/lib/gtnh-daily/server`, including the world if present. That is useful for pre-update rollback, but it is not a replacement for independent long-term world backups. + +- UTC timestamps sort consistently regardless of local timezone. +- `latest.tar.zst` is a symlink to the newest backup for easy rollback. +- Ownership is set on the archive; symlink ownership is usually not meaningful for access control. + +### Update script + +- File descriptor `9` is a conventional spare descriptor used by `flock`. +- If another update already holds the lock, the service fails instead of overlapping. +- The manifest is downloaded before stopping the server to reduce downtime. +- The server stop command uses `|| true` because it is okay if the server was already stopped or exits oddly during shutdown; update status should reflect updater/backup/publish failures, not a harmless stop race. +- `cleanup` removes temporary files. `finish` runs at exit and restarts the server only if it had been active before update. +- The restart trap is installed once; it should not run twice. +- Extras/excludes are reconciled before update so the updater applies declared policy. +- The manifest is published only after updater success. If publishing the manifest/hash fails, the service fails and the client will not record a new applied hash. +- A backup is created before every manual/timer update, even if the updater later finds little to change. + +### Rollback + +`gtnh-daily-rollback` requires root because it moves service-owned files and controls system services. + +- Without an argument it uses `latest.tar.zst`. +- It stops the Daily server, moves the current server directory aside with a timestamp, extracts the backup, fixes ownership, and starts the server. +- If extraction fails after the move, the moved-aside directory remains for manual recovery. +- Rollback restores whatever was in the backup's `server/` tree. It does not restore `/var/lib/gtnh-daily/current-manifest.json`, which lives outside `server/`; run client sync/update carefully after rollback if client/server versions matter. + +### tmpfiles, socket, services, timer, firewall + +- tmpfiles creates directories and fixes basic ownership/modes. It does not overwrite normal file contents. +- `ListenFIFO` creates the named pipe used for server console input. +- `SocketMode=0660` lets owner/group write to the FIFO; others cannot. +- `RemoveOnStop` removes the FIFO when the socket stops; `FlushPending` discards stale pending input. +- Bootstrap is wanted by `multi-user.target` so it runs on boot/activation. +- The server service requires the socket and wants bootstrap/network so command input exists and missing files are created first. +- `path = [...]` makes listed commands available to service scripts without hard-coding every executable. +- `preStart` rewrites `server.properties` to keep the port correct. If the file is absent, it creates a minimal `server-port=25566` line. +- The timer's `Persistent=true` means if the machine was off at 05:00, systemd runs the missed timer after boot. +- Web-map port `8123` is not opened here because exposing a browser map may need separate access-control/reverse-proxy/firewall decisions. + +## `nix/modules/prismlauncher.nix` + +### Overall shape + +This file groups one concern: Prism Launcher and the GTNH Daily client. It exports: + +- Darwin and NixOS wrapper modules that attach a shared Home Manager module; +- the Home Manager module that installs Prism, desktop entries, client scripts, and the user timer. + +Daily sync is Linux-only because it uses systemd user timers/services. Darwin users can still use Prism and could run equivalent commands manually, but this repo does not declare macOS launchd automation for Daily. + +### Instance paths + +- `gtnhDailyInstanceName` must match Prism's instance name exactly because Prism launch commands and paths use that string. +- If the user renames the instance, the declared desktop entry and sync scripts will manage/create the old name again. +- `config.home.homeDirectory` is Home Manager's current user's home directory. +- The code assumes Prism's normal Linux path under `~/.local/share/PrismLauncher`. If Prism is configured to store instances elsewhere, this module would need an option/change. + +### Client assets JSON + +The module declares resource/shader packs as JSON so the Python bootstrap code can iterate over a simple data file. + +- `stripRoot = true` means “after unzipping, remove the single top-level directory and use its contents as the resource pack directory.” Modernity's GitHub archive contains a commit-named top directory, so it needs this. +- SHA-256 values were calculated from the intended downloads and rechecked by downloading each URL. +- When updating a pack version, update the URL, filename/extract directory if needed, and SHA-256 together. +- If a URL disappears, bootstrap will fail clearly instead of silently using an unknown file. +- The Complementary shader sidecar is generated in code because it is tiny text, not a downloaded binary artifact. + +### Client updater state JSON + +The client declares only extras because no client manifest mod is excluded outright. JourneyMap is declared as an extra with the manifest name `JourneyMap`, so it overrides the manifest's FairPlay jar with the Unlimited jar. MineMenu is pinned to exact Modrinth version `HNivj4HD` for repeatability. + +Manual client extras in `.gtnh-daily-updater.json` will be overwritten by Nix reconciliation. Add desired extras to the Nix module instead. + +### `gtnh-daily-client-bootstrap` + +This script exists as an installed user command so the same logic can be run manually, by sync, or by future automation. + +- It defaults `GTNH_DAILY_SERVER` to `nemesis`, this repo's Daily server host. +- SSH `BatchMode=yes` means “do not prompt for passwords”; if SSH keys/config are missing, fail instead of hanging in a timer. +- The process check uses `pgrep` for Prism/Minecraft/LWJGL3ify names. It may false-positive on oddly named processes, but that is safer than changing files while the game is running. +- The server hash is fetched before and after copying the manifest so the script can verify the manifest against the server's published hash. +- Only the first field of the `sha256sum` file is compared because the rest is just the filename. +- Artifact bootstrap uses a recent Prism artifact only when `.minecraft` is missing. It then initializes/updates against the server-published manifest, so a newer bootstrap artifact does not define the final target version; the server manifest does. +- If `.minecraft` exists but is incomplete, bootstrap assumes it is intentional and does not overwrite it. Repair by moving the broken instance aside or deleting it after backing up anything important. +- The artifact is copied into the Prism instance root because Prism/MultiMC artifacts contain both launcher metadata and the `.minecraft` game directory. +- Updater init runs only when `.gtnh-daily-updater.json` is missing. If the state file exists but has the wrong side or bad contents, the updater/sync should fail; inspect or remove the broken state file intentionally. +- `git` is put in `PATH` because updater config merging uses git. +- Resource/shader directories are chmodded to user-writable/readable values to fix old copies that may have arrived read-only from zips. +- `.gtnh-configs/resourcepacks` is also chmodded because the updater may snapshot resource pack config state there. +- Each unpacked resource pack has a `.sha256` marker. If the source zip hash changes, bootstrap deletes and re-extracts the directory. +- If extraction fails partway, the script exits. The next run will retry because the marker will be missing or wrong. +- The Complementary sidecar is written every run so the desired text setting is restored. +- `options.txt` is edited only when it exists. Artifact bootstrap normally creates it. If it is missing, Minecraft/Prism can create defaults later, and a later bootstrap/sync can write the resource-pack line. + +### `gtnh-daily-client-sync` + +Sync is the normal command because it performs both “make sure the instance exists” and “update to the server manifest.” + +- It takes a user lock to avoid overlapping manual/timer syncs. +- It runs bootstrap first so a wiped client can recover with one command. +- It fetches the hash again after bootstrap because bootstrap may have taken time and the server could have updated meanwhile. +- The local applied hash is the server manifest hash from the last successful updater run. It is written only after updater success. +- Comparing full hash-file text can be stricter than comparing only the first field; if whitespace/filename format ever changes, sync may do an unnecessary update rather than skipping one. +- A full instance backup is created before every update. It is a directory copy under Prism's backups directory, not zstd-compressed. Old backups are not pruned by this script. +- If backup creation fails, update does not run. +- If updater succeeds but writing the local hash fails, the next sync may repeat the update; that is safer than claiming success without a marker. + +### Desktop entries and packages + +- `xdg.desktopEntries` creates graphical launcher entries. +- The stable icon path is hard-coded to the known stable instance path; if the icon disappears, the launcher entry may show a generic icon but still launch. +- Daily launches `localhost:25566` instead of `nemesis:25566` so the same entry works for local servers and SSH tunnels. +- `pkgs.prismlauncher.override { jdks = [ pkgs.jdk25 ]; }` installs Prism with Java 25 available in its Java list. +- Bootstrap/sync are installed only on Linux because they rely on Linux paths and user systemd automation. + +### User systemd timer + +- A user systemd service runs as the normal desktop user, not root. That is required because Prism instances live under the user's home directory. +- `After = [ "network-online.target" ]` is a best-effort ordering hint in user systemd; the script still fails cleanly if SSH/network is unavailable. +- The timer is enabled by `Install.WantedBy = [ "timers.target" ]`. +- If the timer fires while Prism/Minecraft is open, sync refuses to mutate files and exits. +- Logs are visible with `journalctl --user -u gtnh-daily-client-sync.service`. + +### Booting after updates + +- The flag `-Dfml.queryResult=confirm` is added as during updates, blocks or items may be removed. Launching with this flag skips the need to send a `/fml confirm` to boot the server. diff --git a/docs/gtnh-daily/recovery.md b/docs/gtnh-daily/recovery.md new file mode 100644 index 00000000..452179ad --- /dev/null +++ b/docs/gtnh-daily/recovery.md @@ -0,0 +1,141 @@ +# Recovery and rebuild guide + +For definitions of commands/services mentioned here, see [glossary.md](./glossary.md). This page is the practical checklist. + +## Fresh machine expectation + +A “fresh machine” can mean a new disk, a reinstalled OS, or a NixOS rebuild where `/var/lib/gtnh-daily` and the Prism instance are missing. After a disk wipe and rebuild, the declarative setup should provide: + +- `gtnh-daily-updater` package with local `--manifest-file` patch; +- `gtnh-daily` system user/group and directories; +- `gtnh-daily-bootstrap.service`; +- `gtnh-daily-server.service` and FIFO socket; +- `gtnh-daily-update.service` and timer; +- `gtnh-daily-rollback` command; +- Prism Launcher with JDK 25; +- `gtnh-daily-client-bootstrap` and `gtnh-daily-client-sync`; +- user sync service/timer; +- declared resource/shader pack downloads and shader sidecar text. + +It should not provide the old `World/` unless you separately restore a world backup. + +## Server from scratch + +On first boot/rebuild activation, `gtnh-daily-bootstrap.service` is wanted by `multi-user.target` and ordered before the server. It creates the server if needed. If GitHub artifact downloads require authentication, make `GITHUB_TOKEN` available to the service environment before running bootstrap. + +If GitHub artifact downloads need authentication, create a token with permission to read public GitHub Actions artifacts and provide it when manually running bootstrap: + +```sh +sudo systemctl set-environment GITHUB_TOKEN= +sudo systemctl start gtnh-daily-bootstrap.service +sudo systemctl unset-environment GITHUB_TOKEN +``` + +For permanent unattended bootstrap on a brand-new host, add an environment file or systemd credential outside git and extend the service to read it; do not commit the token. + +After downloads complete, check status and logs: + +```sh +systemctl status gtnh-daily-bootstrap.service +systemctl status gtnh-daily-server.service +sudo journalctl -u gtnh-daily-bootstrap.service -n 200 --no-pager +sudo journalctl -u gtnh-daily-server.service -n 200 --no-pager +``` + +Good signs are: bootstrap exited successfully, the server service is active/running, and logs show the Minecraft server starting instead of repeated missing-file or download errors. + +If a world backup exists, restore the backup contents so the world folder is exactly: + +```text +/var/lib/gtnh-daily/server/World +``` + +Stop the server before replacing a world. After copying/extracting the backup, ensure ownership: + +```sh +sudo chown -R gtnh-daily:gtnh-daily /var/lib/gtnh-daily/server/World +``` + +A valid world backup should contain Minecraft world files such as `level.dat`, region/dimension folders, player data, and mod data. + +## Client from scratch + +Close Prism/Minecraft first, then run: + +```sh +gtnh-daily-client-sync +``` + +This bootstraps the Prism instance if missing, installs declared packs, and updates against the server-published manifest. The command should be run as the normal desktop user because Prism files live in that user's home directory. + +Override the SSH host when needed. `nemesis` is already the default in this repo, so this example is mainly a template for replacing it with another host: + +```sh +GTNH_DAILY_SERVER=nemesis gtnh-daily-client-sync +``` + +Then launch the Daily desktop entry or: + +```sh +prismlauncher --launch "GT New Horizons (Daily)" --server localhost:25566 +``` + +If the server is remote and not already available at `localhost:25566`, create an SSH tunnel in another terminal before launching: + +```sh +ssh -N -L 25566:localhost:25566 nemesis +``` + +## Routine operations + +Manual server update starts the update service immediately instead of waiting for the daily timer. It may stop/restart the Daily server, so do not run it while players are online unless that interruption is acceptable. + +```sh +sudo systemctl start gtnh-daily-update.service +sudo journalctl -u gtnh-daily-update.service -n 200 --no-pager +``` + +Check logs for backup creation, updater success, manifest publication, and server restart. + +Manual client sync: + +```sh +gtnh-daily-client-sync +journalctl --user -u gtnh-daily-client-sync.service -n 200 --no-pager +``` + +Rollback without an argument uses the latest pre-update backup: + +```sh +sudo gtnh-daily-rollback +``` + +Rollback stops the Daily server and replaces the server directory from backup. It does not automatically roll back clients; run client sync carefully afterward if the server manifest changed. + +Restore only the active world from the latest in-instance FTBUtilities/ServerUtilities zip backup: + +```sh +sudo gtnh-daily-ftbu-rollback +``` + +To restore a specific in-instance zip backup instead: + +```sh +sudo gtnh-daily-ftbu-rollback /var/lib/gtnh-daily/server/backups/2026-06-20-14-46-50.zip +``` + +This stops the Daily server, reads the active world name from `server.properties`, validates that the zip contains that world with `level.dat`, moves aside each top-level path present in the zip, extracts those paths into the server directory, fixes ownership, and starts the server again. This preserves ServerUtilities/FTBUtilities additional backup paths such as `journeymap`, `TCNodeTracker`, NEI saves, or `visualprospecting` when they are included in the zip. + +## Safety guarantees + +The bootstrap/update flow is designed to be idempotent: + +- missing instances are created; +- existing instances are not re-extracted from artifacts; +- declared updater extras/excludes are reconciled; +- server EULA and port policy are enforced; +- client resource/shader pack files are downloaded, hash-verified, and refreshed when needed; +- client update refuses to run while Prism/Minecraft is active; +- server updates back up before mutation and restart only if previously active. + +The flow is not a world backup system. Keep world backups separately if the world must survive a wipe. A good world-backup plan should copy `/var/lib/gtnh-daily/server/World` while the server is stopped or from a storage-level snapshot known to be consistent. diff --git a/docs/gtnh-daily/research-server-management.html b/docs/gtnh-daily/research-server-management.html new file mode 100644 index 00000000..96d1edc1 --- /dev/null +++ b/docs/gtnh-daily/research-server-management.html @@ -0,0 +1,771 @@ + + + + + Minecraft server management research: itzg Docker, GTNH, NixOS, and flakes + + +

Minecraft server management research: itzg Docker, GTNH, NixOS, and flakes

+

Date: 2026-06-15

+

+ This document is an exhaustive implementation-oriented walkthrough of the Minecraft server management options researched for the GTNH Daily setup in this repo. It covers how each implementation works, what problem it solves, what state it owns, how it starts/stops servers, how it handles mods/modpacks, and whether it fits the current GTNH Daily goals. +

+

+ The current GTNH Daily goals are unusual: recreate the server and Prism client after a wipe/rebuild, exclude mutable world/runtime data, use gtnh-daily-updater, pin the exact server-applied Daily manifest, and sync the client to that server-published manifest. Most generic Minecraft server tools solve only part of that problem. +

+ +

Short conclusion

+

+ The current custom NixOS/Home Manager implementation remains justified for GTNH Daily. The researched alternatives are useful references, but none covers the full combination of: +

+
    +
  • GTNH Daily GitHub Actions artifact bootstrap;
  • +
  • gtnh-daily-updater initialization and update;
  • +
  • server-published pinned manifest and SHA-256 hash;
  • +
  • Prism client bootstrap from Daily artifacts;
  • +
  • client update against the exact server-applied manifest;
  • +
  • declared updater extras/excludes;
  • +
  • resource/shader pack download and verification;
  • +
  • clear separation between declarative pack/service/client state and mutable world data.
  • +
+

+ The strongest ideas to borrow are: nixpkgs' simple systemd FIFO service pattern, Infinidoge's multi-server file/symlink model and environment-file support, and itzg's GTNH-specific Java/defaults knowledge. The current custom implementation already uses several of those patterns. +

+ +

Important terms

+
+
GTNH
+
GT New Horizons, a large Minecraft 1.7.10 expert modpack.
+
Daily
+
A frequently rebuilt development/testing GTNH build, produced by GitHub Actions artifacts and represented by a manifest JSON file.
+
Manifest
+
A JSON file describing one modpack build: version metadata, config version, timestamp, and selected mods.
+
Pinned manifest
+
A saved exact manifest file. In this repo, the server publishes the manifest it actually used, and the client updates from that exact file.
+
Artifact
+
A downloadable output of a build job. GTNH Daily artifacts include server zips, Prism/MultiMC client zips, and manifest JSON files.
+
Mutable world data
+
The Minecraft world and runtime state, especially World/, dimensions, chunks, player data, logs, caches, generated map data, and backups. This is not recreated by Nix.
+
Declarative state
+
State described in code/config so it can be recreated automatically after a rebuild.
+
Bootstrap
+
Create missing initial files so the normal updater or server process can work.
+
Idempotent
+
Safe to run repeatedly. If the desired state already exists, it should not clobber it.
+
+ +

Section 1: itzg/docker-minecraft-server

+

+ Repository: itzg/docker-minecraft-server. Researched commit: 1a8844cb625d753409f02f3cfbd46eefd5c5d939. +

+ +

What it is

+

+ itzg/docker-minecraft-server is a general-purpose Docker image for running Minecraft Java Edition servers. Instead of installing Java, server jars, and helper scripts directly on the host, the server runs inside a container. The persistent server data is mounted at the container path /data. +

+

+ The image is configured primarily through environment variables. A typical compose file sets EULA=TRUE, exposes a port, mounts a volume at /data, and selects a server type such as VANILLA, PAPER, FORGE, FABRIC, MODRINTH, AUTO_CURSEFORGE, or GTNH. The docs list core variables such as EULA, TYPE, VERSION, UID, GID, MEMORY, INIT_MEMORY, MAX_MEMORY, TZ, DEBUG, SETUP_ONLY, FORCE_REDOWNLOAD, and proxy options. +

+

+ Documentation references: +

+ + +

How the base container works

+
    +
  1. The user starts the container with Docker or Docker Compose.
  2. +
  3. The image's startup scripts inspect environment variables.
  4. +
  5. The scripts install or update server files in /data.
  6. +
  7. The scripts render or update Minecraft configuration such as server.properties based on environment variables.
  8. +
  9. The container launches Java with the selected server jar or launch command.
  10. +
  11. Persistent data remains in the mounted volume or bind mount, not inside the disposable container filesystem.
  12. +
+ +

Core variables and their meaning

+
+
EULA
+
Must be TRUE to accept the Minecraft EULA. The server will not run without it.
+
TYPE
+
Selects the server implementation or modpack platform, such as PAPER, FORGE, FABRIC, CUSTOM, or GTNH.
+
VERSION
+
Selects the Minecraft version for types that use normal Minecraft versions. GTNH sets this internally to Minecraft 1.7.10.
+
MEMORY, INIT_MEMORY, MAX_MEMORY
+
Control Java heap size. MEMORY sets both initial and max heap unless the more specific variables override it.
+
UID and GID
+
The Linux user/group IDs used inside the container for server files. Defaults are normally 1000:1000.
+
SETUP_ONLY
+
Prepare server files and stop before launching. Useful for inspecting a data directory or doing manual setup.
+
FORCE_REDOWNLOAD
+
For supported server types, forces the server file to be downloaded again.
+
CUSTOM_SERVER
+
For TYPE=CUSTOM, a URL or container path to the custom server jar.
+
+ +

Docker Compose operating model

+

+ Docker Compose stores configuration in compose.yaml. It can run one or more Minecraft containers, attach volumes, expose ports, and add helper services such as backups or RCON web administration. The official docs show examples for Paper, CurseForge, Modrinth, Bedrock-compatible proxy setups, multiple servers, RCON web admin, and automatic backups using itzg/mc-backup. +

+

+ Important operational commands are: +

+
    +
  • docker compose up -d — create/start containers in the background.
  • +
  • docker compose logs -f — follow logs.
  • +
  • docker compose stop — stop gracefully.
  • +
  • docker compose down — remove containers while preserving named volumes.
  • +
  • docker compose pull — update images.
  • +
+ +

Mod platform model

+

+ itzg supports modpack platforms through MODPACK_PLATFORM, TYPE, or MOD_PLATFORM. The mod-platform overview lists common platforms such as Auto CurseForge, Modrinth, FTBA, and GTNH. These platforms can install mod loaders, resolve downloads, apply configuration files, support upgrades/downgrades, and clean old files. +

+

+ The platform comparison says GTNH supports auto-update, no API key, version pinning, and is specifically recommended for GT New Horizons. +

+ +

Generic pack model

+

+ Generic packs are archives applied into the server data directory. GENERIC_PACK points at one zip/tgz file, while GENERIC_PACKS can apply multiple archives. Optional variables such as GENERIC_PACKS_PREFIX and GENERIC_PACKS_SUFFIX reduce repetition. Update-related flags include SKIP_GENERIC_PACK_UPDATE_CHECK, FORCE_GENERIC_PACK_UPDATE, and SKIP_GENERIC_PACK_CHECKSUM. +

+

+ This is relevant because older/manual GTNH container guides used TYPE=CUSTOM plus GENERIC_PACK to apply a downloaded GTNH server zip. The newer dedicated TYPE=GTNH path is more specialized. +

+ +

Section 2: itzg GTNH support

+

+ itzg has first-class GTNH support. The GTNH docs say GTNH has its own TYPE because it has specific deployment/update requirements. Source: GTNH docs lines 1-11. +

+ +

GTNH user-facing configuration

+

The documented GTNH variables are:

+
+
TYPE=GTNH
+
Activates the GTNH-specific deployment script.
+
GTNH_PACK_VERSION
+
Defaults to latest. Can be latest, latest-dev, or a specific version such as 2.8.1. The docs recommend pinning a specific version for manual update control.
+
GTNH_DELETE_BACKUPS
+
Defaults to false. If true, deletes old GTNH upgrade backup folders.
+
SKIP_GTNH_UPDATE_CHECK
+
Defaults to false. If true, skips GTNH install/update checking. The docs warn to set this only after initial setup because it also prevents installation.
+
+ +

GTNH compose example

+

+ The repo includes examples/gtnh/docker-compose-type-gtnh.yaml. It uses: +

+
    +
  • image: itzg/minecraft-server:java25
  • +
  • EULA: "TRUE"
  • +
  • TYPE: GTNH
  • +
  • GTNH_PACK_VERSION: "2.8.1"
  • +
  • MEMORY: 6G
  • +
  • a volume mounted at /data
  • +
+

+ The GTNH wiki container page similarly recommends type-based deployment with itzg/minecraft-server:java25, TYPE: GTNH, a pinned GTNH_PACK_VERSION, MEMORY, optional whitelist/ops, and a persistent /data volume. +

+ +

GTNH resource and Java guidance

+

+ The itzg GTNH docs recommend 2-4 CPU cores, 6 GiB RAM plus extra memory per player/tier, 20+ GiB storage, and SSD where possible. They state GTNH supports Java 8 and Java 17+, recommend Java 17+ for performance, and recommend Java 25 for GTNH 2.8.0 and later. Source: GTNH docs lines 19-34. +

+ +

GTNH version selection internals

+

+ The GTNH script is scripts/start-deployGTNH. It defines getGTNHdownloadPath, which fetches https://downloads.gtnewhorizons.com/versions.json. The selection behavior is: +

+
    +
  • GTNH_PACK_VERSION=latest-dev selects the latest entry whose title is Beta release.
  • +
  • GTNH_PACK_VERSION=latest selects the latest entry whose title is Stable release.
  • +
  • Any other value is matched against the exact version key.
  • +
+

+ Source: start-deployGTNH lines 7-40. +

+ +

GTNH Java/server zip selection internals

+

+ The script checks the container Java version with mc-image-helper java-release. If Java is 8, it chooses .value.server.java8Url. If Java is 17 or newer, it verifies the current Java does not exceed the release's maxJavaVersion, then chooses .value.server.java17_2XUrl. If the Java version is unsupported, startup fails. Source: start-deployGTNH lines 42-60. +

+ +

GTNH install/update marker

+

+ The script uses /data/.gtnh-version as its install/update marker. If that file is missing or does not equal the basename of the selected download URL, it downloads and applies the selected server pack. If the marker matches, it reports that no update is required. Source: start-deployGTNH lines 163-224. +

+ +

GTNH update behavior

+

+ On update, updateGTNH removes/replaces pack-managed directories and files. It deletes libraries, mods, resources, and scripts. It deletes launch files such as lwjgl3ify-forgePatches.jar, java9args.txt, start scripts, Forge jar, and server icon. It backs up config to /data/gtnh-upgrade-<timestamp>, deletes the original config folder, copies new pack folders/files, recreates config if needed, and restores JourneyMapServer from the backup if present. Source: start-deployGTNH lines 73-150. +

+

+ This is a pragmatic container update strategy. It is simple and direct, but it differs from gtnh-daily-updater's config-git merge model. +

+ +

GTNH server.properties defaults

+

+ The script exports GTNH-suitable defaults before continuing normal container setup: +

+
    +
  • ALLOW_FLIGHT=true
  • +
  • LEVEL_TYPE=rwg
  • +
  • DIFFICULTY=3
  • +
  • ENABLE_COMMAND_BLOCK=true
  • +
  • MOTD="Greg Tech New Horizons $GTNH_PACK_VERSION"
  • +
+

+ Source: start-deployGTNH lines 228-235. +

+ +

GTNH launch command internals

+

+ For Java 8, the script sets SERVER=/data/forge-1.7.10-10.13.4.1614-1.7.10-universal.jar. For Java 17+, it sets SERVER=/data/lwjgl3ify-forgePatches.jar. It also sets VERSION=1.7.10 and USES_MODS=true. Source: start-deployGTNH lines 239-259. +

+

+ Later, final Java argument handling adds -Dfml.readTimeout=180. For Java 8 it adds old JVM tuning flags; for Java 17+ it appends @java9args.txt. Source: start-finalExec lines 409-423. +

+ +

GTNH container strengths

+
    +
  • Best ready-made Docker path for GTNH stable/beta release servers.
  • +
  • Officially documented by both itzg and the GTNH wiki.
  • +
  • Simple compose file with one data volume.
  • +
  • Encodes GTNH defaults and Java-specific launch choices.
  • +
  • Good for non-NixOS hosts and users already comfortable with Docker.
  • +
+ +

GTNH container limitations for this repo

+
    +
  • Targets GTNH release downloads from versions.json, not GTNH Daily GitHub Actions artifacts.
  • +
  • Uses .gtnh-version based on a zip basename, not a server-published Daily manifest hash.
  • +
  • Does not initialize or drive gtnh-daily-updater.
  • +
  • Does not publish the exact applied manifest for clients.
  • +
  • Does not bootstrap/sync a Prism client.
  • +
  • Uses a config replacement/backup model, not the updater's config-git merge model.
  • +
  • Adds Docker/container orchestration to a host that is already NixOS-managed.
  • +
+ +

Section 3: nixpkgs services.minecraft-server

+

+ Source: local checkout /home/rafiq/1_repos/nixpkgs, commit b5e9cca1667666e70abf6814c62dc0b1c759d7b1, file nixos/modules/services/games/minecraft-server.nix. +

+ +

What it is

+

+ nixpkgs' services.minecraft-server is the standard NixOS module for running one Minecraft server. It is intentionally simple. You provide a package that has a bin/minecraft-server executable, accept the EULA, optionally declare server.properties and whitelist, and NixOS creates a systemd service. +

+ +

Options

+

+ The module's main options are defined in lines 67-190: +

+
+
enable
+
Start the service.
+
declarative
+
When true, apply Nix-declared whitelist and server.properties.
+
eula
+
Must be true. The module asserts this before running.
+
dataDir
+
Persistent server data directory, default /var/lib/minecraft.
+
openFirewall
+
Open Minecraft ports in the NixOS firewall.
+
whitelist
+
Attribute set of usernames to UUIDs, used only in declarative mode.
+
serverProperties
+
Attribute set rendered to server.properties, used only in declarative mode.
+
package
+
The server package, defaulting to nixpkgs' Minecraft server package.
+
jvmOpts
+
JVM options string, defaulting to -Xmx2048M -Xms2048M.
+
+ +

Rendered files

+

+ The module creates a Nix-store eula.txt with eula=true, a generated whitelist.json, and a generated server.properties. The code for these generated files is in lines 10-34. +

+ +

System user and FIFO socket

+

+ The module creates a system user/group named minecraft, with home at dataDir. It also creates a systemd socket with ListenFIFO=/run/minecraft-server.stdin. This gives the server a named pipe for console input. Source: lines 193-214. +

+ +

Service execution

+

+ The service starts ${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}, runs as user minecraft, uses cfg.dataDir as working directory, takes standard input from the socket, and logs stdout/stderr to journald. Source: lines 216-235. +

+ +

Hardening

+

+ The service includes substantial systemd hardening: empty capabilities, private devices/tmp/users, home protection, kernel protections, invisible proc, address-family restrictions, namespace restrictions, realtime/SUID restrictions, native syscall architecture, and UMask=0077. Source: lines 236-260. +

+ +

Pre-start declarative behavior

+

+ Pre-start always symlinks the Nix-managed EULA file. If declarative=true, it symlinks/copies the generated whitelist and server.properties. The first time declarative mode is enabled, it backs up existing stateful files with a .stateful suffix and creates a .declarative marker. Source: lines 262-295. +

+ +

Firewall behavior

+

+ If openFirewall=true and declarative=true, the module opens the declared server port, query port, and RCON port if enabled. If not declarative, it opens default port 25565. Source: lines 298-313. +

+ +

Strengths

+
    +
  • Simple upstream NixOS-native module.
  • +
  • Good systemd service hardening.
  • +
  • Clean EULA, FIFO, journald, and firewall patterns.
  • +
  • Good fit for vanilla or a prebuilt server package exposing bin/minecraft-server.
  • +
+ +

Limitations for GTNH Daily

+
    +
  • Only one server.
  • +
  • No modpack artifact bootstrap.
  • +
  • No GTNH launch-file awareness.
  • +
  • No gtnh-daily-updater.
  • +
  • No server-published manifest or client sync.
  • +
  • Assumes a package with bin/minecraft-server, while GTNH Daily runs from a mutable server directory with java @java9args.txt -jar lwjgl3ify-forgePatches.jar nogui.
  • +
+ +

Section 4: Infinidoge/nix-minecraft

+

+ Repository: Infinidoge/nix-minecraft. Researched commit: e6f8bec35104ca5955efe73742da58d2823684f7. +

+ +

What it is

+

+ nix-minecraft is a flake focused on Minecraft server packaging and management in the Nix ecosystem. The README says it packages Vanilla, Fabric, Legacy Fabric, Quilt, Paper, Purpur, NeoForge, Velocity, and tools such as nix-modrinth-prefetch, fetchPackwizModpack, and fetchModrinthModpack. Source: README lines 3-22. +

+ +

Installation model

+

+ The README describes adding nix-minecraft.url = "github:Infinidoge/nix-minecraft" to flake inputs, importing inputs.nix-minecraft.nixosModules.minecraft-servers, and adding the overlay. Source: README lines 34-56. +

+ +

Packages and modpack helpers

+

+ The flake provides versioned server package sets, including vanillaServers, fabricServers, quiltServers, legacyFabricServers, paperServers, purpurServers, neoforgeServers, velocityServers, and combined minecraftServers. Source: README lines 66-155. +

+

+ fetchPackwizModpack can fetch a Packwiz modpack and expose its mods and config directories to a server. The docs warn to use stable URLs, such as git tags/commits, for reproducibility. Source: README lines 157-180. +

+ +

Module structure

+

+ The module defines services.minecraft-servers, plural. It supports multiple named servers under services.minecraft-servers.servers. Global options include enable, eula, openFirewall, dataDir, user, group, environmentFile, and a default management system. Source: module lines 180-307. +

+ +

Management systems

+

+ Infinidoge supports two management systems: +

+
    +
  • tmux: starts the server inside a tmux session and lets admins attach to the socket.
  • +
  • systemd socket/FIFO: uses StandardInput=socket, journald output, and writes the stop command to a FIFO.
  • +
+

+ The module asserts that only one management system can be enabled at a time. Source: module lines 80-180. +

+ +

Per-server options

+

+ Each named server supports options including enable, autoStart, openFirewall, restart, enableReload, extra start/stop/reload hooks, stopCommand, whitelist, operators, banned players, serverProperties, allowed symlinks, package, jvmOpts, path, environment, symlinks, files, and per-server management system overrides. Source: module lines 307-617. +

+ +

File management model

+

+ Infinidoge's module has a powerful split between symlinks and files: +

+
    +
  • symlinks put read-only Nix store paths into the server directory.
  • +
  • files copy paths into the server directory, making them writable during runtime, then deleting them after stop so changes are discarded.
  • +
  • Files can be generated from Nix values in formats like JSON, YAML, TOML, INI, properties, and text.
  • +
  • An environmentFile can provide secrets outside the Nix store and substitute @varname@ placeholders.
  • +
+

+ Source: format helpers lines 1-80, environmentFile lines 260-299, and symlinks/files lines 560-617. +

+ +

Start-pre behavior

+

+ On start, the module cleans files it previously managed, backs up unmanaged files it would replace, creates parent directories, symlinks declarative paths, copies writable files, substitutes environment variables in non-binary files, and records managed paths in .nix-minecraft-managed. Source: module lines 720-854. +

+ +

Firewall and port assertions

+

+ The module asserts that open servers do not share the same server port. It opens TCP server/RCON ports and UDP query ports for servers with openFirewall=true. Source: module lines 680-717. +

+ +

Systemd service and hardening

+

+ The generated service uses start/stop/reload scripts, configured restart policy, working directory under the global data dir, service user/group, optional environment file, runtime directory, and systemd hardening. Hardening includes private devices/tmp/users, home and kernel protections, proc protection, address-family restrictions, namespace/realtime/SUID restrictions, and UMask=0007. Source: module lines 940-1010. +

+ +

Strengths

+
    +
  • Most complete Nix-native Minecraft server framework researched.
  • +
  • Supports many server/loaders and multiple named servers.
  • +
  • Good declarative file/symlink model.
  • +
  • Good management choice: tmux or FIFO socket.
  • +
  • Good hardening and firewall/port assertions.
  • +
  • Useful Packwiz and Modrinth tooling.
  • +
+ +

Limitations for GTNH Daily

+
    +
  • No direct old Forge 1.7.10 GTNH Daily packaging path.
  • +
  • No gtnh-daily-updater integration.
  • +
  • No GitHub Actions Daily artifact bootstrap.
  • +
  • No server-published manifest/client sync flow.
  • +
  • Could be a framework to build on, but would still need substantial GTNH-specific code.
  • +
+ +

Section 5: mkaito/nixos-modded-minecraft-servers

+

+ Repository: mkaito/nixos-modded-minecraft-servers. Researched commit: 68f2066499c035fd81c9dacfea2f512d6b0b62e5. +

+ +

What it is

+

+ This flake provides services.modded-minecraft-servers, allowing multiple modded Minecraft server instances. Each instance gets a state folder and system user named from the instance, such as /var/lib/mc-e2es and user mc-e2es. Source: README lines 1-39. +

+ +

Philosophy

+

+ The README explicitly says the module makes no attempt to package or install modpacks. Instead, it gives each server a folder where the operator puts the pack files, and then it runs start.sh in that folder. $JVMOPTS is provided by Nix, but the modpack's own launch script decides how to use it. Source: README lines 72-112. +

+ +

Module behavior

+

+ The module renders server.properties, renders eula.txt, optionally configures rsync access, asserts unique server/RCON/query ports, creates a systemd service per instance, and creates a system user/group per instance. The service runs /var/lib/${fullname}/start.sh, sets JVMOPTS, uses StateDirectory, and works in the instance state directory. Source: module lines 1-203. +

+ +

Rsync feature

+

+ If rsyncSSHKeys is set, the module creates forced-command SSH access for rsync to the instance state directory. This is read/write access, so it is meant for trusted operators. Source: README lines 114-131. +

+ +

Strengths

+
    +
  • Simple multi-instance model.
  • +
  • Per-instance users and directories.
  • +
  • Does not pretend arbitrary modpacks are easy to package.
  • +
  • Useful rsync operator workflow.
  • +
+ +

Limitations for GTNH Daily

+
    +
  • Requires manual modpack installation into the state directory.
  • +
  • No reproducible wipe/rebuild from Nix by itself.
  • +
  • No GTNH Daily updater or manifest pinning.
  • +
  • We would still need most of our custom bootstrap/update code.
  • +
+ +

Section 6: aster-void/nix-mc

+

+ Repository: aster-void/nix-mc. Researched commit: 458b4590d6b41f06a54397a819d7307fc7d63891. +

+ +

What it is

+

+ nix-mc is a NixOS module for Forge, NeoForge, and Bedrock servers. Its README says most users should consider Infinidoge's nix-minecraft instead, and use nix-mc when unsupported loaders or Bedrock support are needed. Source: README lines 1-20. +

+ +

Version-locked source model

+

+ The README recommends storing server files in a version-locked source, such as a flake input pointing at a git tag/commit. A server uses upstreamDir for read-only server files, then declares symlinks, copied files, and serverProperties. Source: README lines 22-58 and README lines 140-180. +

+ +

Sync/start behavior

+

+ The module builds a pre-start sync script. It creates the runtime/data directories, copies configured writable files, symlinks configured paths, and writes server.properties if configured. Source: nix-mc.nix lines 1-45. +

+ +

Service model

+

+ Services run through tmux. The start script kills any existing tmux session for the server, starts a new detached session in the data directory, verifies the session exists, and waits while it runs. The systemd service includes Restart=on-failure, ProtectSystem=strict, ProtectHome=true, ReadWritePaths, and other hardening. Source: nix-mc.nix lines 72-131. +

+ +

Strengths

+
    +
  • Simple Forge/NeoForge/Bedrock abstraction.
  • +
  • Clear read-only upstream + data directory separation.
  • +
  • Good fit for a repo-pinned server tree.
  • +
  • Reasonable hardening.
  • +
+ +

Limitations for GTNH Daily

+
    +
  • Assumes a version-locked pre-installed server source, not runtime discovery of Daily artifacts.
  • +
  • No gtnh-daily-updater.
  • +
  • No client sync.
  • +
  • No server manifest publication.
  • +
+ +

Section 7: TLATER/nix-minecraft-servers

+

+ Repository: TLATER/nix-minecraft-servers. Researched commit: d7271c7db69a7195fd5399f779299580a5fbea48. +

+ +

What it is

+

+ This repo is mostly a design/spec note and information dump. Its README says almost none of the actual functionality is implemented. Source: README lines 1-8. +

+ +

Forge analysis

+

+ The README explains that Forge installers download libraries during installation, which is impure from a Nix perspective. It proposes accounting for those libraries ahead of time and avoiding direct automated Forge installer download. Source: README lines 12-43. +

+ +

Modpack analysis

+

+ The README explains why modpack formats are difficult: CurseForge manifests may not include full direct download links, CurseForge access can be awkward, and many server zips are arbitrary author-defined directories/scripts/installers. It sketches possible future handling for manifest JSON and server-file packs, but does not implement it. Source: README lines 45-138. +

+ +

Relevance

+

+ TLATER's analysis is valuable because it explains why purely packaging old Forge/modpack ecosystems is hard. It supports the pragmatic approach used by the current GTNH Daily module: runtime bootstrap from upstream artifacts, then pin and reconcile the exact updater state needed for repeatability. +

+ +

Section 8: jyooru/nix-minecraft-servers

+

+ Repository: jyooru/nix-minecraft-servers. Researched commit: 48387fe72c74ad7b5bca624606f18d85e697022a. +

+

+ This older flake packaged Minecraft server versions for Nix and exposed them through a package set and overlay. Its README now says the project is no longer maintained and points to Infinidoge's nix-minecraft. Source: README lines 1-8. +

+

+ Relevance: historical only. Use Infinidoge instead for this category. +

+ +

Section 9: Ninlives/minecraft.nix

+

+ Repository: Ninlives/minecraft.nix. Researched commit: a0b850c5a1ca542026b594d14576bacddef6dfeb. +

+

+ minecraft.nix packages vanilla and Fabric client/server derivations. It can run a client or server with nix run. Its withConfig function can add mods, resource packs, and shader packs. Source: README lines 1-27. +

+

+ The options table says client options include mods/resourcePacks/shaderPacks/auth/declarative, while server options include mods and declarative; server declarative mode is currently a no-op. Source: README lines 77-95. +

+

+ Relevance: useful for understanding fully Nix-managed client/server derivations and resource/shader pack handling, but not a NixOS GTNH Daily server management replacement. +

+ +

Section 10: iamanaws/endernix

+

+ Repository: iamanaws/endernix. Researched commit: 05d1f2016f24660b2774ba907fff82824abbcb30. +

+

+ endernix builds on minecraft.nix and manages multiple isolated Minecraft installations. Its README says minecraft.nix has a single shared game directory problem, while endernix gives each installation its own isolated game directory with saves, screenshots, and config. Source: README lines 1-10. +

+

+ It has a Modrinth lockfile flow: write mods.nix, run nix run github:iamanaws/endernix#update-mods -- mods.nix, and use the generated mods.lock.nix in mkInstance. Source: README lines 13-45. +

+

+ mkInstance creates an isolated Minecraft installation with parameters such as name, version, loader, mods, resource packs, shader packs, JVM args, and extra config. Source: README lines 160-187. +

+

+ Relevance: useful for client installation and Modrinth lockfile ideas. It is not a server-service framework for GTNH Daily. +

+ +

Section 11: Current custom GTNH Daily implementation in this repo

+

+ The current implementation is split across nix/modules/gtnh-daily-server.nix and nix/modules/prismlauncher.nix. It exists because GTNH Daily needs both a server and a matching client, and both must follow the same server-published Daily manifest. +

+ +

Server module responsibilities

+
    +
  • Create system user/group gtnh-daily.
  • +
  • Create persistent directories under /var/lib/gtnh-daily.
  • +
  • Bootstrap missing server files from GTNH Daily GitHub Actions artifacts.
  • +
  • Initialize gtnh-daily-updater state if missing.
  • +
  • Reconcile declared server extras/excludes.
  • +
  • Accept the EULA.
  • +
  • Run the server with Java 25, ZGC, GTNH Java 17+ launch files, and port 25566.
  • +
  • Provide a FIFO stdin socket for console commands.
  • +
  • Run scheduled updates with pre-update backups.
  • +
  • Publish the exact applied manifest and SHA-256 hash for clients.
  • +
  • Provide rollback command.
  • +
  • Open the Minecraft port.
  • +
+ +

Client/Home Manager module responsibilities

+
    +
  • Install Prism Launcher with JDK 25.
  • +
  • Create desktop entries for stable and Daily GTNH.
  • +
  • Bootstrap the Daily Prism instance from GTNH Daily Prism/MultiMC artifacts if missing.
  • +
  • Fetch the server-published manifest/hash over SSH.
  • +
  • Verify manifest SHA-256 before use.
  • +
  • Initialize gtnh-daily-updater state if missing.
  • +
  • Reconcile declared client extras.
  • +
  • Download and verify resource/shader packs by URL/hash instead of committing zips.
  • +
  • Unpack resource packs to names used by options.txt.
  • +
  • Write selected resource packs.
  • +
  • Run client sync as the normal user.
  • +
  • Refuse to mutate files while Prism/Minecraft appears active.
  • +
  • Create full Prism instance backups before updates.
  • +
  • Schedule hourly user sync timer.
  • +
+ +

Why it is different from itzg

+

+ itzg's GTNH implementation is optimized for Docker and GTNH release server packs. The current custom module is optimized for NixOS/Home Manager and GTNH Daily. The key difference is the source of truth: itzg tracks selected server pack downloads using .gtnh-version, while our setup tracks the server-applied Daily manifest hash and makes clients use the exact same manifest. +

+ +

Why it is different from nixpkgs/Infinidoge modules

+

+ nixpkgs and Infinidoge assume the Minecraft server package or server files are already available in a form their modules can run. They provide excellent service management, but not the GTNH Daily-specific artifact/updater/client-pinning pipeline. Our module has to include that pipeline. +

+ +

Section 12: Comparison table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Concernitzg GTNHnixpkgs minecraft-serverInfinidoge nix-minecraftCurrent custom module
Runs on NixOS without DockerNoYesYesYes
Runs in DockerYesNoNoNo
GTNH release server supportYesNoNo direct supportDaily-focused, not release-focused
GTNH Daily GitHub Actions artifact bootstrapNoNoNoYes
gtnh-daily-updaterNoNoNoYes
Server-published pinned manifestNoNoNoYes
Prism client syncNoNoNoYes
Multiple serversYes via composeNoYesNo, one Daily server
FIFO consoleContainer helpers / console pipe optionsYesYes, optionalYes
tmux consoleNoNoYes, optional/defaultNo
Declarative file/symlink abstractionEnvironment variables and volume contentsLimited to server.properties/whitelistYesCustom targeted reconciliation
World backup boundaryVolume/operator responsibilitydataDir/operator responsibilitydataDir/operator responsibilityExplicitly documented and excluded from Nix
+ +

Section 13: Design lessons for this repo

+

Keep

+
    +
  • Keep the custom Daily bootstrap/update/client-sync pipeline.
  • +
  • Keep server-published manifest pinning as the core anti-drift mechanism.
  • +
  • Keep mutable world data outside Nix.
  • +
  • Keep not committing large resource/shader pack zips; declare URLs and hashes instead.
  • +
  • Keep the FIFO console pattern.
  • +
  • Keep strong systemd hardening where it does not break GTNH.
  • +
+ +

Consider borrowing later

+
    +
  • From Infinidoge: environmentFile support for secrets such as GITHUB_TOKEN.
  • +
  • From Infinidoge/aster-void: a generic symlinks/files abstraction if this repo grows more Minecraft servers.
  • +
  • From Infinidoge: port duplicate assertions if multiple GTNH-like servers are added.
  • +
  • From itzg: document GTNH server defaults more explicitly, especially ALLOW_FLIGHT, LEVEL_TYPE=rwg, DIFFICULTY=hard, and command blocks.
  • +
  • From itzg/mc-backup patterns: consider a separate world backup service/timer, explicitly outside the declarative bootstrap.
  • +
+ +

Avoid

+
    +
  • Do not switch to Docker just to avoid maintaining the current NixOS module; it would lose Daily-specific reproducibility unless reimplemented inside/around the container.
  • +
  • Do not force the whole GTNH server tree into the Nix store unless prepared to solve mutable configs, updater state, artifact expiry, world data, and old Forge packaging complexity.
  • +
  • Do not use nixpkgs services.minecraft-server directly for GTNH Daily; it is too generic.
  • +
  • Do not use unpinned latest semantics for client and server independently; that recreates the drift problem the pinned manifest flow solves.
  • +
+ +

Section 14: Source index

+ + + diff --git a/docs/gtnh-daily/research-server-management.md b/docs/gtnh-daily/research-server-management.md new file mode 100644 index 00000000..5640ecb6 --- /dev/null +++ b/docs/gtnh-daily/research-server-management.md @@ -0,0 +1,179 @@ +# Research note: Minecraft server management options + +Date: 2026-06-15 + +Scope: itzg `docker-minecraft-server` for GTNH, modded Minecraft server flakes/modules, and the nixpkgs `services.minecraft-server` NixOS module. This note is research only; it does not change the current GTNH Daily implementation. + +## Executive summary + +For this repo's **GTNH Daily** goal, the current custom NixOS module remains justified. + +- `itzg/docker-minecraft-server` is the strongest off-the-shelf **container** option for GTNH stable/beta releases. It has first-class `TYPE=GTNH`, Java 25 guidance, GTNH-specific defaults, and an update/install path. It is less aligned with our Daily manifest-pinning/client-sync design because it targets GTNH download releases via `versions.json`, not the `gtnh-daily-updater` manifest-file workflow. +- nixpkgs `services.minecraft-server` is a solid simple single-server module: EULA, one service, FIFO stdin, hardening, declarative `server.properties`/whitelist, and firewall. It does not solve modpack bootstrap/update, multiple instances, GTNH launch details, Daily artifact discovery, or client/server manifest pinning. +- `Infinidoge/nix-minecraft` is the most mature Nix flake ecosystem option for multiple Minecraft servers. It has multiple server instances, Fabric/Quilt/Paper/Purpur/NeoForge packages, Packwiz/Modrinth helpers, symlinks/files management, tmux or systemd-socket management, hardening, and generated service units. It still does not directly package old Forge 1.7.10 GTNH Daily or `gtnh-daily-updater` semantics. +- Other flakes (`mkaito/nixos-modded-minecraft-servers`, `aster-void/nix-mc`, `TLATER/nix-minecraft-servers`, `jyooru/nix-minecraft-servers`, `Ninlives/minecraft.nix`, `endernix`) are useful design references, but either intentionally punt on modpack installation, are simpler/younger, are deprecated, or are more client/Fabric-focused. + +Best extraction ideas for our module: + +1. Keep the **systemd FIFO stdin pattern** from nixpkgs/Infinidoge; we already use it. +2. Consider adopting a multi-server-style `symlinks`/`files` abstraction only if we later generalize beyond GTNH Daily. +3. Consider a small `environmentFile`/credential option for `GITHUB_TOKEN`, inspired by Infinidoge's environment-file support. +4. Keep GTNH-specific bootstrap/update/client-sync custom because no researched module covers the full Daily manifest-pinning boundary. + +## itzg/docker-minecraft-server for GTNH + +### What it provides + +The upstream docs say GTNH has its own `TYPE` because the pack has special deployment/update needs. Configuration defaults include `GTNH_PACK_VERSION=latest`, `GTNH_DELETE_BACKUPS=false`, and `SKIP_GTNH_UPDATE_CHECK=false` ([docs](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/docs/types-and-platforms/mod-platforms/gtnh.md#L1-L11)). Version selection supports `latest`, `latest-dev`, or a specific version, with the docs recommending a specific version for manual update control ([docs](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/docs/types-and-platforms/mod-platforms/gtnh.md#L13-L17)). + +The official GTNH wiki's container page says `TYPE: GTNH` support was added in `docker-minecraft-server` 2025.12.0 and is the recommended Docker path for most users. Its compose example uses `image: itzg/minecraft-server:java25`, `EULA: TRUE`, `TYPE: GTNH`, and `GTNH_PACK_VERSION: 2.8.1` (GTNH wiki content fetched from `Server_Setup_(Container)`). The repo example matches that shape ([compose example](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/examples/gtnh/docker-compose-type-gtnh.yaml#L1-L24)). + +GTNH resource guidance in the itzg docs is close to our current sizing: 2-4 CPU cores, 6 GiB RAM plus player/tier overhead, and 20+ GiB storage, SSD preferred ([docs](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/docs/types-and-platforms/mod-platforms/gtnh.md#L19-L28)). The docs recommend Java 17+ and Java 25 for GTNH 2.8.0+ ([docs](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/docs/types-and-platforms/mod-platforms/gtnh.md#L30-L34)). + +### How GTNH deployment works internally + +`start-configuration` dispatches `TYPE=GTNH` to `start-deployGTNH` ([dispatch](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-configuration#L222-L246)). The GTNH script selects a download from `https://downloads.gtnewhorizons.com/versions.json`: `latest-dev` chooses the latest beta/RC, `latest` chooses the latest stable, and a specific `GTNH_PACK_VERSION` selects that exact version key ([selection](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L7-L40)). It then selects Java 8 or Java 17+ server URLs based on the container Java version and the release's max Java version ([Java URL choice](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L42-L60)). + +On update, it deletes/replaces `libraries`, `mods`, `resources`, `scripts`, launch jars/scripts, and backs up `config` into `/data/gtnh-upgrade-` before copying the new config tree. It restores `config/JourneyMapServer` from the backup if present ([update function](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L73-L150)). The install/update decision is keyed by `/data/.gtnh-version` matching the selected download basename ([install/update gate](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L163-L224)). + +The script sets GTNH server defaults before normal setup: `ALLOW_FLIGHT=true`, `LEVEL_TYPE=rwg`, `DIFFICULTY=3`, `ENABLE_COMMAND_BLOCK=true`, and a GTNH MOTD ([defaults](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L228-L235)). For Java 17+, it sets `SERVER=/data/lwjgl3ify-forgePatches.jar`, `VERSION=1.7.10`, and `USES_MODS=true` ([server selection](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-deployGTNH#L239-L259)). Final Java args add `-Dfml.readTimeout=180` and, for Java 17+, `@java9args.txt` ([final args](https://github.com/itzg/docker-minecraft-server/blob/1a8844cb625d753409f02f3cfbd46eefd5c5d939/scripts/start-finalExec#L409-L423)). + +### Strengths + +- Very practical for container users: one image, one volume, environment variables. +- Maintained broad Minecraft server image with GTNH-specific support. +- Encodes GTNH defaults and Java-version selection. +- Handles stable/beta GTNH server pack installs/updates automatically. +- Container boundary is convenient for running multiple unrelated servers on non-NixOS hosts. + +### Limitations for our Daily setup + +- It targets GTNH release downloads via `downloads.gtnewhorizons.com/versions.json`, not GitHub Actions Daily artifacts plus `gtnh-daily-updater` manifests. +- Its update marker is `.gtnh-version` based on the server zip basename, not the exact server-applied Daily manifest hash. +- It does not publish a pinned manifest for clients or coordinate a Prism client sync. +- Its update strategy replaces config directories and preserves only selected state such as `JourneyMapServer`; our current updater-based flow uses `gtnh-daily-updater` config merging and declared extras/excludes. +- Docker adds another runtime/orchestration layer on NixOS. That may be worthwhile for portability, but it is less idiomatic if the host is already fully NixOS-managed. + +## nixpkgs `services.minecraft-server` + +The local nixpkgs checkout used for this review is `/home/rafiq/1_repos/nixpkgs` at commit `b5e9cca1667666e70abf6814c62dc0b1c759d7b1`. + +### What it provides + +The module exposes a single `services.minecraft-server` service. Options include `enable`, `declarative`, `eula`, `dataDir`, `openFirewall`, `whitelist`, `serverProperties`, `package`, and `jvmOpts` ([options](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L67-L190)). It creates a system `minecraft` user/group and a FIFO socket at `/run/minecraft-server.stdin` for console input ([user/socket](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L193-L214)). + +The systemd service runs `${cfg.package}/bin/minecraft-server ${cfg.jvmOpts}`, uses the FIFO as standard input, logs to journald, and includes substantial hardening such as `PrivateTmp`, `PrivateUsers`, `ProtectHome`, `ProtectProc`, namespace restrictions, and `UMask=0077` ([service/hardening](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L216-L260)). + +Pre-start always symlinks a Nix-managed `eula.txt`; in declarative mode it writes/symlinks `whitelist.json` and `server.properties`, backing up stateful originals the first time it becomes declarative ([preStart](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L262-L295)). Firewall opening uses declared ports when `declarative=true`, including RCON/query where enabled; otherwise it opens the default 25565 ([firewall](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L298-L313)). The module asserts `eula=true` before running ([assertion](https://github.com/NixOS/nixpkgs/blob/b5e9cca1667666e70abf6814c62dc0b1c759d7b1/nixos/modules/services/games/minecraft-server.nix#L315-L323)). + +### Strengths + +- Minimal, upstream NixOS-native, and well hardened. +- Good pattern for EULA, FIFO stdin, service user, journald, and declarative `server.properties`. +- Perfect for vanilla or a prepackaged server derivation that already knows how to launch as `bin/minecraft-server`. + +### Limitations for GTNH Daily + +- Single server only; no multi-instance attrset. +- No modpack bootstrap/update logic. +- No Forge/GTNH artifact discovery. +- No client sync or server-published manifest flow. +- `ExecStart` assumes package shape `${package}/bin/minecraft-server`; GTNH's Java 17+ launch path is `java ... @java9args.txt -jar lwjgl3ify-forgePatches.jar nogui` inside a mutable server directory. +- Declarative mode owns `server.properties`/whitelist but not GTNH config merging, extras/excludes, or updater state. + +Useful takeaway: our module intentionally mirrors the strong parts—system user, FIFO stdin, journald, hardening, EULA assertion/management, firewall—from nixpkgs, but needs custom GTNH Daily bootstrap/update/client pinning. + +## Modded Minecraft server flakes/modules + +### Infinidoge/nix-minecraft + +This is the strongest Nix ecosystem option found. It focuses on server-side Minecraft and packages Vanilla, Fabric, Legacy Fabric, Quilt, Paper, Purpur, NeoForge, Velocity, and tools including `nix-modrinth-prefetch`, `fetchPackwizModpack`, and `fetchModrinthModpack` ([README](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/README.md#L3-L22)). It is flake-first and installed by importing `nixosModules.minecraft-servers` plus the overlay ([install](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/README.md#L34-L56)). + +It supports multiple named servers under `services.minecraft-servers.servers`, with per-server enable/autostart/firewall/restart/reload hooks/stop command/whitelist/operators/bans/serverProperties/package/jvmOpts/path/environment/symlinks/files/management options ([server options](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L307-L560)). It supports both tmux management and systemd FIFO socket management; the systemd socket mode uses `StandardInput=socket`, journald output, and writes the stop command to the FIFO ([management system](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L80-L180)). + +It has a granular file model: `symlinks` for read-only declarative paths and `files` for copied writable paths, including generated JSON/properties/text formats and optional environment-file substitution for secrets ([file/env options](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L260-L299), [file options](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L560-L617)). At start, it cleans files it previously managed, backs up pre-existing unmanaged targets, creates symlinks/copies, and marks managed files in `.nix-minecraft-managed` ([start-pre](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L720-L854)). It also opens per-server firewall ports and asserts no duplicate open server ports ([firewall/assertions](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L680-L717)). + +The service hardening is similar to nixpkgs and includes `PrivateTmp`, `PrivateUsers`, `ProtectHome`, kernel protections, address-family restrictions, and `UMask=0007` ([service hardening](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/modules/minecraft-servers.nix#L940-L1010)). + +Packwiz integration is documented: a fetched Packwiz modpack can symlink `mods` and copy selected config files, with a warning to use stable URLs/tags/commits for reproducibility ([Packwiz docs](https://github.com/Infinidoge/nix-minecraft/blob/e6f8bec35104ca5955efe73742da58d2823684f7/README.md#L157-L180)). + +Relevance to GTNH Daily: + +- Great architectural reference for multi-server abstractions, file/symlink policy, environment-file secrets, and service management. +- Not currently a direct replacement for GTNH Daily because it does not provide Forge 1.7.10/GTNH Daily artifact/updater integration. +- Could be overkill unless we want to generalize our `gtnh-daily-server.nix` into a broader Minecraft server framework. + +### mkaito/nixos-modded-minecraft-servers + +This flake explicitly supports multiple modded instances but deliberately avoids modpack installation. Its README says the module provides `services.modded-minecraft-servers`, with each instance using its name for the state folder and user (`/var/lib/mc-e2es`, user `mc-e2es`) and optional rsync SSH access ([README usage](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/README.md#L1-L39)). It states that `server.properties` is overwritten and that the module does not support stateful configuration for that file ([server config](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/README.md#L43-L60)). + +For modded packs, it says it makes no attempt to guess how to package or run a modpack; it provides a state folder where files are dumped and runs `start.sh` with `$JVMOPTS` set ([modded philosophy](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/README.md#L72-L112)). The module implementation creates per-instance users/services, renders `server.properties`, symlinks EULA, and runs `/var/lib/${fullname}/start.sh` ([module service](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/nixos/modules/services/games/minecraft-servers/default.nix#L141-L180)). It also asserts unique server/RCON/query ports and opens firewall ports ([assertions/firewall](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/nixos/modules/services/games/minecraft-servers/default.nix#L110-L139), [firewall](https://github.com/mkaito/nixos-modded-minecraft-servers/blob/68f2066499c035fd81c9dacfea2f512d6b0b62e5/nixos/modules/services/games/minecraft-servers/default.nix#L201-L202)). + +Relevance: + +- Useful “minimal modded framework” reference. +- Philosophically opposite to our reproducibility goal: it expects the operator to install modpack files in the state directory. +- Not enough for GTNH Daily after wipe/rebuild without our separate bootstrap/updater layer. + +### aster-void/nix-mc + +`nix-mc` advertises Forge, NeoForge, and Bedrock support with systemd hardening, flexible file management, multi-server support, and automatic firewall ([README](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/README.md#L1-L20)). It also explicitly recommends `Infinidoge/nix-minecraft` for most use cases, using `nix-mc` when a loader is unsupported by nix-minecraft or for Bedrock ([README note](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/README.md#L1-L6)). Its usage model is version-locked server sources via flake inputs or `fetchFromGitHub`, with `upstreamDir`, `symlinks`, and `serverProperties` ([quick start](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/README.md#L22-L58)). The README emphasizes version-locked sources as reproducible, atomic, rollback-friendly, and immutable-store-based ([version-locked benefits](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/README.md#L140-L180)). + +Implementation is simpler than Infinidoge: pre-start sync creates directories, copies declared writable files, symlinks declared paths, and optionally writes `server.properties` ([sync script](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/nixosModules/nix-mc.nix#L1-L45)). Services run through tmux by default and include hardening such as `NoNewPrivileges`, `PrivateTmp`, `ProtectSystem=strict`, `ProtectHome=true`, and `ReadWritePaths` for data/run dirs ([service](https://github.com/aster-void/nix-mc/blob/458b4590d6b41f06a54397a819d7307fc7d63891/nixosModules/nix-mc.nix#L72-L131)). + +Relevance: + +- Good design reference for a simple `upstreamDir` + `symlinks` + `files` model. +- If we ever pin a whole GTNH server artifact into the Nix store, this model becomes relevant. +- Less suited to Daily because Daily artifacts and updater state are mutable/runtime-discovered, and our world/config boundary is more nuanced. + +### TLATER/nix-minecraft-servers + +This repo is primarily a design/spec note rather than a complete implementation: it says almost none of the actual functionality is implemented ([README](https://github.com/TLATER/nix-minecraft-servers/blob/d7271c7db69a7195fd5399f779299580a5fbea48/README.md#L1-L8)). The interesting part is its analysis of Forge/modpacks: Forge installers download libraries and are impure; the proposed approach is to pre-account for those libraries and avoid automating the Forge installer download directly ([Forge analysis](https://github.com/TLATER/nix-minecraft-servers/blob/d7271c7db69a7195fd5399f779299580a5fbea48/README.md#L12-L43)). It also explains why CurseForge/modpack manifests and arbitrary “server files” packs are difficult to package purely: manifests may lack direct URLs, CurseForge access can be awkward, and server zips are arbitrary author-defined directories/scripts ([modpack analysis](https://github.com/TLATER/nix-minecraft-servers/blob/d7271c7db69a7195fd5399f779299580a5fbea48/README.md#L45-L138)). + +Relevance: + +- Strong conceptual justification for why GTNH Daily needs a pragmatic bootstrap/updater boundary instead of pretending every modpack artifact is a pure Nix derivation. +- Supports our choice to keep mutable updater/world state out of the Nix store while pinning/reconciling the parts that matter. + +### jyooru/nix-minecraft-servers + +This older flake packaged server versions and provided overlays/packages for use with `services.minecraft-server`, but the README now says it is no longer maintained and points to Infinidoge's `nix-minecraft` ([README](https://github.com/jyooru/nix-minecraft-servers/blob/48387fe72c74ad7b5bca624606f18d85e697022a/README.md#L1-L8)). + +Relevance: historical only; prefer Infinidoge. + +### Ninlives/minecraft.nix and iamanaws/endernix + +`minecraft.nix` packages vanilla/fabric client and server derivations and supports `withConfig` for mods/resource packs/shader packs; for server, its table says `declarative` is currently a no-op ([README](https://github.com/Ninlives/minecraft.nix/blob/a0b850c5a1ca542026b594d14576bacddef6dfeb/README.md#L1-L27), [options](https://github.com/Ninlives/minecraft.nix/blob/a0b850c5a1ca542026b594d14576bacddef6dfeb/README.md#L77-L95)). + +`endernix` builds on `minecraft.nix` for multiple isolated Minecraft installations, primarily client/installations rather than NixOS server services. It solves the shared-game-directory problem and can generate Modrinth lockfiles with URLs/hashes ([README](https://github.com/iamanaws/endernix/blob/05d1f2016f24660b2774ba907fff82824abbcb30/README.adoc#L1-L16), [lockfile flow](https://github.com/iamanaws/endernix/blob/05d1f2016f24660b2774ba907fff82824abbcb30/README.adoc#L17-L45), [mkInstance API](https://github.com/iamanaws/endernix/blob/05d1f2016f24660b2774ba907fff82824abbcb30/README.adoc#L160-L187)). + +Relevance: useful for client-side/resource-pack thinking and Modrinth lockfile ideas, but not a GTNH server management replacement. + +## Comparison against current GTNH Daily module + +| Concern | itzg Docker GTNH | nixpkgs module | Infinidoge nix-minecraft | Current custom module | +| --- | --- | --- | --- | --- | +| GTNH-specific launch args | Yes | No | Not directly for GTNH 1.7.10 | Yes | +| Stable/RC GTNH server pack install | Yes | No | No direct GTNH support | Not target; Daily only | +| GTNH Daily GitHub Actions artifact bootstrap | No | No | No | Yes | +| `gtnh-daily-updater` integration | No | No | No | Yes | +| Server-published pinned manifest for clients | No | No | No | Yes | +| Prism client bootstrap/sync | No | No | No | Yes | +| Multiple server abstraction | Docker compose can do it | No | Yes | No, one GTNH Daily server | +| Declarative `server.properties` | Env/defaults | Yes | Yes | Only enforced port for now | +| Console management | Docker attach/RCON/pipe helpers | FIFO | tmux or FIFO | FIFO | +| Systemd hardening | Container isolation | Yes | Yes | Yes | +| World backup boundary | Volume responsibility | dataDir responsibility | dataDir responsibility | Explicitly documented | +| Pure Nix derivation for modpack | No | Only if package supplied | Some packwiz/Modrinth support | No; pragmatic runtime bootstrap | + +## Practical recommendations + +1. **Do not replace the current GTNH Daily module with nixpkgs `services.minecraft-server`.** It would remove most of the Daily-specific bootstrap/update/client-sync logic. +2. **Do not replace it with itzg Docker unless the goal changes to container portability or stable GTNH releases.** itzg is excellent for `TYPE=GTNH` release servers, but it does not address our exact Daily manifest pinning and Prism sync goals. +3. **Use Infinidoge as the main design reference if generalizing.** Its `services.minecraft-servers` module has the best patterns for multi-server definitions, file/symlink handling, environment-file secrets, port assertions, and management system selection. +4. **Keep our updater-first boundary.** TLATER's analysis reinforces that arbitrary Forge/modpack server files are hard to make purely reproducible. Our current compromise—bootstrap artifacts at runtime, then pin updater manifests and reconcile small declared policy—is appropriate. +5. **Potential follow-up improvements:** + - add a documented `GITHUB_TOKEN` environment file/credential path for server bootstrap/update; + - factor duplicated GitHub Actions artifact discovery between server and client; + - optionally expose a small Nix option set for GTNH Daily ports, memory, update schedule, extras/excludes, and SSH server name if this module becomes reusable. diff --git a/docs/gtnh-daily/server.md b/docs/gtnh-daily/server.md new file mode 100644 index 00000000..72ddca10 --- /dev/null +++ b/docs/gtnh-daily/server.md @@ -0,0 +1,101 @@ +# Server architecture + +For definitions of Linux, systemd, Minecraft, and Nix terms used here, see [glossary.md](./glossary.md). For line-by-line implementation reasoning, see [implementation-notes.md](./implementation-notes.md). + +## Identity and paths + +The Daily server is isolated from the stable GTNH server: it has a different directory, service name, Linux user, and network port. That lets the stable server and Daily server coexist without overwriting each other's files. + +Stable server: + +- directory: `/srv/gtnh`; +- service: `gtnh-server.service`; +- port: `25565`. + +Daily server: + +- user/group: `gtnh-daily:gtnh-daily`; +- home/root: `/var/lib/gtnh-daily`; +- server directory: `/var/lib/gtnh-daily/server`; +- backups: `/var/lib/gtnh-daily/backups`; +- updater cache: `/var/lib/gtnh-daily/cache`; +- updater config: `/var/lib/gtnh-daily/config`; +- published manifest: `/var/lib/gtnh-daily/current-manifest.json`; +- published manifest hash: `/var/lib/gtnh-daily/current-manifest.sha256`; +- port: `25566`. + +`/var/lib/gtnh-daily` is traversable/readable enough for SSH clients to fetch the published manifest. “Traversable” means other users can pass through the directory path to read specifically world-readable files such as `current-manifest.json`. Sensitive subdirectories, such as the server files, backups, cache, config, and world data, remain private to `gtnh-daily`. + +## Bootstrap service + +`gtnh-daily-bootstrap.service` is a oneshot service: systemd runs it, it prepares files, and then it exits. It is ordered before `gtnh-daily-server.service` so missing server files are created before Java tries to start Minecraft. + +It is idempotent, meaning it is safe to run repeatedly: + +1. creates root/server/backup/cache/config directories with expected ownership/modes; +2. downloads the latest manifest if no published manifest exists; +3. downloads a non-expired GitHub Actions server artifact if launch files are missing; +4. downloads the matching manifest artifact when artifact bootstrap is used; +5. initializes updater state if `.gtnh-daily-updater.json` is missing; +6. reconciles declared extras/excludes into updater state; +7. writes or edits `eula.txt` so `eula=true`. + +It does not delete worlds or overwrite an already bootstrapped server. It only creates missing bootstrap material and reconciles the small declared updater-state fields. Reconcile means “make these few fields match Nix”; manually added updater extras/excludes will be removed unless they are added to the Nix module. + +## Server service + +`gtnh-daily-server.service` is the actual Minecraft server process. It: + +- requires the stdin FIFO socket; +- wants network-online and bootstrap; +- only starts when `java9args.txt` and `lwjgl3ify-forgePatches.jar` exist; +- runs as `gtnh-daily` in `/var/lib/gtnh-daily/server`; +- uses JDK 25 headless; +- rewrites `server-port=25566` before every start; +- starts Java with `-Xms6G -Xmx10G -XX:+UseZGC -Dfml.readTimeout=180 @java9args.txt -jar lwjgl3ify-forgePatches.jar nogui`; + - `-Xms6G` starts Java with 6 GiB of heap; + - `-Xmx10G` caps Java heap at 10 GiB; + - `-XX:+UseZGC` selects the Z garbage collector; + - `-Dfml.readTimeout=180` gives Forge longer to wait for slow clients; + - `@java9args.txt` loads GTNH's packaged modern-Java arguments; + - `nogui` disables the vanilla server GUI; +- writes stdout/stderr to journald; +- uses a restrictive systemd sandbox with `/var/lib/gtnh-daily` as the writable path. + +## Console stdin socket + +`gtnh-daily-server.socket` creates `/run/gtnh-daily-server.stdin` as a FIFO, also called a named pipe. The service reads standard input from that pipe. This lets admin commands be sent without attaching to the Java process: + +```sh +printf 'say hello\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +printf 'stop\n' | sudo tee /run/gtnh-daily-server.stdin >/dev/null +``` + +`ExecStop` writes `stop`, waits up to 120 seconds, then sends SIGTERM if needed. `stop` asks Minecraft to save and shut down cleanly. SIGTERM is the operating system's polite termination signal. Exit status 143 is treated as successful because SIGTERM can be the final shutdown mechanism. + +## Update service and timer + +`gtnh-daily-update.service`: + +1. takes `/run/gtnh-daily-update.lock` with `flock`; +2. downloads the current Daily manifest to a temp file; +3. records whether the server was active; +4. stops only `gtnh-daily-server.service`; +5. creates a pre-update tar.zst backup; +6. runs `gtnh-daily-updater update --manifest-file ` as `gtnh-daily`; +7. publishes the exact manifest and SHA-256 hash; +8. restarts the server only if it was active before the update. + +The timer runs daily around 05:00 with a 30 minute randomized delay. The random delay avoids every timer on the machine firing at exactly the same second. `Persistent=true` means that if the machine was off at the scheduled time, systemd starts the missed update after the machine comes back. + +## Rollback + +`gtnh-daily-rollback` stops the Daily server, moves the current server directory aside, extracts a selected backup (or the newest pre-update backup), fixes ownership, and starts the server again. Moving the current directory aside means it is renamed to a timestamped path instead of immediately deleted; this leaves material for manual recovery if extraction fails. + +## Firewall + +The NixOS module opens TCP and UDP `25566` for Minecraft. GTNH-Web-Map listens on TCP `8123`; expose that separately only if you intentionally want the browser map reachable, and decide separately whether it should be local-only, LAN-only, VPN-only, or reverse-proxied. + +## World data boundary + +`World/` is mutable and intentionally not recreated by Nix. It contains player builds, inventories, dimensions, chunks, and mod world data. A wiped disk without a world backup yields a fresh/no world according to the bootstrapped server files. A restored world can be placed under `/var/lib/gtnh-daily/server/World` without conflicting with declarative bootstrap, then owned with `sudo chown -R gtnh-daily:gtnh-daily /var/lib/gtnh-daily/server/World`. diff --git a/docs/gtnh-daily/state-and-drift.md b/docs/gtnh-daily/state-and-drift.md new file mode 100644 index 00000000..b9cf39fb --- /dev/null +++ b/docs/gtnh-daily/state-and-drift.md @@ -0,0 +1,90 @@ +# Live state and drift captured + +This file records the important non-world state discovered from the live Daily server and Prism client during the 2026-06 setup/review. It is a snapshot of what was found and why it was declared in Nix. Daily versions will move forward over time, so historical version strings here are evidence, not a requirement to stay on that exact version. + +“Live state” means files that existed on disk before this cleanup. “Drift” means those files differed from what the repo previously declared or documented. + +## Server updater state + +`/var/lib/gtnh-daily/server/.gtnh-daily-updater.json` showed: + +- side: `server`; +- mode: `daily`; +- config version: `2.9.0-nightly-2026-06-13-03` at inspection time; +- excluded mods: `JourneyMap Server`; +- extra mods: + - `GTNH-Web-Map` from `github:GTNewHorizons/GTNH-Web-Map`, match `^gtnh-web-map-.*[0-9]\.jar$`, side `SERVER`; + - `MineMenu` from `modrinth:mine-menu/HNivj4HD`, side `SERVER`. + +These entries are now declared in `nix/modules/gtnh-daily-server.nix` and reconciled by bootstrap/update service pre-start logic. + +## Client updater state + +`~/.local/share/PrismLauncher/instances/GT New Horizons (Daily)/.gtnh-daily-updater.json` showed: + +- side: `client`; +- mode: `daily`; +- extra mods: + - `JourneyMap` from `github:TeamJM/journeymap-legacy`, match `unlimited\.jar$`, side `CLIENT`; + - `MineMenu` from `modrinth:mine-menu/HNivj4HD`, side `CLIENT`. + +These entries are now declared in `nix/modules/prismlauncher.nix` and reconciled by `gtnh-daily-client-bootstrap`. + +## Mod jar difference highlights + +Server-only jars included: + +- `gtnh-web-map-0.4-beta-1.jar` — declared as the `GTNH-Web-Map` extra; +- `Morpheus-1.7.10-1.6.21.jar` — a server-side sleep/vote helper found in the pack/server state. + +Client-only jars included expected client-side/render/UI mods such as: + +- Angelica — rendering/performance/shader support; +- CraftPresence — Discord rich presence; +- JourneyMap Unlimited — minimap/world-map client; +- MouseTweaks — inventory mouse interaction helper; +- Schematica — client-side build schematic helper; +- ToroHealth — client-side health display; +- resource/loading helpers and other client-only GTNH components. + +An “ad hoc jar copy” means manually dropping a jar into `mods/` without telling the updater. The updater state, not manual jar copies, is the source of truth for extra/excluded managed mods. + +## Resource and shader packs + +Client selected resource packs were read from: + +```text +~/.local/share/PrismLauncher/instances/GT New Horizons (Daily)/.minecraft/options.txt +``` + +The `resourcePacks:[...]` line is Minecraft's syntax for the enabled resource-pack list. The names in the list are directory names inside `.minecraft/resourcepacks`: + +```text +resourcePacks:["AE2-Dark-Mode.v.1.18","shadowui","Modernity-GTNH-main"] +``` + +Custom resource pack/shader pack state captured into declarative config: + +- `AE2-Dark-Mode.v.1.18.zip` URL/hash; +- `Shadow.UI.v5.30-Modernity.version.zip` URL/hash; +- `Modernity-GTNH-main.zip` URL/hash; +- `ComplementaryReimagined_r5.8.1.zip` URL/hash; +- `ComplementaryUnbound_r5.8.1.zip` URL/hash; +- `ComplementaryUnbound_r5.8.1.zip.txt` text contents. + +The large zip files are intentionally not tracked in git; bootstrap downloads and verifies them. + +## Config drift + +The updater tracks config with `.gtnh-configs`. Both server and client had many config differences relative to their tracked pack refs. That means files under `config/` differed from the updater's remembered pack baseline. + +Those differences can be generated/new config files from mods, migrated pack config changes, or local edits. Copying the entire config tree into Nix would freeze lots of runtime noise and make future pack config merges harder. For reproducibility, we rely on the updater's config-git merge model plus declared extras/excludes/bootstrap rather than copying the entire mutable config tree into Nix. + +The explicit config policy we currently enforce in Nix is: + +- server port is rewritten to `25566` on every start; +- EULA is accepted during bootstrap; +- updater extras/excludes are reconciled; +- client resource pack selection is set in `options.txt` when that file exists. + +World data, logs, generated maps, backups, caches, and runtime/player files were intentionally excluded. diff --git a/flake.lock b/flake.lock index a3a9b7c2..9f167012 100644 --- a/flake.lock +++ b/flake.lock @@ -205,6 +205,22 @@ "type": "github" } }, + "gtnh-daily-updater": { + "flake": false, + "locked": { + "lastModified": 1780928507, + "narHash": "sha256-4GxAYwfR6UEk+7wqOVhe1i55Wtfg1Zrx9RlfBvtPFkI=", + "owner": "Caedis", + "repo": "gtnh-daily-updater", + "rev": "dd84d7a5b84d468fb1cbfea261dbc97d8115666c", + "type": "github" + }, + "original": { + "owner": "Caedis", + "repo": "gtnh-daily-updater", + "type": "github" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -903,6 +919,7 @@ "root": { "inputs": { "flake-parts": "flake-parts", + "gtnh-daily-updater": "gtnh-daily-updater", "home-manager": "home-manager", "homebrew-cask": "homebrew-cask", "homebrew-core": "homebrew-core", diff --git a/flake.nix b/flake.nix index 3d0e4908..cc935b7f 100644 --- a/flake.nix +++ b/flake.nix @@ -8,6 +8,10 @@ flake-parts.url = "github:hercules-ci/flake-parts"; home-manager.inputs.nixpkgs.follows = "nixpkgs"; home-manager.url = "github:nix-community/home-manager"; + gtnh-daily-updater = { + url = "github:Caedis/gtnh-daily-updater"; + flake = false; + }; waybar-peek = { url = "github:rrvsh/waybar_peek"; inputs.nixpkgs.follows = "nixpkgs"; diff --git a/nix/hosts.nix b/nix/hosts.nix index 6465a4dc..ecd64fc8 100644 --- a/nix/hosts.nix +++ b/nix/hosts.nix @@ -76,8 +76,7 @@ in cfg.modules.nixos.steam cfg.modules.nixos.prismlauncher cfg.modules.nixos.daily-midnight-poweroff - cfg.modules.nixos.gtnh-server - cfg.modules.nixos.gtnh-backups + cfg.modules.nixos.gtnh-daily-server ( { config, diff --git a/nix/modules/gtnh-backups.nix b/nix/modules/gtnh-backups.nix deleted file mode 100644 index d18946d3..00000000 --- a/nix/modules/gtnh-backups.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ - config.flake.modules.nixos.gtnh-backups = - { pkgs, ... }: - let - backupDir = "/srv/gtnh-backups"; - in - { - systemd = { - tmpfiles.rules = [ "d ${backupDir} 0750 root root -" ]; - services.gtnh-backup = { - description = "Backup GTNH server"; - serviceConfig = { - Type = "oneshot"; - ExecStart = pkgs.writeShellScript "gtnh-backup" '' - set -euo pipefail - - ts="$(${pkgs.coreutils}/bin/date +%Y%m%d-%H%M%S)" - dest="${backupDir}/gtnh-$ts.tar.zst" - - echo "say Starting server backup" > /run/gtnh-server.stdin || true - echo "save-all" > /run/gtnh-server.stdin || true - ${pkgs.coreutils}/bin/sleep 10 - - ${pkgs.gnutar}/bin/tar \ - --exclude='/srv/gtnh/backups' \ - -C /srv \ - -I '${pkgs.zstd}/bin/zstd -T0 -10' \ - -cf "$dest" \ - gtnh - - ${pkgs.findutils}/bin/find ${backupDir} \ - -name 'gtnh-*.tar.zst' \ - -type f \ - -mtime +14 \ - -delete - - echo "say Server backup complete" > /run/gtnh-server.stdin || true - ''; - }; - }; - timers.gtnh-backup = { - wantedBy = [ "timers.target" ]; - timerConfig = { - OnCalendar = "03:30"; - Persistent = true; - }; - }; - }; - }; -} diff --git a/nix/modules/gtnh-daily-server.nix b/nix/modules/gtnh-daily-server.nix new file mode 100644 index 00000000..ac2eeb21 --- /dev/null +++ b/nix/modules/gtnh-daily-server.nix @@ -0,0 +1,487 @@ +# NixOS module for the reproducible GTNH Daily dedicated server. +# Plain-language docs live in docs/gtnh-daily/; implementation-notes.md explains the Nix, +# shell, Python, systemd, manifest, and backup choices in this file. +# This module defines identity, directories, bootstrap, update, rollback, service sandboxing, +# stdin control, firewall, and declared updater extras/excludes while intentionally excluding +# mutable world data from the declarative guarantee. +{ inputs, ... }: +{ + config.flake.modules.nixos.gtnh-daily-server = + { pkgs, ... }: + let + # Dedicated Linux user/group isolates Daily from the stable `/srv/gtnh` server. + user = "gtnh-daily"; + group = user; + unit = "gtnh-daily-server"; + # Root must be traversable so clients can SSH-read the published manifest files. + rootDir = "/var/lib/gtnh-daily"; + serverDir = "${rootDir}/server"; + backupDir = "${rootDir}/backups"; + cacheDir = "${rootDir}/cache"; + configDir = "${rootDir}/config"; + stdin = "/run/${unit}.stdin"; + lockFile = "/run/gtnh-daily-update.lock"; + # Latest Daily manifest URL used for server bootstrap/update discovery. + manifestUrl = "https://raw.githubusercontent.com/GTNewHorizons/DreamAssemblerXXL/master/releases/manifests/daily.json"; + currentManifest = "${rootDir}/current-manifest.json"; + currentManifestHash = "${rootDir}/current-manifest.sha256"; + # Daily listens on 25566 so it can coexist with the stable server on 25565. + port = 25566; + java = pkgs.jdk25_headless; + updater = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.gtnh-daily-updater; + tar = "${pkgs.gnutar}/bin/tar --use-compress-program=${pkgs.zstd}/bin/zstd --xattrs --acls"; + updaterEnv = "HOME=${rootDir} XDG_CACHE_HOME=${cacheDir} XDG_CONFIG_HOME=${configDir} PATH=${pkgs.git}/bin"; + # Desired server updater policy. These fields are owned by Nix: manual edits to + # `exclude_mods`/`extra_mods` are overwritten unless they are added here. + serverExcludeMods = [ "JourneyMap Server" ]; + serverExtraMods = { + GTNH-Web-Map = { + source = "github:GTNewHorizons/GTNH-Web-Map"; + side = "SERVER"; + match = "^gtnh-web-map-.*[0-9]\\.jar$"; + }; + MineMenu = { + source = "modrinth:mine-menu/HNivj4HD"; + side = "SERVER"; + }; + }; + serverStateJson = pkgs.writeText "gtnh-daily-server-state.json" ( + builtins.toJSON { + exclude_mods = serverExcludeMods; + extra_mods = serverExtraMods; + } + ); + # Reconcile only declared updater-state knobs; leave scanned mods, world data, logs, and configs alone. + reconcileState = pkgs.writeShellScript "gtnh-daily-server-reconcile-state" '' + # Fail on any JSON/IO error so services do not run with partially-written updater state. + set -euo pipefail + state=${serverDir}/.gtnh-daily-updater.json + [ -f "$state" ] || exit 0 + ${pkgs.python3}/bin/python3 - "$state" ${serverStateJson} <<'PY' + import json, sys + state_path, desired_path = sys.argv[1:] + with open(state_path) as f: + state = json.load(f) + with open(desired_path) as f: + desired = json.load(f) + changed = False + if state.get("exclude_mods") != desired["exclude_mods"]: + state["exclude_mods"] = desired["exclude_mods"] + changed = True + if state.get("extra_mods") != desired["extra_mods"]: + state["extra_mods"] = desired["extra_mods"] + changed = True + if changed: + with open(state_path, "w") as f: + json.dump(state, f, indent=2) + f.write("\n") + PY + ${pkgs.coreutils}/bin/chown ${user}:${group} "$state" + ''; + # Bootstrap creates a missing server from Daily artifacts and prepares updater/EULA state. + # It does not overwrite an existing bootstrapped server or delete World/. + bootstrapScript = pkgs.writeShellScript "gtnh-daily-bootstrap" '' + # Stop immediately on errors because a half-bootstrapped modded server is unsafe to start. + set -euo pipefail + ${pkgs.coreutils}/bin/install -d -o ${user} -g ${group} -m 0755 ${rootDir} + ${pkgs.coreutils}/bin/install -d -o ${user} -g ${group} -m 0750 ${serverDir} ${backupDir} ${cacheDir} ${configDir} + manifest="${currentManifest}" + # Fetch the latest published manifest if the server has not yet published an applied manifest. + if [ ! -s "$manifest" ]; then + tmp_manifest="$(${pkgs.coreutils}/bin/mktemp --tmpdir gtnh-daily-bootstrap-manifest.XXXXXX.json)" + ${pkgs.curl}/bin/curl --fail --location --silent --show-error ${manifestUrl} --output "$tmp_manifest" + ${pkgs.coreutils}/bin/install -o ${user} -g ${group} -m 0644 "$tmp_manifest" "$manifest" + ${pkgs.coreutils}/bin/sha256sum "$manifest" > ${currentManifestHash} + ${pkgs.coreutils}/bin/chown ${user}:${group} ${currentManifestHash} + ${pkgs.coreutils}/bin/rm -f "$tmp_manifest" + fi + # Download/extract a server artifact only when core launch files are absent. + if [ ! -e ${serverDir}/java9args.txt ] || [ ! -e ${serverDir}/lwjgl3ify-forgePatches.jar ]; then + tmp="$(${pkgs.coreutils}/bin/mktemp -d --tmpdir gtnh-daily-bootstrap.XXXXXX)" + cleanup() { ${pkgs.coreutils}/bin/rm -rf "$tmp"; } + trap cleanup EXIT + auth=() + if [ -n "''${GITHUB_TOKEN:-}" ]; then + auth=(-H "Authorization: Bearer ''${GITHUB_TOKEN}") + fi + runs_url='https://api.github.com/repos/GTNewHorizons/DreamAssemblerXXL/actions/workflows/daily-modpack-build.yml/runs?status=success&per_page=20' + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "$runs_url" -o "$tmp/runs.json" + ${pkgs.python3}/bin/python3 - "$tmp/runs.json" > "$tmp/run_ids" <<'PY' + import json, sys + for run in json.load(open(sys.argv[1])).get("workflow_runs", []): + print(run["id"]) + PY + server_url="" + manifest_url="" + while read -r run_id; do + [ -n "$run_id" ] || continue + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "https://api.github.com/repos/GTNewHorizons/DreamAssemblerXXL/actions/runs/$run_id/artifacts" -o "$tmp/artifacts.json" + eval "$(${pkgs.python3}/bin/python3 - "$tmp/artifacts.json" <<'PY' + import json, shlex, sys + server = manifest = "" + for artifact in json.load(open(sys.argv[1])).get("artifacts", []): + if artifact.get("expired"): + continue + name = artifact.get("name", "") + if "server-java17-25.zip" in name: + server = artifact["archive_download_url"] + elif name.startswith("gtnh-daily-") and name.endswith("-manifest.json"): + manifest = artifact["archive_download_url"] + print("server_url=" + shlex.quote(server)) + print("manifest_url=" + shlex.quote(manifest)) + PY + )" + if [ -n "$server_url" ] && [ -n "$manifest_url" ]; then + break + fi + done < "$tmp/run_ids" + if [ -z "$server_url" ] || [ -z "$manifest_url" ]; then + echo "Could not find non-expired GTNH Daily server artifacts. Set GITHUB_TOKEN if GitHub requires authentication." >&2 + exit 1 + fi + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "$server_url" -o "$tmp/server-artifact.zip" + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "$manifest_url" -o "$tmp/manifest-artifact.zip" + ${pkgs.unzip}/bin/unzip -q "$tmp/server-artifact.zip" -d "$tmp/server" + ${pkgs.unzip}/bin/unzip -q "$tmp/manifest-artifact.zip" -d "$tmp/manifest" + ${pkgs.coreutils}/bin/cp -a "$tmp/server/." ${serverDir}/ + found_manifest="$(${pkgs.findutils}/bin/find "$tmp/manifest" -type f -name '*.json' | ${pkgs.coreutils}/bin/head -n1)" + if [ -n "$found_manifest" ]; then + ${pkgs.coreutils}/bin/install -o ${user} -g ${group} -m 0644 "$found_manifest" "$manifest" + ${pkgs.coreutils}/bin/sha256sum "$manifest" > ${currentManifestHash} + ${pkgs.coreutils}/bin/chown ${user}:${group} ${currentManifestHash} + fi + ${pkgs.coreutils}/bin/chown -R ${user}:${group} ${serverDir} + fi + # Initialize updater state after files exist; init scans mods and config baselines. + if [ ! -f ${serverDir}/.gtnh-daily-updater.json ]; then + config_version="$(${pkgs.python3}/bin/python3 - ${currentManifest} <<'PY' + import json, sys + print(json.load(open(sys.argv[1]))["config"]) + PY + )" + ${pkgs.util-linux}/bin/runuser -u ${user} -- env ${updaterEnv} ${updater}/bin/gtnh-daily-updater init --instance-dir ${serverDir} --side server --config "$config_version" + fi + ${reconcileState} + if [ -f ${serverDir}/eula.txt ]; then + ${pkgs.gnused}/bin/sed -i 's/^eula=false/eula=true/' ${serverDir}/eula.txt + else + printf 'eula=true\n' > ${serverDir}/eula.txt + fi + ${pkgs.coreutils}/bin/chown ${user}:${group} ${serverDir}/eula.txt + ''; + stopScript = pkgs.writeShellScript "${unit}-stop" '' + set -euo pipefail + if [ -p ${stdin} ]; then + echo stop > ${stdin} || true + fi + timeout=120 + while kill -0 "$1" 2>/dev/null && [ "$timeout" -gt 0 ]; do + sleep 1 + timeout=$((timeout - 1)) + done + if kill -0 "$1" 2>/dev/null; then + kill -TERM "$1" + fi + ''; + backupScript = pkgs.writeShellScript "gtnh-daily-backup" '' + set -euo pipefail + if [ ! -d ${serverDir} ]; then + echo "${serverDir} does not exist" >&2 + exit 1 + fi + ${pkgs.coreutils}/bin/mkdir -p ${backupDir} + ts="$(${pkgs.coreutils}/bin/date -u +%Y%m%d-%H%M%S)" + dest="${backupDir}/pre-update-$ts.tar.zst" + ${tar} -C ${rootDir} -cf "$dest" server + ${pkgs.coreutils}/bin/chown ${user}:${group} "$dest" + ${pkgs.coreutils}/bin/ln -sfn "$(${pkgs.coreutils}/bin/basename "$dest")" ${backupDir}/latest.tar.zst + echo "$dest" + ''; + # Update applies a pinned manifest, backs up first, and republishes the exact applied manifest for clients. + updateScript = pkgs.writeShellScript "gtnh-daily-update" '' + # Treat every failure as fatal so backup/update/publish steps do not silently diverge. + set -euo pipefail + exec 9>${lockFile} + ${pkgs.util-linux}/bin/flock -n 9 + manifest="$(${pkgs.coreutils}/bin/mktemp --tmpdir gtnh-daily-manifest.XXXXXX.json)" + cleanup() { + ${pkgs.coreutils}/bin/rm -f "$manifest" + } + trap cleanup EXIT + ${pkgs.curl}/bin/curl --fail --location --silent --show-error ${manifestUrl} --output "$manifest" + ${pkgs.coreutils}/bin/chmod 0644 "$manifest" + was_active=0 + if ${pkgs.systemd}/bin/systemctl is-active --quiet ${unit}.service; then + was_active=1 + fi + ${pkgs.systemd}/bin/systemctl stop ${unit}.service || true + restart_if_needed() { + if [ "$was_active" = 1 ]; then + ${pkgs.systemd}/bin/systemctl start ${unit}.service || true + fi + } + finish() { + cleanup + restart_if_needed + } + trap finish EXIT + ${backupScript} + ${pkgs.util-linux}/bin/runuser -u ${user} -- \ + env ${updaterEnv} \ + ${updater}/bin/gtnh-daily-updater update --instance-dir ${serverDir} --manifest-file "$manifest" + ${pkgs.coreutils}/bin/install -o ${user} -g ${group} -m 0644 "$manifest" ${currentManifest} + ${pkgs.coreutils}/bin/sha256sum ${currentManifest} > ${currentManifestHash} + ${pkgs.coreutils}/bin/chown ${user}:${group} ${currentManifestHash} + restart_if_needed + cleanup + trap - EXIT + ''; + # Rollback restores the latest or selected pre-update backup and restarts the Daily server. + rollback = pkgs.writeShellScriptBin "gtnh-daily-rollback" '' + set -euo pipefail + if [ "$(${pkgs.coreutils}/bin/id -u)" != 0 ]; then + echo "Run as root: sudo gtnh-daily-rollback [backup.tar.zst]" >&2 + exit 1 + fi + backup="''${1:-}" + if [ -z "$backup" ]; then + backup="$(${pkgs.findutils}/bin/find ${backupDir} -maxdepth 1 -type f -name 'pre-update-*.tar.zst' -printf '%T@ %p\n' | ${pkgs.coreutils}/bin/sort -nr | ${pkgs.gawk}/bin/awk 'NR == 1 { print $2 }')" + fi + if [ -z "$backup" ] || [ ! -f "$backup" ]; then + echo "No backup found under ${backupDir}" >&2 + exit 1 + fi + ts="$(${pkgs.coreutils}/bin/date -u +%Y%m%d-%H%M%S)" + ${pkgs.systemd}/bin/systemctl stop ${unit}.service || true + if [ -e ${serverDir} ]; then + ${pkgs.coreutils}/bin/mv ${serverDir} ${rootDir}/server.rollback-$ts + fi + ${tar} -C ${rootDir} -xf "$backup" + ${pkgs.coreutils}/bin/chown -R ${user}:${group} ${serverDir} + ${pkgs.systemd}/bin/systemctl start ${unit}.service + echo "Rolled back ${serverDir} from $backup" + ''; + # Restore only the active Minecraft world from the latest or selected FTBUtilities/ServerUtilities zip backup. + ftbuRollback = pkgs.writeShellScriptBin "gtnh-daily-ftbu-rollback" '' + set -euo pipefail + if [ "$(${pkgs.coreutils}/bin/id -u)" != 0 ]; then + echo "Run as root: sudo gtnh-daily-ftbu-rollback [backup.zip]" >&2 + exit 1 + fi + if [ "$#" -gt 1 ]; then + echo "Usage: sudo gtnh-daily-ftbu-rollback [backup.zip]" >&2 + exit 1 + fi + backup="''${1:-}" + if [ -z "$backup" ]; then + backup="$(${pkgs.findutils}/bin/find ${serverDir}/backups -maxdepth 1 -type f -name '*.zip' -printf '%T@ %p\n' | ${pkgs.coreutils}/bin/sort -nr | ${pkgs.gawk}/bin/awk 'NR == 1 { print $2 }')" + fi + if [ -z "$backup" ] || [ ! -f "$backup" ]; then + echo "No FTBUtilities/ServerUtilities zip backup found under ${serverDir}/backups" >&2 + exit 1 + fi + world="$(${pkgs.gawk}/bin/awk -F= '$1 == "level-name" { print $2 }' ${serverDir}/server.properties)" + if [ -z "$world" ]; then + echo "Could not read level-name from ${serverDir}/server.properties" >&2 + exit 1 + fi + ts="$(${pkgs.coreutils}/bin/date -u +%Y%m%d-%H%M%S)" + work="$(${pkgs.coreutils}/bin/mktemp -d --tmpdir gtnh-daily-ftbu-rollback.XXXXXX)" + cleanup() { + ${pkgs.coreutils}/bin/rm -rf "$work" + } + trap cleanup EXIT + ${pkgs.python3}/bin/python3 - "$backup" "$work" "$world" <<'PY' + import os + import sys + import zipfile + backup, work, world = sys.argv[1:] + with zipfile.ZipFile(backup) as z: + top_level = sorted({name.split('/', 1)[0] for name in z.namelist() if name and not name.startswith('/')}) + z.extractall(work) + if not os.path.isfile(os.path.join(work, world, 'level.dat')): + found = [] + for root, _, files in os.walk(work): + if 'level.dat' in files: + found.append(os.path.relpath(root, work)) + print(f"Backup {backup} does not contain active world {world!r} with level.dat", file=sys.stderr) + if found: + print('Worlds found in backup:', file=sys.stderr) + for path in sorted(found): + print(f" {path}", file=sys.stderr) + sys.exit(1) + with open(os.path.join(work, 'top-level'), 'w') as f: + for name in top_level: + if name: + f.write(name + '\n') + PY + ${pkgs.systemd}/bin/systemctl stop ${unit}.service || true + while IFS= read -r path; do + [ -n "$path" ] || continue + if [ -e "${serverDir}/$path" ]; then + ${pkgs.coreutils}/bin/mv "${serverDir}/$path" "${serverDir}/$path.pre-ftbu-restore-$ts" + fi + parent="$(${pkgs.coreutils}/bin/dirname "${serverDir}/$path")" + ${pkgs.coreutils}/bin/mkdir -p "$parent" + ${pkgs.coreutils}/bin/mv "$work/$path" "${serverDir}/$path" + ${pkgs.coreutils}/bin/chown -R ${user}:${group} "${serverDir}/$path" + echo "Restored ${serverDir}/$path from $backup" + done < "$work/top-level" + ${pkgs.systemd}/bin/systemctl start ${unit}.service + echo "Previous restored paths were moved aside with suffix .pre-ftbu-restore-$ts" + ''; + in + { + # System user/group declaration makes ownership reproducible across rebuilds. + users = { + users.${user} = { + description = "GT New Horizons daily server user"; + isSystemUser = true; + inherit group; + home = rootDir; + homeMode = "0755"; + createHome = true; + }; + groups.${group} = { }; + }; + environment.systemPackages = [ + rollback + ftbuRollback + ]; + systemd = { + # Tmpfiles enforces directory existence/modes without overwriting mutable contents. + tmpfiles.rules = [ + "d ${rootDir} 0755 ${user} ${group} -" + "d ${serverDir} 0750 ${user} ${group} -" + "d ${backupDir} 0750 ${user} ${group} -" + "d ${cacheDir} 0750 ${user} ${group} -" + "d ${configDir} 0750 ${user} ${group} -" + ]; + # FIFO socket provides a safe stdin path for sending Minecraft console commands via systemd. + sockets.${unit} = { + bindsTo = [ "${unit}.service" ]; + socketConfig = { + ListenFIFO = stdin; + SocketMode = "0660"; + SocketUser = user; + SocketGroup = group; + RemoveOnStop = true; + FlushPending = true; + }; + }; + services = { + # Bootstrap is a oneshot dependency of the main server and is safe to rerun. + gtnh-daily-bootstrap = { + description = "Bootstrap GT New Horizons daily server declaratively"; + wantedBy = [ "multi-user.target" ]; + before = [ "${unit}.service" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = bootstrapScript; + }; + }; + # Main dedicated server JVM service. + ${unit} = { + description = "GT New Horizons daily server"; + wantedBy = [ "multi-user.target" ]; + requires = [ + "${unit}.socket" + "gtnh-daily-bootstrap.service" + ]; + after = [ + "network-online.target" + "${unit}.socket" + "gtnh-daily-bootstrap.service" + ]; + wants = [ + "network-online.target" + "gtnh-daily-bootstrap.service" + ]; + unitConfig.ConditionPathExists = [ + "${serverDir}/java9args.txt" + "${serverDir}/lwjgl3ify-forgePatches.jar" + ]; + path = [ + java + pkgs.bash + pkgs.coreutils + pkgs.gnused + ]; + preStart = '' + if [ -f server.properties ]; then + if grep -q '^server-port=' server.properties; then + sed -i 's/^server-port=.*/server-port=${toString port}/' server.properties + else + printf '\nserver-port=${toString port}\n' >> server.properties + fi + fi + ''; + serviceConfig = { + User = user; + Group = group; + WorkingDirectory = serverDir; + Restart = "on-failure"; + RestartSec = "30s"; + SuccessExitStatus = "0 143"; + StandardInput = "socket"; + StandardOutput = "journal"; + StandardError = "journal"; + ExecStart = '' + ${java}/bin/java \ + -Xms6G \ + -Xmx10G \ + -XX:+UseZGC \ + -Dfml.readTimeout=180 \ + -Dfml.queryResult=confirm \ + @java9args.txt \ + -jar lwjgl3ify-forgePatches.jar \ + nogui + ''; + ExecStop = "${stopScript} $MAINPID"; + UMask = "0027"; + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ReadWritePaths = [ rootDir ]; + ProtectHome = true; + }; + }; + # Scheduled updater service; it reconciles desired updater state before applying Daily updates. + gtnh-daily-update = { + description = "Update GT New Horizons daily server"; + after = [ + "network-online.target" + "gtnh-daily-bootstrap.service" + ]; + wants = [ + "network-online.target" + "gtnh-daily-bootstrap.service" + ]; + unitConfig.ConditionPathExists = [ "${serverDir}/.gtnh-daily-updater.json" ]; + serviceConfig = { + Type = "oneshot"; + ExecStartPre = reconcileState; + ExecStart = updateScript; + }; + }; + }; + # Timer schedules Daily updates around 05:00 with jitter and catch-up behavior. + timers.gtnh-daily-update = { + description = "Update GT New Horizons daily server on schedule"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 05:00:00"; + Persistent = true; + RandomizedDelaySec = "30m"; + }; + }; + }; + # Open only the Minecraft Daily port here; web-map exposure is handled separately if desired. + networking.firewall = { + allowedTCPPorts = [ port ]; + allowedUDPPorts = [ port ]; + }; + }; +} diff --git a/nix/modules/gtnh-server.nix b/nix/modules/gtnh-server.nix deleted file mode 100644 index b53f7372..00000000 --- a/nix/modules/gtnh-server.nix +++ /dev/null @@ -1,102 +0,0 @@ -{ - config.flake.modules.nixos.gtnh-server = - { pkgs, ... }: - let - gtnhDir = "/srv/gtnh"; - java = pkgs.jdk25_headless; - stopScript = pkgs.writeShellScript "gtnh-stop" '' - set -euo pipefail - - if [ -p /run/gtnh-server.stdin ]; then - echo "stop" > /run/gtnh-server.stdin || true - fi - - timeout=120 - while kill -0 "$1" 2>/dev/null && [ "$timeout" -gt 0 ]; do - sleep 1 - timeout=$((timeout - 1)) - done - - if kill -0 "$1" 2>/dev/null; then - kill -TERM "$1" - fi - ''; - in - { - users = { - users.gtnh = { - description = "GT New Horizons server user"; - isSystemUser = true; - group = "gtnh"; - home = gtnhDir; - createHome = true; - }; - groups.gtnh = { }; - }; - systemd = { - tmpfiles.rules = [ "d ${gtnhDir} 0750 gtnh gtnh -" ]; - sockets.gtnh-server = { - bindsTo = [ "gtnh-server.service" ]; - socketConfig = { - ListenFIFO = "/run/gtnh-server.stdin"; - SocketMode = "0660"; - SocketUser = "gtnh"; - SocketGroup = "gtnh"; - RemoveOnStop = true; - FlushPending = true; - }; - }; - services.gtnh-server = { - description = "GT New Horizons 2.8.4 Server"; - wantedBy = [ "multi-user.target" ]; - requires = [ "gtnh-server.socket" ]; - after = [ - "network-online.target" - "gtnh-server.socket" - ]; - wants = [ "network-online.target" ]; - unitConfig.ConditionPathExists = [ - "${gtnhDir}/java9args.txt" - "${gtnhDir}/lwjgl3ify-forgePatches.jar" - ]; - path = [ - java - pkgs.bash - pkgs.coreutils - ]; - serviceConfig = { - User = "gtnh"; - Group = "gtnh"; - WorkingDirectory = gtnhDir; - Restart = "on-failure"; - RestartSec = "30s"; - SuccessExitStatus = "0 143"; - StandardInput = "socket"; - StandardOutput = "journal"; - StandardError = "journal"; - ExecStart = '' - ${java}/bin/java \ - -Xms6G \ - -Xmx10G \ - -XX:+UseZGC \ - -Dfml.readTimeout=180 \ - @java9args.txt \ - -jar lwjgl3ify-forgePatches.jar \ - nogui - ''; - ExecStop = "${stopScript} $MAINPID"; - UMask = "0027"; - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ReadWritePaths = [ gtnhDir ]; - ProtectHome = true; - }; - }; - }; - networking.firewall = { - allowedTCPPorts = [ 25565 ]; - allowedUDPPorts = [ 25565 ]; - }; - }; -} diff --git a/nix/modules/prismlauncher.nix b/nix/modules/prismlauncher.nix index 88c01688..b4abb9ce 100644 --- a/nix/modules/prismlauncher.nix +++ b/nix/modules/prismlauncher.nix @@ -1,7 +1,15 @@ -{ config, ... }: +# Prism Launcher and GTNH Daily client module. +# Plain-language docs live in docs/gtnh-daily/; implementation-notes.md explains the Nix, +# shell, Python, systemd-user, manifest, resource-pack, and shader-pack choices in this file. +# Every declaration below exists to make the launcher, client updater state, resource packs, +# shader packs, desktop entries, and user sync timer reproducible after a wipe/rebuild. +{ config, inputs, ... }: let + # Flake-parts module namespace used to wire platform modules to the Home Manager module. cfg = config.flake; + # Shared OS wrapper: NixOS/Darwin import this and receive the Home Manager Prism module. osModule = { + # Inject Prism configuration into Home Manager rather than duplicating it per host OS. home-manager.sharedModules = [ cfg.modules.homeManager.prismlauncher ]; }; in @@ -10,18 +18,345 @@ in darwin.prismlauncher = osModule; nixos.prismlauncher = osModule; homeManager.prismlauncher = - { config, pkgs, ... }: { - xdg.desktopEntries.gtnh = { - name = "GregTech New Horizons"; - icon = "${config.home.homeDirectory}/.local/share/PrismLauncher/instances/GT_New_Horizons_2.8.4_Java_17-25/icon.png"; - exec = "prismlauncher --launch GT_New_Horizons_2.8.4_Java_17-25"; + config, + lib, + pkgs, + ... + }: + let + # Exact Prism instance name used by launcher metadata, desktop entries, and sync scripts. + gtnhDailyInstanceName = "GT New Horizons (Daily)"; + gtnhDailyInstanceDir = "${config.home.homeDirectory}/.local/share/PrismLauncher/instances/${gtnhDailyInstanceName}"; + # Repo-built updater includes our local manifest pinning patch. + gtnhDailyUpdater = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.gtnh-daily-updater; + # Resource/shader pack artifacts are downloaded at bootstrap time instead of committed to git. + # Updating a pack means changing its URL/name/hash together; bootstrap verifies the hash before use. + clientAssetsJson = pkgs.writeText "gtnh-daily-client-assets.json" ( + builtins.toJSON { + resourcePacks = [ + { + name = "AE2-Dark-Mode.v.1.18.zip"; + url = "https://github.com/Ranzuu/AE2-Dark-Mode/releases/download/v.1.18/AE2-Dark-Mode.v.1.18.zip"; + sha256 = "d8521075c02fecaad6b11f6c9766da7b195ae5d24497da280f755da0a52e23b0"; + extractDir = "AE2-Dark-Mode.v.1.18"; + stripRoot = false; + } + { + name = "Shadow.UI.v5.30-Modernity.version.zip"; + url = "https://github.com/Ranzuu/Shadow-UI/releases/download/v2.9.X/v5.30/Shadow.UI.v5.30-Modernity.version.zip"; + sha256 = "a1a3dd250f42a3ce4c904cb1fef1f6bec313c23869e6591be1fd957a10cc9655"; + extractDir = "shadowui"; + stripRoot = false; + } + { + name = "Modernity-GTNH-main.zip"; + url = "https://github.com/ModernityGTNH/Modernity-GTNH/archive/c3cd734cf5b912debdbcf75b9a88509d19f8fdfa.zip"; + sha256 = "d629e5b6022b208ef3d5707ad95712a7e4d5ff516e5151786fae051d232e6213"; + extractDir = "Modernity-GTNH-main"; + stripRoot = true; + } + ]; + shaderPacks = [ + { + name = "ComplementaryReimagined_r5.8.1.zip"; + url = "https://cdn.modrinth.com/data/HVnmMxH1/versions/yCCduG44/ComplementaryReimagined_r5.8.1.zip"; + sha256 = "3f1cd389e717b2e62f58edff222059b9c60de71b14bb49b517eb58318ce35b15"; + } + { + name = "ComplementaryUnbound_r5.8.1.zip"; + url = "https://cdn.modrinth.com/data/R6NEzAwj/versions/VMHXIk50/ComplementaryUnbound_r5.8.1.zip"; + sha256 = "bb89b1fc54687d4147a837fb2e3c3f7261a13bee51819761e9b6a91cb7915965"; + } + ]; + } + ); + # Desired client updater policy. This field is owned by Nix: manual edits to + # `extra_mods` are overwritten unless they are added here. + clientStateJson = pkgs.writeText "gtnh-daily-client-state.json" ( + builtins.toJSON { + extra_mods = { + JourneyMap = { + source = "github:TeamJM/journeymap-legacy"; + side = "CLIENT"; + match = "unlimited\\.jar$"; + }; + MineMenu = { + source = "modrinth:mine-menu/HNivj4HD"; + side = "CLIENT"; + }; + }; + } + ); + # Bootstrap creates/repairs the Prism instance, updater state, packs, and selected resource packs. + # It refuses to run while Prism/Minecraft appears active to avoid changing files under a running game. + gtnhDailyClientBootstrap = pkgs.writeShellScriptBin "gtnh-daily-client-bootstrap" '' + set -euo pipefail + + server="''${GTNH_DAILY_SERVER:-nemesis}" + remote_manifest="/var/lib/gtnh-daily/current-manifest.json" + remote_hash="/var/lib/gtnh-daily/current-manifest.sha256" + instance_dir="${gtnhDailyInstanceDir}" + game_dir="$instance_dir/.minecraft" + state_file="$instance_dir/.gtnh-daily-updater.json" + cache_dir="''${XDG_CACHE_HOME:-$HOME/.cache}/gtnh-daily-client-sync" + local_manifest="$cache_dir/current-manifest.json" + local_hash="$cache_dir/current-manifest.sha256" + + # Create local cache and Prism instance parent directories before network/bootstrap work. + mkdir -p "$cache_dir" "$HOME/.local/share/PrismLauncher/instances" + if ${pkgs.procps}/bin/pgrep -u "$(${pkgs.coreutils}/bin/id -u)" -f 'prismlauncher|net\.minecraft|minecraft|lwjgl3ify' >/dev/null; then + echo "Prism or Minecraft appears to be running; refusing to bootstrap the client instance." >&2 + exit 1 + fi + fetched_hash="$(${pkgs.openssh}/bin/ssh -o BatchMode=yes "$server" "cat '$remote_hash'")" + ${pkgs.openssh}/bin/scp -q -o BatchMode=yes "$server:$remote_manifest" "$local_manifest" + expected_hash="$(printf '%s\n' "$fetched_hash" | ${pkgs.gawk}/bin/awk '{ print $1 }')" + actual_hash="$(${pkgs.coreutils}/bin/sha256sum "$local_manifest" | ${pkgs.gawk}/bin/awk '{ print $1 }')" + if [ "$expected_hash" != "$actual_hash" ]; then + echo "Manifest hash mismatch: expected $expected_hash, got $actual_hash" >&2 + exit 1 + fi + + # Only download/extract the Prism artifact when the game directory is missing. + if [ ! -d "$game_dir" ]; then + tmp="$(${pkgs.coreutils}/bin/mktemp -d --tmpdir gtnh-daily-client-bootstrap.XXXXXX)" + cleanup() { ${pkgs.coreutils}/bin/rm -rf "$tmp"; } + trap cleanup EXIT + auth=() + if [ -n "''${GITHUB_TOKEN:-}" ]; then + auth=(-H "Authorization: Bearer ''${GITHUB_TOKEN}") + fi + runs_url='https://api.github.com/repos/GTNewHorizons/DreamAssemblerXXL/actions/workflows/daily-modpack-build.yml/runs?status=success&per_page=20' + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "$runs_url" -o "$tmp/runs.json" + ${pkgs.python3}/bin/python3 - "$tmp/runs.json" > "$tmp/run_ids" <<'PY' + import json, sys + for run in json.load(open(sys.argv[1])).get("workflow_runs", []): + print(run["id"]) + PY + client_url="" + while read -r run_id; do + [ -n "$run_id" ] || continue + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "https://api.github.com/repos/GTNewHorizons/DreamAssemblerXXL/actions/runs/$run_id/artifacts" -o "$tmp/artifacts.json" + client_url="$(${pkgs.python3}/bin/python3 - "$tmp/artifacts.json" <<'PY' + import json, sys + for artifact in json.load(open(sys.argv[1])).get("artifacts", []): + name = artifact.get("name", "") + if not artifact.get("expired") and "mmcprism-java17-25.zip" in name: + print(artifact["archive_download_url"]) + break + PY + )" + [ -n "$client_url" ] && break + done < "$tmp/run_ids" + if [ -z "$client_url" ]; then + echo "Could not find non-expired GTNH Daily Prism artifacts. Set GITHUB_TOKEN if GitHub requires authentication." >&2 + exit 1 + fi + ${pkgs.curl}/bin/curl --fail --location --silent --show-error "''${auth[@]}" "$client_url" -o "$tmp/client-artifact.zip" + ${pkgs.unzip}/bin/unzip -q "$tmp/client-artifact.zip" -d "$tmp/client" + ${pkgs.coreutils}/bin/mkdir -p "$instance_dir" + ${pkgs.coreutils}/bin/cp -a "$tmp/client/." "$instance_dir/" + fi + + config_version="$(${pkgs.python3}/bin/python3 - "$local_manifest" <<'PY' + import json, sys + print(json.load(open(sys.argv[1]))["config"]) + PY + )" + # Initialize gtnh-daily-updater state only when missing; existing state is reconciled below. + if [ ! -f "$state_file" ]; then + PATH=${pkgs.git}/bin:$PATH ${gtnhDailyUpdater}/bin/gtnh-daily-updater init --instance-dir "$instance_dir" --side client --config "$config_version" + fi + ${pkgs.python3}/bin/python3 - "$state_file" ${clientStateJson} <<'PY' + import json, sys + state_path, desired_path = sys.argv[1:] + state = json.load(open(state_path)) + desired = json.load(open(desired_path)) + changed = False + if state.get("extra_mods") != desired["extra_mods"]: + state["extra_mods"] = desired["extra_mods"] + changed = True + if changed: + with open(state_path, "w") as f: + json.dump(state, f, indent=2) + f.write("\n") + PY + # Ensure resource/shader directories exist, then fetch declared non-world client assets. + mkdir -p "$game_dir/resourcepacks" "$game_dir/shaderpacks" + ${pkgs.python3}/bin/python3 - ${clientAssetsJson} "$game_dir" <<'PY' + import json, os, shutil, subprocess, sys, tempfile, zipfile + assets_path, game_dir = sys.argv[1:] + with open(assets_path) as f: + assets = json.load(f) + resourcepacks = os.path.join(game_dir, "resourcepacks") + shaderpacks = os.path.join(game_dir, "shaderpacks") + def sha256(path): + out = subprocess.check_output(["${pkgs.coreutils}/bin/sha256sum", path], text=True) + return out.split()[0] + def download(url, dest, expected): + if os.path.exists(dest) and sha256(dest) == expected: + return + tmp = dest + ".tmp" + subprocess.check_call(["${pkgs.curl}/bin/curl", "--fail", "--location", "--silent", "--show-error", url, "--output", tmp]) + actual = sha256(tmp) + if actual != expected: + os.remove(tmp) + raise SystemExit(f"hash mismatch for {dest}: expected {expected}, got {actual}") + os.replace(tmp, dest) + def extract_resource(spec): + dest = os.path.join(resourcepacks, spec["name"]) + download(spec["url"], dest, spec["sha256"]) + extract_dir = os.path.join(resourcepacks, spec["extractDir"]) + marker = extract_dir + ".sha256" + if os.path.exists(extract_dir) and os.path.exists(marker) and open(marker).read().strip() == spec["sha256"]: + return + shutil.rmtree(extract_dir, ignore_errors=True) + with tempfile.TemporaryDirectory(dir=resourcepacks) as tmpdir: + with zipfile.ZipFile(dest) as zf: + zf.extractall(tmpdir) + if spec.get("stripRoot"): + entries = os.listdir(tmpdir) + if len(entries) != 1 or not os.path.isdir(os.path.join(tmpdir, entries[0])): + raise SystemExit(f"expected one top-level directory in {dest}") + shutil.move(os.path.join(tmpdir, entries[0]), extract_dir) + else: + os.makedirs(extract_dir, exist_ok=True) + for entry in os.listdir(tmpdir): + shutil.move(os.path.join(tmpdir, entry), os.path.join(extract_dir, entry)) + with open(marker, "w") as f: + f.write(spec["sha256"] + "\n") + for spec in assets["resourcePacks"]: + extract_resource(spec) + for spec in assets["shaderPacks"]: + download(spec["url"], os.path.join(shaderpacks, spec["name"]), spec["sha256"]) + with open(os.path.join(shaderpacks, "ComplementaryUnbound_r5.8.1.zip.txt"), "w") as f: + f.write("LIGHT_COLOR_MULTS=true\nLIGHT_NIGHT_I=0.01\n") + for root in (resourcepacks, shaderpacks, os.path.join(game_dir, ".gtnh-configs", "resourcepacks")): + if os.path.exists(root): + for dirpath, dirnames, filenames in os.walk(root): + os.chmod(dirpath, 0o755) + for name in filenames: + try: + os.chmod(os.path.join(dirpath, name), 0o644) + except FileNotFoundError: + pass + PY + if [ -f "$game_dir/options.txt" ]; then + ${pkgs.python3}/bin/python3 - "$game_dir/options.txt" <<'PY' + import sys + path = sys.argv[1] + with open(path) as f: + lines = f.read().splitlines() + resource = 'resourcePacks:["AE2-Dark-Mode.v.1.18","shadowui","Modernity-GTNH-main"]' + for i, line in enumerate(lines): + if line.startswith('resourcePacks:'): + lines[i] = resource + break + else: + lines.append(resource) + with open(path, 'w') as f: + f.write('\n'.join(lines) + '\n') + PY + fi + ''; + # Sync is the recurring command: bootstrap first, then update to the server-published manifest. + gtnhDailyClientSync = pkgs.writeShellScriptBin "gtnh-daily-client-sync" '' + set -euo pipefail + + server="''${GTNH_DAILY_SERVER:-nemesis}" + remote_manifest="/var/lib/gtnh-daily/current-manifest.json" + remote_hash="/var/lib/gtnh-daily/current-manifest.sha256" + instance_dir="${gtnhDailyInstanceDir}" + state_file="$instance_dir/.gtnh-daily-updater.json" + cache_dir="''${XDG_CACHE_HOME:-$HOME/.cache}/gtnh-daily-client-sync" + backup_dir="$HOME/.local/share/PrismLauncher/backups" + local_manifest="$cache_dir/current-manifest.json" + local_hash="$cache_dir/current-manifest.sha256" + lock_file="$cache_dir/sync.lock" + + mkdir -p "$cache_dir" + exec 9>"$lock_file" + ${pkgs.util-linux}/bin/flock -n 9 + + # Refuse mutation while Prism/Minecraft is running to avoid jar/config replacement races. + if ${pkgs.procps}/bin/pgrep -u "$(${pkgs.coreutils}/bin/id -u)" -f 'prismlauncher|net\.minecraft|minecraft|lwjgl3ify' >/dev/null; then + echo "Prism or Minecraft appears to be running; refusing to update the client instance." >&2 + exit 1 + fi + ${gtnhDailyClientBootstrap}/bin/gtnh-daily-client-bootstrap + + fetched_hash="$(${pkgs.openssh}/bin/ssh -o BatchMode=yes "$server" "cat '$remote_hash'")" + if [ -f "$local_hash" ] && [ "$fetched_hash" = "$(cat "$local_hash")" ]; then + echo "GTNH Daily client is already synced to $server." + exit 0 + fi + + ${pkgs.openssh}/bin/scp -q -o BatchMode=yes "$server:$remote_manifest" "$local_manifest" + expected_hash="$(printf '%s\n' "$fetched_hash" | ${pkgs.gawk}/bin/awk '{ print $1 }')" + actual_hash="$(${pkgs.coreutils}/bin/sha256sum "$local_manifest" | ${pkgs.gawk}/bin/awk '{ print $1 }')" + if [ "$expected_hash" != "$actual_hash" ]; then + echo "Manifest hash mismatch: expected $expected_hash, got $actual_hash" >&2 + exit 1 + fi + + mkdir -p "$backup_dir" + backup="$backup_dir/gtnh-daily-client-$(${pkgs.coreutils}/bin/date -u +%Y%m%d-%H%M%S).tar.zst" + ${pkgs.gnutar}/bin/tar --use-compress-program=${pkgs.zstd}/bin/zstd -C "$HOME/.local/share/PrismLauncher/instances" -cf "$backup" "${gtnhDailyInstanceName}" + + PATH=${pkgs.git}/bin:$PATH ${gtnhDailyUpdater}/bin/gtnh-daily-updater update \ + --instance-dir "$instance_dir" \ + --manifest-file "$local_manifest" + printf '%s\n' "$fetched_hash" > "$local_hash" + echo "Synced GTNH Daily client to $server manifest $expected_hash" + ''; + in + { + # Desktop entries expose stable and Daily GTNH launches to graphical menus. + xdg.desktopEntries = { + gtnh = { + name = "GregTech New Horizons"; + icon = "${config.home.homeDirectory}/.local/share/PrismLauncher/instances/GT_New_Horizons_2.8.4_Java_17-25/icon.png"; + exec = "prismlauncher --launch GT_New_Horizons_2.8.4_Java_17-25"; + }; + gtnh-daily = { + name = gtnhDailyInstanceName; + icon = "${gtnhDailyInstanceDir}/icon.png"; + exec = "prismlauncher --launch \"${gtnhDailyInstanceName}\" --server localhost:25566"; + }; }; + # User packages install Prism and, on Linux, the bootstrap/sync helper commands. home.packages = [ (pkgs.prismlauncher.override { jdks = [ pkgs.jdk25 ]; }) + ] + ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + gtnhDailyClientBootstrap + gtnhDailyClientSync ]; + # User systemd timer is Linux-only; Darwin can still evaluate and install Prism. + systemd.user = lib.mkIf pkgs.stdenv.hostPlatform.isLinux { + services.gtnh-daily-client-sync = { + Unit = { + Description = "Sync GTNH Daily Prism client to the server manifest"; + After = [ "network-online.target" ]; + }; + Service = { + Type = "oneshot"; + ExecStart = "${gtnhDailyClientSync}/bin/gtnh-daily-client-sync"; + }; + }; + timers.gtnh-daily-client-sync = { + Unit.Description = "Sync GTNH Daily Prism client on schedule"; + Timer = { + OnCalendar = "hourly"; + Persistent = true; + RandomizedDelaySec = "15m"; + }; + Install.WantedBy = [ "timers.target" ]; + }; + }; }; }; } diff --git a/nix/packages/gtnh-daily-updater.nix b/nix/packages/gtnh-daily-updater.nix new file mode 100644 index 00000000..cd33c62e --- /dev/null +++ b/nix/packages/gtnh-daily-updater.nix @@ -0,0 +1,23 @@ +{ inputs, ... }: +{ + perSystem = + { pkgs, ... }: + let + version = "0-unstable-2026-06-05"; + in + { + packages.gtnh-daily-updater = pkgs.buildGoModule { + pname = "gtnh-daily-updater"; + inherit version; + src = inputs.gtnh-daily-updater; + patches = [ ./patches/gtnh-daily-updater-manifest-file.patch ]; + vendorHash = "sha256-qW5Ybfbo+ss/f9QZv1zTCERhk8cN8I3A3kuPs2eA8rw="; + nativeCheckInputs = [ pkgs.git ]; + meta = { + description = "Automated updater for GT: New Horizons daily builds"; + homepage = "https://github.com/Caedis/gtnh-daily-updater"; + mainProgram = "gtnh-daily-updater"; + }; + }; + }; +} diff --git a/nix/packages/patches/gtnh-daily-updater-manifest-file.patch b/nix/packages/patches/gtnh-daily-updater-manifest-file.patch new file mode 100644 index 00000000..b5144f0f --- /dev/null +++ b/nix/packages/patches/gtnh-daily-updater-manifest-file.patch @@ -0,0 +1,142 @@ +diff --git a/cmd/root.go b/cmd/root.go +index aa09fb4..060f4ba 100644 +--- a/cmd/root.go ++++ b/cmd/root.go +@@ -168,6 +168,7 @@ func expandFlagPaths() { + logFile = paths.ExpandTilde(logFile) + cacheDir = paths.ExpandTilde(cacheDir) + cacheDirAll = paths.ExpandTilde(cacheDirAll) ++ manifestFile = paths.ExpandTilde(manifestFile) + } + + func getGithubToken() string { +diff --git a/cmd/update.go b/cmd/update.go +index 391a6a7..39ab4c0 100644 +--- a/cmd/update.go ++++ b/cmd/update.go +@@ -9,12 +9,13 @@ import ( + ) + + var ( +- dryRun bool +- force bool +- latest bool +- concurrency int +- cacheDir string +- noCache bool ++ dryRun bool ++ force bool ++ latest bool ++ concurrency int ++ cacheDir string ++ noCache bool ++ manifestFile string + ) + + var updateCmdName = "update" +@@ -33,6 +34,7 @@ var updateCmd = &cobra.Command{ + CurseForgeKey: getCurseForgeKey(), + CacheDir: cacheDir, + NoCache: noCache, ++ ManifestFile: manifestFile, + } + + result, err := updater.Run(context.Background(), opts) +@@ -71,6 +73,7 @@ func init() { + updateCmd.Flags().IntVar(&concurrency, "concurrency", 6, "Number of concurrent downloads") + updateCmd.Flags().StringVar(&cacheDir, "cache-dir", "", "Directory for caching downloaded mods (default: OS user cache dir + /gtnh-daily-updater/mods/)") + updateCmd.Flags().BoolVar(&noCache, "no-cache", false, "Disable download caching") ++ updateCmd.Flags().StringVar(&manifestFile, "manifest-file", "", "Use a local daily manifest JSON instead of fetching the latest remote manifest") + rootCmd.AddCommand(updateCmd) + } + +diff --git a/internal/updater/run.go b/internal/updater/run.go +index ac0e7a1..ded91c9 100644 +--- a/internal/updater/run.go ++++ b/internal/updater/run.go +@@ -21,7 +21,7 @@ func Run(ctx context.Context, opts Options) (*UpdateResult, error) { + return nil, err + } + +- m, db, mode, err := resolveSharedData(ctx, state, opts.Shared) ++ m, db, mode, err := resolveSharedData(ctx, state, opts) + if err != nil { + return nil, err + } +diff --git a/internal/updater/types.go b/internal/updater/types.go +index c655e4b..310ff85 100644 +--- a/internal/updater/types.go ++++ b/internal/updater/types.go +@@ -24,6 +24,7 @@ type Options struct { + CurseForgeKey string + CacheDir string + NoCache bool ++ ManifestFile string + // Shared optionally supplies pre-fetched manifest and assets DB. + // When non-nil, Run skips those network fetches. + Shared *SharedData +diff --git a/internal/updater/workflow_steps.go b/internal/updater/workflow_steps.go +index 4361908..32f322c 100644 +--- a/internal/updater/workflow_steps.go ++++ b/internal/updater/workflow_steps.go +@@ -2,6 +2,7 @@ package updater + + import ( + "context" ++ "encoding/json" + "errors" + "fmt" + "maps" +@@ -79,6 +80,29 @@ func fetchAndLogManifest(ctx context.Context, mode string) (*manifest.DailyManif + return m, nil + } + ++func loadAndLogManifestFile(path string) (*manifest.DailyManifest, error) { ++ logging.Infof("Loading pinned manifest %s...\n", path) ++ data, err := os.ReadFile(path) ++ if err != nil { ++ return nil, fmt.Errorf("reading manifest file: %w", err) ++ } ++ ++ var m manifest.DailyManifest ++ if err := json.Unmarshal(data, &m); err != nil { ++ return nil, fmt.Errorf("parsing manifest file: %w", err) ++ } ++ ++ logging.Debugf( ++ "Verbose: loaded pinned manifest version=%s updated=%s config=%s github-mods=%d external-mods=%d\n", ++ m.Version, ++ m.LastUpdated, ++ m.Config, ++ len(m.GithubMods), ++ len(m.ExternalMods), ++ ) ++ return &m, nil ++} ++ + func fetchAndLogAssetsDB(ctx context.Context) (*assets.AssetsDB, error) { + logging.Infoln("Fetching assets database...") + db, err := assets.Fetch(ctx) +@@ -108,8 +132,21 @@ func FetchSharedData(ctx context.Context, mode string) (*SharedData, error) { + return &SharedData{Manifest: m, AssetsDB: db, Mode: parsedMode}, nil + } + +-func resolveSharedData(ctx context.Context, state *config.LocalState, shared *SharedData) (*manifest.DailyManifest, *assets.AssetsDB, string, error) { ++func resolveSharedData(ctx context.Context, state *config.LocalState, opts Options) (*manifest.DailyManifest, *assets.AssetsDB, string, error) { + mode := resolveMode(state) ++ if opts.ManifestFile != "" { ++ m, err := loadAndLogManifestFile(opts.ManifestFile) ++ if err != nil { ++ return nil, nil, "", err ++ } ++ db, err := fetchAndLogAssetsDB(ctx) ++ if err != nil { ++ return nil, nil, "", err ++ } ++ return m, db, mode, nil ++ } ++ ++ shared := opts.Shared + if shared != nil && shared.Manifest != nil && shared.AssetsDB != nil { + sharedMode, err := manifest.ParseMode(shared.Mode) + if err != nil { diff --git a/nix/profiles/development.nix b/nix/profiles/development.nix index 14e22bca..7966ca8b 100644 --- a/nix/profiles/development.nix +++ b/nix/profiles/development.nix @@ -80,6 +80,7 @@ in { home.packages = with pkgs; [ inputs.nixpkgs-master.legacyPackages.${pkgs.stdenv.hostPlatform.system}.git-bug + inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.gtnh-daily-updater ddgr gh ripgrep