Compare commits

..
34 Commits
Author SHA1 Message Date
scriptos 8a3ef5241e Release: Docker Image Version 1.2.2 gepusht 2026-03-23 09:50:21 +01:00
scriptos 634cd8c0a6 docs(xaseco): umfassende Chat-Befehlsreferenz ergänzt
Bisherige Kurztabelle durch vollständige Befehlsübersicht ersetzt,
gegliedert nach Kategorien: Hilfe, Rekorde, Rankings, Strecken,
Karma/Voting, Kommunikation, Spieleroptionen und Admin-Befehle.
Verweis auf offizielle Dokumentation (docs.xaseco.org) hinzugefügt.
2026-03-23 09:47:52 +01:00
scriptos daaace417b fix(security): AdminServ-Konfigurationspasswort durch zufälligen Hash ersetzen 2026-03-23 09:43:28 +01:00
scriptos f7d3a9ce83 docs: Doku an Code anpassen, AdminServ-Konfigurationspasswort automatisch absichern 2026-03-23 09:25:48 +01:00
scriptos 59826b2e75 Merge pull request 'release-1.2.1' (#12) from release-1.2.1 into master
Reviewed-on: #12
2026-03-22 22:42:26 +00:00
scriptos 5a321db094 Release: Docker Image Version 1.2.1 gepusht 2026-03-22 23:41:40 +01:00
scriptos 344029c551 feat: TeamSpeak3-Plugin mit eigenem Gateway reaktivieren 2026-03-22 23:35:11 +01:00
scriptos bf3b7858ee Merge pull request 'Release: Docker Image Version 1.2.0 gepusht' (#11) from release-1.1.2 into master
Reviewed-on: #11
2026-03-22 20:30:55 +00:00
scriptos ebd20d4653 Merge branch 'master' into release-1.1.2 2026-03-22 20:30:41 +00:00
scriptos fcea5b4d41 Release: Docker Image Version 1.2.0 gepusht 2026-03-22 21:29:37 +01:00
scriptos 6f4426e080 Merge pull request 'release-1.2.0' (#10) from release-1.1.2 into master
Reviewed-on: #10
2026-03-22 20:28:48 +00:00
scriptos ed5b3d22fb fix: PHP-Warnungen im RemoteCP Mods-Plugin behoben (foreach auf leere Umgebungen) 2026-03-22 21:27:03 +01:00
scriptos 3fb1dac5ba feat: Forced Mods (Skins) per Umgebungsvariable beim Containerstart setzen 2026-03-22 20:58:46 +01:00
scriptos 61deb93273 feat: Automatische MatchSettings-Erkennung & AdminServ-Bugfixes 2026-03-22 17:05:25 +01:00
scriptos fdcc82e935 Workspace wird nicht mehr mit committed 2026-03-22 15:24:31 +01:00
scriptos 75ae777f1e feat: Auto-Import von AdminServ ServerOptions beim Containerstart 2026-03-22 15:23:44 +01:00
scriptos b7a909392e Merge pull request 'release-1.1.1' (#7) from release-1.1.1 into master
Reviewed-on: #7
2026-03-22 11:26:40 +00:00
scriptos ec6418e9ab Release: Docker Image Version 1.1.1 gepusht 2026-03-22 12:25:58 +01:00
scriptos f013ccd61e fix: XMLRPC-Port aus externem Port-Mapping entfernen, Doku anpassen 2026-03-22 12:24:08 +01:00
scriptos 0717088eb4 fix: RemoteCP settings.xml-Erstellung entfernt (verursacht Folgefehler bei fehlenden Defaults) 2026-03-22 12:06:16 +01:00
scriptos 5ccba54e84 Merge pull request 'release-1.1.0' (#6) from release-1.0.1 into master
Reviewed-on: #6
2026-03-22 00:56:53 +00:00
scriptos 9b37dfd2bc Release: Docker Image Version 1.1.0 gepusht 2026-03-22 01:55:16 +01:00
scriptos 503fadf1d6 docs: Projektstruktur aktualisiert und in Doku-Uebersicht verschoben 2026-03-22 01:47:47 +01:00
scriptos 3354ebd541 docs: Fertiges Docker Image aus Container-Registry bewerben 2026-03-22 01:44:09 +01:00
scriptos 7482071f2c README: Hinweis auf Repository-Spiegelung von Gitea nach GitHub ergänzt 2026-03-22 01:36:56 +01:00
scriptos 69940ee56b Kurzbeschreibung des Projekts zur README hinzugefügt 2026-03-22 01:32:18 +01:00
scriptos c081e43375 fix(mariadb): XAseco-DB-Variablen explizit im Compose ergänzt 2026-03-22 01:29:10 +01:00
scriptos 35e485b065 docs: Danksagung an Thomas (retronerd.at) in README ergänzt 2026-03-22 01:24:09 +01:00
scriptos b672f84f55 fix: RemoteCP-Benutzerregistrierung standardmäßig deaktivieren 2026-03-22 01:21:07 +01:00
scriptos 76e739f12a refactor(license): Redundante Namensnennung in Lizenzbedingungen entfernt 2026-03-22 01:15:37 +01:00
scriptos 5891429c83 fix: RemoteCP CustomPoints PHP-Warnungen behoben (undefined constants)
Bare-constant-Zugriffe (pt_custom, pt_points, ...) in CustomPoints/index.php
durch defined()-Prüfungen ersetzt, um PHP 7.2+ Warnungen zu vermeiden.
Zusätzlich werden im Produktivmodus (PHP_DISPLAY_ERRORS=false) Warnungen
und Notices in der PHP error_reporting unterdrückt.

- Gepatchte index.php als assets/config/remotecp/plugins/CustomPoints/index.php
- Dockerfile: COPY der gepatchten Datei statt fragiler sed-Patches
- RunTrackmaniaServer.sh: Auto-Patch für bestehende Volumes beim Container-Start
- error_reporting im Produktivmodus um ~E_WARNING & ~E_NOTICE ergänzt

Closes #4
2026-03-22 01:09:37 +01:00
scriptos ffdc11a02b feat: Warmup deaktivieren & als ENV-Variable steuerbar machen
- Default von allwarmupduration in custom_game_settings.txt auf 0 gesetzt
- Neue Umgebungsvariable ALLWARMUPDURATION (Default: 0)
- Startup-Script wendet ENV auf MatchSettings an (inkl. FORCE_CONFIG_UPDATE)
- Dokumentation ergänzt (umgebungsvariablen.md)

Closes #2
2026-03-22 00:29:12 +01:00
scriptos e05de1fbe2 feat: SERVER_LADDER_LIMIT_MAX als ENV-Variable hinzugefügt (Closes #3) 2026-03-22 00:22:15 +01:00
scriptos 495a5a2663 fix: Berechtigungen bei Volume-Kopie korrigiert (cp -a statt cp -r)
Closes #5
2026-03-22 00:09:35 +01:00
24 changed files with 2046 additions and 104 deletions

No files matched your search

+24
View File
@@ -35,6 +35,7 @@ SERVER_PASSWORD=
SERVER_MAX_SPECTATORS=40
SERVER_SPEC_PASSWORD=
SERVER_LADDER_MODE=forced
SERVER_LADDER_LIMIT_MAX=60000
# --- Netzwerk ---
# Bitte ändere die Ports, wenn sie in deinem Netzwerk bereits verwendet werden.
@@ -54,6 +55,29 @@ SERVER_MODE=internet
# In einer Produktionsumgebung sollte dieser Wert jedoch auf false belassen werden, um zu verhindern, dass die Konfiguration versehentlich überschrieben wird.
FORCE_CONFIG_UPDATE=false
# --- Forced Mods (Skins) ---
# Beim Containerstart kann automatisch ein Mod (Skin) pro Umgebung forciert werden.
# Der Wert ist die vollständige URL zu einer Mod-ZIP-Datei, die Spieler beim Betreten des Servers herunterladen.
# Verfügbare Skins findest du unter: https://assets.techniverse.net/tm/skins/
# Beispiel: FORCE_MOD_STADIUM=https://assets.techniverse.net/tm/skins/Portal.zip
# Leer lassen = kein Mod für diese Umgebung.
FORCE_MOD_STADIUM=
FORCE_MOD_ISLAND=
FORCE_MOD_BAY=
FORCE_MOD_COAST=
FORCE_MOD_SPEED=
FORCE_MOD_ALPINE=
FORCE_MOD_RALLY=
# --- Spieleinstellungen (MatchSettings) ---
# Steuert, welche MatchSettings-Datei beim Serverstart geladen wird.
# "auto" = die neueste .txt-Datei in data/gamedata/Tracks/MatchSettings/ wird automatisch erkannt.
# Alternativ kann ein expliziter Dateiname angegeben werden (z.B. "turnier_settings.txt").
MATCHSETTINGS_FILE=auto
# Warmup-Dauer für alle Runden (0 = deaktiviert, 1 = eine Runde Warmup)
ALLWARMUPDURATION=0
# --- Debugging ---
# Setze diesen Wert auf true, um PHP-Fehlermeldungen anzuzeigen. Dies kann bei der Fehlersuche hilfreich sein, sollte aber in einer Produktionsumgebung auf false belassen werden.
PHP_DISPLAY_ERRORS=false
+2
View File
@@ -3,3 +3,5 @@
# Persistente Server-Daten
data/
.ki-workspace/
+49 -6
View File
@@ -41,7 +41,7 @@ RUN mkdir -p /opt/tmserver/GameData/Config/AdminServ/ServerOptions \
&& chown -R www-data:www-data /opt/tmserver/GameData/Config/AdminServ
# Gesamtes GameData als Default-Template sichern (wird beim ersten Start ins Volume kopiert)
RUN cp -r /opt/tmserver/GameData /opt/tmserver/default-gamedata
RUN cp -a /opt/tmserver/GameData /opt/tmserver/default-gamedata
COPY assets/bin/RunTrackmaniaServer.sh /opt/tmserver/
RUN sed -i 's/\r$//' /opt/tmserver/RunTrackmaniaServer.sh \
@@ -70,8 +70,35 @@ RUN unzip /var/www/html/remoteCP_v4.0.3.5.zip -d /var/www/html \
&& chmod -R 777 /var/www/html/remotecp/xml/settings \
&& chown -R www-data:www-data /var/www/html/remotecp/
# Fix PHP-Warnungen in RemoteCP CustomPoints-Plugin (undefined constants)
# RemoteCP nutzt bare constants (pt_custom, pt_points, ...), die in PHP 7.2+
# Warnungen ausloesen. Das gepatchte Plugin nutzt stattdessen defined()-Pruefungen.
COPY assets/config/remotecp/plugins/CustomPoints/index.php /var/www/html/remotecp/plugins/CustomPoints/index.php
RUN chown www-data:www-data /var/www/html/remotecp/plugins/CustomPoints/index.php
# Fix PHP-Warnungen in RemoteCP Mods-Plugin (foreach auf leere Umgebungen)
# Leere Umgebungen (Island, Bay, …) fuehrten zu "Invalid argument supplied
# for foreach()", weil $this->mods['Env'] nicht initialisiert war.
# Zusaetzlich bare-constant-Warnungen (pt_*) mit defined()-Pruefungen entschaerft.
COPY assets/config/remotecp/plugins/Mods/index.php /var/www/html/remotecp/plugins/Mods/index.php
RUN chown www-data:www-data /var/www/html/remotecp/plugins/Mods/index.php
# RemoteCP Mods-Plugin: Vorkonfigurierte Skin-Liste (techniverse.net)
COPY assets/config/remotecp/plugins/Mods/settings.xml /var/www/html/remotecp/plugins/Mods/settings.xml
RUN chown www-data:www-data /var/www/html/remotecp/plugins/Mods/settings.xml
# Fix AdminServ MatchSettings-Bugs fuer TmForever:
# 1) get_matchset_mapimport.php: Falscher Pfad-Praefix (MatchSettings/ statt
# des tatsaechlichen Map-Ordners) beim Erstellen von MatchSettings.
# 2) maps-creatematchset.php: GetModeScriptInfo-Aufruf (existiert nur in
# ManiaPlanet/TM2) wird fuer TmForever uebersprungen.
COPY assets/config/adminserv/get_matchset_mapimport.php /var/www/html/resources/ajax/get_matchset_mapimport.php
COPY assets/config/adminserv/maps-creatematchset.php /var/www/html/resources/process/maps-creatematchset.php
RUN chown www-data:www-data /var/www/html/resources/ajax/get_matchset_mapimport.php \
&& chown www-data:www-data /var/www/html/resources/process/maps-creatematchset.php
# AdminServ- und RemoteCP-Dateien als Default-Template sichern (wird beim ersten Start ins Volume kopiert)
RUN cp -r /var/www/html /opt/tmserver/default-controlpanel
RUN cp -a /var/www/html /opt/tmserver/default-controlpanel
# XAseco installieren
COPY assets/bin/xaseco_v1.16.zip /opt/tmserver/
@@ -103,12 +130,16 @@ RUN unzip /opt/tmserver/xaseco_v1.16.zip -d /opt/tmserver/ \
# newinstall-Ordner aufraeumen
&& rm -rf /opt/tmserver/xaseco/newinstall
# Nicht mehr verfuegbare Plugins deaktivieren (freezone entfernen, teamspeak3 auskommentieren)
RUN sed -i '/<plugin>plugin\.freezone\.php<\/plugin>/d' /opt/tmserver/xaseco/plugins.xml \
&& sed -i 's/<plugin>plugin\.teamspeak3\.php<\/plugin>/<!-- <plugin>plugin.teamspeak3.php<\/plugin> -->/' /opt/tmserver/xaseco/plugins.xml
# Nicht mehr verfuegbares freezone-Plugin entfernen
RUN sed -i '/<plugin>plugin\.freezone\.php<\/plugin>/d' /opt/tmserver/xaseco/plugins.xml
# TeamSpeak3-Plugin: Eigenes Gateway einbinden (Original-Gateway nicht mehr verfuegbar)
# Die teamspeak3.xml wird direkt in den XAseco-Ordner kopiert, damit das Plugin
# beim Start automatisch den konfigurierten TS3-Server anzeigt.
COPY assets/config/xaseco/teamspeak3.xml /opt/tmserver/xaseco/teamspeak3.xml
# XAseco als Default-Template sichern (wird beim ersten Start ins Volume kopiert)
RUN cp -r /opt/tmserver/xaseco /opt/tmserver/default-xaseco
RUN cp -a /opt/tmserver/xaseco /opt/tmserver/default-xaseco
# PHP-Debug-Konfiguration: wird zur Laufzeit vom Startup-Script gesetzt
# (kein Rebuild noetig – nur Container neustarten)
@@ -136,6 +167,18 @@ ENV SERVER_DOWNLOAD_RATE=8192
ENV SERVER_MODE=internet
ENV FORCE_CONFIG_UPDATE=false
# Spieleinstellungen (MatchSettings)
ENV ALLWARMUPDURATION=0
# Forced Mods (Skins) - URL zu ZIP-Dateien, die beim Start forciert werden
ENV FORCE_MOD_STADIUM=""
ENV FORCE_MOD_ISLAND=""
ENV FORCE_MOD_BAY=""
ENV FORCE_MOD_COAST=""
ENV FORCE_MOD_SPEED=""
ENV FORCE_MOD_ALPINE=""
ENV FORCE_MOD_RALLY=""
# RemoteCP
ENV REMOTECP_DB_HOST=mariadb
ENV REMOTECP_DB_NAME=remotecp
+3 -5
View File
@@ -23,11 +23,9 @@ unterzulizenzieren und/oder Kopien der Software zu verkaufen, und Personen,
denen die Software zur Verfügung gestellt wird, dies unter den folgenden
Bedingungen zu gestatten:
Der obige Urheberrechtshinweis und dieser Genehmigungshinweis müssen in allen
Kopien oder wesentlichen Teilen der Software enthalten sein. Bei jeglicher
Weiterverarbeitung, Veröffentlichung oder Verbreitung der Software müssen der
Name „Patrick Asmus" und die E-Mail-Adresse „support@techniverse.net" des
Urhebers genannt werden.
Der obige Urheberrechtshinweis einschließlich der Angaben zum Lizenzinhaber
sowie dieser Genehmigungshinweis müssen in allen Kopien oder wesentlichen
Teilen der Software enthalten sein.
DIE SOFTWARE WIRD „WIE BESEHEN" OHNE JEGLICHE AUSDRÜCKLICHE ODER
STILLSCHWEIGENDE GEWÄHRLEISTUNG ZUR VERFÜGUNG GESTELLT, EINSCHLIESSLICH,
+30 -29
View File
@@ -1,6 +1,13 @@
# tmserver-docker
Trackmania Nations Forever Docker Server
Ein vollständiges Docker-Setup für einen **TrackMania Nations Forever**-Server – inklusive Web-Verwaltung und Server-Controller:
- **TrackMania Dedicated Server** – der eigentliche Spielserver für Internet- oder LAN-Betrieb
- **[XAseco](docs/xaseco.md)** – Server-Controller, der lokale Rekorde, Dedimania-Weltrekorde, Karma/Votes und eine Track-Jukebox direkt im Spielchat verwaltet
- **[AdminServ](docs/adminserv.md)** – Web-Oberfläche zur komfortablen Verwaltung und Konfiguration des Servers
- **[RemoteCP](docs/remotecp.md)** – alternative Web-Verwaltungsoberfläche mit eigenem Login- und Benutzersystem
Alle Komponenten laufen in einem einzigen Container und werden über Umgebungsvariablen konfiguriert.
> **Hinweis:** Dieses Projekt ist ein Fork von [lduriez/tmserver-docker](https://github.com/lduriez/tmserver-docker?tab=readme-ov-file).
@@ -20,10 +27,20 @@ Passe die Werte in der `.env`-Datei an deine Umgebung an (Passwörter, Masterser
### 2. Server starten
Das fertige Docker Image kann direkt verwendet werden – kein eigener Build nötig:
```bash
docker compose up -d --build
docker compose up -d
```
Das Image wird automatisch aus der Container-Registry geladen:
```
git.techniverse.net/scriptos/trackmania-server:latest
```
> **Tipp:** Alle verfügbaren Tags findest du in der [Container-Registry](https://git.techniverse.net/scriptos/-/packages/container/trackmania-server/). Wenn du das Image lieber selbst bauen möchtest, findest du die Anleitung unter [Schnellstart – Selbst bauen](docs/schnellstart.md#docker-image-selbst-bauen).
### 3. Verwaltungsoberflächen öffnen
- **AdminServ:** `http://<host-ip>/`
@@ -31,33 +48,6 @@ docker compose up -d --build
> **Hinweis:** Für den Internet-Modus müssen `SERVER_LOGIN` und `SERVER_VALIDATION_KEY` in der `.env`-Datei gesetzt sein. Einen Server-Account kannst du auf [players.trackmaniaforever.com](https://players.trackmaniaforever.com) erstellen. Für den LAN-Modus setze `SERVER_MODE=lan`.
## Projektstruktur
```
├── assets/
│ ├── bin/ # Binaries und Startscript
│ │ ├── AdminServ_v2.1.1.zip # AdminServ Web-UI
│ │ ├── remoteCP_v4.0.3.5.zip # RemoteCP Web-UI
│ │ ├── xaseco_v1.16.zip # XAseco Server-Controller
│ │ ├── RunTrackmaniaServer.sh # Container-Startscript
│ │ └── TrackmaniaServer_*.zip # Trackmania Server Binary
│ ├── config/
│ ├── custom_game_settings.txt # MatchSettings (Spielmodus, Map-Rotation)
│ └── dedicated_cfg.txt # Server-Config-Template (mit Platzhaltern)
│ └── db/
│ └── init-xaseco-db.sh # MariaDB Init-Script fuer XAseco-DB
├── docs/ # Dokumentation
├── docker-compose.yml # Docker Compose Konfiguration
├── Dockerfile # Docker Build-Definition
├── .env.example # Vorlage fuer Umgebungsvariablen
├── .env # Lokale Umgebungsvariablen (nicht im Git!)
└── data/ # Persistente Daten (zur Laufzeit)
├── gamedata/ # TM-Server-Daten
├── controlpanel/ # AdminServ + RemoteCP
├── xaseco/ # XAseco-Konfiguration und Logs
└── mariadb/ # MariaDB-Datenbankdateien
```
## Dokumentation
Die vollständige Dokumentation befindet sich im Ordner [`docs/`](docs/README.md):
@@ -71,6 +61,17 @@ Die vollständige Dokumentation befindet sich im Ordner [`docs/`](docs/README.md
- [XAseco](docs/xaseco.md) – Server-Controller für Rekorde, Karma und Jukebox
- [Ports](docs/ports.md) – Freigegebene Ports und deren Verwendung
## Danksagung
Ein herzliches Dankeschön an **[Thomas](https://retronerd.at)** – für seine tatkräftige Unterstützung, sein wertvolles Wissen und seine Mitwirkung an diesem Projekt. Ohne ihn wäre dieses Projekt nicht das, was es heute ist!
## Spiegelung
Dieses Repository wird von **Gitea** auf **GitHub** gespiegelt. Das Master-Repository befindet sich auf Gitea:
- **Gitea (Master):** [git.techniverse.net/scriptos/tmserver-docker](https://git.techniverse.net/scriptos/tmserver-docker.git)
- **GitHub (Spiegel):** [github.com/pscriptos/tmserver-docker](https://github.com/pscriptos/tmserver-docker.git)
---
📝 **Blog:** [www.cleveradmin.de](https://www.cleveradmin.de)
+534 -22
View File
@@ -18,7 +18,7 @@ else
echo "==> PHP-Debug-Modus deaktiviert"
cat > "$PHP_INI_DIR/99-adminserv-debug.ini" <<EOF
display_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_WARNING & ~E_NOTICE
log_errors = On
error_log = /var/log/php_errors.log
EOF
@@ -38,7 +38,7 @@ DEFAULT_CONTROLPANEL="/opt/tmserver/default-controlpanel"
if [ ! -f "$ADMINSERV_DIR/index.php" ]; then
echo "==> Erster Start erkannt: Kopiere AdminServ-Dateien ins Volume..."
cp -r "$DEFAULT_CONTROLPANEL"/* "$ADMINSERV_DIR/"
cp -a "$DEFAULT_CONTROLPANEL"/* "$ADMINSERV_DIR/"
chmod -R 777 "$ADMINSERV_DIR/logs/"
chmod 666 "$ADMINSERV_DIR/config/adminlevel.cfg.php"
chmod 666 "$ADMINSERV_DIR/config/servers.cfg.php"
@@ -71,6 +71,18 @@ EOPHP
chown www-data:www-data "$ADMINSERV_DIR/config/servers.cfg.php"
echo " AdminServ-Server-Eintrag automatisch konfiguriert (Port: ${XMLRPC_PORT})."
# AdminServ-Konfigurationspasswort automatisch sichern
# OnlineConfig::PASSWORD in adminserv.cfg.php schuetzt die /config-Seite
# (Server hinzufuegen/aendern/loeschen). Da der Server-Eintrag bereits
# automatisch konfiguriert wird, ist kein manueller Zugriff noetig.
# Der Standard-Hash aus dem ZIP wird durch einen zufaelligen ersetzt.
ADMINSERV_CFG="$ADMINSERV_DIR/config/adminserv.cfg.php"
if [ -f "$ADMINSERV_CFG" ]; then
RANDOM_HASH=$(head -c 32 /dev/urandom | md5sum | cut -d' ' -f1)
sed -i "s|const PASSWORD = '[^']*';|const PASSWORD = '${RANDOM_HASH}';|" "$ADMINSERV_CFG"
echo " AdminServ-Konfigurationspasswort automatisch gesichert."
fi
echo " AdminServ-Dateien erfolgreich kopiert."
# ============================================================
@@ -247,7 +259,7 @@ XASECO_ENABLED="${XASECO_ENABLED:-true}"
if [ "$XASECO_ENABLED" = "true" ]; then
if [ ! -f "$XASECO_DIR/aseco.php" ]; then
echo "==> Erster Start erkannt: Kopiere XAseco-Dateien ins Volume..."
cp -r "$DEFAULT_XASECO"/* "$XASECO_DIR/"
cp -a "$DEFAULT_XASECO"/* "$XASECO_DIR/"
XMLRPC_PORT="${SERVER_XMLRPC_PORT:-5000}"
XASECO_ADMIN="${XASECO_MASTERADMIN_LOGIN:-}"
@@ -353,10 +365,122 @@ if [ "$XASECO_ENABLED" = "true" ]; then
fi
fi
# ============================================================
# XAseco: TeamSpeak3-Plugin Gateway aktualisieren (fuer bestehende Volumes)
# ============================================================
# Das Original-TS3-Gateway ist nicht mehr verfuegbar. Die eigene
# teamspeak3.xml mit dem Ersatz-Gateway wird in das Volume kopiert,
# falls sie fehlt oder noch das alte (nicht mehr erreichbare) Gateway
# referenziert. Gleichzeitig wird das Plugin reaktiviert, falls es
# in einer frueheren Version auskommentiert wurde.
# ============================================================
XASECO_DIR_TS3="/opt/tmserver/xaseco"
TS3_XML="$XASECO_DIR_TS3/teamspeak3.xml"
TS3_DEFAULT="/opt/tmserver/default-xaseco/teamspeak3.xml"
TS3_PLUGINS_XML="$XASECO_DIR_TS3/plugins.xml"
# teamspeak3.xml aktualisieren: Kopieren wenn fehlend oder veraltet
if [ -f "$TS3_DEFAULT" ]; then
if [ ! -f "$TS3_XML" ]; then
echo "==> TeamSpeak3-Gateway: teamspeak3.xml fehlt, kopiere aus Template..."
cp "$TS3_DEFAULT" "$TS3_XML"
echo " teamspeak3.xml erfolgreich kopiert."
elif ! diff -q "$TS3_DEFAULT" "$TS3_XML" > /dev/null 2>&1; then
echo "==> TeamSpeak3-Gateway: teamspeak3.xml wird aktualisiert..."
cp "$TS3_DEFAULT" "$TS3_XML"
echo " teamspeak3.xml erfolgreich aktualisiert."
fi
fi
# TS3-Plugin reaktivieren, falls es auskommentiert ist
if [ -f "$TS3_PLUGINS_XML" ] && grep -q '<!-- <plugin>plugin\.teamspeak3\.php</plugin> -->' "$TS3_PLUGINS_XML"; then
echo "==> TeamSpeak3-Plugin: Reaktiviere auskommentiertes Plugin..."
sed -i 's|<!-- <plugin>plugin\.teamspeak3\.php</plugin> -->|<plugin>plugin.teamspeak3.php</plugin>|' "$TS3_PLUGINS_XML"
echo " TeamSpeak3-Plugin erfolgreich aktiviert."
fi
# ============================================================
# RemoteCP: PHP-Warnungen in Plugins fixen (fuer bestehende Volumes)
# ============================================================
# RemoteCP nutzt bare constants (pt_custom, pt_points, ...), die in
# PHP 7.2+ Warnungen ausloesen. Die gepatchte Datei aus dem Image
# wird in das Volume kopiert, falls die alte Version noch vorhanden ist.
# ============================================================
CUSTOMPOINTS_FILE="/var/www/html/remotecp/plugins/CustomPoints/index.php"
CUSTOMPOINTS_DEFAULT="/opt/tmserver/default-controlpanel/remotecp/plugins/CustomPoints/index.php"
if [ -f "$CUSTOMPOINTS_FILE" ] && ! grep -q 'defined.*pt_custom' "$CUSTOMPOINTS_FILE"; then
echo "==> Patche CustomPoints-Plugin (PHP-Warnungen beheben)..."
cp "$CUSTOMPOINTS_DEFAULT" "$CUSTOMPOINTS_FILE"
chown www-data:www-data "$CUSTOMPOINTS_FILE"
echo " CustomPoints-Plugin erfolgreich gepatcht."
fi
# ============================================================
# AdminServ: MatchSettings-Bugfixes fuer bestehende Volumes
# ============================================================
# 1) get_matchset_mapimport.php: Berechnet den relativen Pfad aus dem
# absoluten Dropdown-Pfad statt den URL-Parameter 'd' zu verwenden.
# Ohne Fix wird z.B. "MatchSettings/" statt "Challenges/Downloaded/"
# als Praefix in die MatchSettings-Datei geschrieben.
# 2) maps-creatematchset.php: Ueberspringt GetModeScriptInfo fuer
# TmForever (Methode existiert nur in ManiaPlanet/TM2, Fehler -506).
# ============================================================
ADMINSERV_MAPIMPORT="/var/www/html/resources/ajax/get_matchset_mapimport.php"
ADMINSERV_MAPIMPORT_DEFAULT="/opt/tmserver/default-controlpanel/resources/ajax/get_matchset_mapimport.php"
if [ -f "$ADMINSERV_MAPIMPORT" ] && ! grep -q 'relativePath' "$ADMINSERV_MAPIMPORT"; then
echo "==> Patche AdminServ: MatchSettings Map-Import (Pfad-Fix)..."
cp "$ADMINSERV_MAPIMPORT_DEFAULT" "$ADMINSERV_MAPIMPORT"
chown www-data:www-data "$ADMINSERV_MAPIMPORT"
echo " get_matchset_mapimport.php erfolgreich gepatcht."
fi
ADMINSERV_CREATEMATCHSET="/var/www/html/resources/process/maps-creatematchset.php"
ADMINSERV_CREATEMATCHSET_DEFAULT="/opt/tmserver/default-controlpanel/resources/process/maps-creatematchset.php"
if [ -f "$ADMINSERV_CREATEMATCHSET" ] && grep -q "query('GetModeScriptInfo')" "$ADMINSERV_CREATEMATCHSET" && ! grep -q "SERVER_VERSION_NAME != 'TmForever'" "$ADMINSERV_CREATEMATCHSET"; then
echo "==> Patche AdminServ: GetModeScriptInfo-Fix fuer TmForever..."
cp "$ADMINSERV_CREATEMATCHSET_DEFAULT" "$ADMINSERV_CREATEMATCHSET"
chown www-data:www-data "$ADMINSERV_CREATEMATCHSET"
echo " maps-creatematchset.php erfolgreich gepatcht."
fi
# ============================================================
# RemoteCP: Mods-Plugin settings.xml aktualisieren (fuer bestehende Volumes)
# ============================================================
# Die vorkonfigurierte Skin-Liste aus dem Image wird in das Volume
# kopiert, falls die alte Standard-settings.xml noch vorhanden ist
# (erkennbar am Beispiel-Eintrag "blacksunonline.com").
# ============================================================
MODS_SETTINGS_FILE="/var/www/html/remotecp/plugins/Mods/settings.xml"
MODS_SETTINGS_DEFAULT="/opt/tmserver/default-controlpanel/remotecp/plugins/Mods/settings.xml"
if [ -f "$MODS_SETTINGS_FILE" ] && grep -q 'blacksunonline.com' "$MODS_SETTINGS_FILE"; then
echo "==> Aktualisiere RemoteCP Mods-Plugin (Skin-Liste von techniverse.net)..."
cp "$MODS_SETTINGS_DEFAULT" "$MODS_SETTINGS_FILE"
chown www-data:www-data "$MODS_SETTINGS_FILE"
echo " Mods/settings.xml erfolgreich aktualisiert."
fi
# ============================================================
# AdminServ: Konfigurationspasswort absichern (fuer bestehende Volumes)
# ============================================================
# AdminServ wird mit einem oeffentlich bekannten Standard-Hash
# ausgeliefert (0b28a5799a32c687dad2c5183718ceac, aus dem
# AdminServ-GitHub-Repo). Dieser wird durch einen zufaelligen
# MD5-Hash ersetzt, damit die /config-Seite abgesichert ist.
# ============================================================
ADMINSERV_CFG_VOL="/var/www/html/config/adminserv.cfg.php"
if [ -f "$ADMINSERV_CFG_VOL" ] && grep -q "0b28a5799a32c687dad2c5183718ceac" "$ADMINSERV_CFG_VOL"; then
echo "==> SICHERHEIT: AdminServ-Konfigurationspasswort wird ersetzt (Standard-Hash erkannt)..."
RANDOM_HASH=$(head -c 32 /dev/urandom | md5sum | cut -d' ' -f1)
sed -i "s|const PASSWORD = '0b28a5799a32c687dad2c5183718ceac';|const PASSWORD = '${RANDOM_HASH}';|" "$ADMINSERV_CFG_VOL"
echo " Standard-Hash durch zufaelligen Hash ersetzt."
echo " Die /config-Seite ist jetzt abgesichert."
fi
echo "Starting apache server"
service apache2 start
CONFIG="/opt/tmserver/GameData/Config/dedicated_cfg.txt"
GAME_SETTINGS="/opt/tmserver/GameData/Tracks/MatchSettings/custom_game_settings.txt"
GAMEDATA_DIR="/opt/tmserver/GameData"
DEFAULT_GAMEDATA="/opt/tmserver/default-gamedata"
@@ -375,16 +499,20 @@ FORCE_CONFIG_UPDATE="${FORCE_CONFIG_UPDATE:-false}"
if [ ! -f "$CONFIG" ]; then
echo "==> Erster Start erkannt: Kopiere Default-GameData ins Volume..."
cp -r "$DEFAULT_GAMEDATA"/* "$GAMEDATA_DIR/"
cp -a "$DEFAULT_GAMEDATA"/* "$GAMEDATA_DIR/"
chmod -R 777 "$GAMEDATA_DIR/Config/"
mkdir -p "$GAMEDATA_DIR/Config/AdminServ/ServerOptions"
chown -R www-data:www-data "$GAMEDATA_DIR/Config/AdminServ"
# Tracks-Verzeichnis fuer AdminServ beschreibbar machen (Maps-Upload/Download)
chown -R www-data:www-data "$GAMEDATA_DIR/Tracks/"
chmod -R 755 "$GAMEDATA_DIR/Tracks/"
APPLY_ENV=true
elif [ "$FORCE_CONFIG_UPDATE" = "true" ]; then
echo "==> FORCE_CONFIG_UPDATE ist aktiv: Umgebungsvariablen werden erneut angewendet..."
echo " ACHTUNG: Manuelle Aenderungen an den betroffenen Feldern werden ueberschrieben!"
# Template neu kopieren, damit alle Platzhalter vorhanden sind
cp "$DEFAULT_GAMEDATA/Config/dedicated_cfg.txt" "$CONFIG"
cp "$DEFAULT_GAMEDATA/Tracks/MatchSettings/custom_game_settings.txt" "$GAME_SETTINGS"
APPLY_ENV=true
else
echo "==> Vorhandene Konfiguration gefunden. Umgebungsvariablen werden NICHT angewendet."
@@ -417,6 +545,7 @@ if [ "$APPLY_ENV" = "true" ]; then
sed -i "s|%%SERVER_MAX_SPECTATORS%%|${SERVER_MAX_SPECTATORS}|g" "$CONFIG"
sed -i "s|%%SERVER_SPEC_PASSWORD%%|${SERVER_SPEC_PASSWORD}|g" "$CONFIG"
sed -i "s|%%SERVER_LADDER_MODE%%|${SERVER_LADDER_MODE}|g" "$CONFIG"
sed -i "s|%%SERVER_LADDER_LIMIT_MAX%%|${SERVER_LADDER_LIMIT_MAX:-60000}|g" "$CONFIG"
# Netzwerk
sed -i "s|%%SERVER_PORT%%|${SERVER_PORT}|g" "$CONFIG"
@@ -425,9 +554,179 @@ if [ "$APPLY_ENV" = "true" ]; then
sed -i "s|%%SERVER_UPLOAD_RATE%%|${SERVER_UPLOAD_RATE}|g" "$CONFIG"
sed -i "s|%%SERVER_DOWNLOAD_RATE%%|${SERVER_DOWNLOAD_RATE}|g" "$CONFIG"
# Spieleinstellungen (MatchSettings)
sed -i "s|<allwarmupduration>[^<]*</allwarmupduration>|<allwarmupduration>${ALLWARMUPDURATION:-0}</allwarmupduration>|" "$GAME_SETTINGS"
echo "Platzhalter erfolgreich ersetzt."
fi
# ============================================================
# AdminServ ServerOptions: Exportierte Einstellungen anwenden
# ============================================================
# Falls ein AdminServ-Export in GameData/Config/AdminServ/ServerOptions/
# vorhanden ist, werden die darin enthaltenen Werte (z.B. Servername,
# Beschreibung, Spielerzahl) in die dedicated_cfg.txt uebernommen.
# So bleiben Aenderungen, die ueber AdminServ vorgenommen und exportiert
# wurden, auch nach einem Container-Neustart erhalten.
# ============================================================
ADMINSERV_OPTIONS_DIR="$GAMEDATA_DIR/Config/AdminServ/ServerOptions"
if [ -d "$ADMINSERV_OPTIONS_DIR" ]; then
LATEST_EXPORT=$(ls -t "$ADMINSERV_OPTIONS_DIR"/*.txt "$ADMINSERV_OPTIONS_DIR"/*.xml 2>/dev/null | head -1)
if [ -n "$LATEST_EXPORT" ] && [ -f "$LATEST_EXPORT" ]; then
echo "==> AdminServ ServerOptions-Export gefunden: $(basename "$LATEST_EXPORT")"
echo " Uebernehme exportierte Einstellungen in dedicated_cfg.txt..."
php -r '
$xmlFile = $argv[1];
$cfgFile = $argv[2];
// AdminServ-Export parsen
$dom = new DOMDocument();
if (!@$dom->load($xmlFile)) {
echo " WARNUNG: AdminServ-Export konnte nicht gelesen werden.\n";
exit(0);
}
$root = $dom->documentElement;
$exportValues = [];
foreach ($root->childNodes as $node) {
if ($node->nodeType === XML_ELEMENT_NODE) {
$exportValues[$node->nodeName] = $node->nodeValue;
}
}
// Mapping: AdminServ-XML-Feld => dedicated_cfg.txt-Feld
$mapping = [
"Name" => "name",
"Comment" => "comment",
"HideServer" => "hide_server",
"NextMaxPlayers" => "max_players",
"Password" => "password",
"PasswordForSpectator" => "password_spectator",
"NextMaxSpectators" => "max_spectators",
"NextLadderMode" => "ladder_mode",
"NextCallVoteTimeOut" => "callvote_timeout",
"CallVoteRatio" => "callvote_ratio",
"AllowChallengeDownload" => "allow_challenge_download",
"AutoSaveReplays" => "autosave_replays",
"IsP2PUpload" => "enable_p2p_upload",
"IsP2PDownload" => "enable_p2p_download",
];
// Bool-Felder: 1/0 => True/False (dedicated_cfg.txt-Format)
$boolFields = [
"allow_challenge_download", "autosave_replays",
"enable_p2p_upload", "enable_p2p_download",
];
// Ladder-Modus: 0 => inactive, 1 => forced
$ladderMap = ["0" => "inactive", "1" => "forced"];
// Zu ersetzende Werte aufbauen
$replacements = [];
foreach ($mapping as $xmlField => $cfgField) {
if (isset($exportValues[$xmlField])) {
$value = $exportValues[$xmlField];
if (in_array($cfgField, $boolFields)) {
$value = ($value == "1" || strtolower($value) === "true") ? "True" : "False";
}
if ($cfgField === "ladder_mode" && isset($ladderMap[$value])) {
$value = $ladderMap[$value];
}
$replacements[$cfgField] = $value;
}
}
if (empty($replacements)) {
echo " Keine anwendbaren Einstellungen im Export gefunden.\n";
exit(0);
}
// dedicated_cfg.txt zeilenweise verarbeiten
// Nur Tags innerhalb von <server_options> werden ersetzt,
// damit <name> und <password> in <authorization_levels> unangetastet bleiben.
$lines = file($cfgFile);
$inServerOptions = false;
$updated = 0;
foreach ($lines as $i => $line) {
if (strpos($line, "<server_options>") !== false) {
$inServerOptions = true;
}
if (strpos($line, "</server_options>") !== false) {
$inServerOptions = false;
}
if ($inServerOptions) {
foreach ($replacements as $field => $value) {
$pattern = "/(<" . preg_quote($field, "/") . ">)[^<]*(<\/" . preg_quote($field, "/") . ">)/";
if (preg_match($pattern, $line)) {
$safeValue = htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, "UTF-8");
// $ und \ im Replacement escapen, damit preg_replace
// sie nicht als Backreferences interpretiert (wichtig
// fuer TM-Farbcodes wie $03F, $z, $s etc.)
$escapedValue = str_replace(["\\", "$"], ["\\\\", "\\$"], $safeValue);
$lines[$i] = preg_replace($pattern, "\${1}" . $escapedValue . "\${2}", $line, 1);
echo " " . $field . " => " . $value . "\n";
$updated++;
unset($replacements[$field]);
break;
}
}
}
}
if ($updated > 0) {
file_put_contents($cfgFile, implode("", $lines));
echo " " . $updated . " Einstellung(en) aus AdminServ-Export uebernommen.\n";
}
' "$LATEST_EXPORT" "$CONFIG"
fi
else
echo "==> Kein AdminServ ServerOptions-Verzeichnis gefunden. Ueberspringe Import."
fi
# ============================================================
# MatchSettings: Neueste Datei automatisch ermitteln
# ============================================================
# Ueber die Umgebungsvariable MATCHSETTINGS_FILE kann gesteuert werden,
# welche MatchSettings-Datei beim Serverstart geladen wird:
# - "auto" (Standard): Die neueste .txt-Datei im MatchSettings-Ordner
# wird automatisch anhand des Aenderungsdatums ermittelt.
# - "<dateiname.txt>": Eine bestimmte Datei wird direkt verwendet.
# Fallback: custom_game_settings.txt (Standard-MatchSettings aus dem Image).
# ============================================================
MATCHSETTINGS_DIR="$GAMEDATA_DIR/Tracks/MatchSettings"
MATCHSETTINGS_FILE_ENV="${MATCHSETTINGS_FILE:-auto}"
if [ "$MATCHSETTINGS_FILE_ENV" = "auto" ]; then
echo "==> MatchSettings: Automatische Erkennung (MATCHSETTINGS_FILE=auto)..."
# Neueste .txt-Datei im MatchSettings-Ordner anhand des Aenderungsdatums ermitteln
NEWEST_MS=$(ls -t "$MATCHSETTINGS_DIR"/*.txt 2>/dev/null | head -1)
if [ -n "$NEWEST_MS" ] && [ -f "$NEWEST_MS" ]; then
MS_FILENAME=$(basename "$NEWEST_MS")
GAME_SETTINGS_PATH="MatchSettings/${MS_FILENAME}"
echo " Neueste MatchSettings gefunden: ${MS_FILENAME}"
echo " Aenderungsdatum: $(stat -c '%y' "$NEWEST_MS" 2>/dev/null || ls -la "$NEWEST_MS" | awk '{print $6, $7, $8}')"
else
GAME_SETTINGS_PATH="MatchSettings/custom_game_settings.txt"
echo " Keine .txt-Dateien in ${MATCHSETTINGS_DIR} gefunden."
echo " Fallback: ${GAME_SETTINGS_PATH}"
fi
else
# Explizit angegebene Datei verwenden
if [ -f "$MATCHSETTINGS_DIR/$MATCHSETTINGS_FILE_ENV" ]; then
GAME_SETTINGS_PATH="MatchSettings/${MATCHSETTINGS_FILE_ENV}"
echo "==> MatchSettings: Verwende explizit gesetzte Datei: ${MATCHSETTINGS_FILE_ENV}"
else
echo "==> WARNUNG: Angegebene MatchSettings-Datei nicht gefunden: ${MATCHSETTINGS_FILE_ENV}"
echo " Vorhandene Dateien in ${MATCHSETTINGS_DIR}:"
ls -la "$MATCHSETTINGS_DIR"/*.txt 2>/dev/null || echo " (keine .txt-Dateien vorhanden)"
GAME_SETTINGS_PATH="MatchSettings/custom_game_settings.txt"
echo " Fallback: ${GAME_SETTINGS_PATH}"
fi
fi
echo " Aktive MatchSettings: ${GAME_SETTINGS_PATH}"
# Bestimme Server-Modus (Standard: internet)
SERVER_MODE="${SERVER_MODE:-internet}"
@@ -447,41 +746,254 @@ fi
echo "Server config dedicated_cfg.txt is"
cat "$CONFIG"
echo "Launching Server in ${SERVER_MODE} mode"
./TrackmaniaServer /dedicated_cfg=dedicated_cfg.txt /game_settings=MatchSettings/custom_game_settings.txt /nodaemon ${LAUNCH_MODE} &
echo "Launching Server in ${SERVER_MODE} mode (MatchSettings: ${GAME_SETTINGS_PATH})"
./TrackmaniaServer /dedicated_cfg=dedicated_cfg.txt /game_settings=${GAME_SETTINGS_PATH} /nodaemon ${LAUNCH_MODE} &
TM_PID=$!
echo "TrackmaniaServer gestartet (PID: ${TM_PID})"
# ============================================================
# XAseco starten (nach TrackmaniaServer)
# Warte auf XMLRPC-Verfuegbarkeit
# ============================================================
if [ "${XASECO_ENABLED:-true}" = "true" ] && [ -f "/opt/tmserver/xaseco/aseco.php" ]; then
echo "==> Warte auf TrackmaniaServer XMLRPC..."
XMLRPC_PORT="${SERVER_XMLRPC_PORT:-5000}"
XMLRPC_READY=false
for i in $(seq 1 30); do
if php -r "@fsockopen('127.0.0.1', ${XMLRPC_PORT}, \$e, \$m, 2) ? exit(0) : exit(1);" 2>/dev/null; then
XMLRPC_READY=true
# Sowohl XAseco als auch Forced Mods benoetigen eine aktive
# XMLRPC-Verbindung zum TrackmaniaServer.
# ============================================================
echo "==> Warte auf TrackmaniaServer XMLRPC..."
XMLRPC_PORT="${SERVER_XMLRPC_PORT:-5000}"
XMLRPC_READY=false
for i in $(seq 1 30); do
if php -r "@fsockopen('127.0.0.1', ${XMLRPC_PORT}, \$e, \$m, 2) ? exit(0) : exit(1);" 2>/dev/null; then
XMLRPC_READY=true
break
fi
sleep 2
done
if [ "$XMLRPC_READY" = "true" ]; then
echo " XMLRPC-Port ${XMLRPC_PORT} erreichbar."
else
echo " WARNUNG: XMLRPC-Port ${XMLRPC_PORT} nicht erreichbar nach 60s!"
fi
# ============================================================
# Forced Mods (Skins) per XMLRPC setzen
# ============================================================
# Wenn FORCE_MOD_*-Variablen gesetzt sind, werden die
# entsprechenden Mods per SetForcedMods-XMLRPC-Aufruf beim
# Serverstart automatisch aktiviert. Dies funktioniert bei
# jedem Containerstart und ist unabhaengig von FORCE_CONFIG_UPDATE.
# ============================================================
# Pruefen, ob mindestens ein Mod gesetzt ist (Variable fuer spaeter)
HAS_MODS=false
if [ "$XMLRPC_READY" = "true" ]; then
for ENV_NAME in FORCE_MOD_STADIUM FORCE_MOD_ISLAND FORCE_MOD_BAY FORCE_MOD_COAST FORCE_MOD_SPEED FORCE_MOD_ALPINE FORCE_MOD_RALLY; do
eval "MOD_VAL=\${$ENV_NAME:-}"
if [ -n "$MOD_VAL" ]; then
HAS_MODS=true
break
fi
sleep 2
done
fi
if [ "$XMLRPC_READY" = "true" ]; then
# ============================================================
# XAseco starten (nach TrackmaniaServer, VOR Forced Mods)
# ============================================================
# XAseco muss zuerst starten und sich initialisieren, damit
# es die Forced Mods nicht ueberschreibt oder zuruecksetzt.
# ============================================================
if [ "$XMLRPC_READY" = "true" ]; then
if [ "${XASECO_ENABLED:-true}" = "true" ] && [ -f "/opt/tmserver/xaseco/aseco.php" ]; then
echo "==> Starte XAseco..."
cd /opt/tmserver/xaseco
php aseco.php TMN </dev/null >>aseco.log 2>&1 &
XASECO_PID=$!
echo " XAseco gestartet (PID: ${XASECO_PID})"
cd /opt/tmserver
else
echo " WARNUNG: XMLRPC-Port ${XMLRPC_PORT} nicht erreichbar nach 60s!"
echo " XAseco wurde NICHT gestartet. Bitte manuell starten."
fi
else
if [ "${XASECO_ENABLED:-true}" != "true" ]; then
elif [ "${XASECO_ENABLED:-true}" != "true" ]; then
echo "==> XAseco ist deaktiviert (XASECO_ENABLED=${XASECO_ENABLED})."
fi
else
echo " WARNUNG: XMLRPC nicht erreichbar - XAseco und Forced Mods wurden NICHT gestartet."
fi
# ============================================================
# Forced Mods (Skins) per XMLRPC setzen
# ============================================================
# Wird NACH XAseco-Start ausgefuehrt, damit XAseco die Mods
# nicht bei seiner Initialisierung zuruecksetzt.
# ============================================================
if [ "$XMLRPC_READY" = "true" ] && [ "$HAS_MODS" = "true" ]; then
# Warten, damit XAseco und der TM-Server sich vollstaendig initialisieren
echo "==> Forced Mods: Warte 10 Sekunden auf vollstaendige Server-Initialisierung..."
sleep 10
echo "==> Forced Mods: Setze Mods per XMLRPC..."
# JSON-Array der Mods aufbauen
MODS_JSON="["
MODS_FIRST=true
for PAIR in "FORCE_MOD_STADIUM:Stadium" "FORCE_MOD_ISLAND:Island" "FORCE_MOD_BAY:Bay" "FORCE_MOD_COAST:Coast" "FORCE_MOD_SPEED:Speed" "FORCE_MOD_ALPINE:Alpine" "FORCE_MOD_RALLY:Rally"; do
VAR_NAME="${PAIR%%:*}"
ENV_NAME="${PAIR##*:}"
eval "MOD_URL=\${$VAR_NAME:-}"
if [ -n "$MOD_URL" ]; then
if [ "$MODS_FIRST" = "true" ]; then
MODS_FIRST=false
else
MODS_JSON="${MODS_JSON},"
fi
# URL fuer JSON escapen (Backslash und Anfuehrungszeichen)
SAFE_URL=$(printf '%s' "$MOD_URL" | sed 's/\\/\\\\/g; s/"/\\"/g')
MODS_JSON="${MODS_JSON}{\"Env\":\"${ENV_NAME}\",\"Url\":\"${SAFE_URL}\"}"
echo " ${ENV_NAME} => ${MOD_URL}"
fi
done
MODS_JSON="${MODS_JSON}]"
# GBXRemote2-Protokoll: Authenticate + SetForcedMods
SA_PW_MODS="${SERVER_SA_PASSWORD:-SuperAdmin}"
php -r '
$port = (int)$argv[1];
$password = $argv[2];
$modsJson = $argv[3];
$mods = json_decode($modsJson, true);
if (empty($mods)) { echo " Keine Mods zu setzen.\n"; exit(0); }
// GBXRemote2: Verbindung herstellen
$fp = @fsockopen("127.0.0.1", $port, $errno, $errstr, 5);
if (!$fp) { echo " FEHLER: Verbindung zu XMLRPC fehlgeschlagen ($errno: $errstr).\n"; exit(1); }
stream_set_timeout($fp, 10);
// Handshake lesen (4 Bytes Laenge + Protokollstring)
$data = fread($fp, 4);
if (strlen($data) < 4) { echo " FEHLER: Handshake fehlgeschlagen.\n"; fclose($fp); exit(1); }
$info = unpack("Vsize", $data);
$handshake = fread($fp, $info["size"]);
if (strpos($handshake, "GBXRemote") === false) {
echo " FEHLER: Kein GBXRemote-Protokoll.\n"; fclose($fp); exit(1);
}
echo " Protokoll: $handshake\n";
$reqhandle = 0x80000001;
// XML-RPC-Wert kodieren
function encodeVal($v) {
if (is_bool($v)) return "<value><boolean>" . ($v ? "1" : "0") . "</boolean></value>";
if (is_int($v)) return "<value><int>" . $v . "</int></value>";
if (is_string($v)) return "<value><string>" . htmlspecialchars($v, ENT_XML1) . "</string></value>";
if (is_array($v)) {
if (array_keys($v) !== range(0, count($v) - 1)) {
$x = "<value><struct>";
foreach ($v as $k => $val) $x .= "<member><name>" . $k . "</name>" . encodeVal($val) . "</member>";
return $x . "</struct></value>";
} else {
$x = "<value><array><data>";
foreach ($v as $val) $x .= encodeVal($val);
return $x . "</data></array></value>";
}
}
return "<value><string>" . htmlspecialchars((string)$v, ENT_XML1) . "</string></value>";
}
// Ein einzelnes Paket vom Server lesen (Header + Body)
function readPacket($fp) {
$header = "";
while (strlen($header) < 8) {
$chunk = @fread($fp, 8 - strlen($header));
if ($chunk === false || strlen($chunk) === 0) return false;
$header .= $chunk;
}
$info = unpack("Vsize/Vhandle", $header);
$size = $info["size"];
$handle = $info["handle"];
if ($size > 4194304 || $size == 0) return false;
$body = "";
$remaining = $size;
while ($remaining > 0) {
$chunk = @fread($fp, min($remaining, 8192));
if ($chunk === false || strlen($chunk) === 0) break;
$body .= $chunk;
$remaining -= strlen($chunk);
}
return ["handle" => $handle, "body" => $body];
}
// XMLRPC-Request senden und Antwort lesen
// Callbacks (handle < 0x80000000) werden uebersprungen
function gbxQuery($fp, &$reqhandle, $method, $params) {
$xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
. "<methodCall><methodName>" . $method . "</methodName><params>";
foreach ($params as $p) $xml .= "<param>" . encodeVal($p) . "</param>";
$xml .= "</params></methodCall>";
$myHandle = $reqhandle++;
$packet = pack("VV", strlen($xml), $myHandle) . $xml;
$written = @fwrite($fp, $packet);
if ($written === false || $written === 0) {
echo " FEHLER: Konnte Request nicht senden ($method).\n";
return false;
}
// Auf Antwort warten, Callbacks ueberspringen
for ($attempt = 0; $attempt < 30; $attempt++) {
$pkt = readPacket($fp);
if ($pkt === false) {
echo " FEHLER: Keine Antwort fuer $method.\n";
return false;
}
// Callback? (Handle < 0x80000000) -> ueberspringen
if ($pkt["handle"] < 0x80000000) {
continue;
}
// Response gefunden
return $pkt["body"];
}
echo " FEHLER: Zu viele Callbacks, keine Antwort fuer $method.\n";
return false;
}
// 1. Authenticate
echo " Authentifiziere als SuperAdmin...\n";
$resp = gbxQuery($fp, $reqhandle, "Authenticate", ["SuperAdmin", $password]);
if ($resp === false || strpos($resp, "<boolean>1</boolean>") === false) {
echo " FEHLER: Authentifizierung fehlgeschlagen.\n";
if ($resp) echo " Antwort: " . substr(trim($resp), 0, 500) . "\n";
fclose($fp); exit(1);
}
echo " Authentifizierung erfolgreich.\n";
// 2. EnableCallbacks deaktivieren (weniger Rauschen)
gbxQuery($fp, $reqhandle, "EnableCallbacks", [false]);
// 3. SetForcedMods(override=true, mods=[{Env, Url}, ...])
// Debug: Zeige das XML das wir senden
$setXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
. "<methodCall><methodName>SetForcedMods</methodName><params>"
. "<param>" . encodeVal(true) . "</param>"
. "<param>" . encodeVal($mods) . "</param>"
. "</params></methodCall>";
echo " Debug SetForcedMods-XML:\n " . $setXml . "\n";
echo " Sende SetForcedMods (" . count($mods) . " Mod(s))...\n";
$resp = gbxQuery($fp, $reqhandle, "SetForcedMods", [true, $mods]);
echo " SetForcedMods-Antwort: " . trim($resp) . "\n";
if ($resp !== false && strpos($resp, "<boolean>1</boolean>") !== false) {
echo " SetForcedMods: OK\n";
} else {
echo " FEHLER: SetForcedMods fehlgeschlagen.\n";
}
// 4. GetForcedMods zur Verifikation (vollstaendige Antwort)
echo " Verifiziere mit GetForcedMods...\n";
$resp = gbxQuery($fp, $reqhandle, "GetForcedMods", []);
echo " GetForcedMods-Antwort:\n " . trim($resp) . "\n";
fclose($fp);
' "$XMLRPC_PORT" "$SA_PW_MODS" "$MODS_JSON"
elif [ "$XMLRPC_READY" = "true" ]; then
echo "==> Forced Mods: Keine FORCE_MOD_*-Variablen gesetzt. Ueberspringe."
fi
# Auf TrackmaniaServer warten (Hauptprozess)
@@ -0,0 +1,76 @@
<?php
// INCLUDES
session_start();
if( !isset($_SESSION['adminserv']['sid']) ){ exit; }
$configPath = '../../'.$_SESSION['adminserv']['path'].'config/';
require_once $configPath.'adminlevel.cfg.php';
require_once $configPath.'adminserv.cfg.php';
require_once $configPath.'extension.cfg.php';
require_once $configPath.'servers.cfg.php';
require_once '../core/adminserv.php';
AdminServConfig::$PATH_RESOURCES = '../';
AdminServ::getClass();
AdminServUI::lang();
// ISSET
if( isset($_GET['path']) ){ $path = addslashes($_GET['path']); }else{ $path = null; }
if( isset($_GET['d']) ){ $directory = addslashes($_GET['d']); }else{ $directory = null; }
if( isset($_GET['op']) ){ $operation = addslashes($_GET['op']); }else{ $operation = null; }
if( isset($_GET['select']) ){ $selection = $_GET['select']; }else{ $selection = null; }
// DATA
$out = null;
if( AdminServ::initialize() && $path != null ){
// Maps
if($path == 'currentServerSelection'){
$mapsImport = AdminServ::getMapList();
}
else{
// FIX: Den relativen Pfad aus dem absoluten Pfad (Dropdown-Auswahl)
// berechnen, statt den URL-Parameter 'd' (= MatchSettings-Speicherordner)
// zu verwenden. Sonst wird z.B. "MatchSettings/" statt "Challenges/Downloaded/"
// als Pfad-Praefix in die MatchSettings-Datei geschrieben.
$mapsDirectoryPath = AdminServ::getMapsDirectoryPath();
$relativePath = str_replace($mapsDirectoryPath, '', Str::toSlash($path));
// Sicherstellen, dass der Pfad mit / endet (wenn nicht leer)
if($relativePath && substr($relativePath, -1) !== '/'){
$relativePath .= '/';
}
$currentDir = Folder::read($path, AdminServConfig::$MATCHSET_HIDDEN_FOLDERS, AdminServConfig::$MATCHSET_EXTENSION, intval(AdminServConfig::RECENT_STATUS_PERIOD * 3600) );
$mapsImport = AdminServ::getLocalMapList($currentDir, $relativePath);
}
// Faire une sélection
if($operation == 'setSelection'){
// On supprime les maps non sélectionnées
if( $selection != null && count($selection) > 0 ){
foreach($mapsImport['lst'] as $id => $values){
if( !in_array($id, $selection) ){
unset($mapsImport['lst'][$id]);
}
}
}
else{
foreach($mapsImport['lst'] as $id => $values){
unset($mapsImport['lst'][$id]);
}
}
}
// Enregistrement de la sélection du MatchSettings
if($operation != 'getSelection'){
AdminServ::saveMatchSettingSelection($mapsImport);
}
$client->Terminate();
}
// OUT
if($operation == 'getSelection'){
echo json_encode($mapsImport);
}
else{
echo json_encode($_SESSION['adminserv']['matchset_maps_selected']);
}
?>
@@ -0,0 +1,160 @@
<?php
// ENREGISTREMENT
if( isset($_POST['savematchsetting']) && isset($_SESSION['adminserv']['matchset_maps_selected']) ){
// Filename
$matchSettingName = Str::replaceChars($_POST['matchSettingName']);
$filename = $data['mapsDirectoryPath'].$args['directory'].$matchSettingName;
if(File::getExtension($matchSettingName) != 'txt'){
$filename .= '.txt';
}
$struct = array();
// Gameinfos
$gameinfos = AdminServ::getGameInfosStructFromPOST();
$struct['gameinfos'] = array(
'game_mode' => $gameinfos['GameMode'],
'chat_time' => $gameinfos['ChatTime'],
'finishtimeout' => $gameinfos['FinishTimeout'],
'allwarmupduration' => $gameinfos['AllWarmUpDuration'],
'disablerespawn' => $gameinfos['DisableRespawn'],
'forceshowallopponents' => $gameinfos['ForceShowAllOpponents'],
'rounds_pointslimit' => $gameinfos['RoundsPointsLimit'],
'rounds_custom_points' => $gameinfos['RoundCustomPoints'],
'rounds_usenewrules' => $gameinfos['RoundsUseNewRules'],
'rounds_forcedlaps' => $gameinfos['RoundsForcedLaps'],
'rounds_pointslimitnewrules' => $gameinfos['RoundsPointsLimitNewRules'],
'team_pointslimit' => $gameinfos['TeamPointsLimit'],
'team_maxpoints' => $gameinfos['TeamMaxPoints'],
'team_usenewrules' => $gameinfos['TeamUseNewRules'],
'team_pointslimitnewrules' => $gameinfos['TeamPointsLimitNewRules'],
'timeattack_limit' => $gameinfos['TimeAttackLimit'],
'timeattack_synchstartperiod' => $gameinfos['TimeAttackSynchStartPeriod'],
'laps_nblaps' => $gameinfos['LapsNbLaps'],
'laps_timelimit' => $gameinfos['LapsTimeLimit'],
'cup_pointslimit' => $gameinfos['CupPointsLimit'],
'cup_roundsperchallenge' => $gameinfos['CupRoundsPerMap'],
'cup_nbwinners' => $gameinfos['CupNbWinners'],
'cup_warmupduration' => $gameinfos['CupWarmUpDuration']
);
if(SERVER_VERSION_NAME != 'TmForever'){
$struct['gameinfos']['script_name'] = $gameinfos['ScriptName'];
}
// HotSeat
$struct['hotseat'] = array(
'game_mode' => intval($_POST['hotSeatGameMode']),
'time_limit' => TimeDate::secToMillisec( intval($_POST['hotSeatTimeLimit']) ),
'rounds_count' => intval($_POST['hotSeatCountRound'])
);
// Filter
$struct['filter'] = array(
'is_lan' => array_key_exists('filterIsLan', $_POST),
'is_internet' => array_key_exists('filterIsInternet', $_POST),
'is_solo' => array_key_exists('filterIsSolo', $_POST),
'is_hotseat' => array_key_exists('filterIsHotSeat', $_POST),
'sort_index' => intval($_POST['filterSortIndex']),
'random_map_order' => array_key_exists('filterRandomMaps', $_POST),
'force_default_gamemode' => intval($_POST['filterDefaultGameMode']),
);
// ScriptSettings (nur fuer ManiaPlanet/TM2, nicht fuer TmForever)
// TmForever kennt die Methode 'GetModeScriptInfo' nicht (Fehler -506).
if(SERVER_VERSION_NAME != 'TmForever'){
if( !$client->query('GetModeScriptInfo') ){
AdminServ::error();
}
else{
$scriptsettings = $client->getResponse();
if( !empty($scriptsettings['ParamDescs']) ){
foreach($scriptsettings['ParamDescs'] as $param){
$struct['scriptsettings'][] = array(
'name' => $param['Name'],
'type' => $param['Type'],
'value' => $param['Default']
);
}
}
}
}
// Maps
$struct['startindex'] = 1;
$maps = $_SESSION['adminserv']['matchset_maps_selected']['lst'];
if( isset($maps) && is_array($maps) && !empty($maps) ){
$mapsField = (SERVER_VERSION_NAME == 'TmForever') ? 'challenge' : 'map';
foreach($maps as $id => $values){
$struct[$mapsField][$values['UId']] = $values['FileName'];
}
}
// Enregistrement
if( ($result = AdminServ::saveMatchSettings($filename, $struct)) !== true ){
AdminServ::error(Utils::t('Unable to save the MatchSettings').' : '.$matchSettingName.' ('.$result.')');
}
else{
$action = Utils::t('The MatchSettings "!matchSettingName" was successfully created in the folder', array('!matchSettingName' => $matchSettingName)).' : '.$data['mapsDirectoryPath'].$args['directory'];
AdminServ::info($action);
AdminServLogs::add('action', $action);
Utils::redirection(false, '?p='.USER_PAGE .$hasDirectory);
}
}
else{
if( !isset($_GET['f']) ){
unset($_SESSION['adminserv']['matchset_maps_selected']);
}
}
// LECTURE
$data['directoryList'] = Folder::getArborescence($data['mapsDirectoryPath'], AdminServConfig::$MAPS_HIDDEN_FOLDERS, substr_count($data['mapsDirectoryPath'], '/'));
$data['matchSettings'] = array();
// Édition
if( isset($_GET['f']) && $_GET['f'] != null ){
$data['pageTitle'] = Utils::t('Edit');
$data['matchSettings']['name'] = $_GET['f'];
$matchSettingsData = AdminServ::getMatchSettingsData($data['mapsDirectoryPath'].$args['directory'].$data['matchSettings']['name']);
$data['gameInfos'] = array(
'curr' => null,
'next' => $matchSettingsData['gameinfos']
);
unset($matchSettingsData['gameinfos']);
$data['matchSettings'] += $matchSettingsData;
if( isset($data['matchSettings']['maps']) ){
$maps = AdminServ::getMapListFromMatchSetting($data['matchSettings']['maps']);
$data['matchSettings']['nbm'] = $maps['nbm']['count'];
$_SESSION['adminserv']['matchset_maps_selected'] = $maps;
}
else{
$data['matchSettings']['nbm'] = 0;
}
}
else{
$data['pageTitle'] = Utils::t('Create');
$data['matchSettings']['name'] = 'match_settings';
$gameInfos = AdminServ::getGameInfos();
$data['gameInfos'] = array(
'curr' => null,
'next' => $gameInfos['next']
);
$data['matchSettings']['hotseat'] = array(
'GameMode' => 1,
'TimeLimit' => 300000,
'RoundsCount' => 5
);
$data['matchSettings']['filter'] = array(
'IsLan' => 1,
'IsInternet' => 1,
'IsSolo' => 0,
'IsHotseat' => 1,
'SortIndex' => 1000,
'RandomMapOrder' => 0,
'ForceDefaultGameMode' => 1
);
$data['matchSettings']['StartIndex'] = 0;
$data['matchSettings']['nbm'] = 0;
}
?>
+1 -1
View File
@@ -4,7 +4,7 @@
<game_mode>1</game_mode>
<chat_time>10000</chat_time>
<finishtimeout>1</finishtimeout>
<allwarmupduration>1</allwarmupduration>
<allwarmupduration>0</allwarmupduration>
<disablerespawn>0</disablerespawn>
<forceshowallopponents>0</forceshowallopponents>
<rounds_pointslimit>30</rounds_pointslimit>
+1 -1
View File
@@ -35,7 +35,7 @@
<ladder_mode>%%SERVER_LADDER_MODE%%</ladder_mode> <!-- value between 'inactive', 'forced' (or '0', '1') -->
<ladder_serverlimit_min>0</ladder_serverlimit_min> <!-- Those values will be clamped to the limits authorized on http://official.trackmania.com/tmf-ladderserver/ -->
<ladder_serverlimit_max>50000</ladder_serverlimit_max>
<ladder_serverlimit_max>%%SERVER_LADDER_LIMIT_MAX%%</ladder_serverlimit_max>
<enable_p2p_upload>True</enable_p2p_upload>
<enable_p2p_download>True</enable_p2p_download>
@@ -0,0 +1,135 @@
<?php
/**
* remoteCP 4
* ütf-8 release
*
* @package remoteCP
* @author hal.sascha
* @copyright (c) 2006-2009
* @version 4.0.3.5
*
* Patched by tmserver-docker:
* - Bare-constant-Zugriffe (pt_custom, pt_points, ...) durch
* defined()-Pruefungen ersetzt, um PHP 7.2+ Warnungen zu vermeiden.
*/
class CustomPoints extends rcp_plugin
{
public $display = 'side';
public $title = 'Points';
public $author = 'hal.ko.sascha';
public $version = '4.0.3.5';
public $nservstatus = array(2,3,4,5);
public $vpermissions = array('editgamesettings');
public $apermissions = array(
'setPoints' => 'editgamesettings',
'setPointsPreset' => 'editgamesettings'
);
public $presets;
public function onLoad()
{
$this->presets = Core::getObject('session')->loadXML(
Core::getSetting('pluginpath') . $this->id . '/presets.xml'
);
}
public function onOutput()
{
$CustomPoints = array();
if (Core::getObject('gbx')->query('GetRoundCustomPoints')) {
$CustomPoints = Core::getObject('gbx')->getResponse();
}
if (!is_array($CustomPoints)) {
$CustomPoints = array();
}
if (!Core::getObject('session')->checkPerm('editgamesettings')) {
return;
}
echo "<form action='ajax.php' method='post' id='custompoints' name='custompoints' class='postcmd' rel='{$this->display}area'>";
echo "<fieldset>";
echo "<div class='legend'>" . (defined('pt_custom') ? pt_custom : 'Custom Points') . "</div>";
echo "<div class='f-row'>
<label for='points'>" . (defined('pt_points') ? pt_points : 'Points') . "</label>
<div class='f-field'>
<input type='text' name='points' id='points' value='" . implode(',', $CustomPoints) . "' />
<div class='small'>" . (defined('pt_commasep') ? pt_commasep : 'Comma separated') . "</div>
</div>";
echo "</div>";
echo "</fieldset>";
echo "<input type='hidden' name='plugin' value='{$this->id}' />";
echo "<input type='hidden' name='action' value='setPoints' />";
echo "<button type='submit' title='" . (defined('ct_submit') ? ct_submit : 'Submit') . "' class='wide'>" . (defined('ct_submit') ? ct_submit : 'Submit') . "</button>";
echo "</form>";
echo "<form action='ajax.php' method='post' id='custompointspreset' name='custompointspreset' class='postcmd' rel='{$this->display}area'>";
echo "<fieldset>";
echo "<div class='legend'>" . (defined('pt_presets') ? pt_presets : 'Presets') . "</div>";
if ($this->presets && is_object($this->presets)) {
foreach ($this->presets->children() as $preset) {
$name = isset($preset['name']) ? $preset['name'] : 'Preset';
$points = isset($preset['points']) ? (string)$preset['points'] : '';
echo "<div class='f-row'>
<label>{$name}</label>
<div class='f-field'>";
echo "<input style='width:25px;' type='radio' name='preset' value='{$points}' /> ";
if (strlen($points) > 25) {
echo substr($points, 0, 25) . "...";
} else {
echo $points;
}
echo "</div>";
echo "</div>";
}
}
echo "</fieldset>";
echo "<input type='hidden' name='plugin' value='{$this->id}' />";
echo "<input type='hidden' name='action' value='setPointsPreset' />";
echo "<button type='submit' title='" . (defined('ct_submit') ? ct_submit : 'Submit') . "' class='wide'>" . (defined('ct_submit') ? ct_submit : 'Submit') . "</button>";
echo "</form>";
}
public function setPoints()
{
if (!array_key_exists('points', $_REQUEST)) return;
$str = preg_replace("/[^0-9,]/", "", $_REQUEST['points']);
$array = $this->makeIntArray(explode(',', $str));
Core::getObject('actions')->add('SetRoundCustomPoints', $array, true);
}
public function setPointsPreset()
{
if (!array_key_exists('preset', $_REQUEST)) return;
$str = preg_replace("/[^0-9,]/", "", $_REQUEST['preset']);
$array = $this->makeIntArray(explode(',', $str));
Core::getObject('actions')->add('SetRoundCustomPoints', $array, true);
}
private function makeIntArray($array)
{
foreach ($array as $key => $value) {
$array[$key] = (int)$value;
}
return $array;
}
}
@@ -0,0 +1,320 @@
<?php
/**
* remoteCP 4
* ütf-8 release
*
* @package remoteCP
* @author hal.sascha
* @copyright (c) 2006-2009
* @version 4.0.2.6
*
* Patch: foreach()-Warnungen behoben – leere Umgebungen (Island, Bay, …)
* fuehrten zu "Invalid argument supplied for foreach()", weil
* $this->mods['Env'] nicht initialisiert war.
* Zusaetzlich bare-constant-Warnungen (pt_*) mit defined()-Pruefungen entschaerft.
*/
class Mods extends rcp_plugin
{
public $display = 'side';
public $title = 'Mods';
public $author = 'hal.ko.sascha';
public $version = '4.0.3.5';
public $nservstatus = array(2,3,4,5);
public $vpermissions = array('editserversettings');
public $apermissions = array(
'setMods' => 'editserversettings',
'setMusic' => 'editserversettings'
);
private $mods = array();
private $music = array();
/**
* Alle bekannten Umgebungs-Keys mit leeren Arrays vorbelegen,
* damit foreach() auch bei fehlenden Eintraegen nicht warnt.
*/
private function initModDefaults()
{
$envs = array('Stadium', 'Island', 'Bay', 'Coast', 'Speed', 'Alpine', 'Rally');
foreach($envs as $env) {
if(!isset($this->mods[$env])) {
$this->mods[$env] = array();
}
}
}
public function onLoadSettings($settings)
{
// Set defaults
$this->mods = array();
$this->music = array();
// Alle Umgebungen vorinitialisieren
$this->initModDefaults();
// Read mods settings
if(!$settings->mods) return;
foreach($settings->mods->children() AS $env)
{
if(!$env) continue;
$tmp = (string) $env->getName();
if(!isset($this->mods[$tmp])) {
$this->mods[$tmp] = array();
}
foreach($env->children() AS $item)
{
$this->mods[$tmp][] = array(
'url' => (string) $item,
'name' => (string) $item['name']
);
}
}
// Read music settings
if(!$settings->music) return;
foreach($settings->music->children() AS $song)
{
$this->music[] = array(
'url' => (string) $song,
'name' => (string) $song['name']
);
}
}
public function onOutput() {
if(Core::getObject('gbx')->query('GetForcedMods')) {
$ForcedMods = Core::getObject('gbx')->getResponse();
if(!empty($ForcedMods)) {
echo "<fieldset>";
echo "<div class='legend'>".(defined('pt_forcedmods') ? pt_forcedmods : 'Forced Mods')."</div>";
if(is_array($ForcedMods['Mods'])) {
foreach($ForcedMods['Mods'] as $mod)
{
echo "<div class='f-row'>
<label>{$mod['Env']}</label>
<div class='f-field'>". $this->getModName($mod['Env'], $mod['Url']) ."</div>";
echo "</div>";
}
}
echo "</fieldset>";
}
}
echo "<form action='ajax.php' method='post' id='forcemods' name='forcemods' class='postcmd' rel='{$this->display}area'>";
echo "<fieldset>";
echo "<div class='legend'>".(defined('pt_forcemods') ? pt_forcemods : 'Force Mods')."</div>";
// --- Stadium ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_stadium') ? pt_stadium : 'Stadium')."</label>
<div class='f-field'>
<select name='modstadium'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Stadium'])) {
foreach($this->mods['Stadium'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Island ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_island') ? pt_island : 'Island')."</label>
<div class='f-field'>
<select name='modisland'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Island'])) {
foreach($this->mods['Island'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Bay ---
echo "<div class='f-row'>
<label for='points'>".(defined('pt_bay') ? pt_bay : 'Bay')."</label>
<div class='f-field'>
<select name='modbay'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Bay'])) {
foreach($this->mods['Bay'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Coast ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_coast') ? pt_coast : 'Coast')."</label>
<div class='f-field'>
<select name='modcoast'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Coast'])) {
foreach($this->mods['Coast'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Speed ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_speed') ? pt_speed : 'Speed')."</label>
<div class='f-field'>
<select name='modspeed'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Speed'])) {
foreach($this->mods['Speed'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Alpine ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_alpine') ? pt_alpine : 'Alpine')."</label>
<div class='f-field'>
<select name='modalpine'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Alpine'])) {
foreach($this->mods['Alpine'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
// --- Rally ---
echo " <div class='f-row'>
<label for='points'>".(defined('pt_rally') ? pt_rally : 'Rally')."</label>
<div class='f-field'>
<select name='modrally'>";
echo "<option value='0'>".(defined('pt_none') ? pt_none : 'None')."</option>";
if(!empty($this->mods['Rally'])) {
foreach($this->mods['Rally'] AS $mod)
{
echo "<option value='{$mod['url']}'>{$mod['name']}</option>";
}
}
echo " </select>
</div>
</div>";
echo " <div class='f-row'>
<label for='DisableAllMods'>".(defined('pt_disableall') ? pt_disableall : 'Disable All')."</label>
<div class='f-field'><input type='checkbox' class='checkbox' name='DisableAllMods' /></div>
</div>";
echo "</fieldset>";
echo "<input type='hidden' name='plugin' value='{$this->id}' />";
echo "<input type='hidden' name='action' value='setMods' />";
echo "<button type='submit' title='".(defined('ct_submit') ? ct_submit : 'Submit')."' class='wide'>".(defined('ct_submit') ? ct_submit : 'Submit')."</button>";
echo "</form>";
if(Core::getObject('gbx')->query('GetForcedMusic')) {
$ForcedMusic = Core::getObject('gbx')->getResponse();
if(!empty($ForcedMusic)) {
echo "<fieldset>";
echo "<div class='legend'>".(defined('pt_forcedmusic') ? pt_forcedmusic : 'Forced Music')."</div>";
echo "<div class='f-row'>
<label>{$ForcedMusic['File']}</label>
<div class='f-field'>{$ForcedMusic['Url']}</div>";
echo "</div>";
echo "</fieldset>";
}
}
echo "<form action='ajax.php' method='post' id='forcemusic' name='forcemusic' class='postcmd' rel='{$this->display}area'>";
echo "<fieldset>";
echo "<div class='legend'>".(defined('pt_forcemusic') ? pt_forcemusic : 'Force Music')."</div>";
echo "<div class='f-row'>
<label for='song'>".(defined('pt_song') ? pt_song : 'Song')."</label>
<div class='f-field'>
<select name='song'>";
if(!empty($this->music)) {
foreach($this->music AS $song)
{
echo "<option value='{$song['url']}'>{$song['name']}</option>";
}
}
echo " </select>
</div>";
echo " <div class='f-row'>
<label for='DisableAllMusic'>".(defined('pt_disableall') ? pt_disableall : 'Disable All')."</label>
<div class='f-field'><input type='checkbox' class='checkbox' name='DisableAllMusic' /></div>
</div>";
echo "</fieldset>";
echo "<input type='hidden' name='plugin' value='{$this->id}' />";
echo "<input type='hidden' name='action' value='setMusic' />";
echo "<button type='submit' title='".(defined('ct_submit') ? ct_submit : 'Submit')."' class='wide'>".(defined('ct_submit') ? ct_submit : 'Submit')."</button>";
echo "</form>";
}
public function setMods()
{
$array = array();
$override = true;
if(!array_key_exists('DisableAllMods', $_REQUEST)) {
if(!empty($_REQUEST['modstadium']))
$array[] = array('Env' => 'Stadium', 'Url' => $_REQUEST['modstadium']);
if(!empty($_REQUEST['modisland']))
$array[] = array('Env' => 'Island' , 'Url' => $_REQUEST['modisland']);
if(!empty($_REQUEST['modbay']))
$array[] = array('Env' => 'Bay' , 'Url' => $_REQUEST['modbay']);
if(!empty($_REQUEST['modcoast']))
$array[] = array('Env' => 'Coast' , 'Url' => $_REQUEST['modcoast']);
if(!empty($_REQUEST['modspeed']))
$array[] = array('Env' => 'Speed' , 'Url' => $_REQUEST['modspeed']);
if(!empty($_REQUEST['modalpine']))
$array[] = array('Env' => 'Alpine' , 'Url' => $_REQUEST['modalpine']);
if(!empty($_REQUEST['modrally']))
$array[] = array('Env' => 'Rally' , 'Url' => $_REQUEST['modrally']);
} else {
$override = false;
}
Core::getObject('actions')->add('SetForcedMods', $override, $array);
}
public function setMusic()
{
$url = '';
$override = true;
if(!array_key_exists('DisableAllMusic', $_REQUEST)) {
$url = $_REQUEST['song'];
} else {
$override = false;
}
Core::getObject('actions')->add('SetForcedMusic', $override, $url);
}
private function getModName($env, $url)
{
if(!isset($this->mods[$env]) || !is_array($this->mods[$env])) {
return '';
}
foreach($this->mods[$env] AS $value)
{
if($url == $value['url']) {
return $value['name'];
}
}
return '';
}
}
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<settings>
<mods>
<!-- Mods fuer die Stadium-Umgebung -->
<!-- Alle Mods werden von https://assets.techniverse.net/tm/skins/ bereitgestellt -->
<Stadium>
<item name='A Romanorum Superbia'>https://assets.techniverse.net/tm/skins/ARomanorumSuperbia.zip</item>
<item name='Bluemod'>https://assets.techniverse.net/tm/skins/Bluemod.zip</item>
<item name='Candy Box II'>https://assets.techniverse.net/tm/skins/CANDY_BOX_II.zip</item>
<item name='Egyptian Stadium'>https://assets.techniverse.net/tm/skins/Egyptian_Stadium.zip</item>
<item name='FC Mod'>https://assets.techniverse.net/tm/skins/FC-mod.zip</item>
<item name='Forest Mod'>https://assets.techniverse.net/tm/skins/FoResT_MoD.zip</item>
<item name='Imperial Palace'>https://assets.techniverse.net/tm/skins/ImperialPalace.zip</item>
<item name='Inca 3'>https://assets.techniverse.net/tm/skins/Inca3.zip</item>
<item name='Jurassic Park'>https://assets.techniverse.net/tm/skins/JurassicPark.zip</item>
<item name='LMDS Matrix'>https://assets.techniverse.net/tm/skins/LMDSMatrix.zip</item>
<item name='Lego Mod'>https://assets.techniverse.net/tm/skins/LegoMod.zip</item>
<item name='Lego Mod v2'>https://assets.techniverse.net/tm/skins/LegoModv2.zip</item>
<item name='Lego City'>https://assets.techniverse.net/tm/skins/Lego_City.zip</item>
<item name='Mario Mod'>https://assets.techniverse.net/tm/skins/MarioMod.zip</item>
<item name='Moon Base'>https://assets.techniverse.net/tm/skins/MoonBase.zip</item>
<item name='Portal'>https://assets.techniverse.net/tm/skins/Portal.zip</item>
<item name='Quantum Leap'>https://assets.techniverse.net/tm/skins/QuantumLeap.zip</item>
<item name='RDV Urban 01'>https://assets.techniverse.net/tm/skins/RDV-Urban01.zip</item>
<item name='Rainbow Road'>https://assets.techniverse.net/tm/skins/Rainbow%20Road.zip</item>
<item name='Star Wars Mod'>https://assets.techniverse.net/tm/skins/StarWarsMod.zip</item>
<item name='The Pirate Bay'>https://assets.techniverse.net/tm/skins/ThePirateBay.zip</item>
<item name='Toxic'>https://assets.techniverse.net/tm/skins/Toxic.zip</item>
<item name='Transparence V1'>https://assets.techniverse.net/tm/skins/TransparenceV1.zip</item>
<item name='Western Fortress'>https://assets.techniverse.net/tm/skins/WesternFortress.zip</item>
<item name='Wood Mod'>https://assets.techniverse.net/tm/skins/Wood%20Mod.zip</item>
<item name='Wooden Domnann'>https://assets.techniverse.net/tm/skins/Wooden%20Domnann.zip</item>
<item name='Xmas'>https://assets.techniverse.net/tm/skins/Xmas.zip</item>
<item name='N64 Rainbow Road'>https://assets.techniverse.net/tm/skins/_N64_%20Rainbow%20Road.zip</item>
<item name='Blue Light'>https://assets.techniverse.net/tm/skins/bluelight.zip</item>
<item name='Blue Water'>https://assets.techniverse.net/tm/skins/bluewater.zip</item>
<item name='Construct'>https://assets.techniverse.net/tm/skins/construct.zip</item>
<item name='Dark Mirror'>https://assets.techniverse.net/tm/skins/darkmirror.zip</item>
<item name='Formel 1'>https://assets.techniverse.net/tm/skins/formel1.zip</item>
<item name='Future'>https://assets.techniverse.net/tm/skins/future.zip</item>
<item name='Hypercube'>https://assets.techniverse.net/tm/skins/hypercube.zip</item>
<item name='Icebreaker'>https://assets.techniverse.net/tm/skins/icebraker.zip</item>
<item name='Just Black'>https://assets.techniverse.net/tm/skins/justblack.zip</item>
<item name='Lego II'>https://assets.techniverse.net/tm/skins/lego_II.zip</item>
<item name='Mars'>https://assets.techniverse.net/tm/skins/mars.zip</item>
<item name='Modernizer'>https://assets.techniverse.net/tm/skins/modernizer.zip</item>
<item name='Neon Glow'>https://assets.techniverse.net/tm/skins/neonglow.zip</item>
<item name='Pioneer'>https://assets.techniverse.net/tm/skins/pioneer.zip</item>
<item name='Push'>https://assets.techniverse.net/tm/skins/push.zip</item>
<item name='Puzzle'>https://assets.techniverse.net/tm/skins/puzzle.zip</item>
<item name='Robot Mod 2'>https://assets.techniverse.net/tm/skins/robotmod2.zip</item>
<item name='Rubik'>https://assets.techniverse.net/tm/skins/rubik.zip</item>
<item name='Smarties'>https://assets.techniverse.net/tm/skins/smarties.zip</item>
<item name='Sonic'>https://assets.techniverse.net/tm/skins/sonic.zip</item>
<item name='Stadium 2010'>https://assets.techniverse.net/tm/skins/stadium2010.zip</item>
<item name='Stadium Storm'>https://assets.techniverse.net/tm/skins/stadium_storm_mod.zip</item>
<item name='Tomb'>https://assets.techniverse.net/tm/skins/tomb.zip</item>
<item name='Tron Blue'>https://assets.techniverse.net/tm/skins/tronblue.zip</item>
<item name='Tron Green'>https://assets.techniverse.net/tm/skins/trongreen.zip</item>
<item name='Tron Red'>https://assets.techniverse.net/tm/skins/tronred.zip</item>
<item name='Tron Yellow'>https://assets.techniverse.net/tm/skins/tronyellow.zip</item>
<item name='Wipeout'>https://assets.techniverse.net/tm/skins/wipeout.zip</item>
</Stadium>
<!-- Mods fuer die Island-Umgebung -->
<Island>
</Island>
<!-- Mods fuer die Bay-Umgebung -->
<Bay>
</Bay>
<!-- Mods fuer die Coast-Umgebung -->
<Coast>
</Coast>
<!-- Mods fuer die Speed-Umgebung -->
<Speed>
</Speed>
<!-- Mods fuer die Alpine-Umgebung -->
<Alpine>
</Alpine>
<!-- Mods fuer die Rally-Umgebung -->
<Rally>
</Rally>
</mods>
<music>
<!-- Musik-URLs hier eintragen -->
<!-- <item name='Songname'>https://example.com/song.ogg</item> -->
</music>
</settings>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<settings>
<!-- Server Configuration, can be address or ip -->
<server>ts3.techniverse.net</server>
<serverid>1</serverid>
<serverport>9987</serverport>
<queryport>10011</queryport>
<!-- Channel & Update Settings -->
<defaultchannel></defaultchannel>
<subchannel></subchannel>
<channelpassword></channelpassword>
<update_interval>30</update_interval>
<!-- Helpers and images -->
<helperURL>http://assets.techniverse.net/tm/ts3gateway/gateway3.html</helperURL>
<logoURL>http://assets.techniverse.net/tm/ts3gateway/ts3logo.jpg</logoURL>
<!-- Widget position -->
<posx>-64</posx>
<posy>45</posy>
</settings>
+6 -2
View File
@@ -1,6 +1,6 @@
services:
tmserver:
image: git.techniverse.net/scriptos/trackmania-server:1.0.0
image: git.techniverse.net/scriptos/trackmania-server:1.2.2
build:
context: .
container_name: tmserver
@@ -12,7 +12,6 @@ services:
- "${SERVER_PORT:-2350}:${SERVER_PORT:-2350}/tcp"
- "${SERVER_PORT:-2350}:${SERVER_PORT:-2350}/udp"
- "${SERVER_P2P_PORT:-3450}:${SERVER_P2P_PORT:-3450}/tcp"
- "${SERVER_XMLRPC_PORT:-5000}:${SERVER_XMLRPC_PORT:-5000}/tcp"
- "80:80/tcp"
env_file:
- .env
@@ -32,9 +31,14 @@ services:
- .env
environment:
MYSQL_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
# RemoteCP-Datenbank (via MariaDB-Init)
MYSQL_DATABASE: ${REMOTECP_DB_NAME:-remotecp}
MYSQL_USER: ${REMOTECP_DB_USER:-remotecp}
MYSQL_PASSWORD: ${REMOTECP_DB_PASSWORD}
# XAseco-Datenbank (via Init-Script init-xaseco-db.sh)
XASECO_DB_NAME: ${XASECO_DB_NAME:-xaseco}
XASECO_DB_USER: ${XASECO_DB_USER:-xaseco}
XASECO_DB_PASSWORD: ${XASECO_DB_PASSWORD}
volumes:
- ./data/mariadb:/var/lib/mysql
- ./assets/db/init-xaseco-db.sh:/docker-entrypoint-initdb.d/20-init-xaseco-db.sh:ro
+45 -1
View File
@@ -13,6 +13,50 @@
| [Umgebungsvariablen](umgebungsvariablen.md) | Alle verfügbaren Umgebungsvariablen |
| [Server-Modi](server-modi.md) | LAN- und Internet-Dedicated-Modus |
| [AdminServ](adminserv.md) | Einrichtung der Server-Verwaltungsoberfläche |
| [RemoteCP](remotecp.md) | Alternative Server-Verwaltungsoberfläche |
| [RemoteCP](remotecp.md) | Alternative Server-Verwaltungsoberfläche (inkl. Mods/Skins) |
| [XAseco](xaseco.md) | Server-Controller für Rekorde, Karma und Jukebox |
| [Ports](ports.md) | Freigegebene Ports und deren Verwendung |
## Projektstruktur
```
├── assets/
│ ├── bin/ # Binaries und Startscript
│ │ ├── AdminServ_v2.1.1.zip # AdminServ Web-UI
│ │ ├── remoteCP_v4.0.3.5.zip # RemoteCP Web-UI
│ │ ├── RunTrackmaniaServer.sh # Container-Startscript
│ │ ├── TrackmaniaServer_2011-02-21.zip # Trackmania Server Binary
│ │ └── xaseco_v1.16.zip # XAseco Server-Controller
│ ├── config/
│ │ ├── adminserv/ # AdminServ-Konfiguration
│ │ │ ├── get_matchset_mapimport.php # MatchSet Map-Import Script
│ │ │ └── maps-creatematchset.php # MatchSet-Erstellung Script
│ │ ├── custom_game_settings.txt # MatchSettings (Spielmodus, Map-Rotation)
│ │ ├── dedicated_cfg.txt # Server-Config-Template (mit Platzhaltern)
│ │ ├── remotecp/
│ │ │ └── plugins/
│ │ │ ├── CustomPoints/
│ │ │ │ └── index.php # CustomPoints-Plugin fuer RemoteCP
│ │ │ └── Mods/
│ │ │ ├── index.php # Mods-Plugin fuer RemoteCP
│ │ │ └── settings.xml # Skin-Bibliothek (techniverse.net)
│ │ └── xaseco/
│ │ └── teamspeak3.xml # TeamSpeak3-Konfiguration fuer XAseco
│ └── db/
│ └── init-xaseco-db.sh # MariaDB Init-Script fuer XAseco-DB
├── docs/ # Dokumentation
├── docker-compose.yml # Docker Compose Konfiguration
├── Dockerfile # Docker Build-Definition
├── .dockerignore # Docker-Ignore-Regeln
├── .env.example # Vorlage fuer Umgebungsvariablen
├── .env # Lokale Umgebungsvariablen (nicht im Git!)
├── .gitattributes # Git-Attribut-Konfiguration
├── .gitignore # Git-Ignore-Regeln
├── LICENSE # Lizenz
├── README.md # Projektbeschreibung
└── data/ # Persistente Daten (zur Laufzeit)
├── gamedata/ # TM-Server-Daten
├── controlpanel/ # AdminServ + RemoteCP
├── xaseco/ # XAseco-Konfiguration und Logs
└── mariadb/ # MariaDB-Datenbankdateien
```
+55 -9
View File
@@ -4,17 +4,19 @@ Die Server-Verwaltungsoberfläche basiert auf [AdminServ](https://github.com/Chr
## Einrichtung
1. `http://<host-server-des-containers>` im Browser aufrufen
2. Ein Passwort festlegen – dieses wird als AdminServ-Passwort verwendet
3. TM-Server-Informationen eintragen (Standardwerte können beibehalten werden)
4. `Address` auf `localhost` setzen, um den eingebetteten Server zu verwalten
5. Speichern
AdminServ wird beim ersten Container-Start **vollständig automatisch konfiguriert**:
- Serververbindung (Adresse `127.0.0.1`, XML-RPC-Port)
- Server-Eintrag (Name, DisplayServ-Passwort)
- Konfigurationspasswort (siehe [Konfigurationsseite (`/config`)](#konfigurationsseite-config))
Es ist kein manuelles Setup nötig.
## Verbindung zum Server
1. Über den Button „Servers" zur Serverliste navigieren
2. Den gewünschten Server auswählen
3. Admin-Stufe wählen und zugehöriges Passwort eingeben
1. `http://<host-server-des-containers>` im Browser aufrufen
2. Den Server aus der Liste auswählen
3. Admin-Stufe wählen (SuperAdmin, Admin oder User) und das zugehörige Passwort eingeben
## Standard-Passwörter
@@ -24,9 +26,11 @@ Die Server-Verwaltungsoberfläche basiert auf [AdminServ](https://github.com/Chr
| Admin | `Admin` |
| User | `User` |
Diese Passwörter werden über die `.env`-Datei gesetzt (`SERVER_SA_PASSWORD`, `SERVER_ADM_PASSWORD`, `SERVER_USER_PASSWORD`) und beim ersten Start in die `dedicated_cfg.txt` geschrieben. AdminServ liest die Passwörter über die XML-RPC-Verbindung direkt vom TM-Server.
Die Admin-Stufen können unter `http://<host-server-des-containers>/config` geändert werden.
> **Hinweis:** Es wird empfohlen, die Standard-Passwörter über die `.env`-Datei (`SERVER_SA_PASSWORD`, `SERVER_ADM_PASSWORD`) zu ändern. Siehe [Umgebungsvariablen](umgebungsvariablen.md).
> **Hinweis:** Die Standard-Passwörter in der `.env.example` sind öffentlich einsehbar. Ändere sie unbedingt, bevor du den Server produktiv einsetzt. Siehe [Umgebungsvariablen](umgebungsvariablen.md).
## Persistente Speicherung
@@ -66,3 +70,45 @@ rm -rf ./data/controlpanel/*
# Container neu starten – AdminServ wird frisch initialisiert
docker compose up -d
```
## Konfigurationsseite (`/config`)
AdminServ bringt unter `http://<host-ip>/config` eine eigene Konfigurationsseite mit, über die Server-Einträge hinzugefügt, geändert oder gelöscht werden können. Diese Seite ist durch ein separates Passwort geschützt (`OnlineConfig::PASSWORD` in `adminserv.cfg.php`, MD5-gehasht).
Da dieser Container als **Standalone-Setup** läuft und ausschließlich den einen lokalen TrackMania-Server bedient, wird die `/config`-Seite **nicht benötigt** – der Server-Eintrag wird beim ersten Start automatisch angelegt (Adresse, XML-RPC-Port, Passwörter).
Zur Absicherung wird das Konfigurationspasswort beim ersten Container-Start automatisch durch einen **zufällig generierten MD5-Hash** ersetzt. Damit ist die `/config`-Seite vor unbefugtem Zugriff geschützt, ohne dass ein Benutzer ein Passwort vergeben oder sich merken muss.
> **Hinweis:** Falls du dennoch Zugriff auf die `/config`-Seite benötigst (z.B. für Debugging), kannst du den MD5-Hash in `data/controlpanel/config/adminserv.cfg.php` manuell auf ein bekanntes Passwort setzen. Beispiel: `const PASSWORD = '5f4dcc3b5aa765d61d8327deb882cf99';` entspricht dem Passwort `password`.
## Gepatchte AdminServ-Bugs (TmForever)
AdminServ (v2.1.1) enthält zwei Bugs, die speziell im Zusammenspiel mit TmForever auftreten. Diese werden beim Container-Start automatisch gepatcht – auch bei bestehenden Volumes.
### Falscher Pfad in MatchSettings-Dateien
Beim Erstellen einer neuen MatchSettings-Datei über AdminServ wurden die Track-Pfade falsch geschrieben. Statt des tatsächlichen Ordners (z.B. `Challenges/Downloaded/`) wurde immer der Speicherort der MatchSettings (`MatchSettings/`) als Pfad-Präfix verwendet:
```xml
<!-- Fehlerhaft (Original) -->
<file>MatchSettings/speed vs. fullspeed.Challenge.Gbx</file>
<!-- Korrekt (nach Patch) -->
<file>Challenges/Downloaded/speed vs. fullspeed.Challenge.Gbx</file>
```
**Ursache:** Die AJAX-Funktion `get_matchset_mapimport.php` hat den URL-Parameter `d` (= MatchSettings-Speicherordner) als relativen Pfad für die Map-Dateinamen verwendet, anstatt den tatsächlichen Ordner aus der Dropdown-Auswahl zu berechnen.
**Betroffene Datei:** `resources/ajax/get_matchset_mapimport.php`
### GetModeScriptInfo-Fehler (-506)
Beim Speichern einer MatchSettings-Datei erschien die Fehlermeldung:
```
[-506] Method 'GetModeScriptInfo' not defined
```
**Ursache:** `GetModeScriptInfo` ist eine XML-RPC-Methode, die nur in ManiaPlanet/TM2 existiert. AdminServ hat sie ohne Versionsprüfung aufgerufen. An anderen Stellen im Code wurde korrekt mit `SERVER_VERSION_NAME != 'TmForever'` unterschieden – nur hier fehlte die Prüfung.
**Betroffene Datei:** `resources/process/maps-creatematchset.php`
+81 -1
View File
@@ -18,6 +18,7 @@ Das gesamte **GameData-Verzeichnis** wird über ein Bind-Mount (`./data/gamedata
|-----------|----------------|-------------|
| `./data/gamedata` | `/opt/tmserver/GameData` | Gesamtes GameData-Verzeichnis |
| `./data/controlpanel` | `/var/www/html` | AdminServ- und RemoteCP-Daten |
| `./data/xaseco` | `/opt/tmserver/xaseco` | XAseco-Konfiguration und Logs |
| `./data/mariadb` | `/var/lib/mysql` | MariaDB-Datenbankdateien |
### Enthaltene Unterordner
@@ -88,7 +89,7 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
> **Achtung:** Bei `FORCE_CONFIG_UPDATE=true` wird die `dedicated_cfg.txt` komplett aus dem Template neu erzeugt und alle Platzhalter mit den aktuellen Umgebungsvariablen ersetzt. **Manuelle Änderungen an der Config gehen dabei verloren!** Andere Dateien im GameData-Volume (Tracks, Skins, Scores, etc.) bleiben erhalten. Nach dem Update sollte `FORCE_CONFIG_UPDATE` wieder auf `false` gesetzt werden.
@@ -103,6 +104,39 @@ Der Ordner `GameData/Config/` enthält:
| `blacklist.txt` | Liste gesperrter Spieler |
| `guestlist.txt` | Liste erlaubter Spieler |
| `Default.SystemConfig.Gbx` | System-Konfiguration |
| `AdminServ/ServerOptions/` | Von AdminServ exportierte Server-Einstellungen |
## AdminServ ServerOptions-Import
Wenn über AdminServ Änderungen an den Server-Optionen vorgenommen und als Export gespeichert werden (z.B. Servername, Beschreibung, Spielerzahl), werden diese beim nächsten Container-Start **automatisch** in die `dedicated_cfg.txt` übernommen.
**Ablauf:**
1. In AdminServ unter „Server Options" die gewünschten Einstellungen ändern und „Export" klicken
2. Die exportierte Datei wird in `GameData/Config/AdminServ/ServerOptions/` gespeichert
3. Beim nächsten Start des Containers wird die **neueste** Export-Datei erkannt
4. Die darin enthaltenen Werte werden in die `dedicated_cfg.txt` geschrieben
**Unterstützte Felder:**
| AdminServ-Feld | dedicated_cfg.txt-Feld |
|----------------|----------------------|
| `Name` | `<name>` |
| `Comment` | `<comment>` |
| `HideServer` | `<hide_server>` |
| `NextMaxPlayers` | `<max_players>` |
| `Password` | `<password>` |
| `PasswordForSpectator` | `<password_spectator>` |
| `NextMaxSpectators` | `<max_spectators>` |
| `NextLadderMode` | `<ladder_mode>` |
| `NextCallVoteTimeOut` | `<callvote_timeout>` |
| `CallVoteRatio` | `<callvote_ratio>` |
| `AllowChallengeDownload` | `<allow_challenge_download>` |
| `AutoSaveReplays` | `<autosave_replays>` |
| `IsP2PUpload` | `<enable_p2p_upload>` |
| `IsP2PDownload` | `<enable_p2p_download>` |
> **Hinweis:** Die AdminServ-Exports haben **Vorrang** vor den Werten aus den Umgebungsvariablen. Beim ersten Start werden zunächst die Umgebungsvariablen angewendet, danach die AdminServ-Exports (falls vorhanden). Bei weiteren Starts werden nur die AdminServ-Exports angewendet.
## Wichtige Parameter in der dedicated_cfg.txt
@@ -127,3 +161,49 @@ Alle diese Parameter können über [Umgebungsvariablen](umgebungsvariablen.md) g
| `<xmlrpc_port>` | `SERVER_XMLRPC_PORT` | `5000` |
| `<connection_uploadrate>` | `SERVER_UPLOAD_RATE` | `512` |
| `<connection_downloadrate>` | `SERVER_DOWNLOAD_RATE` | `8192` |
## MatchSettings (Spieleinstellungen)
Die MatchSettings-Dateien liegen im Verzeichnis `data/gamedata/Tracks/MatchSettings/` und definieren Spielmodus, Regeln und die aktive Track-Liste des Servers. Sie werden als `.txt`-Dateien im XML-Format gespeichert.
### Automatische Erkennung (Standard)
Standardmäßig wird beim Serverstart automatisch die **neueste** `.txt`-Datei im MatchSettings-Ordner anhand des Änderungsdatums geladen. So werden z.B. über AdminServ erstellte oder bearbeitete MatchSettings beim nächsten Neustart automatisch aktiv.
```bash
# In der .env-Datei (Standardwert):
MATCHSETTINGS_FILE=auto
```
**Ablauf bei jedem Containerstart:**
1. Der Ordner `data/gamedata/Tracks/MatchSettings/` wird nach `.txt`-Dateien durchsucht
2. Die Datei mit dem neuesten Änderungsdatum wird ermittelt
3. Diese Datei wird als `/game_settings`-Parameter an den TM-Server übergeben
4. Dateiname und Änderungsdatum werden in der Konsole ausgegeben
### Bestimmte Datei verwenden
Alternativ kann eine bestimmte MatchSettings-Datei direkt angegeben werden:
```bash
# In der .env-Datei:
MATCHSETTINGS_FILE=turnier_settings.txt
```
Der Dateiname bezieht sich immer auf den Ordner `data/gamedata/Tracks/MatchSettings/`.
### Fallback
Falls die angegebene oder automatisch ermittelte Datei nicht existiert, wird auf die mitgelieferte Standard-Datei `custom_game_settings.txt` zurückgefallen.
### Neue MatchSettings über AdminServ erstellen
1. In AdminServ mit SuperAdmin einloggen
2. Unter „Maps" → „MatchSettings" → „Create" eine neue Datei anlegen
3. Tracks aus den gewünschten Ordnern importieren (z.B. `Challenges/Downloaded/`)
4. Spielmodus und Regeln konfigurieren
5. Speichern – die Datei wird in `MatchSettings/` abgelegt
6. Container neustarten (`docker compose restart`) – die neue Datei wird automatisch als neueste erkannt und geladen
> **Hinweis:** Die aktive MatchSettings-Datei wird beim Serverstart in der Konsole ausgegeben. Mit `docker logs tmserver` kann überprüft werden, welche Datei geladen wurde.
+15 -3
View File
@@ -5,7 +5,7 @@
| 2350 | TCP | Gameserver-Port |
| 2350 | UDP | Gameserver-Port |
| 3450 | TCP | P2P-Gameserver-Port |
| 5000 | TCP | XML-RPC-Port (interne Kommunikation) |
| 5000 | TCP | XML-RPC-Port (nur containerintern, nicht nach außen freigegeben) |
| 80 | TCP | Server-Verwaltungsoberflächen (AdminServ + RemoteCP) |
## Minimale Port-Freigabe
@@ -20,7 +20,19 @@ docker run -d \
-p 3450:3450/tcp \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
> **Hinweis:** Port 5000 (XML-RPC) wird intern von AdminServ verwendet und muss in der Regel nicht nach außen freigegeben werden.
> **Hinweis:** Port 5000 (XML-RPC) wird containerintern von AdminServ, RemoteCP und XAseco verwendet und ist standardmäßig **nicht** nach außen freigegeben.
>
> Falls du den XML-RPC-Port extern benötigst (z. B. für ein externes Tool außerhalb des Containers), kannst du ihn nachträglich in der `docker-compose.yml` unter `ports:` ergänzen:
>
> ```yaml
> - "${SERVER_XMLRPC_PORT:-5000}:${SERVER_XMLRPC_PORT:-5000}/tcp"
> ```
>
> Bzw. bei `docker run`:
>
> ```bash
> -p 5000:5000/tcp
> ```
+56 -1
View File
@@ -93,6 +93,61 @@ Die Konfigurationsdateien befinden sich unter `./data/controlpanel/remotecp/xml/
| `admins.xml` | Benutzer und Zugangsdaten |
| `groups.xml` | Berechtigungsgruppen |
## Mods / Skins
RemoteCP enthält ein **Mods-Plugin**, mit dem Texturpakete (Skins) pro Spielumgebung auf dem Server forciert werden können. Spieler laden den jeweiligen Mod automatisch beim Betreten des Servers herunter.
### Wie funktionieren Mods?
In TrackMania Forever sind Mods ZIP-Archive mit alternativen Texturen für eine Spielumgebung (Stadium, Island, Bay, etc.). Der Ablauf:
1. Der Mod liegt als `.zip`-Datei auf einem **Webserver**, der für die Spieler erreichbar ist
2. Der Serveradmin forciert den Mod über den XML-RPC-Befehl `SetForcedMods` (pro Umgebung eine URL)
3. Wenn ein Spieler dem Server beitritt, lädt sein Client den Mod automatisch von der angegebenen URL herunter
4. Mods sind **flüchtig** – nach einem Serverneustart müssen sie erneut gesetzt werden (das Startup-Script übernimmt das automatisch, siehe unten)
### Vorkonfigurierte Skin-Bibliothek
Das Image wird mit einer vorkonfigurierten Skin-Bibliothek ausgeliefert, die über **50 Skins** für die Stadium-Umgebung enthält. Alle Skins werden von [assets.techniverse.net](https://assets.techniverse.net/tm/skins/) bereitgestellt und sind im RemoteCP-Mods-Plugin als Dropdown-Auswahl verfügbar.
Die Konfiguration befindet sich unter:
| Host-Pfad | Container-Pfad | Beschreibung |
|-----------|----------------|-------------|
| `./data/controlpanel/remotecp/plugins/Mods/settings.xml` | `/var/www/html/remotecp/plugins/Mods/settings.xml` | Mod-Katalog (URLs pro Umgebung) |
> **Hinweis:** Bei bestehenden Installationen (Volumes) wird die alte Standard-`settings.xml` (mit den Original-Beispiel-URLs von blacksunonline.com) beim nächsten Containerstart automatisch durch die neue Version mit den techniverse.net-Skins ersetzt.
### Mods über RemoteCP verwalten
1. RemoteCP öffnen: `http://<host-ip>/remotecp/`
2. Mit SuperAdmin-Zugangsdaten einloggen
3. Im Seitenmenü das **Mods**-Plugin aufrufen
4. Pro Umgebung (Stadium, Island, etc.) einen Skin aus dem Dropdown auswählen
5. "Submit" klicken – der Mod wird sofort auf dem Server aktiviert
### Mods automatisch beim Start forcieren
Über die Umgebungsvariablen `FORCE_MOD_*` kann ein Mod pro Umgebung automatisch bei **jedem** Containerstart gesetzt werden – unabhängig von `FORCE_CONFIG_UPDATE`. Siehe [Umgebungsvariablen – Forced Mods](umgebungsvariablen.md#forced-mods-skins) für Details.
**Beispiel** (in der `.env`-Datei):
```bash
FORCE_MOD_STADIUM=https://assets.techniverse.net/tm/skins/Portal.zip
```
### Eigene Skins hinzufügen
Um eigene Skins in das RemoteCP-Dropdown aufzunehmen, bearbeite die Datei `data/controlpanel/remotecp/plugins/Mods/settings.xml` und füge innerhalb der gewünschten Umgebung einen neuen Eintrag hinzu:
```xml
<Stadium>
<item name='Mein Skin'>https://example.com/mods/mein_skin.zip</item>
</Stadium>
```
> **Wichtig:** Die ZIP-Datei muss von den Spielern über HTTP/HTTPS erreichbar sein.
## Sicherheit
RemoteCP liefert eine `.htaccess`-Datei mit, die den direkten Zugriff auf XML-Konfigurationsdateien über den Browser verhindert. Apache `mod_rewrite` und `AllowOverride` sind im Image aktiviert, damit dieser Schutz funktioniert.
@@ -112,4 +167,4 @@ Falls RemoteCP nicht erreichbar ist oder Fehler anzeigt:
- RemoteCP ist ein älteres Tool (Version 4.0.3.5) und wurde für PHP 5.x entwickelt, läuft aber mit PHP 7.4
- Die Live-Funktionen (`remoteCP[Live]`) benötigen eine laufende Serververbindung
- Die Registrierung neuer Benutzer ist standardmäßig aktiviert und kann in `xml/settings/settings.xml` über `<register>false</register>` deaktiviert werden
- **Sicherheitshinweis:** Die Registrierung neuer Benutzer ist standardmäßig aktiviert. Aus Sicherheitsgründen sollte diese deaktiviert werden, damit sich keine unbefugten Nutzer einen Zugang anlegen können. Dazu in der Datei `data/controlpanel/remotecp/xml/settings/settings.xml` den Wert `<register>false</register>` setzen
+29 -11
View File
@@ -18,24 +18,42 @@ Bearbeite die `.env`-Datei und setze mindestens die gewünschten Passwörter. F
> **Wichtig:** Die `.env`-Datei enthält sensible Daten (Passwörter, Keys) und wird über die `.gitignore` vom Einchecken ausgeschlossen.
## 2. Docker Image bauen
## 2. Server starten
```bash
docker build -t tmserver:latest -t tmserver:1.0.0 .
### Fertiges Docker Image verwenden (empfohlen)
Es steht ein fertiges Docker Image in der Container-Registry bereit – kein eigener Build nötig:
```
git.techniverse.net/scriptos/trackmania-server:latest
```
Damit wird das Image mit zwei Tags erstellt: `tmserver:latest` und `tmserver:1.0.0`.
> **Tipp:** Alle verfügbaren Tags findest du in der [Container-Registry](https://git.techniverse.net/scriptos/-/packages/container/trackmania-server/).
## 3. Server starten
#### Mit Docker Compose
### Mit Docker Compose (empfohlen)
```bash
docker compose up -d
```
Die Konfiguration erfolgt über die `.env`-Datei, die automatisch eingelesen wird. Das Image wird automatisch aus der Registry geladen.
### Docker Image selbst bauen
Alternativ kannst du das Image auch selbst bauen:
```bash
docker build -t tmserver:latest -t tmserver:1.2.1 .
```
Damit wird das Image mit zwei Tags erstellt: `tmserver:latest` und `tmserver:1.2.1`.
Anschließend den Server starten:
```bash
docker compose up -d --build
```
Die Konfiguration erfolgt über die `.env`-Datei, die automatisch eingelesen wird.
### Internet-Modus (docker run)
Für den Internet-Modus wird ein Server-Account benötigt. Dieser kann auf der [Trackmania Players-Seite](https://players.trackmaniaforever.com) erstellt werden.
@@ -50,7 +68,7 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
### LAN-Modus (docker run)
@@ -68,10 +86,10 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
## 4. Verwaltungsoberflächen öffnen
## 3. Verwaltungsoberflächen öffnen
| Tool | URL | Beschreibung |
|------|-----|-------------|
+1 -1
View File
@@ -56,5 +56,5 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
+67 -4
View File
@@ -48,6 +48,7 @@ nano .env
| `SERVER_MAX_SPECTATORS` | Maximale Zuschaueranzahl | `32` |
| `SERVER_SPEC_PASSWORD` | Zuschauer-Passwort (leer = offen) | *(leer)* |
| `SERVER_LADDER_MODE` | Ladder-Modus (`inactive` oder `forced`) | `forced` |
| `SERVER_LADDER_LIMIT_MAX` | Oberes Ladder-Serverlimit (Punktegrenze) | `60000` |
## Netzwerk
@@ -66,6 +67,29 @@ nano .env
| `SERVER_MODE` | Server-Modus (`internet` oder `lan`) | `internet` |
| `FORCE_CONFIG_UPDATE` | Erzwingt erneutes Anwenden aller Umgebungsvariablen auf die Config | `false` |
## Spieleinstellungen (MatchSettings)
| Variable | Beschreibung | Standard |
|----------|-------------|----------|
| `MATCHSETTINGS_FILE` | MatchSettings-Datei beim Serverstart: `auto` = neueste `.txt`-Datei im Ordner wird automatisch geladen, oder ein expliziter Dateiname (z.B. `meine_settings.txt`) | `auto` |
| `ALLWARMUPDURATION` | Warmup-Dauer für alle Runden (`0` = deaktiviert, `1` = eine Runde Warmup) | `0` |
### Automatische MatchSettings-Erkennung
Standardmäßig (`MATCHSETTINGS_FILE=auto`) wird beim Serverstart automatisch die **neueste** `.txt`-Datei im Verzeichnis `data/gamedata/Tracks/MatchSettings/` anhand des Änderungsdatums ermittelt und geladen. So werden z.B. über AdminServ exportierte MatchSettings beim nächsten Neustart automatisch aktiv.
**Beispiele:**
```bash
# Automatisch die neueste Datei laden (Standard)
MATCHSETTINGS_FILE=auto
# Eine bestimmte Datei verwenden
MATCHSETTINGS_FILE=turnier_settings.txt
```
> **Hinweis:** Falls die angegebene oder automatisch ermittelte Datei nicht existiert, wird auf `custom_game_settings.txt` zurückgefallen. Die aktiv geladene Datei wird beim Serverstart in der Konsole ausgegeben.
## RemoteCP
RemoteCP verwendet die SuperAdmin-Zugangsdaten (`SERVER_SA_PASSWORD`) des TM-Servers für den Web-Login. Es werden keine separaten Login-Variablen benötigt.
@@ -79,6 +103,41 @@ RemoteCP verwendet die SuperAdmin-Zugangsdaten (`SERVER_SA_PASSWORD`) des TM-Ser
> **Hinweis:** Diese Werte werden nur beim ersten Start (leeres Volume) angewendet. Weitere Details unter [RemoteCP](remotecp.md).
## Forced Mods (Skins)
Mods sind Texturpakete (Skins), die das Aussehen einer Spielumgebung komplett verändern. Über `FORCE_MOD_*`-Variablen kann beim Containerstart automatisch ein Mod pro Umgebung forciert werden. Spieler laden den Mod dann automatisch beim Betreten des Servers herunter.
| Variable | Beschreibung | Standard |
|----------|-------------|----------|
| `FORCE_MOD_STADIUM` | Mod-URL für die Stadium-Umgebung | *(leer)* |
| `FORCE_MOD_ISLAND` | Mod-URL für die Island-Umgebung | *(leer)* |
| `FORCE_MOD_BAY` | Mod-URL für die Bay-Umgebung | *(leer)* |
| `FORCE_MOD_COAST` | Mod-URL für die Coast-Umgebung | *(leer)* |
| `FORCE_MOD_SPEED` | Mod-URL für die Speed-Umgebung | *(leer)* |
| `FORCE_MOD_ALPINE` | Mod-URL für die Alpine-Umgebung | *(leer)* |
| `FORCE_MOD_RALLY` | Mod-URL für die Rally-Umgebung | *(leer)* |
> **Hinweis:** Die Mods werden per XML-RPC (`SetForcedMods`) bei **jedem** Containerstart gesetzt – unabhängig von `FORCE_CONFIG_UPDATE`. Die URL muss auf eine gültige Mod-ZIP-Datei zeigen, die für die Spieler erreichbar ist.
### Verfügbare Skins
Eine Auswahl vorkonfigurierter Skins steht unter `https://assets.techniverse.net/tm/skins/` bereit und ist auch im RemoteCP-Mods-Plugin als Dropdown auswählbar.
**Beispiel:**
```bash
# Portal-Mod für Stadium forcieren
FORCE_MOD_STADIUM=https://assets.techniverse.net/tm/skins/Portal.zip
# Mehrere Umgebungen gleichzeitig
FORCE_MOD_STADIUM=https://assets.techniverse.net/tm/skins/Xmas.zip
FORCE_MOD_ISLAND=https://example.com/mods/island_mod.zip
```
### Mods über RemoteCP verwalten
Zusätzlich zur automatischen Konfiguration per Umgebungsvariable können Mods auch zur Laufzeit über das RemoteCP-Web-Interface (`http://<host-ip>/remotecp/`) im Mods-Plugin per Dropdown ausgewählt und aktiviert werden.
## MariaDB
| Variable | Beschreibung | Standard |
@@ -109,9 +168,11 @@ XAseco ist ein Server-Controller für Rekorde, Karma, Jukebox und mehr. Siehe [X
| Variable | Beschreibung | Standard |
|----------|-------------|----------|
| `PHP_DISPLAY_ERRORS` | Zeigt PHP-Fehlermeldungen im Browser an (nur zur Fehlersuche!) | `false` |
| `PHP_DISPLAY_ERRORS` | Aktiviert den PHP-Debug-Modus: Fehlermeldungen im Browser + vollständige Warnungen im Log (nur zur Fehlersuche!) | `false` |
> **Hinweis:** Der Debug-Modus erfordert **keinen** Rebuild des Images. Es genügt, die Variable in der `.env`-Datei zu ändern und den Container neu zu starten (`docker compose restart`). Im Produktivbetrieb sollte `PHP_DISPLAY_ERRORS` immer auf `false` stehen.
>
> Bei `false` werden nur schwerwiegende Fehler geloggt (keine Warnungen/Notices). Bei `true` werden zusätzlich alle Warnungen und Hinweise angezeigt und geloggt – nützlich zur Fehlersuche bei Problemen mit RemoteCP oder AdminServ.
> **Hinweis:** Bei `FORCE_CONFIG_UPDATE=true` wird die `dedicated_cfg.txt` aus dem Template neu erzeugt und alle Platzhalter mit den aktuellen Umgebungsvariablen ersetzt. Manuelle Änderungen gehen dabei verloren! Nach dem Update sollte `FORCE_CONFIG_UPDATE` wieder auf `false` gesetzt werden.
@@ -122,9 +183,11 @@ XAseco ist ein Server-Controller für Rekorde, Karma, Jukebox und mehr. Siehe [X
Passe die Werte in der `.env`-Datei an und starte mit:
```bash
docker compose up -d --build
docker compose up -d
```
> **Tipp:** Das fertige Docker Image wird automatisch aus der [Container-Registry](https://git.techniverse.net/scriptos/-/packages/container/trackmania-server/) geladen. Wenn du das Image selbst bauen möchtest, verwende stattdessen `docker compose up -d --build`.
### docker run
```bash
@@ -137,7 +200,7 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
Einzelne Werte können zusätzlich überschrieben werden:
@@ -154,5 +217,5 @@ docker run -d \
-v ./data/gamedata:/opt/tmserver/GameData \
-v ./data/controlpanel:/var/www/html \
-v ./data/xaseco:/opt/tmserver/xaseco \
--name tmserver tmserver:latest
--name tmserver git.techniverse.net/scriptos/trackmania-server:latest
```
+240 -7
View File
@@ -8,6 +8,55 @@ Im Container wird die modifizierte Version **XAseco 1.16** verwendet, die für P
XAseco verbindet sich über XML-RPC mit dem TrackMania-Server und reagiert auf Spielereignisse (neue Rekorde, Spieler-Connects, Chat-Befehle usw.). Die Daten werden in einer eigenen MySQL-Datenbank gespeichert.
## TeamSpeak 3 Integration
XAseco enthält ein Plugin (`plugin.teamspeak3.php`), das im Spiel ein Widget mit den aktuell verbundenen TeamSpeak-3-Nutzern anzeigt. Das Plugin ist **standardmäßig aktiviert** und verwendet ein eigenes Gateway, das im Container mitgeliefert wird (das Original-Gateway des Plugin-Entwicklers ist nicht mehr verfügbar).
### TS3-Server konfigurieren
Die Konfiguration erfolgt über die Datei `data/xaseco/teamspeak3.xml`. Dort kann der eigene TeamSpeak-3-Server eingetragen werden:
```xml
<?xml version="1.0" encoding="utf-8"?>
<settings>
<!-- Server Configuration, can be address or ip -->
<server>ts3.techniverse.net</server>
<serverid>1</serverid>
<serverport>9987</serverport>
<queryport>10011</queryport>
<!-- Channel & Update Settings -->
<defaultchannel></defaultchannel>
<subchannel></subchannel>
<channelpassword></channelpassword>
<update_interval>30</update_interval>
<!-- Helpers and images -->
<helperURL>http://assets.techniverse.net/tm/ts3gateway/gateway3.html</helperURL>
<logoURL>http://assets.techniverse.net/tm/ts3gateway/ts3logo.jpg</logoURL>
<!-- Widget position -->
<posx>-64</posx>
<posy>45</posy>
</settings>
```
| Feld | Beschreibung |
|------|-------------|
| `server` | Hostname oder IP des TS3-Servers |
| `serverid` | Virtuelle-Server-ID (meist `1`) |
| `serverport` | Voice-Port des TS3-Servers (Standard: `9987`) |
| `queryport` | ServerQuery-Port (Standard: `10011`) |
| `defaultchannel` | Standard-Channel (leer = Server-Default) |
| `subchannel` | Sub-Channel (optional) |
| `channelpassword` | Channel-Passwort (optional) |
| `update_interval` | Aktualisierungsintervall in Sekunden |
| `helperURL` | URL zur Gateway-HTML-Seite (nicht ändern) |
| `logoURL` | URL zum TS3-Logo im Widget (nicht ändern) |
| `posx` / `posy` | Widget-Position im Spiel |
> **Hinweis:** Standardmäßig ist der TS3-Server `ts3.techniverse.net` vorkonfiguriert. Zum Anpassen einfach nach dem ersten Start die Datei `data/xaseco/teamspeak3.xml` bearbeiten. Die Felder `helperURL` und `logoURL` verweisen auf das mitgelieferte Gateway und sollten nicht geändert werden.
## Konfiguration
Die Konfiguration erfolgt ausschließlich über Umgebungsvariablen in der `.env`-Datei. Beim **ersten Start** (leeres XAseco-Volume) werden die Werte automatisch in die XML-Konfigurationsdateien eingetragen.
@@ -108,13 +157,197 @@ Die XAseco-Logdatei befindet sich unter:
Bei Problemen ist dies die erste Anlaufstelle für die Fehlersuche.
## Wichtige Chat-Befehle
## Chat-Befehle
Nachfolgend eine Übersicht der wichtigsten Befehle, die im Spielchat verfügbar sind. Eine vollständige Liste aller Befehle findest du in der offiziellen Dokumentation unter: https://docs.xaseco.org/commands.php
### Hilfe & Info
| Befehl | Beschreibung |
|--------|-------------|
| `/helpadmin` | Admin-Befehle anzeigen |
| `/recs` | Lokale Rekorde anzeigen |
| `/dedirecs` | Dedimania-Rekorde anzeigen |
| `/jukebox` | Track-Jukebox öffnen |
| `/stats` | Spieler-Statistiken anzeigen |
| `/admin help` | Alle Admin-Befehle |
| `/help` | Zeigt alle verfügbaren Befehle |
| `/helpall` | Zeigt ausführliche Hilfe zu allen Befehlen |
| `/xaseco` | Zeigt Infos über die XAseco-Version |
| `/server` | Zeigt Infos über den Server |
| `/plugins` | Zeigt Liste der aktiven Plugins |
| `/time` | Zeigt aktuelle Serverzeit und Datum |
### Rekorde & Statistiken
| Befehl | Beschreibung |
|--------|-------------|
| `/recs` | Zeigt alle lokalen Rekorde auf der aktuellen Strecke |
| `/recs pb` | Zeigt deine persönliche Bestzeit |
| `/recs new` | Zeigt neu gefahrene Rekorde |
| `/recs live` | Zeigt Rekorde der Online-Spieler |
| `/pb` | Zeigt deine persönliche Bestzeit auf der aktuellen Strecke |
| `/dedirecs` | Zeigt Dedimania-Rekorde auf der aktuellen Strecke |
| `/dedipb` | Zeigt deine persönliche Dedimania-Bestzeit |
| `/dedistats` | Zeigt Dedimania-Streckenstatistiken |
| `/best` | Zeigt deine besten Rekorde |
| `/worst` | Zeigt deine schlechtesten Rekorde |
| `/summary` | Zeigt eine Zusammenfassung aller deiner Rekorde |
| `/stats` | Zeigt Statistiken des aktuellen Spielers |
| `/wins` | Zeigt Siege des aktuellen Spielers |
### Rankings & Toplisten
| Befehl | Beschreibung |
|--------|-------------|
| `/rank` | Zeigt deinen aktuellen Serverrang |
| `/nextrank` | Zeigt den nächst besser platzierten Spieler |
| `/top10` | Zeigt die 10 bestplatzierten Spieler |
| `/top100` | Zeigt die 100 bestplatzierten Spieler |
| `/topwins` | Zeigt die 100 siegreichsten Spieler |
| `/toprecs` | Zeigt Top 100 der Rekord-Halter |
| `/topsums` | Zeigt Top 100 der Top-3-Rekord-Halter |
| `/active` | Zeigt die 100 aktivsten Spieler |
| `/topclans` | Zeigt die 10 bestplatzierten Clans |
### Strecken & Jukebox
| Befehl | Beschreibung |
|--------|-------------|
| `/track` | Zeigt Infos über die aktuelle Strecke |
| `/nextmap` | Zeigt den Namen der nächsten Strecke |
| `/playtime` | Zeigt, wie lange die aktuelle Strecke läuft |
| `/list` | Listet Strecken auf dem Server (siehe `/list help`) |
| `/list nofinish` | Strecken, auf denen du keinen Rang hast |
| `/list newest` | Die neuesten Strecken |
| `/list <name>` | Suche nach Strecken- oder Autorennamen |
| `/jukebox` | Track-Jukebox (siehe `/jukebox help`) |
| `/jukebox list` | Zeigt kommende Strecken |
| `/jukebox <#>` | Fügt Strecke Nr. `<#>` aus `/list` hinzu |
| `/jukebox drop` | Entfernt deine hinzugefügte Strecke |
| `/add <ID>` | Fügt eine Strecke direkt von TMX hinzu |
| `/history` | Zeigt die 10 zuletzt gespielten Strecken |
### Karma & Voting
| Befehl | Beschreibung |
|--------|-------------|
| `/karma` | Zeigt Karma der aktuellen Strecke |
| `/++` | Positive Bewertung für die aktuelle Strecke |
| `/--` | Negative Bewertung für die aktuelle Strecke |
| `/helpvote` | Zeigt Infos zum Chat-Voting-System |
| `/endround` | Startet Vote zum Beenden der aktuellen Runde |
| `/replay` | Startet Vote zum Wiederholen der Strecke |
| `/skip` | Startet Vote zum Überspringen der Strecke |
| `/kick` | Startet Vote zum Kicken eines Spielers |
| `/y` | Stimmt mit Ja bei einem laufenden Vote |
| `/cancel` | Bricht deinen aktuellen Vote ab |
### Kommunikation
| Befehl | Beschreibung |
|--------|-------------|
| `/pm <login> <msg>` | Sendet eine private Nachricht |
| `/pmlog` | Zeigt Verlauf deiner privaten Nachrichten |
| `/chatlog` | Zeigt Verlauf der letzten Chat-Nachrichten |
| `/me` | Drückt eine Aktion/Emotion aus |
| `/hi` | Sendet eine Hallo-Nachricht an alle |
| `/bye` | Sendet eine Tschüss-Nachricht an alle |
| `/gg` | Sendet "Good Game" an alle |
| `/n1` | Sendet "Nice One" an alle |
| `/brb` | Sendet "Be Right Back" an alle |
| `/afk` | Sendet "Away From Keyboard" an alle |
### Spieleroptionen
| Befehl | Beschreibung |
|--------|-------------|
| `/settings` | Zeigt deine persönlichen Einstellungen |
| `/cps` | Setzt Checkpoint-Tracking für lokale Rekorde |
| `/dedicps` | Setzt Checkpoint-Tracking für Dedimania-Rekorde |
| `/mute <login>` | Chat eines Spielers stummschalten |
| `/unmute <login>` | Stummschaltung aufheben |
| `/mutelist` | Zeigt Liste der stummgeschalteten Spieler |
| `/players` | Zeigt aktuelle Spieler-Liste (Nicks/Logins) |
| `/ranks` | Zeigt Liste der Online-Ränge |
| `/bootme` | Kickt dich selbst vom Server |
### Admin-Befehle (`/admin`)
Diese Befehle sind nur für Admins und MasterAdmins verfügbar.
| Befehl | Beschreibung |
|--------|-------------|
| `/admin help` | Zeigt alle Admin-Befehle |
| `/admin helpall` | Zeigt ausführliche Hilfe zu allen Admin-Befehlen |
**Server-Einstellungen:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin setservername <name>` | Ändert den Servernamen |
| `/admin setpwd <pwd>` | Ändert das Spieler-Passwort |
| `/admin setspecpwd <pwd>` | Ändert das Zuschauer-Passwort |
| `/admin setmaxplayers <#>` | Setzt maximale Spieleranzahl |
| `/admin setmaxspecs <#>` | Setzt maximale Zuschauerzahl |
| `/admin setgamemode <mode>` | Setzt Spielmodus (ta/rounds/team/laps/stunts/cup) |
**Strecken-Verwaltung:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin nextmap` | Erzwingt nächste Strecke |
| `/admin restartmap` | Startet aktuelle Strecke neu |
| `/admin replaymap` | Wiederholt aktuelle Strecke (via Jukebox) |
| `/admin endround` | Erzwingt Ende der aktuellen Runde |
| `/admin add <ID>` | Fügt Strecke von TMX hinzu |
| `/admin addlocal <datei>` | Fügt lokale Strecke hinzu |
| `/admin remove <#>` | Entfernt Strecke aus der Rotation |
| `/admin erasethis` | Entfernt aktuelle Strecke und löscht Datei |
| `/admin shuffle` | Mischt die Streckenliste zufällig |
| `/admin writetracklist` | Speichert aktuelle Streckenliste |
| `/admin readtracklist` | Lädt Streckenliste aus Datei |
**Jukebox:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin dropjukebox <#>` | Entfernt eine Strecke aus der Jukebox |
| `/admin clearjukebox` | Leert die gesamte Jukebox |
| `/admin pass` | Genehmigt einen laufenden Vote |
| `/admin cancel` | Bricht einen laufenden Vote ab |
**Spieler-Moderation:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin warn <login>` | Sendet eine Warnung an einen Spieler |
| `/admin kick <login>` | Kickt einen Spieler vom Server |
| `/admin kickghost <login>` | Kickt einen Ghost-Spieler |
| `/admin ban <login>` | Bannt einen Spieler |
| `/admin unban <login>` | Entbannt einen Spieler |
| `/admin black <login>` | Setzt einen Spieler auf die Blacklist |
| `/admin unblack <login>` | Entfernt einen Spieler von der Blacklist |
| `/admin mute <login>` | Schaltet einen Spieler global stumm |
| `/admin unmute <login>` | Hebt globale Stummschaltung auf |
| `/admin forcespec <login>` | Erzwingt Zuschauer-Modus |
| `/admin forceteam <login>` | Erzwingt Team-Zuordnung (Blue/Red) |
**Admin-Verwaltung:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin addadmin <login>` | Fügt einen neuen Admin hinzu |
| `/admin removeadmin <login>` | Entfernt einen Admin |
| `/admin addop <login>` | Fügt einen neuen Operator hinzu |
| `/admin removeop <login>` | Entfernt einen Operator |
| `/admin listmasters` | Zeigt MasterAdmin-Liste |
| `/admin listadmins` | Zeigt Admin-Liste |
| `/admin listops` | Zeigt Operator-Liste |
**Rekorde & System:**
| Befehl | Beschreibung |
|--------|-------------|
| `/admin delrec <#>` | Löscht einen bestimmten Rekord auf der aktuellen Strecke |
| `/admin wall <msg>` | Zeigt Popup-Nachricht für alle Spieler |
| `/admin pm <msg>` | Sendet private Nachricht an alle Admins |
| `/admin server` | Zeigt detaillierte Server-Einstellungen |
| `/admin shutdown` | Fährt XAseco herunter |
| `/admin shutdownall` | Fährt Server und XAseco herunter |
> **Tipp:** Eine vollständige Referenz aller Befehle (inkl. `/jfreu`-Befehle für erweiterte Moderation) findest du unter: https://docs.xaseco.org/commands.php