From 75ae777f1ed9d23bebfbda019227fb9117834dde Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 22 Mar 2026 15:22:19 +0100 Subject: [PATCH 1/5] feat: Auto-Import von AdminServ ServerOptions beim Containerstart --- assets/bin/RunTrackmaniaServer.sh | 123 ++++++++++++++++++++++++++++++ docs/konfiguration.md | 33 ++++++++ 2 files changed, 156 insertions(+) diff --git a/assets/bin/RunTrackmaniaServer.sh b/assets/bin/RunTrackmaniaServer.sh index d2ef431..42872da 100644 --- a/assets/bin/RunTrackmaniaServer.sh +++ b/assets/bin/RunTrackmaniaServer.sh @@ -453,6 +453,129 @@ if [ "$APPLY_ENV" = "true" ]; then 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 werden ersetzt, + // damit und in unangetastet bleiben. + $lines = file($cfgFile); + $inServerOptions = false; + $updated = 0; + + foreach ($lines as $i => $line) { + if (strpos($line, "") !== false) { + $inServerOptions = true; + } + if (strpos($line, "") !== 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 + # Bestimme Server-Modus (Standard: internet) SERVER_MODE="${SERVER_MODE:-internet}" diff --git a/docs/konfiguration.md b/docs/konfiguration.md index 0781602..f9ae1ee 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -103,6 +103,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` | `` | +| `Comment` | `` | +| `HideServer` | `` | +| `NextMaxPlayers` | `` | +| `Password` | `` | +| `PasswordForSpectator` | `` | +| `NextMaxSpectators` | `` | +| `NextLadderMode` | `` | +| `NextCallVoteTimeOut` | `` | +| `CallVoteRatio` | `` | +| `AllowChallengeDownload` | `` | +| `AutoSaveReplays` | `` | +| `IsP2PUpload` | `` | +| `IsP2PDownload` | `` | + +> **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 -- 2.49.1 From fdcc82e935d5c3948fa0724b39cb4f93210b7467 Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 22 Mar 2026 15:24:31 +0100 Subject: [PATCH 2/5] Workspace wird nicht mehr mit committed --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4796708..dbfda45 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ # Persistente Server-Daten data/ + +.ki-workspace/ -- 2.49.1 From 61deb9327382ef26ab1fb71705007822031e3098 Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 22 Mar 2026 17:05:25 +0100 Subject: [PATCH 3/5] feat: Automatische MatchSettings-Erkennung & AdminServ-Bugfixes --- .env.example | 6 + Dockerfile | 10 ++ assets/bin/RunTrackmaniaServer.sh | 76 ++++++++- .../adminserv/get_matchset_mapimport.php | 76 +++++++++ .../config/adminserv/maps-creatematchset.php | 160 ++++++++++++++++++ docs/adminserv.md | 32 ++++ docs/konfiguration.md | 46 +++++ docs/umgebungsvariablen.md | 17 ++ 8 files changed, 421 insertions(+), 2 deletions(-) create mode 100644 assets/config/adminserv/get_matchset_mapimport.php create mode 100644 assets/config/adminserv/maps-creatematchset.php diff --git a/.env.example b/.env.example index f216c68..66cd619 100644 --- a/.env.example +++ b/.env.example @@ -55,6 +55,12 @@ 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 +# --- 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 + # --- 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 diff --git a/Dockerfile b/Dockerfile index 055b6b6..ec1486a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,16 @@ 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 +# 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 -a /var/www/html /opt/tmserver/default-controlpanel diff --git a/assets/bin/RunTrackmaniaServer.sh b/assets/bin/RunTrackmaniaServer.sh index 42872da..a3e7afe 100644 --- a/assets/bin/RunTrackmaniaServer.sh +++ b/assets/bin/RunTrackmaniaServer.sh @@ -369,6 +369,34 @@ if [ -f "$CUSTOMPOINTS_FILE" ] && ! grep -q 'defined.*pt_custom' "$CUSTOMPOINTS_ 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 + echo "Starting apache server" service apache2 start @@ -576,6 +604,50 @@ 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. +# - "": 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}" @@ -595,8 +667,8 @@ 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})" diff --git a/assets/config/adminserv/get_matchset_mapimport.php b/assets/config/adminserv/get_matchset_mapimport.php new file mode 100644 index 0000000..fceeb8c --- /dev/null +++ b/assets/config/adminserv/get_matchset_mapimport.php @@ -0,0 +1,76 @@ + 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']); + } +?> diff --git a/assets/config/adminserv/maps-creatematchset.php b/assets/config/adminserv/maps-creatematchset.php new file mode 100644 index 0000000..3d809ac --- /dev/null +++ b/assets/config/adminserv/maps-creatematchset.php @@ -0,0 +1,160 @@ + $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; + } +?> diff --git a/docs/adminserv.md b/docs/adminserv.md index 9dc11d9..ed5a726 100644 --- a/docs/adminserv.md +++ b/docs/adminserv.md @@ -66,3 +66,35 @@ rm -rf ./data/controlpanel/* # Container neu starten – AdminServ wird frisch initialisiert docker compose up -d ``` + +## 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 + +MatchSettings/speed vs. fullspeed.Challenge.Gbx + + +Challenges/Downloaded/speed vs. fullspeed.Challenge.Gbx +``` + +**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` diff --git a/docs/konfiguration.md b/docs/konfiguration.md index f9ae1ee..26dd20a 100644 --- a/docs/konfiguration.md +++ b/docs/konfiguration.md @@ -160,3 +160,49 @@ Alle diese Parameter können über [Umgebungsvariablen](umgebungsvariablen.md) g | `` | `SERVER_XMLRPC_PORT` | `5000` | | `` | `SERVER_UPLOAD_RATE` | `512` | | `` | `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. diff --git a/docs/umgebungsvariablen.md b/docs/umgebungsvariablen.md index 705139c..c1d7068 100644 --- a/docs/umgebungsvariablen.md +++ b/docs/umgebungsvariablen.md @@ -71,8 +71,25 @@ nano .env | 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. -- 2.49.1 From 3fb1dac5ba21750d80d33f24347247b0deb52abf Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 22 Mar 2026 20:58:46 +0100 Subject: [PATCH 4/5] feat: Forced Mods (Skins) per Umgebungsvariable beim Containerstart setzen --- .env.example | 14 + Dockerfile | 13 + README.md | 1 + assets/bin/RunTrackmaniaServer.sh | 261 ++++++++++++++++-- .../config/remotecp/plugins/Mods/settings.xml | 93 +++++++ docs/README.md | 8 +- docs/remotecp.md | 55 ++++ docs/umgebungsvariablen.md | 35 +++ 8 files changed, 461 insertions(+), 19 deletions(-) create mode 100644 assets/config/remotecp/plugins/Mods/settings.xml 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 | -- 2.49.1 From ed5b3d22fb8c6c7620dab4ab5588fe5da7130a4d Mon Sep 17 00:00:00 2001 From: "Patrick Asmus (scriptos)" Date: Sun, 22 Mar 2026 21:27:03 +0100 Subject: [PATCH 5/5] fix: PHP-Warnungen im RemoteCP Mods-Plugin behoben (foreach auf leere Umgebungen) --- Dockerfile | 7 + assets/config/remotecp/plugins/Mods/index.php | 320 ++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 assets/config/remotecp/plugins/Mods/index.php diff --git a/Dockerfile b/Dockerfile index 3cc1eda..9c5deaa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,6 +76,13 @@ 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 +# 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 diff --git a/assets/config/remotecp/plugins/Mods/index.php b/assets/config/remotecp/plugins/Mods/index.php new file mode 100644 index 0000000..b2ebac6 --- /dev/null +++ b/assets/config/remotecp/plugins/Mods/index.php @@ -0,0 +1,320 @@ +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 "
"; + echo "
".(defined('pt_forcedmods') ? pt_forcedmods : 'Forced Mods')."
"; + if(is_array($ForcedMods['Mods'])) { + foreach($ForcedMods['Mods'] as $mod) + { + echo "
+ +
". $this->getModName($mod['Env'], $mod['Url']) ."
"; + echo "
"; + } + } + echo "
"; + } + } + + echo "
"; + echo "
"; + echo "
".(defined('pt_forcemods') ? pt_forcemods : 'Force Mods')."
"; + + // --- Stadium --- + echo "
+ +
+ +
+
"; + + // --- Island --- + echo "
+ +
+ +
+
"; + + // --- Bay --- + echo "
+ +
+ +
+
"; + + // --- Coast --- + echo "
+ +
+ +
+
"; + + // --- Speed --- + echo "
+ +
+ +
+
"; + + // --- Alpine --- + echo "
+ +
+ +
+
"; + + // --- Rally --- + echo "
+ +
+ +
+
"; + + echo "
+ +
+
"; + echo "
"; + echo ""; + echo ""; + echo ""; + echo "
"; + + if(Core::getObject('gbx')->query('GetForcedMusic')) { + $ForcedMusic = Core::getObject('gbx')->getResponse(); + + if(!empty($ForcedMusic)) { + echo "
"; + echo "
".(defined('pt_forcedmusic') ? pt_forcedmusic : 'Forced Music')."
"; + echo "
+ +
{$ForcedMusic['Url']}
"; + echo "
"; + echo "
"; + } + } + + echo "
"; + echo "
"; + echo "
".(defined('pt_forcemusic') ? pt_forcemusic : 'Force Music')."
"; + echo "
+ +
+ +
"; + echo "
+ +
+
"; + echo "
"; + echo ""; + echo ""; + echo ""; + echo "
"; + } + + 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 ''; + } +} -- 2.49.1