diff --git a/.env.example b/.env.example
index 66cd619..cd5a740 100644
--- a/.env.example
+++ b/.env.example
@@ -55,6 +55,20 @@ 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=
+
# --- MatchSettings ---
# Steuert, welche MatchSettings-Datei beim Serverstart geladen wird.
# "auto" = die neueste .txt-Datei in data/gamedata/Tracks/MatchSettings/ wird automatisch erkannt.
diff --git a/Dockerfile b/Dockerfile
index ec1486a..3cc1eda 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -76,6 +76,10 @@ RUN unzip /var/www/html/remoteCP_v4.0.3.5.zip -d /var/www/html \
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
+# 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.
@@ -155,6 +159,15 @@ 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
diff --git a/README.md b/README.md
index 4ccf5d9..cafe7d4 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@ Ein vollständiges Docker-Setup für einen **TrackMania Nations Forever**-Server
- **[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
+- **[Mods / Skins](docs/remotecp.md#mods--skins)** – über 50 vorkonfigurierte Stadium-Skins (gehostet auf [assets.techniverse.net](https://assets.techniverse.net/tm/skins/)), automatisch beim Start forcierbar
Alle Komponenten laufen in einem einzigen Container und werden über Umgebungsvariablen konfiguriert.
diff --git a/assets/bin/RunTrackmaniaServer.sh b/assets/bin/RunTrackmaniaServer.sh
index a3e7afe..5e60d82 100644
--- a/assets/bin/RunTrackmaniaServer.sh
+++ b/assets/bin/RunTrackmaniaServer.sh
@@ -397,6 +397,22 @@ if [ -f "$ADMINSERV_CREATEMATCHSET" ] && grep -q "query('GetModeScriptInfo')" "$
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
+
echo "Starting apache server"
service apache2 start
@@ -673,35 +689,248 @@ 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 >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 "" . ($v ? "1" : "0") . "";
+ if (is_int($v)) return "" . $v . "";
+ if (is_string($v)) return "" . htmlspecialchars($v, ENT_XML1) . "";
+ if (is_array($v)) {
+ if (array_keys($v) !== range(0, count($v) - 1)) {
+ $x = "";
+ foreach ($v as $k => $val) $x .= "" . $k . "" . encodeVal($val) . "";
+ return $x . "";
+ } else {
+ $x = "";
+ foreach ($v as $val) $x .= encodeVal($val);
+ return $x . "";
+ }
+ }
+ return "" . htmlspecialchars((string)$v, ENT_XML1) . "";
+ }
+
+ // 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 = ""
+ . "" . $method . "";
+ foreach ($params as $p) $xml .= "" . encodeVal($p) . "";
+ $xml .= "";
+
+ $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, "1") === 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 = ""
+ . "SetForcedMods"
+ . "" . encodeVal(true) . ""
+ . "" . encodeVal($mods) . ""
+ . "";
+ 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, "1") !== 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)
diff --git a/assets/config/remotecp/plugins/Mods/settings.xml b/assets/config/remotecp/plugins/Mods/settings.xml
new file mode 100644
index 0000000..8337ee2
--- /dev/null
+++ b/assets/config/remotecp/plugins/Mods/settings.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+ - https://assets.techniverse.net/tm/skins/ARomanorumSuperbia.zip
+ - https://assets.techniverse.net/tm/skins/Bluemod.zip
+ - https://assets.techniverse.net/tm/skins/CANDY_BOX_II.zip
+ - https://assets.techniverse.net/tm/skins/Egyptian_Stadium.zip
+ - https://assets.techniverse.net/tm/skins/FC-mod.zip
+ - https://assets.techniverse.net/tm/skins/FoResT_MoD.zip
+ - https://assets.techniverse.net/tm/skins/ImperialPalace.zip
+ - https://assets.techniverse.net/tm/skins/Inca3.zip
+ - https://assets.techniverse.net/tm/skins/JurassicPark.zip
+ - https://assets.techniverse.net/tm/skins/LMDSMatrix.zip
+ - https://assets.techniverse.net/tm/skins/LegoMod.zip
+ - https://assets.techniverse.net/tm/skins/LegoModv2.zip
+ - https://assets.techniverse.net/tm/skins/Lego_City.zip
+ - https://assets.techniverse.net/tm/skins/MarioMod.zip
+ - https://assets.techniverse.net/tm/skins/MoonBase.zip
+ - https://assets.techniverse.net/tm/skins/Portal.zip
+ - https://assets.techniverse.net/tm/skins/QuantumLeap.zip
+ - https://assets.techniverse.net/tm/skins/RDV-Urban01.zip
+ - https://assets.techniverse.net/tm/skins/Rainbow%20Road.zip
+ - https://assets.techniverse.net/tm/skins/StarWarsMod.zip
+ - https://assets.techniverse.net/tm/skins/ThePirateBay.zip
+ - https://assets.techniverse.net/tm/skins/Toxic.zip
+ - https://assets.techniverse.net/tm/skins/TransparenceV1.zip
+ - https://assets.techniverse.net/tm/skins/WesternFortress.zip
+ - https://assets.techniverse.net/tm/skins/Wood%20Mod.zip
+ - https://assets.techniverse.net/tm/skins/Wooden%20Domnann.zip
+ - https://assets.techniverse.net/tm/skins/Xmas.zip
+ - https://assets.techniverse.net/tm/skins/_N64_%20Rainbow%20Road.zip
+ - https://assets.techniverse.net/tm/skins/bluelight.zip
+ - https://assets.techniverse.net/tm/skins/bluewater.zip
+ - https://assets.techniverse.net/tm/skins/construct.zip
+ - https://assets.techniverse.net/tm/skins/darkmirror.zip
+ - https://assets.techniverse.net/tm/skins/formel1.zip
+ - https://assets.techniverse.net/tm/skins/future.zip
+ - https://assets.techniverse.net/tm/skins/hypercube.zip
+ - https://assets.techniverse.net/tm/skins/icebraker.zip
+ - https://assets.techniverse.net/tm/skins/justblack.zip
+ - https://assets.techniverse.net/tm/skins/lego_II.zip
+ - https://assets.techniverse.net/tm/skins/mars.zip
+ - https://assets.techniverse.net/tm/skins/modernizer.zip
+ - https://assets.techniverse.net/tm/skins/neonglow.zip
+ - https://assets.techniverse.net/tm/skins/pioneer.zip
+ - https://assets.techniverse.net/tm/skins/push.zip
+ - https://assets.techniverse.net/tm/skins/puzzle.zip
+ - https://assets.techniverse.net/tm/skins/robotmod2.zip
+ - https://assets.techniverse.net/tm/skins/rubik.zip
+ - https://assets.techniverse.net/tm/skins/smarties.zip
+ - https://assets.techniverse.net/tm/skins/sonic.zip
+ - https://assets.techniverse.net/tm/skins/stadium2010.zip
+ - https://assets.techniverse.net/tm/skins/stadium_storm_mod.zip
+ - https://assets.techniverse.net/tm/skins/tomb.zip
+ - https://assets.techniverse.net/tm/skins/tronblue.zip
+ - https://assets.techniverse.net/tm/skins/trongreen.zip
+ - https://assets.techniverse.net/tm/skins/tronred.zip
+ - https://assets.techniverse.net/tm/skins/tronyellow.zip
+ - https://assets.techniverse.net/tm/skins/wipeout.zip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/README.md b/docs/README.md
index 7e000d5..bb10a03 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -13,7 +13,7 @@
| [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 |
@@ -32,8 +32,10 @@
│ │ ├── dedicated_cfg.txt # Server-Config-Template (mit Platzhaltern)
│ │ └── remotecp/
│ │ └── plugins/
-│ │ └── CustomPoints/
-│ │ └── index.php # CustomPoints-Plugin fuer RemoteCP
+│ │ ├── CustomPoints/
+│ │ │ └── index.php # CustomPoints-Plugin fuer RemoteCP
+│ │ └── Mods/
+│ │ └── settings.xml # Skin-Bibliothek (techniverse.net)
│ └── db/
│ └── init-xaseco-db.sh # MariaDB Init-Script fuer XAseco-DB
├── docs/ # Dokumentation
diff --git a/docs/remotecp.md b/docs/remotecp.md
index 13c7860..6283e41 100644
--- a/docs/remotecp.md
+++ b/docs/remotecp.md
@@ -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:///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
+
+ - https://example.com/mods/mein_skin.zip
+
+```
+
+> **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.
diff --git a/docs/umgebungsvariablen.md b/docs/umgebungsvariablen.md
index c1d7068..c439ea4 100644
--- a/docs/umgebungsvariablen.md
+++ b/docs/umgebungsvariablen.md
@@ -103,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:///remotecp/`) im Mods-Plugin per Dropdown ausgewählt und aktiviert werden.
+
## MariaDB
| Variable | Beschreibung | Standard |