fix rewards and bypass ip limits
build container / build (push) Failing after 2s

This commit is contained in:
2026-11-02 02:49:23 +01:00
parent 3de09009d6
commit d589dd4d64
6 changed files with 344 additions and 10 deletions
+15
View File
@@ -74,6 +74,21 @@ RUN node /tmp/patch-custom-game.js /app
COPY docker/patch-auto-verify-email.js /tmp/patch-auto-verify-email.js
RUN node /tmp/patch-auto-verify-email.js /app
# Local patch: make the Steam/MAS auth-id requirement for tutorial and class
# rewards optional (docker/patch-tutorial-reward-auth.js). Controlled at runtime
# by options.tutorial_reward_requires_auth_id.
COPY docker/patch-tutorial-reward-auth.js /tmp/patch-tutorial-reward-auth.js
RUN node /tmp/patch-tutorial-reward-auth.js /app
# Local patch: make the merchant and non-merchant multi-account limits
# configurable (docker/patch-multi-account-limits.js). Upstream hard-codes one
# merchant per IP/owner and three non-merchant characters per IP/owner in the
# backend enforcement cron, while login uses options.ip_limit/character_limit.
# The patch makes both places use options.merchant_ip_limit,
# options.merchant_owner_limit, options.ip_limit and options.character_limit.
COPY docker/patch-multi-account-limits.js /tmp/patch-multi-account-limits.js
RUN node /tmp/patch-multi-account-limits.js /app
FROM ${NODE_IMAGE} AS runtime
+96 -8
View File
@@ -27,6 +27,12 @@ und Game-Server. Bei späteren Starts überspringt sich der Seed selbst.
[Zum Entwickeln]“** klicken, E-Mail + Passwort eingeben. Danach Charakter
erstellen und auf den Charakter klicken – man landet direkt im Spiel.
**Charakter Limits anheben** Folgendes Kommando setzt für alle existierenden Spieler das Charakter Limit auf 18.
```sh
docker compose exec al-mongo mongosh adventureland --eval \
'db.user.updateMany({}, { $set: { "info.slots": 18 } })'
```
**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:
@@ -40,6 +46,37 @@ 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.
**GM werden (Game):** Der Web-Admin ist **kein** Game-GM. Die GM-Rolle hängt am
Charakter, nicht am Benutzerkonto: Der Game-Server prüft `player.role` bzw.
`player.p.role` auf `"gm"`. Charakter vorher ausloggen, dann in MongoDB setzen:
```sh
docker compose exec al-mongo mongosh adventureland --eval \
'db.character.updateOne(
{ name: "CharName" },
{ $set: { role: "gm", "info.p.role": "gm" } }
)'
```
Danach den Charakter neu einloggen. `role: "gm"` gibt die In-Game-GM-Rechte und
ist unabhängig von `user.admin` (Web-Admin). Wer **nur** von den Multi-Account-
Limits ausgenommen werden will, aber keine GM-Rechte braucht, setzt stattdessen
`info.p.free`:
```sh
docker compose exec al-mongo mongosh adventureland --eval \
'db.character.updateOne(
{ name: "CharName" },
{ $set: { "info.p.free": true } }
)'
```
| Feld | Wirkung |
| --- | --- |
| `user.admin: true` | Web-Admin: `/admin/executor`, `/admin/renderer`; keine In-Game-GM-Rechte |
| Charakter `role: "gm"` | In-Game-GM; wird außerdem von den Limits ausgenommen |
| Charakter `info.p.free: true` | nur Limit-Ausnahme; keine GM-Rechte |
## Aufbau
| Service | Image | Aufgabe |
@@ -61,7 +98,8 @@ 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`.
- `config/options.js` – `base_url`, Ports, Serverdefinition `local`,
Multi-Account-Limits.
Beide Dateien sind normale Node-Module; nach Änderungen Container neu starten:
@@ -73,6 +111,39 @@ docker compose restart backend game
> `docker-compose.yml` (`ports`), `config/options.js` (`port`, `local_port`,
> `address`) und `base_url` (inkl. Port).
### Multi-Account-Limits
`config/options.js` steuert, wie viele Charaktere gleichzeitig online sein
dürfen:
| Option | Bedeutung | aktuell |
| --- | --- | --- |
| `ip_limit` | Nicht-Händler pro IP (mal `ipx` einer IP-Ausnahme) | `9` |
| `character_limit` | Nicht-Händler pro Konto | `3` |
| `merchant_ip_limit` | Händler pro IP | `3` |
| `merchant_owner_limit` | Händler pro Konto | `1` |
Die Werte gelten beim Login (`is_player_allowed`) und im Backend-Cron
(`enforce_limitations`). Der Patch
`docker/patch-multi-account-limits.js` ersetzt dabei die früher fest codierten
Grenzen (3 Nicht-Händler, 1 Händler). Mit den aktuellen Werten können z. B. drei
Konten von derselben IP jeweils drei Nicht-Händler-Charaktere online haben
(9 insgesamt); ein zehntes Nicht-Händler-Charakter bzw. ein vierter Händler wird
abgewiesen. `character_limit` ist die Zahl der **gleichzeitig online** erlaubten
Charaktere, nicht die Zahl der Charakterplätze (`info.slots`); `ip_limit` ist
ebenfalls keine Slotzahl. Eine `IP_<ip>`-Ausnahme mit
`{ exception: true, info: { limit: N } }` multipliziert nur `ip_limit` für
Nicht-Händler, nicht die Händler-Limits. Nicht-freie Händler zählen; `p.free`
und GM-Charaktere fallen heraus. `merchant_owner_limit: 1` erlaubt einen Händler
pro Konto; Admin- und User-Händler auf getrennten Konten sind damit kein
Problem. Sollen beide auf **demselben** Konto laufen, `merchant_owner_limit` auf
`2` setzen.
> Hinter einem Reverse-Proxy (Caddy) muss die echte Client-IP als
> `X-Forwarded-For` ankommen. Sonst sieht der Server nur die Proxy-/Docker-IP
> und alle Spieler teilen sich dieselben Limits. Siehe
> `deploy/Caddyfile.example`.
### MongoDB-Zugriff
Die Datenbank ist nur im Compose-Netz erreichbar. Für einen Blick hinein:
@@ -145,6 +216,21 @@ Login als verifiziert und überspringt die Verifikationsmail. Ohne den Schalter
bleibt das Upstream-Verhalten unverändert. Auch dieser Patch bricht den Build
ab, wenn ein Anker in `api.js` fehlt.
`docker/patch-tutorial-reward-auth.js` macht die `auth_id`-Pflicht der
Tutorial- und Klassenbelohnungen abschaltbar. Bei
`options.tutorial_reward_requires_auth_id: false` können auch reine Web-Accounts
`c0` und Klassenbelohnungen abholen; fehlt die Option oder steht sie auf `true`,
gilt das Upstream-Verhalten. Fehlt einer der beiden Anker in `node/server.js`,
bricht der Build ab.
`docker/patch-multi-account-limits.js` macht die Händler- und Nicht-Händler-
Limits konfigurierbar. Upstream prüft beim Login `options.ip_limit` und
`options.character_limit`, würgt im Backend-Cron aber pauschal nach 3
Nicht-Händlern bzw. 1 Händler ab. Der Patch liest beide Stellen aus
`config/options.js` (`ip_limit`, `character_limit`, `merchant_ip_limit`,
`merchant_owner_limit`). Fehlt ein Anker in `node/server_functions.js` oder
`adventure_functions.js`, bricht der Build ab.
## Spieldaten anpassen (`files/custom/game.js`)
Werte bestehender Elemente und neue Elemente lassen sich ohne Quelltextänderung
@@ -207,15 +293,17 @@ Custom-Datei zurückgesetzt werden; dann den Game-Server neu starten.
auf `true` setzen (siehe Admin-Befehl oben). Die `/ev`-Route bleibt in beiden
Fällen funktionsfähig.
- **Tutorial-Belohnung (`c0`):** Jedes Konto mit `auth_id` (Steam/MAS) kann nach
- **Tutorial-Belohnung (`c0`):** Jedes verifizierte Konto kann nach
abgeschlossenem Tutorial einmal `c0` abholen. Die Belohnung ist ein zufälliger
kosmetischer Look aus dem `cosmo0`-Pool (über 100 Varianten). Der Game-Server
prüft dafür `player.verified` **und** `player.auth_id`
(`node/server.js:5272`). Bei reinen Web-Accounts ohne Steam/MAS bleibt
`auth_id` leer, deshalb ist die Belohnung trotz Auto-Verifizierung weiterhin
gesperrt. Die alten Klassen-Belohnungen (`creward`) sind in
`design/classes.js` auskommentiert und werden vom Client nicht angefragt;
Hardcore-Rewards gelten nur auf dem Hardcore-Server.
prüft `player.verified` und normalerweise `player.auth_id` (Steam/MAS,
`node/server.js`, `ureward`/`creward`). Bei reinen Web-Accounts ist `auth_id`
leer; deshalb steht `options.tutorial_reward_requires_auth_id` hier auf
`false`, damit auch sie die Belohnung erhalten. Auf `true` gestellt, gilt
wieder die Upstream-Sperre für Web-Accounts. Die alten Klassen-Belohnungen
(`creward`) nutzen denselben Schalter, sind aber in `design/classes.js`
auskommentiert und werden vom Client nicht angefragt; Hardcore-Rewards gelten
nur auf dem Hardcore-Server.
- **„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
+8 -1
View File
@@ -20,7 +20,12 @@ al.example.com {
@game path /socket.io/* /socket.io-msgpack/* /server.api/*
handle @game {
reverse_proxy 10.10.16.28:7192 {
}
# Overwrite, not append: Adventure Land reads the first
# X-Forwarded-For entry for its IP limits. Without this a client
# could send a fake header and bypass the limits.
header_up X-Forwarded-For {remote_host}
}
}
# --- Web backend (container port 8090) ------------------------------
# Everything else: HTML, JS, /api/*, static files.
@@ -30,6 +35,8 @@ al.example.com {
# auth cookie domain from it.
header_up Host {host}
# Overwrite the client IP; see the game handler above.
header_up X-Forwarded-For {remote_host}
}
}
# Internal-only TLS (no Let's Encrypt). Clients must trust Caddy's CA.
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env node
// Local patch: make the multi-account limits configurable.
//
// Upstream hard-codes:
// - one merchant per IP and per owner,
// - three non-merchant characters per IP and per owner,
// and only checks some of those values at login.
//
// This patch reads the limits from options.js in both places:
// - is_player_allowed() (login, node/server_functions.js)
// - enforce_limitations() (backend cron, adventure_functions.js)
//
// New options (defaults match upstream):
// merchant_ip_limit: 1
// merchant_owner_limit: 1
// ip_limit: 3
// character_limit: 3
//
// The patch is idempotent and fails loudly if an anchor is missing.
const fs = require("node:fs");
const path = require("node:path");
const app = path.resolve(process.argv[2] || "/app");
const serverFunctionsFile = path.join(app, "node", "server_functions.js");
const adventureFunctionsFile = path.join(app, "adventure_functions.js");
function fail(message) {
console.error("patch-multi-account-limits: " + message);
process.exit(1);
}
function replaceOnce(source, from, to, label) {
const count = source.split(from).length - 1;
if (count !== 1) {
fail(label + ": expected 1 anchor, found " + count);
}
return source.replace(from, to);
}
function patchServerFunctions(source) {
const marker = "function get_multi_account_identity(player) {";
const oldMerchant =
'\tif (player.type == "merchant") {\n' +
"\t\tfor (var id in players) {\n" +
"\t\t\tif ((players[id].stones && 0) || id == player.socket.id || is_free(player)) {\n" +
"\t\t\t\tcontinue;\n" +
"\t\t\t}\n" +
'\t\t\tif (players[id].owner == player.owner && players[id].type == "merchant") {\n' +
"\t\t\t\treturn false;\n" +
"\t\t\t}\n" +
"\t\t}\n" +
"\t\treturn true;\n" +
"\t}";
if (source.includes(marker)) {
if (source.includes(oldMerchant)) {
fail("server_functions.js has a partial merchant patch");
}
console.log("patch-multi-account-limits: server_functions.js already patched");
return source;
}
source = replaceOnce(
source,
'function get_limit_identity(player) {\n\treturn player && player.mainframe === true ? "mainframe:" + player.owner : get_ip_server(player);\n}',
'function get_limit_identity(player) {\n\treturn player && player.mainframe === true ? "mainframe:" + player.owner : get_ip_server(player);\n}\n\n' +
"// Multi-account identity; mirrors the auth/temp_auth handling in enforce_limitations().\n" +
"function get_multi_account_identity(player) {\n" +
'\tif (player.temp_auth) return "owner:" + player.owner;\n' +
'\tif (player.auth_id) return "auth:" + player.auth_id;\n' +
"\treturn get_limit_identity(player);\n" +
"}",
"get_limit_identity",
);
const newMerchant =
"\t// Multi-account limits: configured in options.js; the backend cron uses the same values.\n" +
'\tif (player.type == "merchant") {\n' +
"\t\tvar merchant_limit = options.merchant_owner_limit === undefined ? 1 : options.merchant_owner_limit;\n" +
"\t\tvar merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;\n" +
"\t\tvar merchants = 0;\n" +
"\t\tvar merchant_ips = 0;\n" +
"\t\tvar merchant_identity = get_multi_account_identity(player);\n" +
"\t\tfor (var id in players) {\n" +
'\t\t\tif (id == player.socket.id || is_free(players[id]) || players[id].type != "merchant") {\n' +
"\t\t\t\tcontinue;\n" +
"\t\t\t}\n" +
"\t\t\tif (players[id].owner == player.owner) {\n" +
"\t\t\t\tmerchants++;\n" +
"\t\t\t}\n" +
"\t\t\tif (get_multi_account_identity(players[id]) == merchant_identity) {\n" +
"\t\t\t\tmerchant_ips++;\n" +
"\t\t\t}\n" +
"\t\t}\n" +
"\t\tif (merchants >= merchant_limit || merchant_ips >= merchant_ip_limit) {\n" +
"\t\t\treturn false;\n" +
"\t\t}\n" +
"\t\treturn true;\n" +
"\t}";
source = replaceOnce(source, oldMerchant, newMerchant, "merchant branch");
return source;
}
function patchAdventureFunctions(source) {
const marker = "var merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;";
const oldMerchant =
'\t\t\t\tif (player.type === "merchant" && (mips[player.ip] > 1 || mowners[player.owner] > 1) && to_disconnect.indexOf(player.name) === -1) {';
const oldOther =
'\t\t\t\tif (player.type !== "merchant" && (ips[player.ip] > 3 * (ipx[player.ip] || 1) || owners[player.owner] > 3) && to_disconnect.indexOf(player.name) === -1) {';
if (source.includes(marker)) {
if (source.includes(oldMerchant) || source.includes(oldOther)) {
fail("adventure_functions.js has a partial limits patch");
}
console.log("patch-multi-account-limits: adventure_functions.js already patched");
return source;
}
source = replaceOnce(
source,
"\t\t\tvar to_disconnect = [];",
"\t\t\tvar to_disconnect = [];\n" +
"\t\t\tvar merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;\n" +
"\t\t\tvar merchant_owner_limit = options.merchant_owner_limit === undefined ? 1 : options.merchant_owner_limit;\n" +
"\t\t\tvar ip_limit = options.ip_limit === undefined ? 3 : options.ip_limit;\n" +
"\t\t\tvar character_limit = options.character_limit === undefined ? 3 : options.character_limit;",
"limit variables",
);
source = replaceOnce(
source,
oldMerchant,
"\t\t\t\tif (\n" +
'\t\t\t\t\tplayer.type === "merchant" &&\n' +
"\t\t\t\t\t(mips[player.ip] > merchant_ip_limit || mowners[player.owner] > merchant_owner_limit) &&\n" +
"\t\t\t\t\tto_disconnect.indexOf(player.name) === -1\n" +
"\t\t\t\t) {",
"merchant condition",
);
source = replaceOnce(
source,
oldOther,
"\t\t\t\tif (\n" +
'\t\t\t\t\tplayer.type !== "merchant" &&\n' +
"\t\t\t\t\t(ips[player.ip] > ip_limit * (ipx[player.ip] || 1) || owners[player.owner] > character_limit) &&\n" +
"\t\t\t\t\tto_disconnect.indexOf(player.name) === -1\n" +
"\t\t\t\t) {",
"non-merchant condition",
);
return source;
}
let serverFunctions = fs.readFileSync(serverFunctionsFile, "utf8");
serverFunctions = patchServerFunctions(serverFunctions);
fs.writeFileSync(serverFunctionsFile, serverFunctions);
let adventureFunctions = fs.readFileSync(adventureFunctionsFile, "utf8");
adventureFunctions = patchAdventureFunctions(adventureFunctions);
fs.writeFileSync(adventureFunctionsFile, adventureFunctions);
console.log("patch-multi-account-limits: wrote " + serverFunctionsFile + " and " + adventureFunctionsFile);
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env node
// Local patch: make the platform auth-id requirement for rewards optional.
//
// Adventure Land's tutorial (ureward) and class (creward) handlers require
// player.verified and player.auth_id. auth_id is only populated for Steam/MAS
// accounts; plain web accounts have an empty pid/auth_id and therefore can
// never claim these rewards. On a self-hosted server that requirement is
// usually unnecessary.
//
// options.tutorial_reward_requires_auth_id:
// true/absent -> upstream behaviour (auth_id required)
// false -> verified web accounts may claim the rewards too
//
// The patch is idempotent and fails loudly if the two anchors are gone.
const fs = require("node:fs");
const path = require("node:path");
const app = path.resolve(process.argv[2] || "/app");
const file = path.join(app, "node", "server.js");
let source = fs.readFileSync(file, "utf8");
const from = `\t\t\tif (!player.verified || !player.auth_id) {`;
const to = `\t\t\tif (
\t\t\t\t!player.verified ||
\t\t\t\t((typeof options === "undefined" || options.tutorial_reward_requires_auth_id !== false) && !player.auth_id)
\t\t\t) {`;
function fail(message) {
console.error("patch-tutorial-reward-auth: " + message);
process.exit(1);
}
const unpatched = source.split(from).length - 1;
const patched = source.split(to).length - 1;
if (unpatched === 2) {
source = source.split(from).join(to);
console.log("patch-tutorial-reward-auth: patched ureward and creward gates");
} else if (unpatched === 0 && patched === 2) {
console.log("patch-tutorial-reward-auth: already patched");
} else {
fail("expected 2 reward gates, found " + unpatched + " unpatched and " + patched + " patched; refusing partial patch");
}
fs.writeFileSync(file, source);
console.log("patch-tutorial-reward-auth: wrote " + file);
+13 -1
View File
@@ -49,12 +49,24 @@ module.exports = {
merchant_map: "main",
port: 8090,
close_timeout: 4000,
ip_limit: 3,
// Max concurrent non-merchant characters per IP (multiplied by an IP
// exception's limit) and per account. Login and the backend cron both use
// these values; 9 here allows 3 accounts with 3 characters each from one IP.
ip_limit: 9,
character_limit: 3,
// Max concurrent merchant characters per IP and per account. Upstream is
// hard-coded to 1/1; this local build reads these options in login and in
// the backend cron. 3 here means three merchant accounts may share one IP.
merchant_ip_limit: 3,
merchant_owner_limit: 1,
// Without an email service, mark new accounts as verified at signup and
// keep existing accounts verified on login. Set to false to restore the
// upstream verification flow (requires AWS SES).
auto_verify_email: true,
// Tutorial/class rewards normally require a platform auth id (Steam/MAS)
// as an anti-alt measure. Web-only accounts have no auth id; set to false
// to let verified web accounts claim them too. Default is true.
tutorial_reward_requires_auth_id: false,
fast_sdk: 0,
machines: machines,
servers: servers,