diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e6791bc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +*.sh text eol=lf +scripts/rcon-cli text eol=lf +Dockerfile text eol=lf +*.py text eol=lf diff --git a/.github/workflows/docker-publish-arm64.yml b/.github/workflows/docker-publish-arm64.yml new file mode 100644 index 0000000..4e376a1 --- /dev/null +++ b/.github/workflows/docker-publish-arm64.yml @@ -0,0 +1,62 @@ +name: Build and Push ARM64 Docker Image + +on: + push: + branches: ["main", "arm64", "arm64-v2", "v2-layout"] + paths: + - 'Dockerfile.arm64' + - 'scripts/**' + - '.github/workflows/docker-publish-arm64.yml' + release: + types: [published] + workflow_dispatch: + +jobs: + build-and-push: + environment: production + # Native aarch64 runner - free for public repos (this one is public). + # The Multi-Stage Dockerfile compiles build2 and iw4x-launcher from source, + # so emulated QEMU build on ubuntu-latest would take 60-90 min. Native + # aarch64 cuts that to ~20-30 min and avoids QEMU edge cases entirely. + runs-on: ubuntu-24.04-arm + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=arm64,enable={{is_default_branch}} + type=raw,value=arm64-v2,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2-layout' || github.ref == 'refs/heads/arm64-v2' }} + type=ref,event=tag,suffix=-arm64 + type=sha,suffix=-arm64 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: Dockerfile.arm64 + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=arm64 + cache-to: type=gha,mode=max,scope=arm64 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 2dae91f..5947850 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -2,7 +2,7 @@ name: Build and Push Docker Image on: push: - branches: [ "main" ] + branches: [ "main", "v2-layout" ] paths-ignore: - 'README.md' - 'LICENSE' @@ -39,6 +39,7 @@ jobs: images: ghcr.io/${{ github.repository }} tags: | type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=v2,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/v2-layout' }} type=ref,event=tag type=ref,event=pr type=sha diff --git a/CLAUDE.md b/CLAUDE.md index dc92dce..cb0e98e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Plutainer is a Docker image for running Plutonium, IW4x, and Alterware dedicated game servers (Call of Duty titles: T4/WaW, T5/BO1, T6/BO2, IW5/MW3, IW4x/MW2, T7x/BO3). It uses Wine on Debian to run the Windows game server binaries, configured entirely via environment variables. +Plutainer is a Docker image for running Plutonium, IW4x, and Alterware dedicated game servers (Call of Duty titles: T4/WaW, T5/BO1, T6/BO2, IW5/MW3, IW4x/MW2, T7x/BO3). It uses Wine on Arch Linux to run the Windows game server binaries, configured entirely via environment variables. ## Build & Test @@ -12,14 +12,14 @@ Plutainer is a Docker image for running Plutonium, IW4x, and Alterware dedicated # Build the Docker image locally docker build -t plutainer . -# Run a Plutonium container example -docker run -e PLUTO_GAME=t6zm -e PLUTO_SERVER_KEY= -e PLUTO_CONFIG_FILE=dedicated.cfg \ +# Run a Plutonium container example (v2 env vars) +docker run -e PLUTAINER_GAME=t6zm -e PLUTO_SERVER_KEY= -e PLUTAINER_CONFIG_FILE=dedicated.cfg \ -v /path/to/game_files:/home/plutainer/gamefiles:ro \ -v ./server-data:/home/plutainer/app \ -p 4976:4976/udp plutainer # Run an IW4x container example -docker run -e IW4X_GAME=iw4x -e IW4X_CONFIG_FILE=server.cfg \ +docker run -e PLUTAINER_GAME=iw4x -e PLUTAINER_CONFIG_FILE=server.cfg \ -v /path/to/game_files:/home/plutainer/gamefiles:ro \ -v ./server-data:/home/plutainer/app \ -p 28960:28960/udp plutainer @@ -27,40 +27,94 @@ docker run -e IW4X_GAME=iw4x -e IW4X_CONFIG_FILE=server.cfg \ There are no automated tests or linters. The CI pipeline (`.github/workflows/docker-publish.yml`) builds and pushes to `ghcr.io` on pushes to `main` and on releases. +## Tags + +- `ghcr.io/ayymoss/plutainer:v2` — built from `v2-layout` branch. New volume layout + unified `PLUTAINER_*` env vars. Opt-in. CI workflow tags it only on pushes to `v2-layout`; never promotes to `:latest`. +- `ghcr.io/ayymoss/plutainer:latest` — built from `main`. Deprecated v1 layout. No further v2 work merges here; bug-only updates if any. + +CI logic in `.github/workflows/docker-publish.yml`: +- `type=raw,value=latest,enable={{is_default_branch}}` — only on main. +- `type=raw,value=v2,enable=${{ github.ref == 'refs/heads/v2-layout' }}` — only on v2-layout. +- Both branches also get `:sha-`. Branches stay completely separated. + ## Architecture -Everything runs as the `plutainer` user from `/home/plutainer/.plutainer`. +Everything runs as the `plutainer` user from `/home/plutainer/.plutainer`. All entry scripts run with `set -euo pipefail`. + +1. **`entrypoint.sh`** — Top-level dispatcher. Sources `game-config.sh`, calls `detect_game_type` (requires `PLUTAINER_GAME`), `check_volume_version` (refuses v1 volumes), then `exec`s the family-specific entry script. On any failure: `hold_indefinitely` (sleep infinity) instead of exiting, to avoid restart loops. + +2. **`plutoentry.sh`** — Plutonium server entrypoint. Symlinks game files from the read-only gamefiles mount, runs `plutonium-updater`, seeds bundled configs into the SOT location, fans out symlinks from `app/configs/` to the engine and (if `PLUTAINER_MOD` is set) the mod config dir, calls `ensure_config_present` (auto-lift + refusal), then `launch_game wine ...` (30s exit-throttle wrapper). + +3. **`iw4xentry.sh`** — IW4x server entrypoint. Same shape: symlinks game files, runs `iw4x-launcher`, fans out config symlinks (engine + optional mod dir), validates, `launch_game wine iw4x.exe`. No seed bundle. + +4. **`alterentry.sh`** — Alterware (T7x/BO3) entrypoint. Symlinks game files, uses `wget -N` (timestamping) to fetch `t7x.exe` only when upstream is newer, seeds Dss0/t7-server-config bundle, fans out config symlinks, starts `Xvfb` (T7x requires a display), `launch_game wine t7x.exe`. No mod dir (alterware MOD is a Steam Workshop ID). -1. **`entrypoint.sh`** — Dispatcher that detects game type via `PLUTO_GAME`, `IW4X_GAME`, or `ALTER_GAME` env var and delegates to the appropriate entrypoint script. +5. **`game-config.sh`** — Shared shell library sourced by all other scripts. Key helpers: + - Volume path constants: `PLUTAINER_APP_DIR`, `PLUTAINER_CONFIGS_DIR`, `PLUTAINER_RUNTIME_DIR`, `PLUTAINER_GAMEFILES_DIR`, `PLUTAINER_PLUTONIUM_DIR`, `PLUTAINER_SOURCE_DIR`. + - `hold_indefinitely `: print the error, then `exec sleep infinity` so the container stays `Up` instead of looping through restarts. Used for any startup validation failure. + - `launch_game ...`: wraps the game invocation; on exit, sleeps 30s before letting the script exit, so docker's restart policy throttles to ~1 restart per 30s. + - `derive_family `: returns `plutonium`/`iw4x`/`alterware`. + - `detect_game_type`: validates `PLUTAINER_GAME` (no shim — only PLUTAINER_* accepted), sets `GAME_TYPE`/`GAME_NAME`/`BASE_GAME`/`CONFIG_FILE`/`CUSTOM_PORT`/`HEALTHCHECK_FLAG`. + - `resolve_default_port`, `resolve_engine_config_dir`, `resolve_mod_config_dir`. + - `resolve_config_layout`: sets `CONFIG_SOT_DIR` and `ALT_CONFIG_DIR` based on `PLUTAINER_USE_RAW_CONFIGS`. Default: SOT = `configs/`, ALT = engine dir. With raw mode on: swapped. + - `resolve_config_path`: convenience wrapper that resolves the engine dir + layout in one call so healthcheck/rcon-cli only need this. + - `link_files ...`: existence-guarded symlink helper; replaces unsafe `ln -sf src/{a,b,c} dest/` bash brace expansion. + - `seed_configs `: walks bundled seed, lifts top-level `*.cfg` files inside `cfg-root-rel` into `CONFIG_SOT_DIR`, places everything else under `asset-root`. Idempotent. + - `link_configs [engine-dir2 ...]`: variadic. Fans out symlinks from every `configs/*.cfg` into each engine dir using relative paths. Refuses to overwrite a real (non-symlink) file at engine path (warns instead). Reaps dangling cfg symlinks. No-op when `PLUTAINER_USE_RAW_CONFIGS=true`. + - `ensure_config_present`: checks that `CONFIG_FILE` exists at `CONFIG_SOT_DIR`. If absent there but present as a real file at the ALT location, moves it (auto-lift). If absent everywhere, prints a refusal with a `find -iname` case-insensitive hint, returns non-zero. + - `check_volume_version`: refuses v1 volumes with explicit migration instructions; initialises fresh volumes; writes `.plutainer-version=2`. + - `extract_rcon_password`: parses `rcon_password` from `CONFIG_PATH`. Handles double-quoted, single-quoted, and unquoted values. Strips `//` comments. On failure, prints a structured `[WARN]` (don't block startup) telling the user the accepted forms and not to set the password via `PLUTAINER_EXTRA_ARGS`. -2. **`plutoentry.sh`** — Plutonium server entrypoint. Symlinks game-specific files from the read-only gamefiles mount, runs the plutonium-updater, validates env vars, resolves game-specific defaults, then `exec`s `wine bin/plutonium-bootstrapper-win32.exe`. +6. **`migrate-v1-to-v2.sh`** — One-shot migration tool, run via `docker run --entrypoint`. Moves `app/gamefiles` → `app/runtime/gamefiles`, `app/plutonium` → `app/runtime/plutonium`, lifts top-level cfg files from known engine config dirs into `app/configs/` and replaces them with relative symlinks, clears stale `app/logs/` entries, writes `.plutainer-version=2`. Supports `--dry-run`. -3. **`iw4xentry.sh`** — IW4x server entrypoint. Similar flow: symlinks game files, runs iw4x-launcher for updates, validates env vars, then `exec`s `wine iw4x.exe`. +7. **`log-watcher.sh`** — Background poller started by each entrypoint before `exec wine`. Discovers every `*.log` under `/home/plutainer/app/` (excluding `app/logs/` itself to avoid cycles) and maintains relative symlinks at `/home/plutainer/app/logs/` pointing at the active one. Active = newest mtime >= container boot time. Agnostic to log name. Symlinks are relative so they resolve the same on host, in this container, or in a sidecar IW4MAdmin container. Disable with `PLUTAINER_LOG_SYMLINKS=false`; poll interval via `PLUTAINER_LOG_POLL_INTERVAL` (default 2s). -4. **`alterentry.sh`** — Alterware server entrypoint (T7x/Black Ops 3). Symlinks game files, creates writable `zone/` directory with symlinked contents, downloads `t7x.exe` from upstream, validates env vars, then `exec`s `wine t7x.exe`. +8. **`healthcheck.sh`** — Sources `game-config.sh`, then uses `pyquake3.py` to send an RCON `status` command. Enabled by default; disable with `PLUTAINER_HEALTHCHECK=false`. HEALTHCHECK directive uses `--start-period=5m` to accommodate first-run downloads. -5. **`game-config.sh`** — Shared shell library sourced by all other scripts. Single source of truth for game detection, port defaults, config path resolution, and RCON password extraction. +9. **`rcon-cli`** — Python script providing interactive and one-shot RCON access via `docker exec`. Calls `game-config.sh` to resolve port/credentials. Supports Plutonium, IW4x, and Alterware. -6. **`log-watcher.sh`** — Background poller started by each entrypoint before `exec wine`. Discovers every `*.log` under `/home/plutainer/app/` (excluding `app/logs/` itself to avoid cycles) and maintains relative symlinks at `/home/plutainer/app/logs/` pointing at the active one. Active = newest mtime >= container boot time, so stale logs from prior sessions and abandoned mod dirs are ignored. Agnostic to log name (handles user-defined names like `games_koth.log`). Symlinks are relative so they resolve the same on host, this container, or a sidecar IW4MAdmin container. Disable with `PLUTAINER_LOG_SYMLINKS=false`; poll interval via `PLUTAINER_LOG_POLL_INTERVAL` (default 2s). +10. **`pyquake3.py`** — Python 3 Quake 3 protocol library (UDP). Used by the health check and `rcon-cli` for RCON queries. -7. **`healthcheck.sh`** — Sources `game-config.sh`, then uses `pyquake3.py` to send an RCON `status` command. Enabled by default; disable with `PLUTO_HEALTHCHECK=false`, `IW4X_HEALTHCHECK=false`, or `ALTER_HEALTHCHECK=false`. +## Volume Layout (v2) -8. **`rcon-cli`** — Python script providing interactive and one-shot RCON access via `docker exec`. Calls `game-config.sh` to resolve port/credentials. Supports Plutonium, IW4x, and Alterware. +``` +/home/plutainer/gamefiles # read-only host gamefiles bind +/home/plutainer/app/ + configs/ # User-facing real *.cfg files (flat). + # Edit here. Engine paths symlink in. + logs/ # Stable symlinks to active *.log files + # (maintained by log-watcher.sh). + # Sidecars (IW4MAdmin) mount this dir. + runtime/ + gamefiles/ # Symlinks into host gamefiles plus + # writable game state (mods, .iwd, etc). + plutonium/ # Plutonium binaries + storage state. + .plutainer-version # "2" — layout marker. +/home/plutainer/.plutainer/ # Scripts, updaters, pyquake3, seed-configs. +``` -9. **`pyquake3.py`** — Python 3 Quake 3 protocol library (UDP). Used by the health check and `rcon-cli` for RCON queries. +**Config flow:** user edits `app/configs/.cfg` → entrypoint places a relative symlink at the engine's expected path → game reads via symlink. RCON `writeconfig` writes through the symlink, modifying the real file in `configs/`. ## Game-Specific Behavior -For Plutonium, `BASE_GAME` is derived by stripping the last two chars from `PLUTO_GAME` (e.g., `t6zm` → `t6`). This drives: +For Plutonium, `BASE_GAME` is derived by stripping the last two chars from `PLUTAINER_GAME` (e.g., `t6zm` → `t6`). This drives: + +- **Default ports**: iw4x→28960, iw5→27016, t4/t5→28960, t6→4976, t7x→27017. +- **Engine config dirs** (where the game reads `+exec`'d cfg files): t4 → `runtime/gamefiles/main/`, iw5 → `runtime/gamefiles/admin/`, iw4x → `runtime/gamefiles/userraw/`, t7x → `runtime/gamefiles/zone/`, others → `runtime/plutonium/storage//`. +- **Command args**: iw5 uses `+set sv_config` and `+start_map_rotate`; others use `+exec` and `+map_rotate`. +- **Game-file symlinks** differ per base game (see `plutoentry.sh` case statement). + +## Compatibility surface + +The `:v2` image is a **clean break** from `:latest`. The two share no env vars (other than the always-unique `PLUTO_SERVER_KEY`, `PLUTO_MAX_CLIENTS`, `IW4X_NET_LOG_IP`) and no volume layout. There is no env-var shim in `:v2` — legacy `PLUTO_*`/`IW4X_*`/`ALTER_*` names are silently ignored. + +A v1 `app/` volume is refused on startup (`check_volume_version`) with explicit instructions to run `migrate-v1-to-v2.sh`. + +## Restart behavior -- **Default ports**: iw4x→28960, iw5→27016, t4/t5→28960, t6→4976, t7x→27017 -- **Config file paths**: t4→`app/gamefiles/main/`, iw5→`app/gamefiles/admin/`, iw4x→`app/gamefiles/userraw/`, t7x→`app/gamefiles/zone/`, others→`app/plutonium/storage/{base_game}/` -- **Command args**: iw5 uses `+set sv_config` and `+start_map_rotate`; others use `+exec` and `+map_rotate` -- **Game file symlinks** differ per base game (see `plutoentry.sh` case statement) +Two distinct failure modes: -## Container Layout +- **Configuration errors** (validation failures, missing env vars, missing config file, v1 volume, unknown game): `hold_indefinitely` → `exec sleep infinity`. Container stays `Up`; healthcheck eventually marks it unhealthy. No restart loop. User fixes and runs `docker restart `. +- **Runtime crashes** (wine exits): `launch_game` wrapper catches the exit, sleeps 30s, then exits with the original return code. Docker's restart policy fires after that, giving ~1 restart per 30s instead of immediate churn. -- `/home/plutainer/gamefiles` — bind-mounted read-only game files from host -- `/home/plutainer/app` — persistent volume (gamefiles symlinks, plutonium data, configs, logs) -- `/home/plutainer/app/logs` — stable symlinks to active game logs, maintained by `log-watcher.sh`. Host-side IW4MAdmin should bind-mount this dir and read logs from there instead of reaching into nested mod/storage paths. -- `/home/plutainer/.plutainer` — working directory containing scripts, updaters, and pyquake3 +`STOPSIGNAL` is `SIGKILL`, so neither path interferes with `docker stop` — that's instant by design. diff --git a/Dockerfile b/Dockerfile index 4a918e0..262c920 100644 --- a/Dockerfile +++ b/Dockerfile @@ -75,7 +75,8 @@ RUN set -eux; \ find /home/plutainer/.plutainer/seed-configs -type f \( -iname '*.bat' -o -iname '*.sh' -o -iname 'README*' \) -delete COPY --chown=plutainer:plutainer scripts/ . -RUN chmod +x entrypoint.sh healthcheck.sh plutoentry.sh iw4xentry.sh alterentry.sh log-watcher.sh rcon-cli game-config.sh +RUN chmod +x entrypoint.sh healthcheck.sh plutoentry.sh iw4xentry.sh alterentry.sh \ + log-watcher.sh rcon-cli game-config.sh migrate-v1-to-v2.sh USER root RUN ln -s /home/plutainer/.plutainer/rcon-cli /usr/local/bin/rcon-cli @@ -83,7 +84,7 @@ USER plutainer STOPSIGNAL SIGKILL -HEALTHCHECK --interval=1m --timeout=10s --start-period=1m --retries=3 \ +HEALTHCHECK --interval=1m --timeout=10s --start-period=5m --retries=3 \ CMD ./healthcheck.sh ENTRYPOINT ["./entrypoint.sh"] diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 new file mode 100644 index 0000000..0df3240 --- /dev/null +++ b/Dockerfile.arm64 @@ -0,0 +1,154 @@ +# ARM64 variant (Raspberry Pi 5, Ampere, Apple silicon under Linux). +# +# Architecture translation strategy: +# * x86_64 Windows PE (Plutonium/IW4x/T7x .exe) -> Wine 11 thunked WoW64 + +# libarm64ecfex.dll (FEX as CPU plug-in, shipped by Hangover 11.4). +# * Linux helper CLIs (plutonium-updater, iw4x-launcher) -> built natively +# for aarch64 from source. Upstream ships no aarch64-linux release, so the +# alternative would be running their x86_64 binaries under box64; building +# native instead drops the user-space x86 emulator from the runtime image. +# +# Base: Debian 13 trixie. Hangover publishes official .deb bundles for this +# exact distro/arch, so Wine 11 is consumed pre-built. + +# --------------------------------------------------------------------------- +# Stage 1: build plutonium-updater (Rust) natively for aarch64-linux. +# --------------------------------------------------------------------------- +FROM --platform=linux/arm64 rust:1-trixie AS rust-builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git ca-certificates pkg-config libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build +RUN git clone --depth 1 https://github.com/mxve/plutonium-updater.rs.git +WORKDIR /build/plutonium-updater.rs +RUN cargo build --release + +# --------------------------------------------------------------------------- +# Stage 2: build iw4x-launcher (C++ via build2) natively for aarch64-linux. +# --------------------------------------------------------------------------- +FROM --platform=linux/arm64 debian:trixie-slim AS cpp-builder + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl wget git \ + g++ gcc make \ + tar xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# build2 ships no aarch64-linux binaries; the install script compiles the +# toolchain from source. The installer uses curl internally to fetch the +# source tarball (see check_cmd curl in build2-install-*.sh). --trust yes +# accepts the cppget.org repo cert without manual fingerprint entry +# (acceptable inside a one-shot build container). +RUN curl -fL -o build2-install.sh https://download.build2.org/0.18.1/build2-install-0.18.1.sh \ + && sh build2-install.sh --trust yes --no-check --jobs $(nproc) --yes /usr/local \ + && rm build2-install.sh + +WORKDIR /build +RUN git clone --depth 1 https://github.com/iw4x/launcher.git iw4x-launcher +WORKDIR /build/iw4x-launcher + +# bindist-gcc-static.sh prompts for confirmation; -y answers yes. The script +# emits a flattened .tar.xz under dist-output/ containing only the binary. +RUN sh bindist-gcc-static.sh -y -j $(nproc) "launcher@https://github.com/iw4x/launcher.git#main" + +RUN mkdir -p /out \ + && tar -xJf dist-output/*.tar.xz -C /out \ + && find /out -type f -name 'launcher' -exec mv {} /out/iw4x-launcher \; + +# --------------------------------------------------------------------------- +# Stage 3: runtime image. +# --------------------------------------------------------------------------- +FROM --platform=linux/arm64 debian:trixie-slim + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + wget \ + tar \ + xz-utils \ + unzip \ + jq \ + python3 \ + procps \ + findutils \ + xvfb \ + xauth \ + && rm -rf /var/lib/apt/lists/* + +# Hangover 11.4 = Wine 11 + FEX 2603 + box64 as Wine CPU plug-ins (the +# in-Wine box64 is unrelated to a Linux-user-space box64; we do not need +# the latter because the helper CLIs are native aarch64). +RUN HANGOVER_TAG="hangover-11.4" \ + && HANGOVER_TAR="hangover_11.4_debian13_trixie_arm64.tar" \ + && wget -q "https://github.com/AndreRH/hangover/releases/download/${HANGOVER_TAG}/${HANGOVER_TAR}" \ + && mkdir hangover-pkg \ + && tar -xf "${HANGOVER_TAR}" -C hangover-pkg \ + && apt-get update \ + && apt-get install -y --no-install-recommends ./hangover-pkg/*.deb \ + && rm -rf hangover-pkg "${HANGOVER_TAR}" /var/lib/apt/lists/* + +RUN useradd -m plutainer +RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix + +ENV WINEDLLOVERRIDES="mscoree,mshtml=" \ + DISPLAY=:99 + +USER plutainer +WORKDIR /home/plutainer/.plutainer + +RUN Xvfb :99 -screen 0 320x240x24 & \ + sleep 1 && \ + wineboot -u && \ + wineserver -w && \ + pkill -f Xvfb || true && \ + rm -f /tmp/.X99-lock + +COPY --from=rust-builder --chown=plutainer:plutainer \ + /build/plutonium-updater.rs/target/release/plutonium-updater \ + /home/plutainer/.plutainer/plutonium-updater + +COPY --from=cpp-builder --chown=plutainer:plutainer \ + /out/iw4x-launcher \ + /home/plutainer/.plutainer/iw4x-launcher + +# Bundle community config seeds for first-run scaffolding. Entrypoint copies +# these into the bind mount on start with cp -n (never overwrites user files). +# Source repos: xerxes-at/T{4,5,6}ServerConfig*, xerxes-at/IW5ServerConfig, +# Dss0/t7-server-config. Disable per-stack via PLUTO_SKIP_SEED/ALTER_SKIP_SEED. +RUN set -eux; \ + mkdir -p seed-configs/{t4,t5,t6,iw5,t7x}; \ + cd /tmp; \ + wget -q -O t4.zip https://github.com/xerxes-at/T4ServerConfigs/archive/refs/heads/main.zip && unzip -q t4.zip; \ + wget -q -O t5.zip https://github.com/xerxes-at/T5ServerConfig/archive/refs/heads/master.zip && unzip -q t5.zip; \ + wget -q -O t6.zip https://github.com/xerxes-at/T6ServerConfigs/archive/refs/heads/master.zip && unzip -q t6.zip; \ + wget -q -O iw5.zip https://github.com/xerxes-at/IW5ServerConfig/archive/refs/heads/master.zip && unzip -q iw5.zip; \ + wget -q -O t7x.zip https://github.com/Dss0/t7-server-config/archive/refs/heads/main.zip && unzip -q t7x.zip; \ + cp -r T4ServerConfigs-main/main/. /home/plutainer/.plutainer/seed-configs/t4/; \ + cp -r T5ServerConfig-master/localappdata/Plutonium/storage/t5/. /home/plutainer/.plutainer/seed-configs/t5/; \ + cp -r T6ServerConfigs-master/localappdata/Plutonium/storage/t6/. /home/plutainer/.plutainer/seed-configs/t6/; \ + cp -r IW5ServerConfig-master/admin/. /home/plutainer/.plutainer/seed-configs/iw5/; \ + cp -r t7-server-config-main/zone /home/plutainer/.plutainer/seed-configs/t7x/; \ + cp -r t7-server-config-main/t7x /home/plutainer/.plutainer/seed-configs/t7x/; \ + rm -rf /tmp/*.zip /tmp/T4ServerConfigs-main /tmp/T5ServerConfig-master /tmp/T6ServerConfigs-master /tmp/IW5ServerConfig-master /tmp/t7-server-config-main; \ + find /home/plutainer/.plutainer/seed-configs -type d -iname '*REFERENCE*' -exec rm -rf {} +; \ + find /home/plutainer/.plutainer/seed-configs -type f \( -iname '*.bat' -o -iname '*.sh' -o -iname 'README*' \) -delete + +COPY --chown=plutainer:plutainer scripts/ . +RUN chmod +x entrypoint.sh healthcheck.sh plutoentry.sh iw4xentry.sh alterentry.sh \ + log-watcher.sh rcon-cli game-config.sh migrate-v1-to-v2.sh + +USER root +RUN ln -s /home/plutainer/.plutainer/rcon-cli /usr/local/bin/rcon-cli +USER plutainer + +STOPSIGNAL SIGKILL + +HEALTHCHECK --interval=1m --timeout=10s --start-period=1m --retries=3 \ + CMD ./healthcheck.sh + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/EXAMPLE-docker-compose.yml b/EXAMPLE-docker-compose.yml index 104d1f2..b92ad7d 100644 --- a/EXAMPLE-docker-compose.yml +++ b/EXAMPLE-docker-compose.yml @@ -1,24 +1,24 @@ services: iw4xmp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: iw4xmp-1 restart: unless-stopped ports: - "28970:28970/udp" #- "28970:28970/tcp" # TCP is needed for modlist metadata (not required if not hosting mods) volumes: - - /opt/GameFiles/IW4xServerFiles:/home/plutainer/gamefiles + - /opt/GameFiles/IW4xServerFiles:/home/plutainer/gamefiles:ro - ./iw4xmp-1:/home/plutainer/app environment: - - IW4X_PORT=28970 - - IW4X_CONFIG_FILE=server.cfg - - IW4X_GAME=iw4x - - IW4X_AUTO_UPDATE=false + - PLUTAINER_GAME=iw4x + - PLUTAINER_CONFIG_FILE=server.cfg + - PLUTAINER_PORT=28970 + - PLUTAINER_AUTO_UPDATE=false networks: - games-net t6zm-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t6zm-1 hostname: t6zm-1 restart: unless-stopped @@ -28,15 +28,15 @@ services: - /opt/GameFiles/T6ServerFiles:/home/plutainer/gamefiles:ro - ./t6zm-1:/home/plutainer/app environment: - - PLUTO_PORT=4976 + - PLUTAINER_GAME=t6zm + - PLUTAINER_CONFIG_FILE=dedicated_zm.cfg + - PLUTAINER_PORT=4976 - PLUTO_SERVER_KEY=${T6ZM_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=dedicated_zm.cfg - - PLUTO_GAME=t6zm networks: - games-net t6mp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t6mp-1 hostname: t6mp-1 restart: unless-stopped @@ -46,15 +46,15 @@ services: - /opt/GameFiles/T6ServerFiles:/home/plutainer/gamefiles:ro - ./t6mp-1:/home/plutainer/app environment: - - PLUTO_PORT=4977 + - PLUTAINER_GAME=t6mp + - PLUTAINER_CONFIG_FILE=dedicated.cfg + - PLUTAINER_PORT=4977 - PLUTO_SERVER_KEY=${T6MP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=dedicated.cfg - - PLUTO_GAME=t6mp networks: - games-net - + t4sp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t4sp-1 hostname: t4sp-1 restart: unless-stopped @@ -64,15 +64,15 @@ services: - /opt/GameFiles/T4ServerFiles:/home/plutainer/gamefiles:ro - ./t4sp-1:/home/plutainer/app environment: - - PLUTO_PORT=28960 + - PLUTAINER_GAME=t4sp + - PLUTAINER_CONFIG_FILE=server_zm.cfg + - PLUTAINER_PORT=28960 - PLUTO_SERVER_KEY=${T4SP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=server_zm.cfg - - PLUTO_GAME=t4sp networks: - games-net - + t4mp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t4mp-1 hostname: t4mp-1 restart: unless-stopped @@ -82,15 +82,15 @@ services: - /opt/GameFiles/T4ServerFiles:/home/plutainer/gamefiles:ro - ./t4mp-1:/home/plutainer/app environment: - - PLUTO_PORT=28961 + - PLUTAINER_GAME=t4mp + - PLUTAINER_CONFIG_FILE=server.cfg + - PLUTAINER_PORT=28961 - PLUTO_SERVER_KEY=${T4MP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=server.cfg - - PLUTO_GAME=t4mp networks: - games-net - + t5mp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t5mp-1 hostname: t5mp-1 restart: unless-stopped @@ -100,16 +100,16 @@ services: - /opt/GameFiles/T5ServerFiles:/home/plutainer/gamefiles:ro - ./t5mp-1:/home/plutainer/app environment: - - PLUTO_PORT=28962 + - PLUTAINER_GAME=t5mp + - PLUTAINER_CONFIG_FILE=dedicated.cfg + - PLUTAINER_PORT=28962 - PLUTO_SERVER_KEY=${T5MP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=dedicated.cfg - - PLUTO_GAME=t5mp - PLUTO_MAX_CLIENTS=18 networks: - games-net - + t5sp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t5sp-1 hostname: t5sp-1 restart: unless-stopped @@ -119,17 +119,17 @@ services: - /opt/GameFiles/T5ServerFiles:/home/plutainer/gamefiles:ro - ./t5sp-1:/home/plutainer/app environment: - - PLUTO_PORT=28963 + - PLUTAINER_GAME=t5sp + - PLUTAINER_CONFIG_FILE=dedicated_sp.cfg + - PLUTAINER_PORT=28963 + - PLUTAINER_MOD=mods/Stairway_to_hell_bo1 - PLUTO_SERVER_KEY=${T5SP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=dedicated_sp.cfg - - PLUTO_GAME=t5sp - - PLUTO_MOD=mods/Stairway_to_hell_bo1 - PLUTO_MAX_CLIENTS=4 networks: - games-net - + iw5mp-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: iw5mp-1 hostname: iw5mp-1 restart: unless-stopped @@ -139,15 +139,15 @@ services: - /opt/GameFiles/IW5ServerFiles:/home/plutainer/gamefiles:ro - ./iw5mp-1:/home/plutainer/app environment: - - PLUTO_PORT=27016 + - PLUTAINER_GAME=iw5mp + - PLUTAINER_CONFIG_FILE=server.cfg + - PLUTAINER_PORT=27016 - PLUTO_SERVER_KEY=${IW5MP_PLUTO_SERVER_KEY} - - PLUTO_CONFIG_FILE=server.cfg - - PLUTO_GAME=iw5mp networks: - games-net - + t7x-zm-1: - image: ghcr.io/ayymoss/plutainer:latest + image: ghcr.io/ayymoss/plutainer:v2 container_name: t7x-zm-1 restart: unless-stopped ports: @@ -156,11 +156,11 @@ services: - /opt/GameFiles/T7xServerFiles:/home/plutainer/gamefiles:ro - ./t7x-zm-1:/home/plutainer/app environment: - - ALTER_GAME=t7x - - ALTER_CONFIG_FILE=server_zm.cfg - - ALTER_PORT=27017 - #- ALTER_MOD=2833912385 - #- ALTER_AUTO_UPDATE=false + - PLUTAINER_GAME=t7x + - PLUTAINER_CONFIG_FILE=server_zm.cfg + - PLUTAINER_PORT=27017 + #- PLUTAINER_MOD=2833912385 + #- PLUTAINER_AUTO_UPDATE=false networks: - games-net @@ -181,14 +181,16 @@ services: - ./IW4MAdmin/Localization:/app/Localization - ./IW4MAdmin/Database:/app/Database - ./IW4MAdmin/Log:/app/Log - - ./iw4xmp-1/gamefiles/userraw/logs/games_mp.log:/app/gamelogs/iw4xmp-1/gamefiles/userraw/logs/games_mp.log:ro - - ./iw5mp-1/plutonium/storage/iw5/logs/games_mp.log:/app/gamelogs/iw5mp-1/plutonium/storage/iw5/logs/games_mp.log:ro - - ./t4mp-1/plutonium/storage/t4/main/games_mp.log:/app/gamelogs/t4mp-1/plutonium/storage/t4/main/games_mp.log:ro - - ./t4sp-1/plutonium/storage/t4/main/games_zm.log:/app/gamelogs/t4sp-1/plutonium/storage/t4/main/games_zm.log:ro - - ./t5mp-1/plutonium/storage/t5/main/logs/games_mp.log:/app/gamelogs/t5mp-1/plutonium/storage/t5/main/logs/games_mp.log:ro - - ./t5sp-1/plutonium/storage/t5/mods/Stairway_to_hell_bo1/logs/games_zm.log:/app/gamelogs/t5sp-1/plutonium/storage/t5/mods/Stairway_to_hell_bo1/logs/games_zm.log:ro - - ./t6mp-1/plutonium/storage/t6/main/logs/games_mp.log:/app/gamelogs/t6mp-1/plutonium/storage/t6/main/logs/games_mp.log:ro - - ./t6zm-1/plutonium/storage/t6/main/logs/games_zm.log:/app/gamelogs/t6zm-1/plutonium/storage/t6/main/logs/games_zm.log:ro + # v2: mount each server's app/logs/ as a single read-only dir. log-watcher.sh + # maintains stable symlinks inside, regardless of where the game writes them. + - ./iw4xmp-1/logs:/app/gamelogs/iw4xmp-1:ro + - ./iw5mp-1/logs:/app/gamelogs/iw5mp-1:ro + - ./t4mp-1/logs:/app/gamelogs/t4mp-1:ro + - ./t4sp-1/logs:/app/gamelogs/t4sp-1:ro + - ./t5mp-1/logs:/app/gamelogs/t5mp-1:ro + - ./t5sp-1/logs:/app/gamelogs/t5sp-1:ro + - ./t6mp-1/logs:/app/gamelogs/t6mp-1:ro + - ./t6zm-1/logs:/app/gamelogs/t6zm-1:ro depends_on: iw4xmp-1: condition: service_healthy @@ -217,7 +219,7 @@ networks: config: - subnet: 100.64.10.0/30 gateway: 100.64.10.1 - + games-net: driver: bridge ipam: diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..a897f2e --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,155 @@ +# Migration: Plutainer v1 → v2 + +Plutainer v2 is now published as `ghcr.io/ayymoss/plutainer:latest` (and `:v2` as a permanent alias). The previous v1 image is frozen at `ghcr.io/ayymoss/plutainer:v1-final` and `:v1`. + +This page covers everything you need to migrate an existing v1 deployment. + +If you do not want to migrate right now, [pin v1-final](#path-a--stay-on-v1) instead and come back when you are ready. v1-final will continue to start, but it shows a deprecation banner every time and receives no further updates. + +--- + +## What changed? + +1. **Unified environment variables.** The old `PLUTO_*`, `IW4X_*`, and `ALTER_*` prefixes are gone. Everything is now `PLUTAINER_*` and game family is derived from `PLUTAINER_GAME`. +2. **New volume layout** inside `/home/plutainer/app/`: + ``` + v1 layout v2 layout + ───────── ───────── + app/gamefiles/ app/configs/ ← your *.cfg files (flat) + app/plutonium/ app/runtime/gamefiles/ + app/logs/ app/runtime/plutonium/ + app/logs/ ← stable symlinks to active logs + .plutainer-version ← layout marker (contains "2") + ``` + You now edit configs in one flat `app/configs/` directory. The entrypoint places relative symlinks from there into wherever the game engine expects them, so a single edit reaches both the engine path and any mod path. +3. **`app/logs/`** is now maintained by `log-watcher.sh` — every `*.log` under `app/` is surfaced as a stable relative symlink in this directory, so sidecars (IW4MAdmin) can read from one predictable path regardless of which mod dir the game is writing to. + +--- + +## Path A — Stay on v1 + +```yaml +# docker-compose.yml +services: + myserver: + image: ghcr.io/ayymoss/plutainer:v1-final # was: :latest + # ... everything else stays as-is +``` + +Then: + +```bash +docker compose pull +docker compose up -d +``` + +A deprecation banner appears in `docker logs` on every start. The server otherwise runs unchanged. + +--- + +## Path B — Migrate to v2 + +### Step 1 — Stop the container + +```bash +docker compose down +``` + +### Step 2 — Migrate the volume layout + +Run the bundled migration tool against your existing volume. The tool moves files in place, replacing engine-path config files with relative symlinks back into the new `app/configs/` tree. **No data is deleted.** + +```bash +docker run --rm \ + -v :/home/plutainer/app \ + --entrypoint /home/plutainer/.plutainer/migrate-v1-to-v2.sh \ + ghcr.io/ayymoss/plutainer:v2 +``` + +`` is whatever host path is bound to `/home/plutainer/app` in your compose. For a directory-style mount like `./t6zm-1:/home/plutainer/app`, that is `./t6zm-1`. + +**Preview first** by appending `--dry-run` — the tool prints every move it would make without touching the filesystem. + +After the tool runs, your volume contains: +- `app/configs/` (real files, moved from old engine paths) +- Symlinks from the old engine paths back into `app/configs/` so the game still finds them +- `app/.plutainer-version` containing `2` +- `app/runtime/gamefiles/`, `app/runtime/plutonium/` (your old `app/gamefiles/`, `app/plutonium/` moved here) + +### Step 3 — Rename your environment variables + +Anything not listed in this table keeps its old name (notably `PLUTO_SERVER_KEY`, `PLUTO_MAX_CLIENTS`, `IW4X_NET_LOG_IP` — these are unchanged). + +| v1 name | v2 name | +| -------------------- | ---------------------------------- | +| `PLUTO_GAME` | `PLUTAINER_GAME` | +| `PLUTO_CONFIG_FILE` | `PLUTAINER_CONFIG_FILE` | +| `PLUTO_PORT` | `PLUTAINER_PORT` | +| `PLUTO_HEALTHCHECK` | `PLUTAINER_HEALTHCHECK` | +| `PLUTO_MOD` | `PLUTAINER_MOD` | +| `PLUTO_SKIP_SEED` | `PLUTAINER_SKIP_SEED` | +| `PLUTO_AUTO_UPDATE` | `PLUTAINER_AUTO_UPDATE` | +| `PLUTO_SERVER_NAME` | `PLUTAINER_SERVER_NAME` | +| `PLUTO_EXTRA_ARGS` | `PLUTAINER_EXTRA_ARGS` | +| `IW4X_GAME` | `PLUTAINER_GAME=iw4x` | +| `IW4X_CONFIG_FILE` | `PLUTAINER_CONFIG_FILE` | +| `IW4X_PORT` | `PLUTAINER_PORT` | +| `IW4X_HEALTHCHECK` | `PLUTAINER_HEALTHCHECK` | +| `IW4X_MOD` | `PLUTAINER_MOD` | +| `IW4X_AUTO_UPDATE` | `PLUTAINER_AUTO_UPDATE` | +| `IW4X_SERVER_NAME` | `PLUTAINER_SERVER_NAME` | +| `IW4X_EXTRA_ARGS` | `PLUTAINER_EXTRA_ARGS` | +| `ALTER_GAME` | `PLUTAINER_GAME` (e.g. `t7x`) | +| `ALTER_CONFIG_FILE` | `PLUTAINER_CONFIG_FILE` | +| `ALTER_PORT` | `PLUTAINER_PORT` | +| `ALTER_HEALTHCHECK` | `PLUTAINER_HEALTHCHECK` | +| `ALTER_MOD` | `PLUTAINER_MOD` | +| `ALTER_SKIP_SEED` | `PLUTAINER_SKIP_SEED` | +| `ALTER_AUTO_UPDATE` | `PLUTAINER_AUTO_UPDATE` | +| `ALTER_SERVER_NAME` | `PLUTAINER_SERVER_NAME` | +| `ALTER_EXTRA_ARGS` | `PLUTAINER_EXTRA_ARGS` | + +Update the `image:` line in your compose: + +```yaml +image: ghcr.io/ayymoss/plutainer:latest # or :v2 (alias) +``` + +### Step 4 — Start + +```bash +docker compose up -d +docker compose logs -f +``` + +You should see a fresh boot ending in `Starting Plutonium Server: ...` (or the equivalent for IW4x/Alterware) followed by the game process. Healthcheck flips to `healthy` once RCON responds. + +--- + +## Troubleshooting + +**Container holds with a v1 deployment refusal block.** +You started v2 against either a v1 volume, v1 env vars, or both. The block lists exactly what was detected. Fix what it lists, then `docker compose up -d`. The container does not restart-loop — it stays `Up` with `sleep infinity` so you can read the message in `docker logs`. + +**Container holds with "Config file not found".** +Your `PLUTAINER_CONFIG_FILE` does not match a file in `app/configs/`. The block prints a case-insensitive hint (`Did you mean: ...?`) — filenames on Linux are case-sensitive. + +**RCON / healthcheck warns "Could not parse rcon_password".** +The parser accepts `set rcon_password "..."`, `seta rcon_password '...'`, unquoted values, and the bare `rcon_password "..."` form. It strips `//` line comments. Do **not** set `rcon_password` via `PLUTAINER_EXTRA_ARGS` — it cannot be read back. + +**I want the old behaviour where I edit configs directly in the engine path.** +Set `PLUTAINER_USE_RAW_CONFIGS=true`. The fan-out symlinks are skipped and the engine path becomes the source of truth. + +--- + +## Rolling back + +If something goes wrong, you can return to v1 immediately: + +```yaml +image: ghcr.io/ayymoss/plutainer:v1-final +``` + +`docker compose up -d`. The v2 migration tool only moved files inside the bind-mounted volume — it did not delete anything. v1-final will read the v1 layout it remembers, ignore the new `app/configs/` and `app/runtime/` dirs, and start as before. + +If you need to fully restore the pre-migration state (so the dirs from v2 are removed), back up first and then `mv` the contents of `app/runtime/gamefiles/` back to `app/gamefiles/` and `app/runtime/plutonium/` back to `app/plutonium/`. Symlinks at engine paths can be replaced with the real files from `app/configs/`. Most users will not need this. diff --git a/README.md b/README.md index 5264c53..2bba048 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ This repository contains the necessary files to build and run dedicated game ser The container is available on GitHub Container Registry: `ghcr.io/ayymoss/plutainer:latest` +> **Tag layout:** +> - `:latest` (and the `:v2` alias) — current Plutainer v2. New volume layout, unified `PLUTAINER_*` environment variables. Built from `main`. +> - `:v1-final` (and the `:v1` alias) — frozen snapshot of the old v1 image. Legacy `PLUTO_*`/`IW4X_*`/`ALTER_*` env vars, flat `app/gamefiles/` + `app/plutonium/` layout. Shows a deprecation banner on every start. No further updates, fixes, or security patches. +> +> **Upgrading from v1?** See [MIGRATION.md](MIGRATION.md) — covers the env var rename, the volume migration command (one `docker run`), and how to pin `:v1-final` if you want to defer the migration. + ## Overview The primary goal of this Docker image is to simplify the setup and management of dedicated servers for the following games: @@ -33,89 +39,138 @@ Instead of using a long `docker run` command, it is highly recommended to use `d ### Environment Variables -The container is configured entirely through environment variables. You must specify one of `PLUTO_GAME`, `IW4X_GAME`, or `ALTER_GAME`. +The container is configured entirely through environment variables. You must set `PLUTAINER_GAME` to one of the supported game tags. -#### General Variables +#### Unified (`PLUTAINER_*`) — apply to all games | Variable | Description | Default | | --- | --- | --- | -| `PLUTO_GAME` | The Plutonium game to run. **Required** for Plutonium. | | -| `IW4X_GAME` | The IW4x game to run. Must be `iw4x`. **Required** for IW4x. | | -| `ALTER_GAME` | The Alterware game to run. Must be `t7x`. **Required** for Alterware. | | +| `PLUTAINER_GAME` | **Required.** Game tag: `t4mp`, `t4sp`, `t5mp`, `t5sp`, `t6mp`, `t6zm`, `iw5mp`, `iw4x`, or `t7x`. | | +| `PLUTAINER_CONFIG_FILE` | **Required.** Filename of your server's config (e.g., `dedicated.cfg`). Lives in `app/configs/` (see [Volume Layout](#volume-layout)). | | +| `PLUTAINER_PORT` | Network port for the server. | Game-specific (see [Default Ports](#default-ports)). | +| `PLUTAINER_SERVER_NAME` | Display name used in startup logs. | Game-family-specific default. | +| `PLUTAINER_MOD` | Mod folder name (Plutonium/IW4x) or Steam Workshop ID (T7x). Omit if no mod. | | +| `PLUTAINER_AUTO_UPDATE` | Set to `"false"` to skip update checks at startup. | `true` | +| `PLUTAINER_HEALTHCHECK` | Set to `"false"` to disable the RCON health check. | `true` | +| `PLUTAINER_SKIP_SEED` | Set to `"true"` to skip first-run [config seeding](#bundled-config-seeds). | `false` | +| `PLUTAINER_EXTRA_ARGS` | Extra arguments appended to the launch command. | | +| `PLUTAINER_USE_RAW_CONFIGS` | Set to `"true"` to put cfg files directly in the engine path under `app/runtime/...` and skip the `app/configs/` symlink system. See [Raw Configs Mode](#raw-configs-mode). | `false` | +| `PLUTAINER_LOG_SYMLINKS` | Set to `"false"` to disable the [log symlink watcher](#log-symlinks). | `true` | +| `PLUTAINER_LOG_POLL_INTERVAL` | Seconds between log watcher polls. | `2` | + +#### Game-specific (unique to one stack) + +These cannot be unified because they only apply to a single engine family: + +| Variable | Description | Applies to | +| --- | --- | --- | +| `PLUTO_SERVER_KEY` | **Required for Plutonium.** Server key from . | Plutonium only | +| `PLUTO_MAX_CLIENTS` | Maximum players (Plutonium T5 only — other games set this in the cfg). | Plutonium T5 only | +| `IW4X_NET_LOG_IP` | IP:port for IW4x remote netlogging (`g_log_add`). | IW4x only | -See above for the game tags to use. (eg, `t6zm`) +#### Default ports -#### Plutonium Variables +| Game | Default | +| --- | --- | +| iw4x | 28960 | +| iw5 | 27016 | +| t4, t5 | 28960 | +| t6 | 4976 | +| t7x | 27017 | -| Variable | Description | Default | -| --- | --- | --- | -| `PLUTO_SERVER_KEY` | **Required.** Your server key from the Plutonium website. | | -| `PLUTO_CONFIG_FILE` | **Required.** The filename of your server's configuration file. | | -| `PLUTO_SERVER_NAME` | The name of your server. | "Plutonium Docker Server" | -| `PLUTO_PORT` | The network port for the server. | Game-specific default (e.g., 4976 for T6) | -| `PLUTO_MOD` | The name of the mod folder to load. Omit this if no mod needed. | | -| `PLUTO_MAX_CLIENTS` | **T5 Only!** The maximum number of players allowed. | | -| `PLUTO_AUTO_UPDATE` | Set to `"false"` to prevent the container from checking for updates on start. | `true` | -| `PLUTO_HEALTHCHECK` | Set to `"false"` to disable the health check. | `true` | -| `PLUTO_SKIP_SEED` | Set to `"true"` to skip first-run config seeding. See [Bundled Config Seeds](#bundled-config-seeds). | `false` | -| `PLUTO_EXTRA_ARGS` | Allows you to extend the start param. | | +> The legacy `PLUTO_*`/`IW4X_*`/`ALTER_*` prefixed env vars from the `:latest` (v1) image are **not accepted** on `:v2`. Use the unified `PLUTAINER_*` names above. The only old names that remain are `PLUTO_SERVER_KEY`, `PLUTO_MAX_CLIENTS`, and `IW4X_NET_LOG_IP` — they are single-family vars and never had a unified form. -(eg. `- "PLUTO_EXTRA_ARGS=+set SOMETHING=1 +set SOMETHING_ELSE=FOO"`) +*** -#### IW4x Variables +### Volume Layout -| Variable | Description | Default | +The container expects two volume mounts: + +| Container path | Purpose | Recommended host mount | | --- | --- | --- | -| `IW4X_CONFIG_FILE` | **Required.** The filename of your server's configuration file. | | -| `IW4X_SERVER_NAME` | The name of your server. | "IW4x Docker Server" | -| `IW4X_PORT` | The network port for the server. | `28960` | -| `IW4X_MOD` | The name of the mod folder to load. Omit this if no mod needed. | | -| `IW4X_AUTO_UPDATE` | Set to `"false"` to prevent the container from checking for updates on start. | `true` | -| `IW4X_HEALTHCHECK` | Set to `"false"` to disable the health check. | `true` | -| `IW4X_NET_LOG_IP` | The IP address and port for remote netlogging. | | -| `IW4X_EXTRA_ARGS` | Allows you to extend the start param. | | +| `/home/plutainer/gamefiles` | Read-only base game files you own. | Bind-mount with `:ro`. | +| `/home/plutainer/app` | Persistent server state, configs, and logs. | Bind-mount or named volume. | -#### Alterware Variables +On a fresh `app/` mount, the container initialises this layout on first start: -| Variable | Description | Default | -| --- | --- | --- | -| `ALTER_CONFIG_FILE` | **Required.** The filename of your server's configuration file (placed in `zone/`). | | -| `ALTER_SERVER_NAME` | The name of your server. | "T7x Docker Server" | -| `ALTER_PORT` | The network port for the server. | `27017` | -| `ALTER_MOD` | The Steam Workshop ID of the mod to load. Omit this if no mod needed. | | -| `ALTER_AUTO_UPDATE` | Set to `"false"` to prevent the container from checking for updates on start. | `true` | -| `ALTER_HEALTHCHECK` | Set to `"false"` to disable the health check. | `true` | -| `ALTER_SKIP_SEED` | Set to `"true"` to skip first-run config seeding. See [Bundled Config Seeds](#bundled-config-seeds). | `false` | -| `ALTER_EXTRA_ARGS` | Allows you to extend the start param. | | +``` +app/ + configs/ # Your server's *.cfg files. Edit here. + logs/ # Stable symlinks to active *.log files (see Log Symlinks). + runtime/ + gamefiles/ # Symlinks into the read-only gamefiles mount, plus + # writable game state (mods, maps, plutonium storage). + plutonium/ # Plutonium binaries and storage. + .plutainer-version # "2" — marks volume layout version. +``` + +**Where to put your `*.cfg` files:** drop them in `app/configs/` and set `PLUTAINER_CONFIG_FILE` to the filename. The container creates a symlink at the engine's expected path on each start, so the game still reads from its usual location — you just have one predictable place to edit. + +Example: for a T6 server with `PLUTAINER_CONFIG_FILE=dedicated_zm.cfg`, you edit `app/configs/dedicated_zm.cfg`, and the container symlinks `app/runtime/plutonium/storage/t6/dedicated_zm.cfg → ../../../../configs/dedicated_zm.cfg`. + +If you set `PLUTAINER_MOD`, the same cfg also gets symlinked into the mod's config dir (e.g. `app/runtime/plutonium/storage/t6//dedicated_zm.cfg`), so the engine finds it whether it looks in the base dir or the mod-scoped one. + +Nested configs (e.g. cfg files referenced by mods using subdirectories) stay at their engine path under `app/runtime/` and are not lifted to `configs/`. You can still edit them there. + +**Auto-lift:** if you set `PLUTAINER_CONFIG_FILE=dedicated.cfg` but the file is at `app/runtime/.../dedicated.cfg` (as a real file, not symlink) rather than `app/configs/dedicated.cfg`, the container moves it into `app/configs/` on next start and the symlink fan-out picks it up. One-time fix, no manual migration. + +**Filename mismatch:** if the configured file doesn't exist anywhere, the container refuses to start with a clear error and (if there's a case-only mismatch like `Server.cfg` vs `server.cfg`) tells you which case-insensitive match it found. Filenames remain case-sensitive — the container won't auto-rename. *** -### Volumes and Configuration Files +### Raw Configs Mode + +Set `PLUTAINER_USE_RAW_CONFIGS=true` to opt out of the `app/configs/` SOT model. With this on: + +- The engine config dir under `app/runtime/...` becomes the source of truth. +- `app/configs/` is left untouched (whatever's there is ignored). +- No symlinks are placed; cfg files live where the game reads them. +- Seed configs go directly into the engine dir. + +Use this when you want the v1 editing experience inside the v2 directory layout (e.g. tooling on your host expects to find `t6zm-1/runtime/plutonium/storage/t6/dedicated.cfg` as a real file). Default-off so most users get the "edit in one folder" affordance without thinking about it. + +You can toggle this between restarts. Plutainer doesn't migrate files when you flip the flag — that's on you. + +*** + +### Upgrading from v1 + +If you have an existing deployment that was running an older `:latest` (now `:v1-final`), pulling the new `:latest` (v2) refuses to start: v2 detects either v1 environment variables (`PLUTO_*`/`IW4X_*`/`ALTER_*`) or the v1 volume layout (`app/gamefiles/`, `app/plutonium/` at the top level with no `.plutainer-version` marker) and prints a combined refusal block in `docker logs` listing exactly what was detected, plus the two paths forward. -To persist your server configurations and provide the necessary game files, you must use Docker volumes. +**Full step-by-step guide:** [MIGRATION.md](MIGRATION.md) — env var mapping table, the one-command volume migration, dry-run option, and how to pin `:v1-final` if you would rather defer. -* **Game Files:** You need to mount your host machine's game files directory into the container at `/home/plutainer/gamefiles`. It is highly recommended to mount this as read-only (`:ro`) to prevent the container from modifying your base game files. -* **Config Files:** Your server's `.cfg` files should be included in the default expected locations for the games. Mount `/home/plutainer/app`. The expected relative game config defaults are... - * **IW4x:** `./gamefiles/userraw/` - * **Plutonium T4 (WaW):** `./gamefiles/main/` - * **Plutonium T5 (BO1):** `./plutonium/storage/t5/` - * **Plutonium IW5 (MW3):** `./gamefiles/admin/` - * **Plutonium T6 (BO2):** `./plutonium/storage/t6/` - * **Alterware T7x (BO3):** `./gamefiles/zone/` +Quick summary of the migration path: + +1. `docker compose down` +2. Run the migration tool once per volume (append `--dry-run` to preview): + ```sh + docker run --rm \ + -v :/home/plutainer/app \ + --entrypoint /home/plutainer/.plutainer/migrate-v1-to-v2.sh \ + ghcr.io/ayymoss/plutainer:v2 + ``` +3. Rename `PLUTO_*`/`IW4X_*`/`ALTER_*` env vars to `PLUTAINER_*` in your compose (full table in MIGRATION.md). +4. `docker compose up -d` + +If you also have IW4MAdmin sidecar mounts pointing at log paths like `./t6zm-1/plutonium/storage/...`, update them to `./t6zm-1/runtime/plutonium/storage/...` — or better, switch to the stable [log symlink directory](#log-symlinks). + +*** ### Bundled Config Seeds To make first-run setup painless, the image bundles default configs from community repos and copies them into the bind-mounted `app/` volume on container start. Files that already exist are **never overwritten** — existing user configs are always kept as-is. +Top-level `*.cfg` files from each seed bundle land in `app/configs/` (flat). Other assets (mod scripts, maps, nested cfgs, lobby scripts) land under `app/runtime/` at their engine-expected paths. + | Game | Source repo | | --- | --- | | Plutonium T4 | [xerxes-at/T4ServerConfigs](https://github.com/xerxes-at/T4ServerConfigs) | | Plutonium T5 | [xerxes-at/T5ServerConfig](https://github.com/xerxes-at/T5ServerConfig) | | Plutonium T6 | [xerxes-at/T6ServerConfigs](https://github.com/xerxes-at/T6ServerConfigs) | | Plutonium IW5 | [xerxes-at/IW5ServerConfig](https://github.com/xerxes-at/IW5ServerConfig) | -| Alterware T7x | [Dss0/t7-server-config](https://github.com/Dss0/t7-server-config) (includes the `t7x/lobby_scripts/` required for `sv_lobby_mode` to work) | +| Alterware T7x | [Dss0/t7-server-config](https://github.com/Dss0/t7-server-config) (includes `t7x/lobby_scripts/` required for `sv_lobby_mode`) | -To opt out — for example if you manage configs entirely yourself and don't want any default files appearing in your bind mount — set `PLUTO_SKIP_SEED=true` (Plutonium) or `ALTER_SKIP_SEED=true` (Alterware). +To opt out — for example if you manage configs entirely yourself and don't want any default files appearing in your bind mount — set `PLUTAINER_SKIP_SEED=true`. The seed snapshot is frozen at image build time. Pulling a newer image only seeds files that don't yet exist in your bind mount, so the upstream repo never silently overwrites your edits. @@ -125,19 +180,19 @@ The seed snapshot is frozen at image build time. Pulling a newer image only seed #### Mount Permissions -When you mount volumes from your host machine into the container, the `plutainer` (with UID `1000`) needs to have the appropriate permissions to read and write to those directories. If the ownership on your host directories is incorrect, the server may fail to start or be unable to save data. +When you mount volumes from your host machine into the container, the `plutainer` user (with UID `1000`) needs to have the appropriate permissions to read and write to those directories. If the ownership on your host directories is incorrect, the server may fail to start or be unable to save data. -On many desktop Linux distributions (like Debian), the first user you create is automatically assigned UID `1000`. If you are that user, you may not need to do anything. However, if you created the directories as `root` (e.g., using `sudo mkdir`), you will need to update their ownership. +On many desktop Linux distributions, the first user you create is automatically assigned UID `1000`. If you are that user, you may not need to do anything. However, if you created the directories as `root` (e.g., using `sudo mkdir`), you will need to update their ownership. #### How to Fix Permissions -To ensure the container has the correct access, you should change the ownership of your persistent data directory to match the container's user. Run the following command on your host machine, adjusting the path to match your setup: +To ensure the container has the correct access, change the ownership of your persistent data directory to match the container's user. Run the following command on your host machine, adjusting the path to match your setup: ```sh sudo chown -R 1000:1000 /opt/pluto-servers/t6zm-server-1/ ``` -The `-R` flag applies the ownership recursively, ensuring all files and sub-folders have the correct permissions. While the container only needs to *read* the game files, applying correct ownership to that volume as well is good practice to avoid any potential read-related issues. +The `-R` flag applies the ownership recursively, ensuring all files and sub-folders have the correct permissions. *** @@ -153,7 +208,43 @@ docker exec rcon-cli status docker exec -i rcon-cli ``` -Your server configuration file must have `rcon_password` set for `rcon-cli` to work. +Your server configuration file must have `rcon_password` set for `rcon-cli` to work. Accepted forms in the cfg: + +``` +set rcon_password "your_password_here" +seta rcon_password 'also_works' +set rcon_password unquoted_also_ok +``` + +Comment-only lines (`// ...`) are ignored. If multiple uncommented `rcon_password` lines exist, the last one wins. If the parser can't find one, the container prints a `[WARN]` at startup but keeps running — healthcheck and rcon-cli are then unavailable until you add it. **Do not** set `rcon_password` via `PLUTAINER_EXTRA_ARGS` — Plutainer cannot read it back from there. + +*** + +### Restart behavior + +Plutainer distinguishes between *configuration errors* (your fault) and *runtime crashes* (the game's fault): + +- **Configuration error** (e.g. missing `PLUTAINER_CONFIG_FILE`, wrong volume version, unparseable `PLUTAINER_GAME`): the container prints the error and then `sleep infinity` to **hold** in the `Up` state. No restart loop. Fix the issue and run `docker restart `. +- **Runtime crash** (wine exits, game segfaults, etc): the container sleeps **30 seconds** after the game process exits, then exits itself. Docker's `restart: unless-stopped` (or your chosen policy) then restarts it. This rate-limits crash loops to ~1 restart per 30s instead of hammering immediately. + +Healthcheck still runs on a held container and will eventually mark it unhealthy — useful signal for orchestration. + +*** + +### Log Symlinks + +The container maintains a flat directory of symlinks at `app/logs/` pointing at the active `*.log` file for each basename. Game logs move around per game/mod (e.g. `runtime/plutonium/storage/t5/mods//logs/games_zm.log`); the watcher surfaces them all in one predictable place so IW4MAdmin (or any other log reader) doesn't have to chase the exact path. + +Mount `app/logs/` as the source for downstream log consumers: + +```yaml +volumes: + - ./t6zm-1/logs:/app/gamelogs/t6zm-1:ro +``` + +Symlinks are relative, so they resolve correctly from the host, this container, or a sidecar container mounting the same `app/` volume. + +Disable with `PLUTAINER_LOG_SYMLINKS=false`; change poll interval with `PLUTAINER_LOG_POLL_INTERVAL` (default 2s). *** @@ -196,24 +287,24 @@ This issue does **not** occur if you are running IW4MAdmin directly on the host The container includes a robust health check script that verifies the server is running and responsive. It works by: 1. Detecting the game type and port. -2. Locating your server configuration file. +2. Locating your server configuration file in `app/configs/`. 3. Extracting your `rcon_password` from the config. 4. Sending an RCON `status` command to the server. 5. Checking for a valid response. -The health check is enabled by default. You can disable it by setting the corresponding environment variable (`PLUTO_HEALTHCHECK`, `IW4X_HEALTHCHECK`, or `ALTER_HEALTHCHECK`) to `"false"`. This can be useful for debugging or if you do not wish to set an RCON password. +The health check is enabled by default. You can disable it by setting `PLUTAINER_HEALTHCHECK=false`. This can be useful for debugging or if you do not wish to set an RCON password. -Please note that for the healthcheck to work correctly, games that support RCon whitelists need to have localhost permitted and/or "127.0.0.1" +For the healthcheck to work correctly, games that support RCon whitelists need to have localhost permitted and/or `127.0.0.1`. -To have your servers restarted automatically, simply add [Auto Heal](https://github.com/willfarrell/docker-autoheal) to the compose. +To have your servers restarted automatically, add [Auto Heal](https://github.com/willfarrell/docker-autoheal) to the compose. *** ### Support? -Discord Support: https://discord.gg/PjrFw4tNES +Discord Support: -Please note that I will not be supporting Plutonium-specific issues. There is an expectation that you're already familiar with Docker. If you're brand new, please visit https://docs.docker.com/get-started/ +Please note that I will not be supporting Plutonium-specific issues. There is an expectation that you're already familiar with Docker. If you're brand new, please visit This Discord is to be specific to Plutainer and its setup and configuration (including IW4MAdmin). @@ -221,5 +312,5 @@ This Discord is to be specific to Plutainer and its setup and configuration (inc #### Credits -- Corey, for a production testing ground @ https://cukservers.net/ -- HGM, for the name 'Plutainer' @ https://hgmserve.rs/ +- Corey, for a production testing ground @ +- HGM, for the name 'Plutainer' @ diff --git a/scripts/alterentry.sh b/scripts/alterentry.sh index d07e4bd..181a582 100644 --- a/scripts/alterentry.sh +++ b/scripts/alterentry.sh @@ -1,20 +1,27 @@ #!/bin/bash # -# This entrypoint script is responsible for validating the container's -# environment, setting sensible defaults, and launching the specified -# T7x (Black Ops 3) game server. +# Validate environment, prepare the game-files tree and configs/ symlinks, +# fetch t7x.exe, then launch the Alterware (T7x) server. # +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/game-config.sh" -# --- Step 1: Link Game Files --- -SOURCE_DIR="/home/plutainer/gamefiles" -DEST_DIR="/home/plutainer/app/gamefiles" +detect_game_type || hold_indefinitely "detect_game_type failed." +check_volume_version || hold_indefinitely "check_volume_version failed." +resolve_engine_config_dir +resolve_mod_config_dir +resolve_config_layout + +SOURCE_DIR="$PLUTAINER_SOURCE_DIR" +DEST_DIR="$PLUTAINER_GAMEFILES_DIR" mkdir -p "$DEST_DIR" +# --- Step 1: Link Game Files --- echo "Linking files for t7x (Black Ops III)..." -ln -sf "$SOURCE_DIR"/{codlogo.bmp,machinecfg,steam_api64.dll,steamclient64.dll,tier0_s64.dll,vstdlib_s64.dll} "$DEST_DIR"/ +link_files "$SOURCE_DIR" "$DEST_DIR" \ + codlogo.bmp machinecfg steam_api64.dll steamclient64.dll tier0_s64.dll vstdlib_s64.dll # T7x detects dedicated server mode by checking which executables exist: # is_server = has_flag("dedicated") || (!has_client && has_server) @@ -28,7 +35,7 @@ elif [[ -f "$SOURCE_DIR/BlackOps3.exe" ]]; then fi # Create zone/ as a real directory with symlinked contents so configs can be -# placed alongside the read-only game data (same approach as T4's main/) +# placed alongside the read-only game data (same approach as T4's main/). mkdir -p "$DEST_DIR/zone" # Guard the glob: bash leaves unmatched `*` literal, so an empty/missing # source zone/ would create a bogus symlink named `*`. @@ -36,85 +43,74 @@ if compgen -G "$SOURCE_DIR/zone/*" > /dev/null; then ln -sf "$SOURCE_DIR"/zone/* "$DEST_DIR"/zone/ fi - # --- Step 2: Download/Update T7x --- +# wget -N: timestamping. Only downloads when upstream is newer than local. ALTER_EXE_LOC="$DEST_DIR/t7x.exe" -if [[ ! -f "${ALTER_EXE_LOC}" ]]; then - echo "First container run detected. Downloading T7x... This may take a moment." - wget -q -O "$ALTER_EXE_LOC" https://master.bo3.eu/t7x/t7x.exe +if [[ -f "$ALTER_EXE_LOC" && "${PLUTAINER_AUTO_UPDATE:-}" == "false" ]]; then + echo "Skipping T7x update because PLUTAINER_AUTO_UPDATE is set to 'false'." else - if [[ "${ALTER_AUTO_UPDATE}" == "false" ]]; then - echo "Skipping T7x update because ALTER_AUTO_UPDATE is set to 'false'." - else + if [[ -f "$ALTER_EXE_LOC" ]]; then echo "Checking for T7x updates..." - wget -q -O "$ALTER_EXE_LOC" https://master.bo3.eu/t7x/t7x.exe + else + echo "First container run detected. Downloading T7x... This may take a moment." fi + wget -q -N -P "$DEST_DIR" https://master.bo3.eu/t7x/t7x.exe fi cd "$DEST_DIR" -# --- Step 2.5: Seed default configs from bundled community repo --- -# Idempotent: cp -n means existing user files are never overwritten. -# Skip with ALTER_SKIP_SEED=true. -if [[ "${ALTER_SKIP_SEED}" != "true" ]]; then - seed_configs t7x /home/plutainer/app/gamefiles -fi +# --- Step 3a: Auto-lift any user-placed real cfg from engine path --- +auto_lift_user_config -# --- Step 3: Validate Required Environment Variables --- -MISSING_VAR=false -INVALID_VAR=false -VALID_GAMES="t7x" -ALTER_SERVER_NAME=${ALTER_SERVER_NAME:-"T7x Docker Server"} - -if [[ -z "${ALTER_GAME}" ]]; then - echo "[ERROR] The 'ALTER_GAME' environment variable is not set." >&2 - MISSING_VAR=true -elif [[ ! " ${VALID_GAMES} " =~ " ${ALTER_GAME} " ]]; then - echo "[ERROR] Invalid value for 'ALTER_GAME': \"${ALTER_GAME}\"." >&2 - INVALID_VAR=true +# --- Step 3b: Seed default configs from bundled community repo --- +# t7x seed bundle has two top-level dirs: `zone/` (configs) and `t7x/` (lobby +# scripts). cfg_root_rel="zone" lifts top-level `zone/*.cfg` files into +# CONFIG_SOT_DIR; everything else stays under runtime/gamefiles/. +if [[ "${PLUTAINER_SKIP_SEED:-}" != "true" ]]; then + seed_configs t7x "$DEST_DIR" "zone" fi -if [[ -z "${ALTER_CONFIG_FILE}" ]]; then - echo "[ERROR] The 'ALTER_CONFIG_FILE' environment variable is not set." >&2 - echo " > You must specify the name of the server configuration file (e.g., 'server_zm.cfg')." >&2 - MISSING_VAR=true -fi +# --- Step 4: Fan-out configs/ → engine config dir --- +# (Alterware MOD is a Steam Workshop ID, not a filesystem path — no MOD dir.) +link_configs "$ENGINE_CONFIG_DIR" -if [[ "$MISSING_VAR" == "true" || "$INVALID_VAR" == "true" ]]; then - echo "-------------------------------------------------" >&2 - if [[ "$INVALID_VAR" == "true" ]]; then - echo "An invalid value was provided. Valid game modes are: ${VALID_GAMES}" >&2 - fi - echo "One or more configuration errors found. Halting startup." >&2 - echo "Exiting in 10 seconds..." >&2 - sleep 10 - exit 1 +# --- Step 5: Validate environment + ensure config file exists --- +PLUTAINER_SERVER_NAME="${PLUTAINER_SERVER_NAME:-T7x Docker Server}" + +if [[ "${PLUTAINER_GAME}" != "t7x" ]]; then + hold_indefinitely "PLUTAINER_GAME must be 't7x' for the Alterware entrypoint." +fi +if [[ -z "${PLUTAINER_CONFIG_FILE:-}" ]]; then + hold_indefinitely "PLUTAINER_CONFIG_FILE is not set. Specify the filename of your server config (e.g. 'server_zm.cfg')." +fi +if ! ensure_config_present; then + hold_indefinitely "Config file not found. See [ERROR] above." fi -# --- Step 4: Set Default Port (If Needed) --- -if [[ -z "${ALTER_PORT}" ]]; then - echo "Optional ALTER_PORT is not set, determining default for ${ALTER_GAME}..." - resolve_default_port "${ALTER_GAME}" || { sleep 10; exit 1; } - ALTER_PORT="${DEFAULT_PORT}" - echo "Default port set to ${ALTER_PORT}" +# --- Step 6: Resolve port --- +if [[ -z "${PLUTAINER_PORT:-}" ]]; then + echo "PLUTAINER_PORT not set, using default for t7x..." + resolve_default_port "t7x" || hold_indefinitely "Could not resolve default port." + PLUTAINER_PORT="${DEFAULT_PORT}" + echo "Default port set to ${PLUTAINER_PORT}" fi -# --- Step 5: Build Server Command Arguments --- +# --- Step 7: Build Server Command Arguments --- declare -a CMD_ARGS=( -dedicated - +set fs_game "${ALTER_MOD:-}" - +set net_port "${ALTER_PORT}" + +set fs_game "${PLUTAINER_MOD:-}" + +set net_port "${PLUTAINER_PORT}" +set logfile "2" - +exec "${ALTER_CONFIG_FILE}" + +exec "${PLUTAINER_CONFIG_FILE}" ) -if [[ -n "${ALTER_EXTRA_ARGS}" ]]; then - CMD_ARGS+=(${ALTER_EXTRA_ARGS}) +if [[ -n "${PLUTAINER_EXTRA_ARGS:-}" ]]; then + CMD_ARGS+=(${PLUTAINER_EXTRA_ARGS}) fi -# --- Step 6: Launch the T7x Server --- +# --- Step 8: Launch (with 30s crash throttle) --- # T7x requires a display even in dedicated/headless mode, so start a virtual -# framebuffer for Wine before launching +# framebuffer for Wine before launching. echo "Starting virtual display..." rm -f /tmp/.X99-lock Xvfb :99 -screen 0 320x240x24 & @@ -122,6 +118,6 @@ sleep 1 /home/plutainer/.plutainer/log-watcher.sh & -echo "Starting T7x Server: ${ALTER_SERVER_NAME}" -echo "EXECUTING: wine t7x.exe ${CMD_ARGS[@]}" -exec wine t7x.exe "${CMD_ARGS[@]}" +echo "Starting T7x Server: ${PLUTAINER_SERVER_NAME}" +echo "EXECUTING: wine t7x.exe ${CMD_ARGS[*]}" +launch_game wine t7x.exe "${CMD_ARGS[@]}" diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 04fc11e..cda3a57 100644 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -1,8 +1,12 @@ #!/bin/bash # -# This entrypoint script is responsible for branding and delegating the -# server startup to the appropriate game-specific script. +# Top-level entrypoint. Detects the game family, validates the volume +# version, then delegates to the appropriate game-specific entry script. # +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/game-config.sh" # --- Branding --- cat << "EOF" @@ -19,20 +23,50 @@ echo echo "Brought to you by Ayymoss" echo -if [[ -n "${PLUTO_GAME}" ]]; then - echo "Plutonium game type detected. Handing off to Plutonium entrypoint..." - exec /home/plutainer/.plutainer/plutoentry.sh -elif [[ -n "${IW4X_GAME}" ]]; then - echo "IW4x game type detected. Handing off to IW4x entrypoint..." - exec /home/plutainer/.plutainer/iw4xentry.sh -elif [[ -n "${ALTER_GAME}" ]]; then - echo "Alterware game type detected (${ALTER_GAME}). Handing off to Alterware entrypoint..." - exec /home/plutainer/.plutainer/alterentry.sh -else - echo "-------------------------------------------------" >&2 - echo "[ERROR] No game type specified." >&2 - echo " > Please set PLUTO_GAME, IW4X_GAME, or ALTER_GAME environment variable." >&2 - echo "Exiting in 10 seconds..." >&2 - sleep 10 - exit 1 +# --- v1 deployment detection (combined env + volume) --- +# Order matters: run BOTH detectors before deciding to hold, so the user sees +# every reason for refusal in a single block instead of fixing one thing, +# restarting, and hitting the next refusal. +HAS_LEGACY_ENV=false +HAS_V1_VOLUME=false + +detect_legacy_env_vars || HAS_LEGACY_ENV=true + +# check_volume_version handles fresh-init and v2 marker validation as side +# effects. It sets V1_VOLUME_DETECTED=true when v1 dirs are present without +# a marker (without printing); any other return-1 path means a real volume +# state error (e.g. unknown future-version marker), which already printed. +VOLUME_CHECK_RC=0 +check_volume_version || VOLUME_CHECK_RC=$? +if [[ "${V1_VOLUME_DETECTED:-false}" == "true" ]]; then + HAS_V1_VOLUME=true +elif [[ $VOLUME_CHECK_RC -ne 0 ]]; then + # Non-v1 volume error (already printed its own message). + hold_indefinitely "Volume layout check failed. See above." +fi + +if [[ "$HAS_LEGACY_ENV" == "true" || "$HAS_V1_VOLUME" == "true" ]]; then + print_v1_migration_block "$HAS_LEGACY_ENV" "$HAS_V1_VOLUME" + hold_indefinitely "v1 deployment detected. Pick one of the paths above, then restart." +fi + +# --- Detect game type from PLUTAINER_GAME --- +if ! detect_game_type; then + hold_indefinitely "Set PLUTAINER_GAME to one of: t4mp, t4sp, t5mp, t5sp, t6mp, t6zm, iw5mp, iw4x, t7x" fi + +# --- Dispatch to game-specific entrypoint --- +case "$GAME_TYPE" in + plutonium) + echo "Plutonium game detected (${GAME_NAME}). Handing off to Plutonium entrypoint..." + exec "$SCRIPT_DIR/plutoentry.sh" + ;; + iw4x) + echo "IW4x game detected. Handing off to IW4x entrypoint..." + exec "$SCRIPT_DIR/iw4xentry.sh" + ;; + alterware) + echo "Alterware game detected (${GAME_NAME}). Handing off to Alterware entrypoint..." + exec "$SCRIPT_DIR/alterentry.sh" + ;; +esac diff --git a/scripts/game-config.sh b/scripts/game-config.sh index 8a8f2f1..68eb919 100644 --- a/scripts/game-config.sh +++ b/scripts/game-config.sh @@ -1,42 +1,95 @@ #!/bin/bash # # Shared game configuration library. -# Sourced by entrypoint scripts, healthcheck, and rcon-cli to avoid -# duplicating port defaults, config path resolution, and game detection. +# Sourced by entrypoint scripts, healthcheck, and rcon-cli. # +# Volume layout (v2): +# /home/plutainer/app/ +# configs/ # User-facing config files (flat). +# # Real files unless PLUTAINER_USE_RAW_CONFIGS=true. +# logs/ # Stable symlinks to active *.log files +# # (maintained by log-watcher.sh). +# runtime/ +# gamefiles/ # Symlinks into host /home/plutainer/gamefiles +# # plus writable game state. +# plutonium/ # Plutonium binaries + storage state. +# .plutainer-version # Layout marker (contains "2"). +# + +PLUTAINER_VOLUME_VERSION=2 +PLUTAINER_APP_DIR="/home/plutainer/app" +PLUTAINER_CONFIGS_DIR="$PLUTAINER_APP_DIR/configs" +PLUTAINER_RUNTIME_DIR="$PLUTAINER_APP_DIR/runtime" +PLUTAINER_GAMEFILES_DIR="$PLUTAINER_RUNTIME_DIR/gamefiles" +PLUTAINER_PLUTONIUM_DIR="$PLUTAINER_RUNTIME_DIR/plutonium" +PLUTAINER_SOURCE_DIR="/home/plutainer/gamefiles" + +# Halt without exiting. Container stays in the "running" state, docker +# restart policies won't fire a loop, healthchecks will eventually mark it +# unhealthy — user fixes config and runs `docker restart`. +hold_indefinitely() { + local msg="${1:-Refusing to start.}" + echo "-------------------------------------------------" >&2 + echo "$msg" >&2 + echo "[INFO] Holding container running (sleep infinity) to prevent a restart loop." >&2 + echo "[INFO] Fix the issue, then run: docker restart " >&2 + exec sleep infinity +} + +# Run the game binary, then sleep 30s before letting the container exit. +# Restart policies (e.g. `restart: unless-stopped`) react to container exit +# but docker compose has no native min-delay knob — the in-script sleep is +# how we throttle real crashes to one restart per ~30s. +# Args: command + its arguments (e.g. wine ...). +launch_game() { + set +e + "$@" + local rc=$? + set -e + + echo "[INFO] Game process exited (rc=$rc)." >&2 + echo "[INFO] Sleeping 30s before container exit to throttle restart." >&2 + sleep 30 + exit "$rc" +} + +# Derive the game family ("plutonium", "iw4x", "alterware") from PLUTAINER_GAME. +# Returns 1 if unknown. +derive_family() { + case "$1" in + iw5mp|t4mp|t4sp|t5mp|t5sp|t6mp|t6zm) echo "plutonium" ;; + iw4x) echo "iw4x" ;; + t7x) echo "alterware" ;; + *) return 1 ;; + esac +} -# Detect the game type from environment variables and set unified variables: -# GAME_TYPE, GAME_NAME, BASE_GAME, CONFIG_FILE, CUSTOM_PORT, HEALTHCHECK_FLAG +# Populate GAME_TYPE, GAME_NAME, BASE_GAME, CONFIG_FILE, CUSTOM_PORT, +# HEALTHCHECK_FLAG from PLUTAINER_*. detect_game_type() { - if [[ -n "${PLUTO_GAME}" ]]; then - GAME_TYPE="plutonium" - GAME_NAME="${PLUTO_GAME}" - BASE_GAME="${PLUTO_GAME%??}" - CONFIG_FILE="${PLUTO_CONFIG_FILE}" - CUSTOM_PORT="${PLUTO_PORT}" - HEALTHCHECK_FLAG="${PLUTO_HEALTHCHECK}" - elif [[ -n "${IW4X_GAME}" ]]; then - GAME_TYPE="iw4x" - GAME_NAME="${IW4X_GAME}" - BASE_GAME="iw4x" - CONFIG_FILE="${IW4X_CONFIG_FILE}" - CUSTOM_PORT="${IW4X_PORT}" - HEALTHCHECK_FLAG="${IW4X_HEALTHCHECK}" - elif [[ -n "${ALTER_GAME}" ]]; then - GAME_TYPE="alterware" - GAME_NAME="${ALTER_GAME}" - BASE_GAME="${ALTER_GAME}" - CONFIG_FILE="${ALTER_CONFIG_FILE}" - CUSTOM_PORT="${ALTER_PORT}" - HEALTHCHECK_FLAG="${ALTER_HEALTHCHECK}" - else - echo "[ERROR] No game type detected. Set PLUTO_GAME, IW4X_GAME, or ALTER_GAME." >&2 + if [[ -z "${PLUTAINER_GAME:-}" ]]; then + echo "[ERROR] No game specified. Set PLUTAINER_GAME (e.g. t6zm, iw4x, t7x)." >&2 return 1 fi + + GAME_NAME="${PLUTAINER_GAME}" + GAME_TYPE="$(derive_family "$GAME_NAME")" || { + echo "[ERROR] Unknown PLUTAINER_GAME value: '${GAME_NAME}'." >&2 + return 1 + } + + case "$GAME_TYPE" in + plutonium) BASE_GAME="${GAME_NAME%??}" ;; + iw4x) BASE_GAME="iw4x" ;; + alterware) BASE_GAME="${GAME_NAME}" ;; + esac + + CONFIG_FILE="${PLUTAINER_CONFIG_FILE:-}" + CUSTOM_PORT="${PLUTAINER_PORT:-}" + HEALTHCHECK_FLAG="${PLUTAINER_HEALTHCHECK:-}" } -# Resolve the default port for a given BASE_GAME. -# Sets DEFAULT_PORT. Returns 1 if the game is unknown. +# Set DEFAULT_PORT based on BASE_GAME (or the arg). resolve_default_port() { local base_game="${1:-$BASE_GAME}" case "${base_game}" in @@ -52,29 +105,20 @@ resolve_default_port() { esac } -# Resolve the config file path for the current game. -# Sets CONFIG_PATH. Requires GAME_TYPE, BASE_GAME, and CONFIG_FILE to be set. -resolve_config_path() { +# Set ENGINE_CONFIG_DIR — the directory where the game engine reads cfg files +# from. Entrypoints place symlinks here that point back into CONFIG_SOT_DIR +# (unless PLUTAINER_USE_RAW_CONFIGS is true, in which case engine path IS the SOT). +resolve_engine_config_dir() { case "${GAME_TYPE}" in - "plutonium") - case "${BASE_GAME}" in - "t4") CONFIG_PATH="/home/plutainer/app/gamefiles/main/${CONFIG_FILE}" ;; - "iw5") CONFIG_PATH="/home/plutainer/app/gamefiles/admin/${CONFIG_FILE}" ;; - *) CONFIG_PATH="/home/plutainer/app/plutonium/storage/${BASE_GAME}/${CONFIG_FILE}" ;; - esac - ;; - "iw4x") - CONFIG_PATH="/home/plutainer/app/gamefiles/userraw/${CONFIG_FILE}" - ;; - "alterware") + plutonium) case "${BASE_GAME}" in - "t7x") CONFIG_PATH="/home/plutainer/app/gamefiles/zone/${CONFIG_FILE}" ;; - *) - echo "[ERROR] Unknown Alterware game '${BASE_GAME}'." >&2 - return 1 - ;; + t4) ENGINE_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/main" ;; + iw5) ENGINE_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/admin" ;; + *) ENGINE_CONFIG_DIR="$PLUTAINER_PLUTONIUM_DIR/storage/${BASE_GAME}" ;; esac ;; + iw4x) ENGINE_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/userraw" ;; + alterware) ENGINE_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/zone" ;; *) echo "[ERROR] Unknown game type '${GAME_TYPE}'." >&2 return 1 @@ -82,27 +126,379 @@ resolve_config_path() { esac } -# Copy bundled seed configs into a destination directory without overwriting -# anything the user has already placed there. -# Args: +# Set MOD_CONFIG_DIR — the dir where the game looks for cfg files inside the +# active mod (fs_game), if PLUTAINER_MOD is set. Empty when no mod or N/A +# (e.g. alterware uses Steam Workshop IDs, not filesystem paths). +resolve_mod_config_dir() { + MOD_CONFIG_DIR="" + [[ -z "${PLUTAINER_MOD:-}" ]] && return 0 + case "$GAME_TYPE" in + plutonium) + case "$BASE_GAME" in + t4|iw5) MOD_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/$PLUTAINER_MOD" ;; + *) MOD_CONFIG_DIR="$PLUTAINER_PLUTONIUM_DIR/storage/$BASE_GAME/$PLUTAINER_MOD" ;; + esac + ;; + iw4x) + MOD_CONFIG_DIR="$PLUTAINER_GAMEFILES_DIR/$PLUTAINER_MOD" + ;; + # alterware: PLUTAINER_MOD is a Steam Workshop ID; no filesystem cfg dir. + esac +} + +# Decide the config source-of-truth. Default: configs/ (symlink fan-out to +# engine dir). With PLUTAINER_USE_RAW_CONFIGS=true: engine dir IS the SOT +# (no symlinks, cfgs live where the game reads them). +# Requires ENGINE_CONFIG_DIR set first (call resolve_engine_config_dir). +# Sets CONFIG_SOT_DIR, ALT_CONFIG_DIR, CONFIG_PATH. +resolve_config_layout() { + if [[ "${PLUTAINER_USE_RAW_CONFIGS:-}" == "true" ]]; then + CONFIG_SOT_DIR="$ENGINE_CONFIG_DIR" + ALT_CONFIG_DIR="$PLUTAINER_CONFIGS_DIR" + else + CONFIG_SOT_DIR="$PLUTAINER_CONFIGS_DIR" + ALT_CONFIG_DIR="$ENGINE_CONFIG_DIR" + fi + CONFIG_PATH="$CONFIG_SOT_DIR/$CONFIG_FILE" +} + +# Convenience for callers (healthcheck.sh, rcon-cli) that only need +# CONFIG_PATH set. Runs the chain end-to-end. +resolve_config_path() { + resolve_engine_config_dir || return 1 + resolve_config_layout +} + +# Symlink specific named entries from a source dir into a destination dir. +# Skips (with a warning) any names that don't exist — avoids the dangling +# symlink trap that bash brace expansion `{a,b,c}` creates when files are +# missing. Existing dest entries are replaced (ln -sf). +# Usage: link_files [name2 ...] +link_files() { + local src="$1" dest="$2" + shift 2 + local name + for name in "$@"; do + if [[ -e "$src/$name" ]]; then + ln -sf "$src/$name" "$dest/" + else + echo "[WARN] missing $src/$name — skipping symlink" >&2 + fi + done +} + +# Copy bundled community seed configs into the volume on first run. +# Strategy: +# - Top-level *.cfg files inside the seed's "config root" subdir +# (cfg_root_rel within the seed bundle) land in CONFIG_SOT_DIR. +# - Everything else (assets, mod scripts, nested cfgs, etc) lands under +# asset_root, preserving the seed's relative path. +# Always idempotent: never overwrites a file that already exists. +# Args: seed_configs() { - local game="$1" dest="$2" + local game="$1" asset_root="$2" cfg_root_rel="${3:-}" local src="/home/plutainer/.plutainer/seed-configs/${game}" [[ -d "$src" ]] || return 0 - mkdir -p "$dest" - cp -rn "$src/." "$dest/" 2>/dev/null || true + mkdir -p "$asset_root" "$CONFIG_SOT_DIR" + + local rel parent dest + while IFS= read -r -d '' relpath; do + rel="${relpath#./}" + parent="$(dirname "$rel")" + [[ "$parent" == "." ]] && parent="" + if [[ "$rel" == *.cfg && "$parent" == "$cfg_root_rel" ]]; then + dest="$CONFIG_SOT_DIR/$(basename "$rel")" + else + dest="$asset_root/$rel" + fi + mkdir -p "$(dirname "$dest")" + [[ -e "$dest" ]] || cp "$src/$rel" "$dest" + done < <(cd "$src" && find . -type f -print0) +} + +# For every *.cfg in configs/, place a relative symlink at each provided +# engine dir. Skips dirs that don't exist; warns on real-file collisions +# (refuses to overwrite a non-symlink); reaps cfg symlinks under each engine +# dir whose target no longer exists. +# No-op when PLUTAINER_USE_RAW_CONFIGS is true (engine path IS the SOT). +# Args: [engine_dir2 ...] +link_configs() { + [[ "${PLUTAINER_USE_RAW_CONFIGS:-}" == "true" ]] && return 0 + [[ -d "$PLUTAINER_CONFIGS_DIR" ]] || return 0 + + local engine_dir f base link target_rel + for engine_dir in "$@"; do + [[ -n "$engine_dir" ]] || continue + mkdir -p "$engine_dir" + + # Fan-out: configs/.cfg -> engine_dir/.cfg + for f in "$PLUTAINER_CONFIGS_DIR"/*.cfg; do + [[ -e "$f" ]] || continue + base="$(basename "$f")" + link="$engine_dir/$base" + if [[ -e "$link" && ! -L "$link" ]]; then + echo "[link_configs] WARNING: real file at $link blocks symlink to $f" >&2 + echo " Move or delete the real file if you want it managed via configs/." >&2 + continue + fi + target_rel=$(realpath --relative-to="$engine_dir" "$f") + ln -sfn "$target_rel" "$link" + done + + # Reap: drop cfg symlinks here whose source no longer resolves. + for link in "$engine_dir"/*.cfg; do + [[ -L "$link" && ! -e "$link" ]] || continue + echo "[link_configs] reaping dangling: $link" >&2 + rm -f "$link" + done + done +} + +# If the user has placed a REAL (non-symlink) cfg file at the engine path, +# treat that as authoritative and move it into the SOT location. Engine-path +# real file is the strongest signal of user intent: they manually wrote it +# where the game reads from. Overrides anything already in SOT. +# Runs BEFORE seed_configs so seed (cp -n) doesn't paper over user intent. +# Requires CONFIG_SOT_DIR, ALT_CONFIG_DIR, CONFIG_FILE set. +auto_lift_user_config() { + local cfg="${CONFIG_FILE:-}" + [[ -z "$cfg" ]] && return 0 + [[ "${PLUTAINER_USE_RAW_CONFIGS:-}" == "true" ]] && return 0 + + local alt_path="$ALT_CONFIG_DIR/$cfg" + local sot_path="$CONFIG_SOT_DIR/$cfg" + + if [[ -f "$alt_path" && ! -L "$alt_path" ]]; then + echo "[INFO] Auto-lift: real file at $alt_path — moving to $sot_path (v2 SOT)." + mkdir -p "$CONFIG_SOT_DIR" + mv -f "$alt_path" "$sot_path" + fi } -# Extract the RCON password from a config file. -# Sets RCON_PASSWORD. Requires CONFIG_PATH to be set. +# Verify $CONFIG_FILE exists at the SOT location. Returns 1 with a +# case-insensitive find hint if absent. Run AFTER seed_configs + link_configs +# so any gap-fill has had its chance. +# Requires CONFIG_SOT_DIR, ALT_CONFIG_DIR, CONFIG_FILE set. +ensure_config_present() { + local cfg="$CONFIG_FILE" + local sot_path="$CONFIG_SOT_DIR/$cfg" + + if [[ -e "$sot_path" ]]; then + return 0 + fi + + echo "[ERROR] PLUTAINER_CONFIG_FILE='$cfg' but no such file exists." >&2 + echo " Looked at:" >&2 + echo " $sot_path" >&2 + echo " $ALT_CONFIG_DIR/$cfg" >&2 + local match + match=$(find "$CONFIG_SOT_DIR" "$ALT_CONFIG_DIR" -maxdepth 1 -type f -iname "$cfg" 2>/dev/null | head -1) + if [[ -n "$match" ]]; then + echo " Did you mean: $(basename "$match") ? (filenames are case-sensitive on Linux)" >&2 + else + echo " Available cfgs in $CONFIG_SOT_DIR/:" >&2 + if compgen -G "$CONFIG_SOT_DIR/*.cfg" > /dev/null; then + (cd "$CONFIG_SOT_DIR" && ls -1 *.cfg) | sed 's/^/ /' >&2 + else + echo " (none)" >&2 + fi + fi + return 1 +} + +# Scan environment for v1-era legacy env var names. Populates +# LEGACY_ENVS_FOUND with each name that is set+non-empty. Returns 0 if none +# found (clean v2 env), 1 if any are present. Caller is responsible for +# printing the unified refusal block. +detect_legacy_env_vars() { + LEGACY_ENVS_FOUND=() + local v + local legacy_names=( + PLUTO_GAME PLUTO_CONFIG_FILE PLUTO_PORT PLUTO_HEALTHCHECK + PLUTO_SKIP_SEED PLUTO_AUTO_UPDATE PLUTO_MOD PLUTO_SERVER_NAME PLUTO_EXTRA_ARGS + IW4X_GAME IW4X_CONFIG_FILE IW4X_PORT IW4X_HEALTHCHECK + IW4X_AUTO_UPDATE IW4X_MOD IW4X_SERVER_NAME IW4X_EXTRA_ARGS + ALTER_GAME ALTER_CONFIG_FILE ALTER_PORT ALTER_HEALTHCHECK + ALTER_SKIP_SEED ALTER_AUTO_UPDATE ALTER_MOD ALTER_SERVER_NAME ALTER_EXTRA_ARGS + ) + for v in "${legacy_names[@]}"; do + if [[ -n "${!v:-}" ]]; then + LEGACY_ENVS_FOUND+=("$v") + fi + done + [[ ${#LEGACY_ENVS_FOUND[@]} -eq 0 ]] +} + +# Check the mounted app/ volume's layout state. +# Outcomes (silent on v1 detection — the unified refusal block is printed by +# entrypoint.sh via print_v1_migration_block): +# - Marker present + matches PLUTAINER_VOLUME_VERSION: ensure expected dirs +# exist, return 0. +# - Marker present but version mismatch (e.g. future v3 marker under v2 +# image): print specific error, return 1. +# - Marker absent + v1 layout dirs present: set V1_VOLUME_DETECTED=true, +# return 1. No print. +# - Marker absent + no v1 dirs: fresh volume — initialise as v2, return 0. +check_volume_version() { + V1_VOLUME_DETECTED=false + local marker="$PLUTAINER_APP_DIR/.plutainer-version" + + if [[ -f "$marker" ]]; then + local v + v="$(cat "$marker" 2>/dev/null || echo "")" + if [[ "$v" != "$PLUTAINER_VOLUME_VERSION" ]]; then + echo "[ERROR] Volume marker reports version '$v'; this image expects '$PLUTAINER_VOLUME_VERSION'." >&2 + echo "[ERROR] You appear to be running an older image against a newer volume, or vice-versa." >&2 + return 1 + fi + mkdir -p "$PLUTAINER_CONFIGS_DIR" "$PLUTAINER_APP_DIR/logs" "$PLUTAINER_RUNTIME_DIR" + return 0 + fi + + # Marker missing. Distinguish v1 volume vs fresh volume. + if [[ -d "$PLUTAINER_APP_DIR/plutonium" || -d "$PLUTAINER_APP_DIR/gamefiles" ]]; then + V1_VOLUME_DETECTED=true + return 1 + fi + + # Fresh volume — initialise v2. + mkdir -p "$PLUTAINER_CONFIGS_DIR" "$PLUTAINER_APP_DIR/logs" "$PLUTAINER_RUNTIME_DIR" + echo "$PLUTAINER_VOLUME_VERSION" > "$marker" + echo "[INFO] Initialised fresh v2 volume at $PLUTAINER_APP_DIR" +} + +# Combined v1-deployment refusal block. Adapts to what was detected. +# Args: $1=has_legacy_env (true/false), $2=has_v1_volume (true/false). +# Reads LEGACY_ENVS_FOUND[] populated by detect_legacy_env_vars. +print_v1_migration_block() { + local has_legacy_env="${1:-false}" has_v1_volume="${2:-false}" + + cat >&2 <<'HEADER' +======================================================================== +[ERROR] Plutainer v2 cannot start against a v1 deployment. + +Detected on this container: +HEADER + + if [[ "$has_legacy_env" == "true" ]]; then + echo " - Legacy env vars set: ${LEGACY_ENVS_FOUND[*]}" >&2 + fi + if [[ "$has_v1_volume" == "true" ]]; then + echo " - v1 volume layout (app/plutonium/ or app/gamefiles/ present, no .plutainer-version marker)" >&2 + fi + + cat >&2 <<'PATHS' + +You have two paths. Pick one. + +──────────────────────────────────────────────────────────────────────── +PATH A — Stay on v1 (frozen, no further updates) + + In your compose file, pin: + image: ghcr.io/ayymoss/plutainer:v1-final + + Then: docker compose up -d + Server starts as before. A deprecation banner will appear on every + start until you migrate. + +──────────────────────────────────────────────────────────────────────── +PATH B — Migrate to v2 (recommended) + + 1. docker compose down + + 2. Migrate the volume layout (no data is deleted; --dry-run previews): + docker run --rm \ + -v :/home/plutainer/app \ + --entrypoint /home/plutainer/.plutainer/migrate-v1-to-v2.sh \ + ghcr.io/ayymoss/plutainer:v2 + + is the host path bound to /home/plutainer/app + in your compose (e.g. ./t6zm-1). + + 3. Rename env vars in your compose (mapping table — anything not listed + here keeps its old name, e.g. PLUTO_SERVER_KEY is unchanged): + PLUTO_GAME → PLUTAINER_GAME + PLUTO_CONFIG_FILE → PLUTAINER_CONFIG_FILE + PLUTO_PORT → PLUTAINER_PORT + PLUTO_HEALTHCHECK → PLUTAINER_HEALTHCHECK + PLUTO_MOD → PLUTAINER_MOD + PLUTO_SKIP_SEED → PLUTAINER_SKIP_SEED + PLUTO_AUTO_UPDATE → PLUTAINER_AUTO_UPDATE + PLUTO_SERVER_NAME → PLUTAINER_SERVER_NAME + PLUTO_EXTRA_ARGS → PLUTAINER_EXTRA_ARGS + IW4X_GAME → PLUTAINER_GAME=iw4x + IW4X_CONFIG_FILE → PLUTAINER_CONFIG_FILE + IW4X_PORT → PLUTAINER_PORT + IW4X_HEALTHCHECK → PLUTAINER_HEALTHCHECK + IW4X_MOD → PLUTAINER_MOD + IW4X_AUTO_UPDATE → PLUTAINER_AUTO_UPDATE + IW4X_SERVER_NAME → PLUTAINER_SERVER_NAME + IW4X_EXTRA_ARGS → PLUTAINER_EXTRA_ARGS + ALTER_GAME → PLUTAINER_GAME (e.g. t7x) + ALTER_CONFIG_FILE → PLUTAINER_CONFIG_FILE + ALTER_PORT → PLUTAINER_PORT + ALTER_HEALTHCHECK → PLUTAINER_HEALTHCHECK + ALTER_MOD → PLUTAINER_MOD + ALTER_SKIP_SEED → PLUTAINER_SKIP_SEED + ALTER_AUTO_UPDATE → PLUTAINER_AUTO_UPDATE + ALTER_SERVER_NAME → PLUTAINER_SERVER_NAME + ALTER_EXTRA_ARGS → PLUTAINER_EXTRA_ARGS + + Full guide: https://github.com/Ayymoss/Plutainer/blob/main/MIGRATION.md + + 4. docker compose up -d +======================================================================== +PATHS +} + +_rcon_missing_warning() { + echo "[WARN] Could not parse rcon_password from ${CONFIG_PATH}." >&2 + echo " Healthcheck and rcon-cli will not work until this is set." >&2 + echo " Expected format in your cfg file:" >&2 + echo " set rcon_password \"your_password_here\"" >&2 + echo " Single quotes and unquoted values are accepted; commented (//) lines are ignored." >&2 + echo " Do NOT set rcon_password via PLUTAINER_EXTRA_ARGS — Plutainer cannot read it back." >&2 +} + +# Extract the RCON password from the user's config file. Handles: +# set rcon_password "value" (double-quoted, default) +# seta rcon_password 'value' (single-quoted) +# set rcon_password value (unquoted; value is first token) +# rcon_password "value" (bare; T6/T7 community cfgs do this) +# Strips line comments (//...) before searching. Picks the last uncommented +# match, in case the cfg overrides itself. +# Sets RCON_PASSWORD on success; returns 1 + warning on failure. extract_rcon_password() { + RCON_PASSWORD="" if [[ ! -f "${CONFIG_PATH}" ]]; then - echo "[ERROR] Config file not found at ${CONFIG_PATH}" >&2 + echo "[WARN] Config file not found at ${CONFIG_PATH} — cannot extract rcon_password." >&2 return 1 fi - RCON_PASSWORD=$(grep -v '^[[:space:]]*//' "${CONFIG_PATH}" | grep -i 'rcon_password' | sed -n 's/.*"\([^"]*\)".*/\1/p' | tail -1) - if [[ -z "${RCON_PASSWORD}" ]]; then - echo "[ERROR] Could not find 'rcon_password' in ${CONFIG_PATH}" >&2 + + local line + line=$(sed -e 's|//.*$||' "${CONFIG_PATH}" \ + | grep -iE '^[[:space:]]*(set[a]?[[:space:]]+)?rcon_password[[:space:]]+' \ + | tail -1) + + if [[ -z "$line" ]]; then + _rcon_missing_warning + return 1 + fi + + local dq_pat='"([^"]*)"' + local sq_pat="'([^']*)'" + if [[ "$line" =~ $dq_pat ]]; then + RCON_PASSWORD="${BASH_REMATCH[1]}" + elif [[ "$line" =~ $sq_pat ]]; then + RCON_PASSWORD="${BASH_REMATCH[1]}" + else + # Unquoted: value is the token immediately after 'rcon_password'. + # Strip optional 'set'/'seta' prefix so $2 is always the value. + RCON_PASSWORD=$(echo "$line" | awk '{ if ($1 ~ /^[Ss][Ee][Tt][Aa]?$/) print $3; else print $2 }') + fi + + if [[ -z "$RCON_PASSWORD" ]]; then + _rcon_missing_warning return 1 fi } diff --git a/scripts/iw4xentry.sh b/scripts/iw4xentry.sh index ba3598a..360ca00 100644 --- a/scripts/iw4xentry.sh +++ b/scripts/iw4xentry.sh @@ -1,99 +1,96 @@ #!/bin/bash # -# This entrypoint script is responsible for validating the container's -# environment, setting sensible defaults, and launching the specified -# IW4x game server. +# Validate environment, prepare the game-files tree and configs/ symlinks, +# update iw4x via the iw4x-launcher, then launch the iw4x server. # +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/game-config.sh" -# --- Step 1: Update IW4x Files --- -mkdir -p /home/plutainer/app/gamefiles -ln -sf /home/plutainer/gamefiles/{main,zone,binkw32.dll,localization.txt,mss32.dll} /home/plutainer/app/gamefiles/ +detect_game_type || hold_indefinitely "detect_game_type failed." +check_volume_version || hold_indefinitely "check_volume_version failed." +resolve_engine_config_dir +resolve_mod_config_dir +resolve_config_layout -IW4X_CACHE_LOC="/home/plutainer/app/gamefiles/launcher/cache.json" -if [[ ! -f "${IW4X_CACHE_LOC}" ]]; then - echo "First container run detected. Downloading iw4x (IW4x) initial files... This may take a few minutes." - /home/plutainer/.plutainer/iw4x-launcher --path /home/plutainer/app/gamefiles --skip-launch --no-self-update +SOURCE_DIR="$PLUTAINER_SOURCE_DIR" +DEST_DIR="$PLUTAINER_GAMEFILES_DIR" +mkdir -p "$DEST_DIR" + +# --- Step 1: Link Game Files --- +echo "Linking files for iw4x..." +link_files "$SOURCE_DIR" "$DEST_DIR" main zone binkw32.dll localization.txt mss32.dll + +# --- Step 2: Update iw4x --- +IW4X_CACHE_LOC="$DEST_DIR/launcher/cache.json" +if [[ -f "$IW4X_CACHE_LOC" && "${PLUTAINER_AUTO_UPDATE:-}" == "false" ]]; then + echo "Skipping iw4x update because PLUTAINER_AUTO_UPDATE is set to 'false'." else - if [[ "${IW4X_AUTO_UPDATE}" == "false" ]]; then - echo "Skipping iw4x update because IW4X_AUTO_UPDATE is set to 'false'." + if [[ -f "$IW4X_CACHE_LOC" ]]; then + echo "Checking for iw4x updates..." else - echo "Checking for iw4x updates... This may take a few minutes if an update is available." - /home/plutainer/.plutainer/iw4x-launcher --path /home/plutainer/app/gamefiles --skip-launch --no-self-update + echo "First container run detected. Downloading iw4x initial files..." fi + /home/plutainer/.plutainer/iw4x-launcher --path "$DEST_DIR" --skip-launch --no-self-update fi -cd /home/plutainer/app/gamefiles +cd "$DEST_DIR" -# --- Step 2: Validate Required Environment Variables --- -MISSING_VAR=false -INVALID_VAR=false -VALID_GAMES="iw4x" -IW4X_SERVER_NAME=${IW4X_SERVER_NAME:-"IW4x Docker Server"} +# --- Step 3a: Auto-lift any user-placed real cfg from engine path --- +auto_lift_user_config -if [[ -z "${IW4X_GAME}" ]]; then - echo "[ERROR] The 'IW4X_GAME' environment variable is not set." >&2 - MISSING_VAR=true -elif [[ ! " ${VALID_GAMES} " =~ " ${IW4X_GAME} " ]]; then - echo "[ERROR] Invalid value for 'IW4X_GAME': \"${IW4X_GAME}\"." >&2 - INVALID_VAR=true -fi +# --- Step 3b: Fan-out configs/ → engine + mod config dirs --- +# No seed_configs call: iw4x has no bundled community seed. +link_configs "$ENGINE_CONFIG_DIR" "$MOD_CONFIG_DIR" -if [[ -z "${IW4X_CONFIG_FILE}" ]]; then - echo "[ERROR] The 'IW4X_CONFIG_FILE' environment variable is not set." >&2 - echo " > You must specify the name of the server configuration file (e.g., 'server.cfg')." >&2 - MISSING_VAR=true -fi +# --- Step 4: Validate environment + ensure config file exists --- +PLUTAINER_SERVER_NAME="${PLUTAINER_SERVER_NAME:-IW4x Docker Server}" -if [[ "$MISSING_VAR" == "true" || "$INVALID_VAR" == "true" ]]; then - echo "-------------------------------------------------" >&2 - if [[ "$INVALID_VAR" == "true" ]]; then - echo "An invalid value was provided. Valid game modes are: ${VALID_GAMES}" >&2 - fi - echo "One or more configuration errors found. Halting startup." >&2 - echo "Exiting in 10 seconds..." >&2 - sleep 10 - exit 1 +if [[ "${PLUTAINER_GAME}" != "iw4x" ]]; then + hold_indefinitely "PLUTAINER_GAME must be 'iw4x' for the iw4x entrypoint." +fi +if [[ -z "${PLUTAINER_CONFIG_FILE:-}" ]]; then + hold_indefinitely "PLUTAINER_CONFIG_FILE is not set. Specify the filename of your server config (e.g. 'server.cfg')." +fi +if ! ensure_config_present; then + hold_indefinitely "Config file not found. See [ERROR] above." fi -# --- Step 3: Set Default Port (If Needed) --- -if [[ -z "${IW4X_PORT}" ]]; then - echo "Optional IW4X_PORT is not set, determining default for ${IW4X_GAME}..." - resolve_default_port "iw4x" || { sleep 10; exit 1; } - IW4X_PORT="${DEFAULT_PORT}" - echo "Default port set to ${IW4X_PORT}" +# --- Step 5: Resolve port --- +if [[ -z "${PLUTAINER_PORT:-}" ]]; then + echo "PLUTAINER_PORT not set, using default for iw4x..." + resolve_default_port "iw4x" || hold_indefinitely "Could not resolve default port." + PLUTAINER_PORT="${DEFAULT_PORT}" + echo "Default port set to ${PLUTAINER_PORT}" fi -# --- Step 4: Build Server Command Arguments --- +# --- Step 6: Build Server Command Arguments --- declare -a CMD_ARGS=( -dedicated -stdout +set sv_lanonly "0" - +set net_port "${IW4X_PORT}" - +exec "${IW4X_CONFIG_FILE}" + +set net_port "${PLUTAINER_PORT}" + +exec "${PLUTAINER_CONFIG_FILE}" +set logfile "1" +set party_enable "0" ) -if [[ -n "${IW4X_MOD}" ]]; then - CMD_ARGS+=(+set fs_game "${IW4X_MOD}") +if [[ -n "${PLUTAINER_MOD:-}" ]]; then + CMD_ARGS+=(+set fs_game "${PLUTAINER_MOD}") fi - -if [[ -n "${IW4X_NET_LOG_IP}" ]]; then +if [[ -n "${IW4X_NET_LOG_IP:-}" ]]; then CMD_ARGS+=(+set g_log_add "${IW4X_NET_LOG_IP}") fi - -if [[ -n "${IW4X_EXTRA_ARGS}" ]]; then - CMD_ARGS+=(${IW4X_EXTRA_ARGS}) +if [[ -n "${PLUTAINER_EXTRA_ARGS:-}" ]]; then + CMD_ARGS+=(${PLUTAINER_EXTRA_ARGS}) fi CMD_ARGS+=(+map_rotate) -# --- Step 5: Launch the iw4x Server --- +# --- Step 7: Launch (with 30s crash throttle) --- /home/plutainer/.plutainer/log-watcher.sh & -echo "Starting ${IW4X_GAME} Server: ${IW4X_SERVER_NAME}" -echo "EXECUTING: wine iw4x.exe ${CMD_ARGS[@]}" -exec wine iw4x.exe "${CMD_ARGS[@]}" +echo "Starting iw4x Server: ${PLUTAINER_SERVER_NAME}" +echo "EXECUTING: wine iw4x.exe ${CMD_ARGS[*]}" +launch_game wine iw4x.exe "${CMD_ARGS[@]}" diff --git a/scripts/log-watcher.sh b/scripts/log-watcher.sh index aaefdc5..7c0d3cf 100644 --- a/scripts/log-watcher.sh +++ b/scripts/log-watcher.sh @@ -38,21 +38,55 @@ mkdir -p "$STABLE_DIR" # Atomically place a symlink at $link -> $rel_target. Works whether $link # currently does not exist, is a regular file (legacy stub), or is a symlink. +# If $link is a directory (typically created by a sidecar bind-mounting a +# non-existent log file, which makes Docker auto-create a dir on the host), +# strip it first — mv -T refuses to replace a directory. Without this strip, +# mv fails every poll and litters $STABLE_DIR with .XXX temp symlinks. place_symlink() { local link="$1" rel_target="$2" local tmp + if [[ -d "$link" && ! -L "$link" ]]; then + echo "[log-watcher] stray directory at $link (sidecar bind-mount artifact); removing" + if ! rmdir -- "$link" 2>/dev/null && ! rm -rf -- "$link" 2>/dev/null; then + echo "[log-watcher] ERROR: cannot remove $link — likely root-owned with root-owned contents." >&2 + echo "[log-watcher] Fix sidecar volume to mount the logs/ DIRECTORY, not individual log FILES." >&2 + return 1 + fi + fi tmp=$(mktemp -u -p "$STABLE_DIR" ".$(basename "$link").XXXXXX") ln -s "$rel_target" "$tmp" mv -Tf "$tmp" "$link" } -# Startup heal: any dangling symlinks from prior container runs become empty -# stub files so sidecar readers always see a file. If the old target is still -# valid, leave the symlink alone — IW4MAdmin can keep reading continuously -# until a fresh target is identified by the poll loop below. +# Startup heal: +# - Dangling symlinks from prior runs → empty stub files so sidecar readers +# always see a file. If the old target is still valid, leave the symlink +# alone — IW4MAdmin can keep reading continuously until a fresh target is +# identified by the poll loop below. +# - Stray directories (sidecar bind-mount of a non-existent log makes Docker +# auto-create a dir on the host) → remove. Otherwise place_symlink's +# mv -Tf fails forever and the namespace silts up with .XXX temp symlinks. +# - Stray orphan temp symlinks from prior failed place_symlink runs → remove. +if compgen -G "$STABLE_DIR/.*" > /dev/null 2>&1; then + for entry in "$STABLE_DIR"/.*; do + base=$(basename "$entry") + [[ "$base" == "." || "$base" == ".." ]] && continue + if [[ -L "$entry" && "$base" =~ ^\..+\.[A-Za-z0-9]{6}$ ]]; then + echo "[log-watcher] removing orphan temp symlink: $base" + rm -f -- "$entry" + fi + done +fi if compgen -G "$STABLE_DIR/*" > /dev/null; then for entry in "$STABLE_DIR"/*; do [[ -e "$entry" || -L "$entry" ]] || continue + if [[ -d "$entry" && ! -L "$entry" ]]; then + echo "[log-watcher] stray directory at $(basename "$entry") (sidecar bind-mount artifact); removing" + if ! rmdir -- "$entry" 2>/dev/null && ! rm -rf -- "$entry" 2>/dev/null; then + echo "[log-watcher] WARN: cannot remove $(basename "$entry") — likely root-owned with root-owned contents. Fix sidecar volume to mount the logs/ DIRECTORY, not individual log FILES." >&2 + fi + continue + fi if [[ -L "$entry" && ! -e "$entry" ]]; then echo "[log-watcher] healing dangling symlink: $(basename "$entry")" rm -f "$entry" diff --git a/scripts/migrate-v1-to-v2.sh b/scripts/migrate-v1-to-v2.sh new file mode 100644 index 0000000..f6c4175 --- /dev/null +++ b/scripts/migrate-v1-to-v2.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# +# Migrate a v1 Plutainer volume to the v2 layout. +# +# v1: app/{gamefiles,plutonium,logs} +# v2: app/{configs,runtime/{gamefiles,plutonium},logs,.plutainer-version} +# +# Run via: +# docker run --rm \ +# -v :/home/plutainer/app \ +# --entrypoint /home/plutainer/.plutainer/migrate-v1-to-v2.sh \ +# ghcr.io/ayymoss/plutainer:v2 +# +# Add --dry-run as the first argument to preview without making changes. +# +# Effects: +# 1. mkdir runtime/, configs/ +# 2. mv app/gamefiles -> app/runtime/gamefiles (if present) +# 3. mv app/plutonium -> app/runtime/plutonium (if present) +# 4. For each known engine config dir, move every top-level *.cfg into +# app/configs/ and leave a relative symlink in its place. +# 5. Wipe app/logs/* (stale symlinks; log-watcher re-creates on next start). +# 6. Write app/.plutainer-version=2 +# +set -euo pipefail + +APP_DIR="/home/plutainer/app" +CONFIGS_DIR="$APP_DIR/configs" +RUNTIME_DIR="$APP_DIR/runtime" +GAMEFILES_DIR="$RUNTIME_DIR/gamefiles" +PLUTONIUM_DIR="$RUNTIME_DIR/plutonium" +MARKER="$APP_DIR/.plutainer-version" + +DRY_RUN=false +if [[ "${1:-}" == "--dry-run" ]]; then + DRY_RUN=true + echo "[DRY-RUN] no changes will be made" +fi + +run() { + if [[ "$DRY_RUN" == "true" ]]; then + echo " + $*" + else + echo " + $*" + "$@" + fi +} + +# --- Sanity checks --- +if [[ ! -d "$APP_DIR" ]]; then + echo "[ERROR] $APP_DIR is not mounted." >&2 + exit 1 +fi + +if [[ -f "$MARKER" ]]; then + v="$(cat "$MARKER" 2>/dev/null || echo "")" + if [[ "$v" == "2" ]]; then + echo "[INFO] Volume is already v2 (marker present). Nothing to do." + exit 0 + else + echo "[ERROR] Unexpected version marker: '$v'. Refusing to migrate." >&2 + exit 1 + fi +fi + +if [[ ! -d "$APP_DIR/gamefiles" && ! -d "$APP_DIR/plutonium" ]]; then + echo "[INFO] No v1 markers found and no version file — treating as fresh volume." + run mkdir -p "$CONFIGS_DIR" "$APP_DIR/logs" "$RUNTIME_DIR" + if [[ "$DRY_RUN" != "true" ]]; then echo 2 > "$MARKER"; else echo " + echo 2 > $MARKER"; fi + echo "[OK] Initialised fresh v2 volume." + exit 0 +fi + +echo "[INFO] v1 layout detected — beginning migration." +echo + +# --- Step 1: create runtime/, configs/ --- +echo "--- Creating new directories ---" +run mkdir -p "$RUNTIME_DIR" "$CONFIGS_DIR" "$APP_DIR/logs" +echo + +# --- Step 2: move gamefiles, plutonium under runtime/ --- +echo "--- Relocating gamefiles/ and plutonium/ under runtime/ ---" +if [[ -d "$APP_DIR/gamefiles" ]]; then + run mv "$APP_DIR/gamefiles" "$GAMEFILES_DIR" +fi +if [[ -d "$APP_DIR/plutonium" ]]; then + run mv "$APP_DIR/plutonium" "$PLUTONIUM_DIR" +fi +echo + +# --- Step 3: lift top-level *.cfg from engine config dirs into configs/ --- +echo "--- Lifting top-level *.cfg files into configs/ ---" +# Known engine config dirs in v2 paths (after the move above). +ENGINE_DIRS=( + "$GAMEFILES_DIR/main" # plutonium t4 + "$GAMEFILES_DIR/admin" # plutonium iw5 + "$GAMEFILES_DIR/userraw" # iw4x + "$GAMEFILES_DIR/zone" # alterware t7x + "$PLUTONIUM_DIR/storage/t5" # plutonium t5 + "$PLUTONIUM_DIR/storage/t6" # plutonium t6 +) + +lift_cfg() { + local src="$1" base dest rel + base="$(basename "$src")" + dest="$CONFIGS_DIR/$base" + if [[ -e "$dest" ]]; then + echo " [SKIP] $base already exists in configs/ — leaving $src untouched" + return 0 + fi + run mv "$src" "$dest" + if [[ "$DRY_RUN" == "true" ]]; then + echo " + ln -sfn ..//$base $src" + else + rel=$(realpath --relative-to="$(dirname "$src")" "$dest") + ln -sfn "$rel" "$src" + echo " + ln -sfn $rel $src" + fi +} + +shopt -s nullglob +for d in "${ENGINE_DIRS[@]}"; do + [[ -d "$d" ]] || continue + found_any=false + for f in "$d"/*.cfg; do + [[ -f "$f" && ! -L "$f" ]] || continue + if [[ "$found_any" == "false" ]]; then + echo " in $d:" + found_any=true + fi + lift_cfg "$f" + done +done +shopt -u nullglob +echo + +# --- Step 4: clear app/logs/ stale entries --- +echo "--- Clearing stale entries in logs/ (log-watcher will repopulate) ---" +if compgen -G "$APP_DIR/logs/*" > /dev/null; then + run find "$APP_DIR/logs" -mindepth 1 -maxdepth 1 -exec rm -rf {} + +fi +echo + +# --- Step 5: write marker --- +echo "--- Writing version marker ---" +if [[ "$DRY_RUN" == "true" ]]; then + echo " + echo 2 > $MARKER" +else + echo 2 > "$MARKER" + echo " + wrote $MARKER" +fi +echo + +if [[ "$DRY_RUN" == "true" ]]; then + echo "[DRY-RUN] No changes made. Re-run without --dry-run to apply." +else + echo "[OK] Migration complete. You can now start the v2 container." +fi diff --git a/scripts/plutoentry.sh b/scripts/plutoentry.sh index 87aafcd..cfd9ac8 100644 --- a/scripts/plutoentry.sh +++ b/scripts/plutoentry.sh @@ -1,139 +1,129 @@ #!/bin/bash # -# This entrypoint script is responsible for validating the container's -# environment, setting sensible defaults, and launching the specified -# Plutonium game server. +# Validate environment, prepare the game-files tree and configs/ symlinks, +# update Plutonium binaries, then launch the requested Plutonium server. # +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/game-config.sh" -# --- Step 1: Update Plutonium Files --- -BASE_GAME=${PLUTO_GAME%??} -SOURCE_DIR="/home/plutainer/gamefiles" -DEST_DIR="/home/plutainer/app/gamefiles" +detect_game_type || hold_indefinitely "detect_game_type failed." +check_volume_version || hold_indefinitely "check_volume_version failed." +resolve_engine_config_dir +resolve_mod_config_dir +resolve_config_layout + +SOURCE_DIR="$PLUTAINER_SOURCE_DIR" +DEST_DIR="$PLUTAINER_GAMEFILES_DIR" mkdir -p "$DEST_DIR" + +# --- Step 1: Link Game Files --- case "$BASE_GAME" in iw5) echo "Linking files for iw5 (Modern Warfare 3)..." - ln -sf "$SOURCE_DIR"/{main,miles,zone,binkw32.dll,localization.txt,mss32.dll} "$DEST_DIR"/ + link_files "$SOURCE_DIR" "$DEST_DIR" main miles zone binkw32.dll localization.txt mss32.dll ;; t4) echo "Linking files for t4 (World at War)..." - ln -sf "$SOURCE_DIR"/{zone,binkw32.dll,localization.txt,cod.bmp,codlogo.bmp} "$DEST_DIR"/ + link_files "$SOURCE_DIR" "$DEST_DIR" zone binkw32.dll localization.txt cod.bmp codlogo.bmp mkdir -p "$DEST_DIR/main" - ln -sf "$SOURCE_DIR"/main/{iw_00.iwd,iw_14.iwd,iw_21.iwd,iw_22.iwd,iw_24.iwd,iw_26.iwd,localized_english_iw00.iwd,localized_english_iw04.iwd} "$DEST_DIR"/main/ + link_files "$SOURCE_DIR/main" "$DEST_DIR/main" \ + iw_00.iwd iw_14.iwd iw_21.iwd iw_22.iwd iw_24.iwd iw_26.iwd \ + localized_english_iw00.iwd localized_english_iw04.iwd ;; t5) echo "Linking files for t5 (Black Ops)..." - ln -sf "$SOURCE_DIR"/{main,zone,binkw32.dll,localization.txt} "$DEST_DIR"/ + link_files "$SOURCE_DIR" "$DEST_DIR" main zone binkw32.dll localization.txt ;; t6) echo "Linking files for t6 (Black Ops II)..." - ln -sf "$SOURCE_DIR"/{zone,binkw32.dll,codlogo.bmp} "$DEST_DIR"/ + link_files "$SOURCE_DIR" "$DEST_DIR" zone binkw32.dll codlogo.bmp ;; *) - echo "[ERROR] Unknown BASE_GAME value '$BASE_GAME'." >&2 - exit 1 + hold_indefinitely "Unknown BASE_GAME value '$BASE_GAME'." ;; esac -PLUTO_CDN_INFO_LOC="/home/plutainer/app/plutonium/cdn_info.json" -if [[ ! -f "${PLUTO_CDN_INFO_LOC}" ]]; then - echo "First container run detected. Downloading Plutonium initial files... This may take a few minutes." - /home/plutainer/.plutainer/plutonium-updater --directory /home/plutainer/app/plutonium +# --- Step 2: Update Plutonium --- +mkdir -p "$PLUTAINER_PLUTONIUM_DIR" +PLUTO_CDN_INFO_LOC="$PLUTAINER_PLUTONIUM_DIR/cdn_info.json" +if [[ -f "$PLUTO_CDN_INFO_LOC" && "${PLUTAINER_AUTO_UPDATE:-}" == "false" ]]; then + echo "Skipping Plutonium update because PLUTAINER_AUTO_UPDATE is set to 'false'." else - if [[ "${PLUTO_AUTO_UPDATE}" == "false" ]]; then - echo "Skipping Plutonium update because PLUTO_AUTO_UPDATE is set to 'false'." + if [[ -f "$PLUTO_CDN_INFO_LOC" ]]; then + echo "Checking for Plutonium updates..." else - echo "Checking for Plutonium updates... This may take a few minutes if an update is available." - /home/plutainer/.plutainer/plutonium-updater --directory /home/plutainer/app/plutonium + echo "First container run detected. Downloading Plutonium initial files..." fi + /home/plutainer/.plutainer/plutonium-updater --directory "$PLUTAINER_PLUTONIUM_DIR" fi -cd /home/plutainer/app/plutonium +cd "$PLUTAINER_PLUTONIUM_DIR" + +# --- Step 3a: Auto-lift any user-placed real cfg from engine path --- +auto_lift_user_config -# --- Step 1.5: Seed default configs from bundled community repos --- -# Idempotent: cp -n means existing user files are never overwritten. -# Skip with PLUTO_SKIP_SEED=true. -if [[ "${PLUTO_SKIP_SEED}" != "true" ]]; then +# --- Step 3b: Seed default configs from bundled community repos --- +if [[ "${PLUTAINER_SKIP_SEED:-}" != "true" ]]; then case "$BASE_GAME" in - t4) seed_configs t4 /home/plutainer/app/gamefiles/main ;; - iw5) seed_configs iw5 /home/plutainer/app/gamefiles/admin ;; - t5|t6) seed_configs "$BASE_GAME" /home/plutainer/app/plutonium/storage/"$BASE_GAME" ;; + t4) seed_configs t4 "$PLUTAINER_GAMEFILES_DIR/main" "" ;; + iw5) seed_configs iw5 "$PLUTAINER_GAMEFILES_DIR/admin" "" ;; + t5|t6) seed_configs "$BASE_GAME" "$PLUTAINER_PLUTONIUM_DIR/storage/$BASE_GAME" "" ;; esac fi -# --- Step 2: Validate Required Environment Variables --- -MISSING_VAR=false -INVALID_VAR=false -VALID_GAMES="iw5mp t4mp t4sp t5mp t5sp t6mp t6zm" +# --- Step 4: Fan-out configs/ → engine + mod config dirs --- +link_configs "$ENGINE_CONFIG_DIR" "$MOD_CONFIG_DIR" -PLUTO_SERVER_NAME=${PLUTO_SERVER_NAME:-"Plutonium Docker Server"} +# --- Step 5: Validate environment + ensure config file exists --- +PLUTAINER_SERVER_NAME="${PLUTAINER_SERVER_NAME:-Plutonium Docker Server}" +VALID_GAMES="iw5mp t4mp t4sp t5mp t5sp t6mp t6zm" -if [[ -z "${PLUTO_GAME}" ]]; then - echo "[ERROR] The 'PLUTO_GAME' environment variable is not set." >&2 - MISSING_VAR=true -elif [[ ! " ${VALID_GAMES} " =~ " ${PLUTO_GAME} " ]]; then - echo "[ERROR] Invalid value for 'PLUTO_GAME': \"${PLUTO_GAME}\"." >&2 - INVALID_VAR=true +if [[ ! " ${VALID_GAMES} " =~ " ${PLUTAINER_GAME} " ]]; then + hold_indefinitely "Invalid PLUTAINER_GAME for Plutonium: \"${PLUTAINER_GAME}\". Valid: ${VALID_GAMES}" fi - -if [[ -z "${PLUTO_SERVER_KEY}" ]]; then - echo "[ERROR] The 'PLUTO_SERVER_KEY' environment variable is not set." >&2 - echo " > You must provide a server key from https://platform.plutonium.pw/serverkeys" >&2 - MISSING_VAR=true +if [[ -z "${PLUTO_SERVER_KEY:-}" ]]; then + hold_indefinitely "PLUTO_SERVER_KEY is not set. Get a server key from https://platform.plutonium.pw/serverkeys" fi - -if [[ -z "${PLUTO_CONFIG_FILE}" ]]; then - echo "[ERROR] The 'PLUTO_CONFIG_FILE' environment variable is not set." >&2 - echo " > You must specify the name of the server configuration file (e.g., 'dedicated.cfg')." >&2 - MISSING_VAR=true +if [[ -z "${PLUTAINER_CONFIG_FILE:-}" ]]; then + hold_indefinitely "PLUTAINER_CONFIG_FILE is not set. Specify the filename of your server config (e.g. 'dedicated.cfg')." fi - -if [[ "$MISSING_VAR" == "true" || "$INVALID_VAR" == "true" ]]; then - echo "-------------------------------------------------" >&2 - if [[ "$INVALID_VAR" == "true" ]]; then - echo "An invalid value was provided. Valid game modes are: ${VALID_GAMES}" >&2 - fi - echo "One or more configuration errors found. Halting startup." >&2 - echo "Exiting in 10 seconds..." >&2 - sleep 10 - exit 1 +if ! ensure_config_present; then + hold_indefinitely "Config file not found. See [ERROR] above." fi -# --- Step 3: Set Game-Aware Default Port (If Needed) --- -if [[ -z "${PLUTO_PORT}" ]]; then - echo "Optional PLUTO_PORT is not set, determining default port for ${BASE_GAME}..." - resolve_default_port "${BASE_GAME}" || { sleep 10; exit 1; } - PLUTO_PORT="${DEFAULT_PORT}" - echo "Default port set to ${PLUTO_PORT}" +# --- Step 6: Resolve port --- +if [[ -z "${PLUTAINER_PORT:-}" ]]; then + echo "PLUTAINER_PORT not set, using default for ${BASE_GAME}..." + resolve_default_port "${BASE_GAME}" || hold_indefinitely "Could not resolve default port." + PLUTAINER_PORT="${DEFAULT_PORT}" + echo "Default port set to ${PLUTAINER_PORT}" fi -# --- Step 4: Build Server Command Arguments --- +# --- Step 7: Build Server Command Arguments --- declare -a CMD_ARGS=( - "${PLUTO_GAME}" - /home/plutainer/app/gamefiles + "${PLUTAINER_GAME}" + "$PLUTAINER_GAMEFILES_DIR" -dedicated +set key "${PLUTO_SERVER_KEY}" - +set net_port "${PLUTO_PORT}" + +set net_port "${PLUTAINER_PORT}" ) if [[ "${BASE_GAME}" == "iw5" ]]; then - CMD_ARGS+=(+set sv_config "${PLUTO_CONFIG_FILE}") + CMD_ARGS+=(+set sv_config "${PLUTAINER_CONFIG_FILE}") else - CMD_ARGS+=(+exec "${PLUTO_CONFIG_FILE}") + CMD_ARGS+=(+exec "${PLUTAINER_CONFIG_FILE}") fi -if [[ -n "${PLUTO_MOD}" ]]; then - CMD_ARGS+=(+set fs_game "${PLUTO_MOD}") +if [[ -n "${PLUTAINER_MOD:-}" ]]; then + CMD_ARGS+=(+set fs_game "${PLUTAINER_MOD}") fi -if [[ -n "${PLUTO_MAX_CLIENTS}" ]]; then +if [[ -n "${PLUTO_MAX_CLIENTS:-}" ]]; then CMD_ARGS+=(+set sv_maxclients "${PLUTO_MAX_CLIENTS}") fi - -if [[ -n "${PLUTO_EXTRA_ARGS}" ]]; then - CMD_ARGS+=(${PLUTO_EXTRA_ARGS}) +if [[ -n "${PLUTAINER_EXTRA_ARGS:-}" ]]; then + CMD_ARGS+=(${PLUTAINER_EXTRA_ARGS}) fi if [[ "${BASE_GAME}" == "iw5" ]]; then @@ -142,9 +132,9 @@ else CMD_ARGS+=(+map_rotate) fi -# --- Step 5: Launch the Plutonium Server --- +# --- Step 8: Launch (with 30s crash throttle) --- /home/plutainer/.plutainer/log-watcher.sh & -echo "Starting Plutonium ${PLUTO_GAME} Server: ${PLUTO_SERVER_NAME}" -echo "EXECUTING: wine bin/plutonium-bootstrapper-win32.exe ${CMD_ARGS[@]}" -exec wine bin/plutonium-bootstrapper-win32.exe "${CMD_ARGS[@]}" +echo "Starting Plutonium ${PLUTAINER_GAME} Server: ${PLUTAINER_SERVER_NAME}" +echo "EXECUTING: wine bin/plutonium-bootstrapper-win32.exe ${CMD_ARGS[*]}" +launch_game wine bin/plutonium-bootstrapper-win32.exe "${CMD_ARGS[@]}"