initial version

This commit is contained in:
2026-10-29 21:13:01 +01:00
parent a076b082b8
commit 5532211785
10 changed files with 613 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Build context for the Adventure Land image. The game itself is cloned from
# GitHub inside the Dockerfile, so only the Dockerfile and helper scripts are
# needed here.
config
data
*.md
.env
.git
.gitignore
+73
View File
@@ -0,0 +1,73 @@
# syntax=docker/dockerfile:1
# Adventure Land (MongoDB Edition) - image build
#
# The game is split across three upstream repositories:
# adventureland_mongodb -> /app
# common_engine -> /app/common
# adventureland_secretsandconfig -> /app/secretsandconfig (template only,
# replaced at runtime by ./config)
#
# All three are cloned at build time, so `docker compose build --pull` is the
# way to update the game.
ARG NODE_IMAGE=node:22-bookworm-slim
FROM ${NODE_IMAGE} AS build
RUN apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
ENV GIT_TERMINAL_PROMPT=0
WORKDIR /app
ARG AL_REPO=https://github.com/kaansoral/adventureland_mongodb.git
ARG AL_REF=main
ARG COMMON_REPO=https://github.com/kaansoral/common_engine.git
ARG COMMON_REF=main
ARG CONFIG_REPO=https://github.com/kaansoral/adventureland_secretsandconfig.git
ARG CONFIG_REF=main
RUN git clone --depth 1 --single-branch --branch "${AL_REF}" "${AL_REPO}" . \
&& git clone --depth 1 --single-branch --branch "${COMMON_REF}" "${COMMON_REPO}" common \
&& git clone --depth 1 --single-branch --branch "${CONFIG_REF}" "${CONFIG_REPO}" secretsandconfig \
&& rm -rf .git common/.git secretsandconfig/.git
# Backend (Express) and game server (Socket.IO) have separate dependency trees.
RUN npm install --omit=dev --no-audit --no-fund \
&& cd node && npm install --omit=dev --no-audit --no-fund \
&& rm -rf /app/node/node_modules/geoip-lite \
&& rm -rf /app/node_modules/geoip-lite
# Local patch: make the DRM login check switchable (docker/patch-drm-check.js).
# A self-hosted server cannot produce Steam/MAS auth ids, so the check would
# otherwise flag every account with the "authfail" condition. Kept after the
# npm install so the dependency layers stay cached across patch changes.
COPY docker/patch-drm-check.js /tmp/patch-drm-check.js
RUN node /tmp/patch-drm-check.js /app/node/server.js
FROM ${NODE_IMAGE} AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates socat \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=build --chown=node:node /app /app
COPY --chown=node:node docker/ /app/docker/
# Local/dev mode (see config/options.js). The seed script refuses to run when
# NODE_ENV=production, so keep this out of the way for the seeding container.
ENV NODE_ENV=development
USER node
EXPOSE 8090 7192
ENTRYPOINT ["/app/docker/entrypoint.sh"]
CMD ["node", "main.js"]
+179
View File
@@ -1,3 +1,182 @@
# Adventure Land – Docker-Setup
Lokale Installation des [Adventure Land MongoDB Ports](https://github.com/kaansoral/adventureland_mongodb)
mit Docker Compose: MongoDB, Backend (Web-Client) und Game-Server, inklusive
automatischem Datenbank-Seed.
## Voraussetzungen
- Docker Engine mit Compose v2 (`docker compose version`)
- Ports **8090** (Web) und **7192** (Game-Server) müssen auf dem Host frei sein
- Internetzugang beim ersten Build (die Quellcode-Repos werden geklont, Node-Pakete installiert)
## Start
```sh
docker compose up -d --build
```
Danach im Browser öffnen: **http://localhost:8090**
Der erste Start dauert ein paar Minuten (Image-Build inkl. `npm install`). Beim
ersten Start laufen zwei Hilfs-Container: `mongo-init` initialisiert das
Replica-Set, `seed` importiert die 49 Kartendaten. Erst danach starten Backend
und Game-Server. Bei späteren Starts überspringt sich der Seed selbst.
**Konto anlegen:** Auf der Startseite unter „Steam“ auf **„Registrieren
[Zum Entwickeln]“** klicken, E-Mail + Passwort eingeben. Danach Charakter
erstellen und auf den Charakter klicken – man landet direkt im Spiel.
**Admin werden:** Die Admin-Seiten (`/admin/executor`, `/admin/renderer`) sind
standardmäßig gesperrt. `is_admin` in `common/admin.js` prüft das Feld
`user.admin`; setzen lässt es sich direkt in MongoDB:
```sh
docker compose exec mongo mongosh adventureland --eval \
'db.user.updateOne({"info.email": "du@example.com"}, { $set: { admin: true } })'
```
Alternativ in `config/options.js` `unsecure_admin: true` setzen – dann gilt
**jeder** Besucher als Admin (`/admin/executor` erlaubt Code-Ausführung), daher
nur bei rein lokalem Zugriff sinnvoll.
## Aufbau
| Service | Image | Aufgabe |
| ------------ | ------------------- | ------------------------------------------------------------- |
| `mongo` | `mongo:8.0` | Datenbank, läuft als Single-Node-Replica-Set `rs0` (Transaktionen) |
| `mongo-init` | `mongo:8.0` | Einmalig: `rs.initiate()` falls noch nicht geschehen |
| `seed` | `adventureland:local` | Einmalig: importiert die Kartengeometrie, überspringt sich selbst |
| `backend` | `adventureland:local` | Express-Backend, Web-Client, API – Port 8090 |
| `game` | `adventureland:local` | Socket.IO-Game-Server – Port 7192 |
Die Spieldaten (Image) werden beim Build aus drei Repositories geklont und
liegen im Container unter `/app` – es wird nichts in diesen Ordner geklont.
## Konfiguration
`config/` wird als `/app/secretsandconfig` in die Container gemountet und
ersetzt das Template aus dem Upstream-Repo:
- `config/keys.js` – MongoDB-URI, Master-Keys, Stripe/Steam/SES (alles leer bzw.
feste Entwicklungs-Defaults). Die Werte sind absichtlich stabil, damit
Sitzungen und Serverdatensätze Neustarts überleben.
- `config/options.js` – `base_url`, Ports, Serverdefinition `local`.
Beide Dateien sind normale Node-Module; nach Änderungen Container neu starten:
```sh
docker compose restart backend game
```
> Wenn du die Ports änderst, passe sie an **drei** Stellen an:
> `docker-compose.yml` (`ports`), `config/options.js` (`port`, `local_port`,
> `address`) und `base_url` (inkl. Port).
### MongoDB-Zugriff
Die Datenbank ist nur im Compose-Netz erreichbar. Für einen Blick hinein:
```sh
docker compose exec mongo mongosh adventureland --eval 'db.getCollectionNames()'
```
## Logs & Status
```sh
docker compose ps
docker compose logs -f backend game
```
## Stoppen, Zurücksetzen, Sichern
```sh
docker compose stop # anhalten
docker compose down # Container entfernen, Daten behalten
docker compose down -v # ALLES löschen, inkl. Konten, Charakteren, Karten
```
Nach `down -v` importiert der `seed`-Container die Karten beim nächsten Start
wieder. Backup/Restore des Datenbank-Volumes:
```sh
docker compose stop backend game
docker run --rm -v adventureland_mongo-data:/data -v "$PWD":/backup alpine \
tar czf /backup/mongo-data-$(date +%F).tar.gz -C /data .
docker compose start backend game
```
Restore: `docker compose down`, das Volume leeren und das Archiv
(`tar xzf mongo-data-….tar.gz -C <volume-mountpoint>`) zurückkopieren, dann
`docker compose up -d` – `seed` erkennt die vorhandenen Collections und
überspringt den Import.
## Aktualisieren
Das Image klont beim Build den jeweils aktuellen `main`-Branch:
```sh
docker compose build --pull --no-cache
docker compose up -d
```
Ein Neu-Seed ist nicht nötig; bestehende Konten, Charaktere und Karten bleiben
erhalten.
Der Build wendet zusätzlich den lokalen Patch aus `docker/patch-drm-check.js` an
(siehe „Gut zu wissen“). Verschiebt Upstream die betroffene Zeile in
`node/server.js`, bricht der Build mit einer klaren Meldung ab – dann das
Suchmuster im Skript anpassen.
## Gut zu wissen
- **E-Mail-Verifizierung:** AWS SES ist nicht konfiguriert. Beim Registrieren
erscheint im Backend-Log `send_email error InvalidClientTokenId`. Das ist
erwartet – die Registrierung funktioniert trotzdem, es wird nur keine Mail
verschickt. Bis zur Verifizierung hängt das Konto an „Not Verified“
(Gold/Glück −25 %). Der Verifizierungslink funktioniert auch ohne Mailversand;
er lässt sich in MongoDB nachsehen:
```sh
docker compose exec mongo mongosh --quiet adventureland --eval \
'db.user.find({}, {"info.email": 1, "info.everification": 1}).forEach((u) =>
print("http://localhost:8090/ev/" + u._id + "/" + u.info.everification))'
```
Link im Browser öffnen, danach neu einloggen. Alternativ `info.verified` direkt
auf `true` setzen (siehe Admin-Befehl oben).
- **„Authorization Failure“ (Gold/Glück −85 %, XP −20 %):** Das ist die
DRM-Prüfung des Game-Servers. Sie verlangt für jedes Konto ab 2019 eine
Steam-/Mac-App-Store-ID, die nur der jeweilige Game-Client ausstellen kann.
Eine selbst gehostete Instanz kann das nicht: Steam-Tickets brauchen den
privaten Publisher-Key, und die Steam-Anmeldung im Browser setzt diese ID
nicht. Deshalb patcht das Image die Prüfung über `docker/patch-drm-check.js`
und Compose setzt `AL_DRM_CHECK: "0"`; nach dem nächsten Login verschwindet die
Bedingung. Mit `AL_DRM_CHECK: "1"` (oder ohne die Variable) gilt wieder das
Upstream-Verhalten.
- **Der Relay in `docker/entrypoint.sh`:** Das Backend spricht Game-Server über
die Adresse an, die auch der Browser benutzt (`localhost:7192`). Innerhalb des
Backend-Containers wäre das es selbst, deshalb leitet ein kleines
`socat`-Relay `127.0.0.1:7192` an den `game`-Container weiter. Ohne das
schlagen Admin-/Server-Funktionen (`server_eval`) fehl.
- **Nur lokal erreichbar:** Die Ports sind standardmäßig an `127.0.0.1` des
Hosts gebunden. Für Zugriff aus dem LAN in `docker-compose.yml` die
`127.0.0.1:`-Präfixe entfernen und `base_url` (sowie ggf.
`servers.local.address`) in `config/options.js` auf die Host-IP bzw. den
Hostnamen umstellen. Der Browser verbindet sich direkt mit
`servers.local.address`.
- **Passwort-/Key-Änderungen:** `config/keys.js` enthält
Entwicklungs-Platzhalter. Vor einem öffentlich erreichbaren Betrieb ersetzen.
- **Quellcode anpassen:** Der Code liegt im Image, nicht im Host-Ordner. Für
eigene Änderungen im `Dockerfile` über die Build-Args `AL_REPO`, `AL_REF`,
`COMMON_REPO`/`COMMON_REF` bzw. `CONFIG_REPO`/`CONFIG_REF` einen Fork oder
Branch eintragen und mit `docker compose build` neu bauen. Alternativ in
`docker-compose.override.yml` einen Checkout nach `/app` mounten und dabei
`node_modules` aus dem Image in ein eigenes Volume auslagern.
# docker-builder
## setup
+129
View File
@@ -0,0 +1,129 @@
name: adventureland
# ensure both containers use same version of mongo
x-mongo_image: &mongo_image mongo:8.0
# Shared settings for the two game containers. Both use the same image, so
# `docker compose build` builds it once.
x-app: &app
image: git.ao-it.net/gaming/adventureland:latest
build:
context: .
args:
NODE_IMAGE: node:22-bookworm-slim
AL_REF: main
COMMON_REF: main
CONFIG_REF: main
restart: unless-stopped
init: true
environment:
MONGODB_URI: mongodb://al-mongo:27017/adventureland?replicaSet=rs0
MONGODB_NAME: adventureland
# See docker/patch-drm-check.js: without Steam/MAS auth a self-hosted
# server would flag every account with "Authorization Failure".
# Set to "1" to restore the upstream DRM check.
AL_DRM_CHECK: "0"
volumes:
# Editable keys/options from the host (replaces the cloned template).
- ./files/config:/app/secretsandconfig:ro
services:
# MongoDB runs as a single-node replica set: the game uses transactions,
# which MongoDB only allows on replica set members.
al-mongo:
image: *mongo_image
restart: unless-stopped
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
networks:
- adventureland
volumes:
- ./files/db:/data/db
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "quit(db.adminCommand({ ping: 1 }).ok ? 0 : 1)"]
interval: 5s
timeout: 10s
retries: 24
start_period: 10s
# One-shot: initiates the replset on a fresh database volume. Safe to rerun.
al-mongo-init:
image: *mongo_image
restart: "no"
networks:
- adventureland
depends_on:
al-mongo:
condition: service_healthy
entrypoint: ["/bin/bash", "/scripts/mongo-init.sh"]
volumes:
- ./docker/mongo-init.sh:/scripts/mongo-init.sh:ro
# One-shot: imports the 49 map documents from scripts/seed_mongodb.js.
#
# The seed script only accepts a direct local connection, so this container
# shares MongoDB's network namespace and talks to 127.0.0.1. It skips the
# import (exit 0) when the database already has collections.
al-seed:
<<: *app
restart: "no"
environment:
MONGODB_URI: mongodb://127.0.0.1:27017/adventureland
MONGODB_NAME: adventureland
network_mode: "service:al-mongo"
depends_on:
al-mongo-init:
condition: service_completed_successfully
entrypoint: ["/bin/sh", "/app/docker/seed.sh"]
# Web backend: serves the game client on http://localhost:8090
al-backend:
<<: *app
command: ["node", "main.js"]
environment:
# See docker/entrypoint.sh: lets the backend reach the game server at
# the address it advertises to browsers (localhost:7192).
AL_GAME_RELAY: "1"
AL_GAME_HOST: al-game
AL_GAME_PORT: "7192"
networks:
- adventureland
- backend
ports:
- 8090:8090
depends_on:
al-seed:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "node", "-e", 'fetch("http://127.0.0.1:8090/").then((r) => process.exit(r.status < 500 ? 0 : 1)).catch(() => process.exit(1))']
interval: 10s
timeout: 10s
retries: 12
start_period: 30s
# Game server (Socket.IO) - clients connect to localhost:7192 directly.
al-game:
<<: *app
command: ["node", "server.js", "local"]
working_dir: /app/node
networks:
- adventureland
- backend
ports:
- 7192:7192
depends_on:
al-seed:
condition: service_completed_successfully
al-backend:
condition: service_started
healthcheck:
test: ["CMD", "node", "-e", 'fetch("http://127.0.0.1:7192/").then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))']
interval: 10s
timeout: 10s
retries: 12
start_period: 30s
networks:
adventureland:
backend:
# external: true
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
# Container entrypoint.
#
# The backend talks to game servers through the address that is advertised to
# browsers (servers.<key>.address, e.g. localhost:7192). Inside the backend
# container "localhost" is the container itself, not the game container, so
# with AL_GAME_RELAY=1 a small TCP forwarder publishes the game port on
# 127.0.0.1 for the backend only.
set -eu
if [ "${AL_GAME_RELAY:-0}" = "1" ]; then
GAME_HOST="${AL_GAME_HOST:-al-game}"
GAME_PORT="${AL_GAME_PORT:-7192}"
echo "Relaying 127.0.0.1:${GAME_PORT} -> ${GAME_HOST}:${GAME_PORT}"
socat "TCP-LISTEN:${GAME_PORT},fork,reuseaddr,bind=127.0.0.1" "TCP:${GAME_HOST}:${GAME_PORT}" &
fi
exec "$@"
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# Initiates the single-node replica set "rs0" if it does not exist yet and
# waits until the member is primary. Runs as a one-shot container.
set -euo pipefail
HOST="${MONGO_HOST:-al-mongo}"
PORT="${MONGO_PORT:-27017}"
echo "Waiting for MongoDB at ${HOST}:${PORT} ..."
until mongosh --host "${HOST}" --port "${PORT}" --quiet \
--eval 'db.adminCommand({ ping: 1 }).ok' >/dev/null 2>&1; do
sleep 2
done
if [ "$(mongosh --host "${HOST}" --port "${PORT}" --quiet \
--eval 'try { rs.status().ok } catch (e) { 0 }')" != "1" ]; then
echo "Initiating replica set rs0 ..."
mongosh --host "${HOST}" --port "${PORT}" --quiet \
--eval 'rs.initiate({ _id: "rs0", members: [{ _id: 0, host: "al-mongo:27017" }] })'
fi
echo "Waiting for primary ..."
until mongosh --host "${HOST}" --port "${PORT}" --quiet \
--eval 'db.hello().isWritablePrimary' | grep -q true; do
sleep 2
done
echo "Replica set rs0 is ready."
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env node
// Local patch for the Adventure Land game server (applied at image build time).
//
// The upstream DRM check in node/server.js (start_character) treats every
// account created after 2019-02-01 as unverified unless it carries a Steam or
// Mac App Store auth id. A self-hosted server cannot issue those ids (Steam
// tickets need the game's private publisher key, and Steam OpenID sign-in needs
// a publisher web API key), so every login gets the "authfail" condition:
// gold/luck -85%, xp -20%.
//
// AL_DRM_CHECK=0 disables the check. Unset or any other value keeps the
// upstream behaviour. The script fails loudly when the upstream line is gone,
// so a code update cannot silently skip the patch.
const fs = require("node:fs");
const file = process.argv[2] || "/app/node/server.js";
const original =
'if (owner.created > new Date(2019, 1, 1) && !gf(owner, "legacy_override")) drm = true;';
const patched =
'if (process.env.AL_DRM_CHECK !== "0" && owner.created > new Date(2019, 1, 1) && !gf(owner, "legacy_override")) drm = true;';
const source = fs.readFileSync(file, "utf8");
if (source.includes(patched)) {
console.log("patch-drm-check: already applied");
process.exit(0);
}
if (source.split(original).length !== 2) {
console.error(
"patch-drm-check: expected exactly one occurrence of the upstream DRM check in " + file,
);
process.exit(1);
}
fs.writeFileSync(file, source.replace(original, patched));
console.log("patch-drm-check: applied to " + file);
Executable
+36
View File
@@ -0,0 +1,36 @@
#!/bin/sh
# Seeds the bundled map geometry into MongoDB.
#
# scripts/seed_mongodb.js only touches a database that has no collections and
# refuses a second run, so this wrapper checks first and skips the import on an
# already seeded volume.
set -eu
cd /app
: "${MONGODB_URI:?MONGODB_URI is not set}"
DB_NAME="${MONGODB_NAME:-adventureland}"
existing=$(node -e '
const { MongoClient } = require("mongodb");
const uri = process.env.MONGODB_URI;
const name = process.env.MONGODB_NAME || "adventureland";
(async () => {
const client = new MongoClient(uri, { serverSelectionTimeoutMS: 15000 });
await client.connect();
const collections = await client.db(name).listCollections({}, { nameOnly: true }).toArray();
await client.close();
process.stdout.write(String(collections.length));
})().catch((error) => {
console.error("MongoDB check failed:", error.message);
process.exit(1);
});
')
if [ "${existing}" -gt 0 ]; then
echo "Database '${DB_NAME}' already has ${existing} collection(s) - skipping seed."
exit 0
fi
echo "Database '${DB_NAME}' is empty - importing map data ..."
exec node scripts/seed_mongodb.js --confirm "${DB_NAME}"
+40
View File
@@ -0,0 +1,40 @@
// Adventure Land - local Docker configuration (keys)
//
// Replaces secretsandconfig/keys.js from the upstream repository. Values are
// development defaults: change them before exposing this stack to anyone but
// yourself. Unlike the upstream template, the values here are stable across
// restarts, so sessions and server records survive a `docker compose restart`.
//
// MongoDB connection settings can be overridden per container through the
// MONGODB_URI / MONGODB_NAME environment variables (see docker-compose.yml).
const path = require("node:path");
const env = (name, fallback) => process.env[name] || fallback;
module.exports = {
server_keyword: env("SERVER_KEYWORD", "local-server-keyword-change-me"),
mongodb_uri: env("MONGODB_URI", "mongodb://al-mongo:27017/adventureland?replicaSet=rs0"),
mongodb_name: env("MONGODB_NAME", "adventureland"),
// e.g. { tlsCAFile: path.resolve(__dirname, "ca.crt") }
mongodb_config: {},
stripe_test_api_key: "",
stripe_test_pkey: "",
stripe_api_key: "",
stripe_pkey: "",
steam_web_apikey: "",
steam_publisher_web_apikey: "",
sdk_password: env("SDK_PASSWORD", "local-sdk-password-change-me"),
amazon_ses_user: "",
amazon_ses_key: "",
// Admin / server-to-server keys, only meaningful for local play.
ACCESS_MASTER: env("ACCESS_MASTER", "local-access-master-change-me"),
BOT_MASTER: env("BOT_MASTER", "local-bot-master-change-me"),
SERVER_MASTER: env("SERVER_MASTER", "local-server-master-change-me"),
discord_token: env("DISCORD_TOKEN", ""),
apple_token: "",
steam_key: "",
};
+62
View File
@@ -0,0 +1,62 @@
// Adventure Land - local Docker configuration (options)
//
// Replaces secretsandconfig/options.js from the upstream repository.
//
// base_url must match the origin you open in the browser. If you change the
// published port in docker-compose.yml, change it here as well:
// base_url -> http://localhost:<backend port>
// servers.local.address -> localhost:<game server port>
const path = require("node:path");
machines = {
local: {
key: "",
ip: "0.0.0.0",
user: "",
},
};
servers = {
local: {
region: "DE",
name: "I",
path: "/socket.io/",
// Separate Socket.IO endpoint for the MessagePack parser; must differ
// from `path` and is advertised to clients through the server record.
msgpack_path: "/socket.io-msgpack/",
api_path: "/server.api/",
local_ip: "0.0.0.0",
local_port: 7192,
address: "localhost:7192",
machine: "local",
db: "dev",
secure: false,
nginx: false,
Dev: true,
},
};
module.exports = {
project_name: "adventureland",
name: "Adventure Land",
base_url: "http://localhost:8090",
Dev: true,
Local: true,
Prod: false,
Staging: false,
Engine: "mongodb",
observer_map: "main",
merchant_map: "main",
port: 8090,
close_timeout: 4000,
ip_limit: 3,
character_limit: 3,
fast_sdk: 0,
machines: machines,
servers: servers,
cookie_key: "auth",
// SECURITY: with Local + unsecure_admin every visitor counts as admin, which
// opens /admin/executor (arbitrary code execution). Off by default; only
// enable it while the port is reachable from your machine alone.
unsecure_admin: true,
};