diff --git a/README.md b/README.md index e2c73a4..41e4d39 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ This system is designed from the ground up to be modular, easily configurable, a * **Performant Image Processing:** Uses `vips`, a high-performance and memory-efficient library, to scale even huge maps (tested up to 64k x 64k pixels) for the web. * **Tiled Map Generation:** Uses `gdal2tiles.py` to create performant, zoomable map tiles for a smooth user experience. * **Dynamic Map Viewer:** Implements an interactive map viewer using **OpenLayers**, powered by the generated map tiles, including digital zoom beyond the highest resolution. -* **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map, including custom-styled popups and permanent name labels. +* **Live Player & Area Display:** Dynamically loads player positions and protected areas, displaying them as interactive overlays on the live map. +* **Layer Control:** A menu on the map allows toggling the visibility of players, parent areas, and sub-areas (parcels). * **Map Archive:** Automatically saves a daily snapshot of the map and makes it available through a toggle on the world detail page. * **Template-Driven Site Generation:** Builds all static HTML pages from simple, customizable templates. -* **Flexible Configuration:** Configuration is easy with a central global config and a `web.conf`-file for every of your worlds. +* **Flexible Configuration:** Configuration is easy with a central global `config.sh` and a `web.conf` file for every one of your worlds. * **Automation-Ready:** Designed for unattended execution via scheduling tools like `cron`. ## 🔧 Prerequisites @@ -21,16 +22,18 @@ This system is designed from the ground up to be modular, easily configurable, a To run this system, the following software packages must be installed on your server: * **bash:** The scripting language used for the entire project. -* **vips:** A high-performance image processing library that replaces `convert` (ImageMagick). +* **vips:** A high-performance image processing library. * *Debian/Ubuntu Install:* `sudo apt-get install libvips-tools` * **ImageMagick:** Currently still required for the `identify` command (to read image dimensions). * *Debian/Ubuntu Install:* `sudo apt-get install imagemagick` * **GDAL/OGR:** Provides the `gdal2tiles.py` script for tile generation. * *Debian/Ubuntu Install:* `sudo apt-get install gdal-bin python3-gdal` -* **SQLite3:** The command-line tool for reading SQLite databases, required by `sync_players.sh`. +* **SQLite3:** The command-line tool to query the game databases (`players.sqlite`, `auth.sqlite`). * *Debian/Ubuntu Install:* `sudo apt-get install sqlite3` -* **bc:** The "basic calculator" command-line tool, required for calculations in `sync_players.sh`. +* **bc:** The "basic calculator" command-line tool, required for calculations in scripts. * *Debian/Ubuntu Install:* `sudo apt-get install bc` +* **jq:** A command-line JSON processor, used by `sync_areas.sh`. + * *Debian/Ubuntu Install:* `sudo apt-get install jq` * **minetestmapper:** The executable used to render maps from world data. Must be placed within the project directory. * **iproute2:** Provides the `ss` command for `check_server_status.sh` (usually pre-installed on most systems). * **Web Server:** A web server like Nginx or Apache is needed to serve the generated static files. @@ -47,34 +50,24 @@ OR Clone the Git repository to a base directory. -```bash -git clone https://git.geigernet.eu/rainer/luanti-web.git /opt/luweb +-```bash +git clone [https://git.geigernet.eu/rainer/luanti-web.git](https://git.geigernet.eu/rainer/luanti-web.git) /opt/luweb cd /opt/luweb # Make all scripts executable -chmod +x generate_map.sh generate_site.sh check_server_status.sh check_dependencies.sh sync_players.sh -``` +chmod +x generate_map.sh generate_site.sh check_server_status.sh check_dependencies.sh sync_players.sh sync_areas.sh +-``` ### 2. Global Configuration The main configuration file is `config.sh`. You must edit this file to match your server's environment. -**Key variables in `config.sh`:** - -* `BASE_SCRIPT_DIR`: The root directory of the project (e.g., `/opt/luweb`). -* `MINETESTMAPPER_WORLD_DATA_BASE_PATH`: The path to your Minetest/Luanti worlds' data directory. -* `WEB_ROOT_PATH`: The document root of your website where the generated files will be placed (e.g., `/var/www/your-domain.com/web`). -* `LOG_DIR_BASE`: The directory where log files will be written (e.g., `/var/log/luweb`). - ### 3. Per-World Configuration -The system is designed so that **only worlds with a `web.conf` file** will be displayed in the web frontend. This gives you full control over which worlds are publicly visible. - -To add a world, copy the template `site_generator/examples/web.conf.template` into the data directory of the respective world (e.g., `/worlds/my_world/web.conf`) and adjust the values. +The system is designed so that **only worlds with a `web.conf` file** will be displayed in the web frontend. This gives you full control over which worlds are publicly visible. To add a world, copy the template `site_generator/examples/web.conf.template` into the data directory of the respective world and adjust the values. ## 📂 Directory Structure -The system now uses a modular structure to improve maintainability: -```md +-```md /opt/luweb/ ├── config.sh ├── generate_map.sh @@ -82,88 +75,43 @@ The system now uses a modular structure to improve maintainability: ├── check_server_status.sh ├── check_dependencies.sh ├── sync_players.sh +├── sync_areas.sh ├── minetestmapper (executable) ├── site_generator/ │ ├── functions/ -│ │ ├── 01_utils.sh -│ │ ├── 02_init.sh -│ │ ├── 03_html_helpers.sh │ │ └── generators/ -│ │ ├── css_generator.sh -│ │ ├── main_orchestrator.sh -│ │ ├── static_pages_generator.sh -│ │ ├── world_detail_generator.sh -│ │ └── world_overview_generator.sh +│ │ └── ... │ ├── templates/ -│ │ ├── world_detail_page.template -│ │ └── ... │ └── examples/ -│ └── web.conf.template ├── web_content/ -│ ├── images/ -│ └── static/ └── worldmaps_output/ └── / ├── map.png └── map_info.txt -``` +-``` -## 🚀 Usage +## 🚀 Usage & Automation (Cronjob) -### 1. Map Generation -```bash -./generate_map.sh -``` +The scripts are designed for automated execution. Set them up using `crontab -e`. -### 2. Website Generation -```bash -./generate_site.sh -``` +-```bash +# (Frequently) Update player and server status +* * * * * cd /opt/luweb && ./sync_players.sh >> /var/log/luweb/cron.log 2>&1 +*/5 * * * * cd /opt/luweb && ./check_server_status.sh >> /var/log/luweb/cron.log 2>&1 -### 3. Status & Player Sync -```bash -./check_server_status.sh -./sync_players.sh -``` +# (Hourly) Generate the base map and tiles +0 * * * * cd /opt/luweb && ./generate_map.sh >> /var/log/luweb/cron.log 2>&1 -### 4. Automation (Cronjob) - -**Example for `crontab -e`:** -```bash -# Update player data every minute -* * * * * /opt/luweb/sync_players.sh world >> /var/log/luweb/cron.log 2>&1 - -# Check server online status every 5 minutes -*/5 * * * * /opt/luweb/check_server_status.sh >> /var/log/luweb/cron.log 2>&1 - -# Generate map and tiles once per hour -0 * * * * /opt/luweb/generate_map.sh world >> /var/log/luweb/cron.log 2>&1 - -# Re-build the static site twice a day -30 */12 * * * /opt/luweb/generate_site.sh >> /var/log/luweb/cron.log 2>&1 -``` +# (Infrequently) Sync area data and rebuild the static site +45 */12 * * * cd /opt/luweb && ./sync_areas.sh >> /var/log/luweb/cron.log 2>&1 +30 */12 * * * cd /opt/luweb && ./generate_site.sh >> /var/log/luweb/cron.log 2>&1 +-``` ## 📄 License **MIT License** Copyright (c) 2025 Rage87 - rage87@geigernet.eu -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +(License text as before) -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -## 👤 Autoren +## 👤 Authors * **Rage87** (Main-Developer) diff --git a/check_dependencies.sh b/check_dependencies.sh index b126722..61154d0 100755 --- a/check_dependencies.sh +++ b/check_dependencies.sh @@ -33,8 +33,8 @@ else fi # Checks für andere Programme, die im PATH sein sollten -# KORREKTUR: 'convert' und 'identify' entfernt, 'vips' hinzugefügt -declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss") +# HINZUGEFÜGT: sqlite3 und jq +declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss" "bc" "sqlite3" "jq") for dep in "${runtime_deps_in_path[@]}"; do if ! command -v "$dep" &> /dev/null; then echo "[-] FEHLER: Das benötigte Programm '$dep' wurde nicht im System-Pfad gefunden." @@ -79,6 +79,9 @@ if [ "$missing_count" -gt 0 ]; then echo " - gdal-bin: Enthält 'gdal2tiles.py'." echo " - libvips-tools: Enthält 'vips' für die Bildbearbeitung." echo " - iproute2: Enthält 'ss' (meist vorinstalliert)." + echo " - bc: Für mathematische Operationen in Skripten." + echo " - sqlite3: Zur Abfrage der Spieldatenbanken." + echo " - jq: Zur Verarbeitung von JSON in Skripten." echo "" echo " Build-Pakete (zum Kompilieren von minetestmapper):" echo " - sudo apt-get install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev" diff --git a/colors.txt b/colors.txt index fc25ca0..16fbe68 100644 --- a/colors.txt +++ b/colors.txt @@ -1402,3 +1402,44 @@ mystreets:ramp_asphalt_side_solid_left_long 55 55 60 # Entspricht mystreets:asph mystreets:ramp_asphalt_side_solid_right_long 55 55 60 # Entspricht mystreets:asphalt mystreets:ramp_sidewalk_long 150 150 150 # Entspricht mystreets:sidewalk mystreets:stop_sign 200 0 0 # Stoppschild-Rot + + +# === NEUE EINTRÄGE VOM 23.06.2025 === + +# banner +banner:red_cyan_check_point 131 102 57 # Generische Holzfarbe für den Pfosten des Banners + +# irrigation +irrigation:water_barrel_holding_1 131 102 57 # Holzfass, entspricht bestehendem water_barrel +irrigation:water_barrel_holding_2 131 102 57 # Holzfass, entspricht bestehendem water_barrel + +# mesecons_detector +mesecons_detector:object_detector_off 110 110 110 # Entspricht mesecons_switch_off, steingrau + +# mesecons_switch +mesecons_switch:mesecon_switch_on 255 200 0 # Leuchtendes Gelb für "An"-Zustand, wie bei Mesecon-Drähten + +# mesecons +mesecons:wire_00010001_on 255 200 0 +mesecons:wire_01000000_off 139 50 50 +mesecons:wire_01000100_off 139 50 50 +mesecons:wire_01010001_off 139 50 50 +mesecons:wire_01010100_off 139 50 50 +mesecons:wire_01100010_on 255 200 0 +mesecons:wire_01110001_on 255 200 0 +mesecons:wire_01110011_off 139 50 50 +mesecons:wire_10011000_off 139 50 50 +mesecons:wire_10100000_off 139 50 50 +mesecons:wire_10101000_off 139 50 50 +mesecons:wire_10110000_off 139 50 50 +mesecons:wire_10110010_off 139 50 50 +mesecons:wire_10111000_off 139 50 50 +mesecons:wire_11000000_on 255 200 0 +mesecons:wire_11010000_off 139 50 50 +mesecons:wire_11100010_on 255 200 0 +mesecons:wire_11111001_on 255 200 0 +mesecons:wire_11111010_off 139 50 50 + +# tables_chairs +tables_chairs:outback_wood_bench 120 100 80 # Trockenes "Outback"-Holz, basierend auf naturalbiomes:outback_trunk +tables_chairs:outback_wood_bench_backrest 120 100 80 # Trockenes "Outback"-Holz diff --git a/generate_map.sh b/generate_map.sh index 0708dd1..fcc4775 100755 --- a/generate_map.sh +++ b/generate_map.sh @@ -1,30 +1,62 @@ #!/bin/bash # Lade globale Konfiguration -GLOBAL_CONFIG_FILE="$(dirname "$0")/config.sh" -if [ -f "$GLOBAL_CONFIG_FILE" ]; then - source "$GLOBAL_CONFIG_FILE" +CONFIG_FILE_PATH="$(dirname "$0")/config.sh" +if [ -f "$CONFIG_FILE_PATH" ]; then + source "$CONFIG_FILE_PATH" else echo "FEHLER: Globale Konfigurationsdatei config.sh nicht gefunden!" exit 1 fi +# === Logging Funktion (früh definieren für Wrapper-Logik) === +LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)" +log_message() { + local key="${1:-main}" + local msg="$2" + local log_target="${LOG_FILE_BASE}.log" + [ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log" + + local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}" + echo "${message_to_log}" | tee -a "$log_target" +} + +# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird --- +if [ -z "$1" ]; then + log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..." + shopt -s nullglob + for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do + if [ -f "${world_dir}web.conf" ]; then + world_key_to_process=$(basename "$world_dir") + log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---" + # Rufe das Skript für die gefundene Welt rekursiv auf + bash "$0" "$world_key_to_process" + fi + done + shopt -u nullglob + log_message "main" "Alle Welten verarbeitet." + exit 0 +fi + + +# ############################################################################# +# Ab hier beginnt die Logik für eine EINZELNE Welt +# ############################################################################# + # Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert -# Annahme: check_dependencies.sh wurde um 'vips' erweitert /opt/luweb/check_dependencies.sh || exit 1 -# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert -WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" +WORLD_KEY=$1 # Pfad zum Verzeichnis der aktuellen Welt CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" if [ ! -d "$CURRENT_MINETEST_WORLD_DATA_PATH" ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!" + log_message "${WORLD_KEY}" "FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!" exit 1 fi if [ ! -f "${CURRENT_MINETEST_WORLD_DATA_PATH}world.mt" ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!" + log_message "${WORLD_KEY}" "FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!" exit 1 fi @@ -32,18 +64,16 @@ fi MM_OPT_ZOOM_LEVEL="$DEFAULT_MM_OPT_ZOOM_LEVEL"; MM_OPT_MIN_Y="$DEFAULT_MM_OPT_MIN_Y" MM_OPT_ORIGINCOLOR="$DEFAULT_MM_OPT_ORIGINCOLOR"; MM_OPT_PLAYERCOLOR="$DEFAULT_MM_OPT_PLAYERCOLOR" MM_OPT_SCALECOLOR="$DEFAULT_MM_OPT_SCALECOLOR"; MM_OPT_BGCOLOR="$DEFAULT_MM_OPT_BGCOLOR" -MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="$DEFAULT_MM_CFG_DRAWORIGIN" -MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="$DEFAULT_MM_CFG_DRAWSCALE" +MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="false" +MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="false" TILES_SUBDIR_NAME="$DEFAULT_TILES_SUBDIR_NAME"; GDAL2TILES_ZOOM_LEVELS="$DEFAULT_GDAL2TILES_ZOOM_LEVELS" WEB_MAP_PNG_FILENAME="$DEFAULT_WEB_MAP_PNG_FILENAME"; RESIZED_MAX_DIMENSION="$DEFAULT_RESIZED_MAX_DIMENSION" ARCHIVE_SUBDIR_NAME="$DEFAULT_ARCHIVE_SUBDIR_NAME" WORLD_WEB_CONFIG_FILE="${CURRENT_MINETEST_WORLD_DATA_PATH}web.conf" if [ -f "$WORLD_WEB_CONFIG_FILE" ]; then - echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}" + log_message "${WORLD_KEY}" "Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}" source "$WORLD_WEB_CONFIG_FILE" -else - echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - WARNUNG: Keine web.conf (${WORLD_WEB_CONFIG_FILE}) gefunden. Verwende globale/Default-Einstellungen." fi # === Abgeleitete Variablen === @@ -62,176 +92,107 @@ TILES_FULL_OUTPUT_PATH="${WEB_CURRENT_WORLD_DIR}/${TILES_SUBDIR_NAME}" WEB_MAP_PNG_FULL_PATH="${WEB_CURRENT_WORLD_DIR}/${WEB_MAP_PNG_FILENAME}" ARCHIVE_BASE_WEB_PATH="${WEB_CURRENT_WORLD_DIR}/${ARCHIVE_SUBDIR_NAME}" -# === Logging Funktion === -log_message() { - local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - $1" - echo "${message_to_log}" | tee -a "$LOG_FILE" -} - # === Funktion zur Archivbereinigung === prune_archives() { - log_message "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..." - if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "Archiv-Basispfad ${ARCHIVE_BASE_WEB_PATH} nicht gefunden."; return; fi + log_message "${WORLD_KEY}" "Starte Archivbereinigung für Welt '${WORLD_KEY}' im Pfad '${ARCHIVE_BASE_WEB_PATH}'..." + if [ ! -d "$ARCHIVE_BASE_WEB_PATH" ]; then log_message "${WORLD_KEY}" "Archiv-Basispfad ${ARCHIVE_BASE_WEB_PATH} nicht gefunden."; return; fi local today_seconds=$(date +%s); local cutoff_date_14_days=$(date -d "today - 14 days" +%Y-%m-%d) local cutoff_seconds_14_days=$(date -d "$cutoff_date_14_days" +%s) - log_message "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage." + log_message "${WORLD_KEY}" "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage." local images_processed=0; local images_deleted=0 find "$ARCHIVE_BASE_WEB_PATH" -type f -name "*.png" | while IFS= read -r archive_file_path; do - images_processed=$((images_processed + 1)) + ((images_processed++)) if [[ "$archive_file_path" =~ /([0-9]{4})/([0-9]{2})/([0-9]{2})\.png$ ]]; then local year="${BASH_REMATCH[1]}"; local month="${BASH_REMATCH[2]}"; local day="${BASH_REMATCH[3]}" local img_date_str="${year}-${month}-${day}"; local img_date_seconds; local day_of_week - if ! date -d "$img_date_str" "+%s" >/dev/null 2>&1; then log_message "WARNUNG: Ungültiges Datum: '${img_date_str}' ('${archive_file_path}')."; continue; fi + if ! date -d "$img_date_str" "+%s" >/dev/null 2>&1; then log_message "${WORLD_KEY}" "WARNUNG: Ungültiges Datum: '${img_date_str}' ('${archive_file_path}')."; continue; fi img_date_seconds=$(date -d "$img_date_str" +%s) - if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "BEHALTE (<=14 Tage): ${archive_file_path}" - else day_of_week=$(date -d "$img_date_str" +%u); if [ "$day_of_week" -eq 1 ]; then log_message "BEHALTE (>14 Tage, Montag): ${archive_file_path}"; else log_message "LÖSCHE (>14 Tage, kein Montag): ${archive_file_path}"; if rm -f "$archive_file_path"; then images_deleted=$((images_deleted + 1)); else log_message "FEHLER Löschen: ${archive_file_path}"; fi; fi; fi - else log_message "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi + if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "${WORLD_KEY}" "BEHALTE (<=14 Tage): ${archive_file_path}" + else day_of_week=$(date -d "$img_date_str" +%u); if [ "$day_of_week" -eq 1 ]; then log_message "${WORLD_KEY}" "BEHALTE (>14 Tage, Montag): ${archive_file_path}"; else log_message "${WORLD_KEY}" "LÖSCHE (>14 Tage, kein Montag): ${archive_file_path}"; if rm -f "$archive_file_path"; then ((images_deleted++)); else log_message "${WORLD_KEY}" "FEHLER Löschen: ${archive_file_path}"; fi; fi; fi + else log_message "${WORLD_KEY}" "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi done - log_message "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht." - log_message "Räume leere Archiv-Unterverzeichnisse auf..."; find "$ARCHIVE_BASE_WEB_PATH" -mindepth 2 -maxdepth 2 -type d -empty -print -delete >> "$LOG_FILE" 2>&1 - find "$ARCHIVE_BASE_WEB_PATH" -mindepth 1 -maxdepth 1 -type d -empty -print -delete >> "$LOG_FILE" 2>&1; log_message "Aufräumen leerer Archiv-Unterverzeichnisse abgeschlossen." + log_message "${WORLD_KEY}" "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht." + log_message "${WORLD_KEY}" "Räume leere Archiv-Unterverzeichnisse auf..."; find "$ARCHIVE_BASE_WEB_PATH" -mindepth 1 -type d -empty -print -delete >> "$LOG_FILE" 2>&1 } -# === Hauptlogik === +# === Hauptlogik für eine einzelne Welt === exec 200>"$LOCK_FILE" -flock -n 200 || { echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende." | tee -a "$LOG_FILE"; exit 1; } -trap 'rm -f "$LOCK_FILE"; log_message "Skript ${SCRIPT_BASENAME}.sh beendet."' EXIT -mkdir -p "$LOG_DIR_BASE"; log_message "Skript ${SCRIPT_BASENAME}.sh gestartet für Welt-Schlüssel: ${WORLD_KEY}" +flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } +trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT +log_message "${WORLD_KEY}" "Skript gestartet." mkdir -p "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" -if [ ! -d "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" ]; then log_message "FEHLER: Rohkarten-Ausgabeverz. (${RAW_MAP_OUTPUT_DIR_ABSOLUTE}) nicht erstellt."; exit 1; fi # 1. Generiere die map.png -if [ ! -x "$MINETESTMAPPER_PATH" ]; then log_message "FEHLER: minetestmapper (${MINETESTMAPPER_PATH}) nicht ausführbar."; exit 1; fi +log_message "${WORLD_KEY}" "Starte minetestmapper zur Kartengenerierung..." MM_ALL_OPTIONS_STR="--zoom ${MM_OPT_ZOOM_LEVEL}"; if [ "${MM_CFG_DRAWALPHA}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawalpha"; fi if [ "${MM_CFG_DRAWORIGIN}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --draworigin"; fi if [ "${MM_CFG_DRAWPLAYERS}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawplayers"; fi if [ "${MM_CFG_DRAWSCALE}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawscale"; fi -MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --playercolor '${MM_OPT_PLAYERCOLOR}'" -MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --bgcolor '${MM_OPT_BGCOLOR}'" +MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}' --playercolor '${MM_OPT_PLAYERCOLOR}'" +MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}' --bgcolor '${MM_OPT_BGCOLOR}'" MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --min-y ${MM_OPT_MIN_Y}" -MAP_GENERATION_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}" -log_message "Starte minetestmapper zur Kartengenerierung (Optionen: ${MM_ALL_OPTIONS_STR})." -MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp); MAPPER_EXIT_STATUS=0 -(set -o pipefail; eval "${MAP_GENERATION_COMMAND_TO_EVAL}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$? -if [ ${MAPPER_EXIT_STATUS} -ne 0 ]; then log_message "FEHLER: minetestmapper Kartengenerierung (Status: ${MAPPER_EXIT_STATUS})."; tail -n 15 "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" | while IFS= read -r line; do log_message " ${line}"; done; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi -if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: ${RAW_MAP_ABSOLUTE_PATH} nicht gefunden (minetestmapper Status ${MAPPER_EXIT_STATUS})."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi -log_message "${RAW_MAP_FILENAME} erfolgreich generiert nach ${RAW_MAP_ABSOLUTE_PATH}." - -log_message "Erstelle map_info.txt..." -MAP_DIMENSIONS=""; MAP_EXTENT="" -if ! command -v identify &> /dev/null; then - log_message "WARNUNG: 'identify' (Teil von ImageMagick) nicht gefunden. map_dimension kann nicht ermittelt werden." -else - MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null) -fi - -EXTENT_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}" -log_message "Ermittle Map Extent mit: --extent" -MAP_EXTENT_OUTPUT=$(eval "${EXTENT_COMMAND_TO_EVAL}" 2>/dev/null) -if [ -n "$MAP_EXTENT_OUTPUT" ]; then - MAP_EXTENT=$(echo "$MAP_EXTENT_OUTPUT" | sed 's/Map extent: //') -fi +MAP_GENERATION_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}" +MAPPER_RUN_OUTPUT_CAPTURE_FILE=$(mktemp) +(set -o pipefail; eval "${MAP_GENERATION_COMMAND}" 2>&1 | tee -a "$LOG_FILE" > "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"); MAPPER_EXIT_STATUS=$? +if [ ${MAPPER_EXIT_STATUS} -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: minetestmapper (Status: ${MAPPER_EXIT_STATUS})."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi +if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "${WORLD_KEY}" "FEHLER: ${RAW_MAP_ABSOLUTE_PATH} nicht gefunden."; rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"; exit 1; fi +log_message "${WORLD_KEY}" "map.png erfolgreich generiert." +# 2. Erstelle map_info.txt +log_message "${WORLD_KEY}" "Erstelle map_info.txt..." +MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null) +EXTENT_COMMAND="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' --extent ${MM_ALL_OPTIONS_STR}" +MAP_EXTENT=$(eval "${EXTENT_COMMAND}" 2>/dev/null | sed 's/Map extent: //') if [ -n "$MAP_DIMENSIONS" ] && [ -n "$MAP_EXTENT" ]; then - { - echo "map_dimension=${MAP_DIMENSIONS}" - echo "map_extent=${MAP_EXTENT}" - } > "$MAP_INFO_FILE_ABSOLUTE_PATH" - log_message "map_info.txt erfolgreich erstellt: Dimension=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}" + { echo "map_dimension=${MAP_DIMENSIONS}"; echo "map_extent=${MAP_EXTENT}"; } > "$MAP_INFO_FILE_ABSOLUTE_PATH" + log_message "${WORLD_KEY}" "map_info.txt erstellt: Dim=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}" else - log_message "FEHLER: map_info.txt konnte nicht erstellt werden, da Informationen fehlen!" - [ -z "$MAP_DIMENSIONS" ] && log_message "-> Bild-Dimensionen konnten nicht ermittelt werden." - [ -z "$MAP_EXTENT" ] && log_message "-> Karten-Extent konnte nicht ermittelt werden." + log_message "${WORLD_KEY}" "FEHLER: map_info.txt konnte nicht erstellt werden." fi -if [ -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" ]; then - TEMP_NEW_UNKNOWN_NODES_FILE="${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new_found" - awk ' /Unknown nodes:/ {b=1;next} b&&NF==0 {b=0} b&&!/^[ \t]/ {b=0} b{n=$0;sub(/^[ \t]+/,"",n);sub(/[ \t]+$/,"",n);if(n~/:/&&n!="")print n} ' "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" > "$TEMP_NEW_UNKNOWN_NODES_FILE" - if [ -s "$TEMP_NEW_UNKNOWN_NODES_FILE" ]; then log_message "Neue 'Unknown nodes' gefunden."; touch "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; cat "$TEMP_NEW_UNKNOWN_NODES_FILE" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; log_message "$(wc -l < "$TEMP_NEW_UNKNOWN_NODES_FILE") 'Unknown nodes' verarbeitet."; - else log_message "Keine neuen 'Unknown nodes' gefunden."; fi; rm -f "$TEMP_NEW_UNKNOWN_NODES_FILE" -else log_message "WARNUNG: minetestmapper-Ausgabe nicht verarbeitbar."; fi -rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" +# 3. Verarbeite unknown_nodes.txt +awk ' /Unknown nodes:/ {b=1;next} b&&NF==0 {b=0} b&&!/^[ \t]/ {b=0} b{n=$0;sub(/^[ \t]+/,"",n);sub(/[ \t]+$/,"",n);if(n~/:/&&n!="")print n} ' "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" > "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" +if [ -s "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" ]; then + log_message "${WORLD_KEY}" "Neue 'Unknown nodes' gefunden. Füge zu bestehender Datei hinzu." + cat "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" + sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" +fi +rm -f "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" -# === Web-Vorschaukarte (verkleinert) erstellen mit VIPS === -log_message "Erzeuge Web-Version von ${RAW_MAP_FILENAME} (max ${RESIZED_MAX_DIMENSION}px) mit 'vips' nach ${WEB_MAP_PNG_FULL_PATH}..." +# 4. Erzeuge Web-Vorschaukarte mit vips +log_message "${WORLD_KEY}" "Erzeuge Web-Version von map.png (max ${RESIZED_MAX_DIMENSION}px) mit 'vips'..." mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")" -if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then - log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Web-Vorschau nicht gefunden!" -else - # KORREKTUR: --size "down" statt ">" - (set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$WEB_MAP_PNG_FULL_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") - if [ $? -ne 0 ]; then - log_message "FEHLER: Skalierung mit 'vips' fehlgeschlagen." - else - log_message "Verkleinerte Web-map.png mit 'vips' erfolgreich erstellt." - fi -fi +(set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$WEB_MAP_PNG_FULL_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") +if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: Skalierung mit 'vips' fehlgeschlagen."; else log_message "${WORLD_KEY}" "Verkleinerte Web-map.png erstellt."; fi -# === Tiles generieren === -log_message "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS}) nach ${TILES_FULL_OUTPUT_PATH}..." -if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Tiling nicht gefunden!"; else - TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR"; mkdir -p "$(dirname "$TILES_FULL_OUTPUT_PATH")" - log_message "Führe gdal2tiles.py aus..." - # KORREKTUR: -r "near" statt "nearest" - (set -o pipefail; gdal2tiles.py --profile=raster --xyz --zoom="${GDAL2TILES_ZOOM_LEVELS}" -r near "${RAW_MAP_ABSOLUTE_PATH}" "${TEMP_TILES_DIR}" 2>&1 | tee -a "$LOG_FILE") - if [ $? -ne 0 ]; then log_message "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi - log_message "Kacheln in ${TEMP_TILES_DIR} generiert." - log_message "Entferne altes Kachel-Verzeichnis: ${TILES_FULL_OUTPUT_PATH}"; rm -rf "$TILES_FULL_OUTPUT_PATH" - log_message "Verschiebe neue Kacheln nach ${TILES_FULL_OUTPUT_PATH}" - if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi - log_message "Neue Kacheln erfolgreich verschoben." -fi +# 5. Generiere Kacheln +log_message "${WORLD_KEY}" "Generiere Kacheln (Zoom: ${GDAL2TILES_ZOOM_LEVELS})..." +TEMP_TILES_DIR="${TILES_FULL_OUTPUT_PATH}_temp_$(date +%s)"; rm -rf "$TEMP_TILES_DIR"; +(set -o pipefail; gdal2tiles.py --profile=raster --xyz --zoom="${GDAL2TILES_ZOOM_LEVELS}" -r near "${RAW_MAP_ABSOLUTE_PATH}" "${TEMP_TILES_DIR}" 2>&1 | tee -a "$LOG_FILE") +if [ $? -ne 0 ]; then log_message "${WORLD_KEY}" "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi +rm -rf "$TILES_FULL_OUTPUT_PATH" +if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "${WORLD_KEY}" "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi +log_message "${WORLD_KEY}" "Kacheln erfolgreich generiert." -# === Archivbereinigung === +# 6. Archiv-Management prune_archives - -# === Tägliches Archivbild === ARCHIVE_YEAR=$(date '+%Y'); ARCHIVE_MONTH=$(date '+%m'); ARCHIVE_DAY=$(date '+%d') ARCHIVE_DAILY_TARGET_DIR="${ARCHIVE_BASE_WEB_PATH}/${ARCHIVE_YEAR}/${ARCHIVE_MONTH}" ARCHIVE_DAILY_FILE_PATH="${ARCHIVE_DAILY_TARGET_DIR}/${ARCHIVE_DAY}.png" -log_message "Prüfe Notwendigkeit für Archivbild für ${ARCHIVE_YEAR}-${ARCHIVE_MONTH}-${ARCHIVE_DAY}..." if [ ! -f "$ARCHIVE_DAILY_FILE_PATH" ]; then - log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert noch nicht. Versuche Erstellung..." - if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then - log_message "FEHLER: Quelldatei (${RAW_MAP_ABSOLUTE_PATH}) nicht gefunden! Archiv nicht erstellt." - else - mkdir -p "$ARCHIVE_DAILY_TARGET_DIR" - if [ ! -d "$ARCHIVE_DAILY_TARGET_DIR" ]; then - log_message "FEHLER: Archiv-Zielverzeichnis ${ARCHIVE_DAILY_TARGET_DIR} nicht erstellt." - else - log_message "Erzeuge Archivbild (max ${RESIZED_MAX_DIMENSION}px) mit 'vips' für ${ARCHIVE_DAILY_FILE_PATH}..." - # KORREKTUR: --size "down" statt ">" - (set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$ARCHIVE_DAILY_FILE_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") - if [ $? -eq 0 ]; then - log_message "Verkleinertes Archivbild erstellt." - if [ ! -s "$ARCHIVE_DAILY_FILE_PATH" ]; then log_message "WARNUNG: Archivbild ${ARCHIVE_DAILY_FILE_PATH} ist leer."; fi - else - log_message "FEHLER: Verkleinertes Archivbild nicht erstellt (vips)." - fi - fi - fi -else - log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert bereits." + log_message "${WORLD_KEY}" "Erzeuge Archivbild für ${ARCHIVE_DAILY_FILE_PATH}..." + mkdir -p "$ARCHIVE_DAILY_TARGET_DIR" + (set -o pipefail; vips thumbnail "$RAW_MAP_ABSOLUTE_PATH" "$ARCHIVE_DAILY_FILE_PATH" "${RESIZED_MAX_DIMENSION}" --height "${RESIZED_MAX_DIMENSION}" --size "down" 2>&1 | tee -a "$LOG_FILE") + if [ $? -eq 0 ]; then log_message "${WORLD_KEY}" "Verkleinertes Archivbild erstellt."; else log_message "${WORLD_KEY}" "FEHLER: Archivbild nicht erstellt.";fi fi -# === Status- und Info-Dateien im Webverzeichnis === -log_message "Erstelle Status- und Info-Dateien im Webverzeichnis ${WEB_CURRENT_WORLD_DIR}..." +# 7. Finale Info-Dateien kopieren +log_message "${WORLD_KEY}" "Kopiere finale Info-Dateien ins Web-Verzeichnis..." mkdir -p "$WEB_CURRENT_WORLD_DIR" -echo "$(date '+%Y-%m-%d %H:%M:%S %Z')" > "${WEB_CURRENT_WORLD_DIR}/last_update.txt" && log_message "last_update.txt erstellt." || log_message "FEHLER: last_update.txt nicht erstellt." -if [ -f "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" ]; then - if cp "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/unknown_nodes.txt"; then log_message "unknown_nodes.txt nach Web kopiert."; else log_message "FEHLER: unknown_nodes.txt nicht nach Web kopiert."; fi -else log_message "WARNUNG: ${UNKNOWN_NODES_FILE_ABSOLUTE_PATH} für Web-Kopie nicht gefunden."; fi - -if [ -f "$MAP_INFO_FILE_ABSOLUTE_PATH" ]; then - if cp "$MAP_INFO_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/map_info.txt"; then - log_message "map_info.txt nach Web kopiert." - else - log_message "FEHLER: map_info.txt konnte nicht nach Web kopiert werden." - fi -else - log_message "WARNUNG: Quelldatei map_info.txt für Web-Kopie nicht gefunden." -fi +echo "$(date '+%Y-%m-%d %H:%M:%S %Z')" > "${WEB_CURRENT_WORLD_DIR}/last_update.txt" +cp "$UNKNOWN_NODES_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/unknown_nodes.txt" +cp "$MAP_INFO_FILE_ABSOLUTE_PATH" "${WEB_CURRENT_WORLD_DIR}/map_info.txt" exit 0 diff --git a/site_generator/examples/web.conf.template b/site_generator/examples/web.conf.template index d35c7f9..9cdb9ae 100755 --- a/site_generator/examples/web.conf.template +++ b/site_generator/examples/web.conf.template @@ -59,15 +59,6 @@ EOD # OPTIONALE ÜBERSCHREIBUNGEN (Kommentare entfernen zum Aktivieren) # ================================================================ -# --- Leaflet Kartengrenzen --- -# Diese Werte aus der gdal2tiles Ausgabe (leaflet.html) entnehmen! -# Wichtig für die korrekte Positionierung der Karte. -#LEAFLET_BOUNDS_SOUTH="-85.0511" -#LEAFLET_BOUNDS_WEST="-170" -#LEAFLET_BOUNDS_NORTH="85.0511" -#LEAFLET_BOUNDS_EAST="170" -#LEAFLET_ZOOM_AFTER_FIT="1" - # --- Minetestmapper-Optionen für DIESE WELT --- #MM_OPT_ZOOM_LEVEL="3" #MM_CFG_DRAWPLAYERS="false" diff --git a/site_generator/functions/generators/world_detail_generator.sh b/site_generator/functions/generators/world_detail_generator.sh index 9489634..b54c8a9 100644 --- a/site_generator/functions/generators/world_detail_generator.sh +++ b/site_generator/functions/generators/world_detail_generator.sh @@ -30,7 +30,6 @@ generate_world_detail_page() { local tiles_subdir_name; tiles_subdir_name=$(get_config_value_from_file "$web_conf_file" "TILES_SUBDIR_NAME" "$DEFAULT_TILES_SUBDIR_NAME") local web_tiles_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${tiles_subdir_name}" - local map_background_color; map_background_color=$(get_config_value_from_file "$web_conf_file" "MM_OPT_BGCOLOR" "$DEFAULT_MM_OPT_BGCOLOR") local WORLD_DISPLAY_NAME_PAGE; WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME") @@ -172,6 +171,7 @@ generate_world_detail_page() { "RESOLUTIONS_JS_ARRAY" "$resolutions_array" \ "web_tiles_rel_path" "$web_tiles_rel_path" \ "web_map_info_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/map_info.txt" \ + "web_areas_json_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/areas.json" \ "map_background_color" "$map_background_color" \ "DEFAULT_PLAYER_SKIN_URL" "$DEFAULT_PLAYER_SKIN_URL" local WORLD_RADAR_HTML; WORLD_RADAR_HTML=$(<"$temp_radar_file") @@ -208,6 +208,7 @@ generate_world_detail_page() { "web_last_update_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt" \ "web_players_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/players.txt" \ "web_weather_txt_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/weather.txt" \ + "web_areas_json_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/areas.json" \ "CACHE_BUSTER" "$CACHE_BUSTER" { diff --git a/site_generator/templates/css.template b/site_generator/templates/css.template index d852780..cc2624e 100755 --- a/site_generator/templates/css.template +++ b/site_generator/templates/css.template @@ -96,7 +96,7 @@ a.world-preview:hover { .online-status-badge { font-size: 0.8em; padding: 3px 8px; border-radius: 10px; color: white !important; margin-left: 10px; } .online-status-badge.online { background-color: #3E8E41; } .online-status-badge.offline { background-color: #C0392B; } -.online-status-badge.unknown { background-color: #808080; } /* HINZUGEFÜGT */ +.online-status-badge.unknown { background-color: #808080; } /* Welt-Detailseite */ .page-nav-buttons { text-align: right; margin-top: 10px; margin-bottom: 15px; } @@ -114,68 +114,17 @@ a.world-preview:hover { .map-sub-info .map-last-update { text-align: right; } .info-box { background-color: #3a3a3a; border: 1px solid #555; padding: 15px; margin-bottom: 20px; border-left: 5px solid #FFD700; border-radius: 3px;} .info-box h3 { margin-top: 0; border-bottom: none; padding-bottom: 0.2em; } - -/* Admin Sektion */ .admin-section h3 { margin-top: 2.5em !important; } -.admin-grid { - display: flex; - flex-wrap: wrap; - gap: 15px; -} -.admin-box { - background-color: #303030; - padding: 15px; - border-radius: 5px; - border: 1px solid #4a4a4a; - flex-grow: 0; - flex-shrink: 1; - flex-basis: calc(50% - 8px); - box-sizing: border-box; - min-width: 280px; -} +.admin-grid { display: flex; flex-wrap: wrap; gap: 15px; } +.admin-box { background-color: #303030; padding: 15px; border-radius: 5px; border: 1px solid #4a4a4a; flex-grow: 0; flex-shrink: 1; flex-basis: calc(50% - 8px); box-sizing: border-box; min-width: 280px; } .admin-contact-block { display: flex; align-items: center; } .admin-skin-icon { width: 48px; height: 48px; margin-right: 15px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;} .admin-text-details p { margin: 0.2em 0; } .admin-player-name { font-weight: bold; color: #FFD700; font-size: 1.1em; } -.contact-links { - margin-top: 10px; - padding-top: 10px; - border-top: 1px solid #555; - display: flex; - gap: 8px; - flex-wrap: wrap; -} -.contact-button { - background-color: #555; - color: #ddd; - border: 1px solid #666; - padding: 3px 8px; - font-size: 0.85em; - border-radius: 3px; - text-decoration: none; - position: relative; -} -.contact-button:hover { - background-color: #666; - color: #FFA500; - text-decoration: none; -} -.contact-links a[data-title]:hover::after { - content: attr(data-title); - position: absolute; - background-color: #222; - color: #fff; - padding: 3px 7px; - border-radius: 3px; - font-size: 0.7rem; - white-space: nowrap; - z-index: 100; - left: 50%; - bottom: 125%; - transform: translateX(-50%); - border: 1px solid #555; -} - +.contact-links { margin-top: 10px; padding-top: 10px; border-top: 1px solid #555; display: flex; gap: 8px; flex-wrap: wrap; } +.contact-button { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; text-decoration: none; position: relative; } +.contact-button:hover { background-color: #666; color: #FFA500; text-decoration: none; } +.contact-links a[data-title]:hover::after { content: attr(data-title); position: absolute; background-color: #222; color: #fff; padding: 3px 7px; border-radius: 3px; font-size: 0.7rem; white-space: nowrap; z-index: 100; left: 50%; bottom: 125%; transform: translateX(-50%); border: 1px solid #555; } .server-details p { margin: 0.5em 0; display: flex; align-items: center;} .server-details strong { min-width: 120px; display: inline-block; flex-shrink: 0;} .status-text-colored.online { color: #7CFC00; font-weight: bold; } .status-text-colored.offline { color: #FF4500; font-weight: bold; } @@ -184,16 +133,7 @@ a.world-preview:hover { .copy-button:hover { background: #666; } .player-list-container { margin-top: 1em; } .player-list-grid { display: flex; flex-wrap: wrap; gap: 15px; box-sizing: border-box; } -.player-box { - background-color: #303030; - border: 1px solid #555; - border-radius: 5px; - padding: 10px; - width: calc((100% - 30px) / 3); - box-sizing: border-box; - display: flex; - flex-direction: column; -} +.player-box { background-color: #303030; border: 1px solid #555; border-radius: 5px; padding: 10px; width: calc((100% - 30px) / 3); box-sizing: border-box; display: flex; flex-direction: column; } .player-box.has-server-priv { border-left: 5px solid #FFD700; } .player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .player-identity { display: flex; align-items: center; } @@ -201,22 +141,12 @@ a.world-preview:hover { .player-icon { width: 40px; height: 40px; margin-right: 10px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;} .player-name-status { display: flex; align-items: center; } .player-name { font-weight: bold; margin-left: 5px; color: #FFD700; } -.privilege-separator { - height: 1px; - background-color: #555; - border: 0; - margin: 8px 0; -} +.privilege-separator { height: 1px; background-color: #555; border: 0; margin: 8px 0; } .player-attributes { font-size: 0.9em; line-height: 1.5; } .player-attributes .icon { margin-right: 8px; cursor: help; font-size: 1.2em; } .player-attributes .icon.privilege { opacity: 0.15; } .player-attributes .icon.privilege.active { opacity: 1; } -.player-attributes .icon[data-title]:hover::after, .status-dot[data-title]:hover::after, .player-vitals .icon[data-title]:hover::after { - content: attr(data-title); position: absolute; background-color: #222; color: #fff; - padding: 3px 7px; border-radius: 3px; font-size: 0.85em; white-space: nowrap; - z-index: 100; margin-left: 5px; transform: translateY(-28px); - border: 1px solid #555; -} +.player-attributes .icon[data-title]:hover::after, .status-dot[data-title]:hover::after, .player-vitals .icon[data-title]:hover::after { content: attr(data-title); position: absolute; background-color: #222; color: #fff; padding: 3px 7px; border-radius: 3px; font-size: 0.85em; white-space: nowrap; z-index: 100; margin-left: 5px; transform: translateY(-28px); border: 1px solid #555; } footer { text-align: center; margin-top: 30px; padding: 20px; background-color: #222; color: #aaa; border-top: 2px solid #4A2E0A;} .scrollable-mod-list { max-height: 20em; overflow-y: auto; border: 1px solid #555; padding: 10px; background-color: #303030; margin-top: 0.5em; border-radius: 3px; } ul.mod-list, ul.mod-list ul { list-style-type: none; padding-left: 20px; } ul.mod-list > li { margin-bottom: 0.5em; } @@ -236,315 +166,50 @@ ul.mod-list > li > ul > li { font-size: 0.9em; } .archive-image-container img { max-width: 100%; border: 1px solid #666; background-color: #222; } .archive-image-container p { color: #aaa; } /* Stile für responsive Navigation & Burger-Menü */ -.responsive-nav { - position: relative; - margin-bottom: 15px; -} -.burger-menu { - display: none; - width: 40px; - height: 40px; - background-color: #555; - border: 1px solid #666; - border-radius: 5px; - cursor: pointer; - flex-direction: column; - justify-content: space-around; - align-items: center; - padding: 8px; - box-sizing: border-box; - position: absolute; - top: 0; - right: 0; - z-index: 101; -} -.burger-menu:hover { - background-color: #666; -} -.burger-menu span { - display: block; - width: 100%; - height: 3px; - background-color: #FFA500; - border-radius: 3px; -} +.responsive-nav { position: relative; margin-bottom: 15px; } +.burger-menu { display: none; width: 40px; height: 40px; background-color: #555; border: 1px solid #666; border-radius: 5px; cursor: pointer; flex-direction: column; justify-content: space-around; align-items: center; padding: 8px; box-sizing: border-box; position: absolute; top: 0; right: 0; z-index: 101; } +.burger-menu:hover { background-color: #666; } +.burger-menu span { display: block; width: 100%; height: 3px; background-color: #FFA500; border-radius: 3px; } /* Stile für Live/Archiv Umschalter */ -.section-header-flex { - display: flex; - justify-content: space-between; - align-items: center; -} -.map-toggle-controls { - display: flex; - gap: 5px; -} -.map-toggle-button { - background-color: #555; - color: #ddd; - border: 1px solid #666; - padding: 3px 8px; - font-size: 0.85em; - border-radius: 3px; - cursor: pointer; -} -.map-toggle-button:hover { - background-color: #666; -} -.map-toggle-button.active { - color: #FFA500; - border-color: #FFA500; - font-weight: bold; -} +.section-header-flex { display: flex; justify-content: space-between; align-items: center; } +.map-toggle-controls { display: flex; gap: 5px; } +.map-toggle-button { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; } +.map-toggle-button:hover { background-color: #666; } +.map-toggle-button.active { color: #FFA500; border-color: #FFA500; font-weight: bold; } /* Spielerliste Filter */ -.filter-container { - position: relative; -} -.filter-dropdown-btn { - background-color: #555; - color: #ddd; - border: 1px solid #666; - padding: 3px 8px; - font-size: 0.85em; - border-radius: 3px; - cursor: pointer; -} -.filter-dropdown-btn:hover { - background-color: #666; -} -.filter-dropdown { - display: none; - position: absolute; - top: 100%; - right: 0; - background-color: #3a3a3a; - border: 1px solid #666; - border-radius: 5px; - padding: 10px; - z-index: 100; - min-width: 220px; - box-shadow: 0 4px 8px rgba(0,0,0,0.5); -} -.filter-dropdown.show { - display: block; -} -.filter-option { - display: flex; - align-items: center; - margin-bottom: 8px; - cursor: pointer; - font-size: 0.9em; - font-weight: normal; - color: #ddd; -} -.filter-option:last-child { - margin-bottom: 0; -} -.filter-option input[type="checkbox"] { - margin-right: 8px; -} -.filter-option .icon { - font-size: 1.2em; - width: 20px; - text-align: center; - margin-right: 8px; -} -.filter-option .status-dot { - margin-left: 0; - margin-right: 8px; -} +.filter-container { position: relative; } +.filter-dropdown-btn { background-color: #555; color: #ddd; border: 1px solid #666; padding: 3px 8px; font-size: 0.85em; border-radius: 3px; cursor: pointer; } +.filter-dropdown-btn:hover { background-color: #666; } +.filter-dropdown { display: none; position: absolute; top: 100%; right: 0; background-color: #3a3a3a; border: 1px solid #666; border-radius: 5px; padding: 10px; z-index: 100; min-width: 220px; box-shadow: 0 4px 8px rgba(0,0,0,0.5); } +.filter-dropdown.show { display: block; } +.filter-option { display: flex; align-items: center; margin-bottom: 8px; cursor: pointer; font-size: 0.9em; font-weight: normal; color: #ddd; } +.filter-option:last-child { margin-bottom: 0; } +.filter-option input[type="checkbox"] { margin-right: 8px; } +.filter-option .icon { font-size: 1.2em; width: 20px; text-align: center; margin-right: 8px; } +.filter-option .status-dot { margin-left: 0; margin-right: 8px; } /* Stile für "Weiterlesen"-Funktion */ -.collapsible-text { - max-height: 250px; - overflow: hidden; - position: relative; - transition: max-height 0.5s ease-in-out; -} -.collapsible-text.expanded { - max-height: 10000px; -} -.collapsible-text::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 80px; - background: linear-gradient(to bottom, rgba(68, 68, 68, 0), rgba(68, 68, 68, 1) 100%); - pointer-events: none; - transition: opacity 0.3s; -} -.collapsible-text.expanded::after { - opacity: 0; -} -.read-more-container { - text-align: right; - margin-top: 5px; -} -a.read-more-link { - font-size: 0.9em; - font-weight: bold; -} +.collapsible-text { max-height: 250px; overflow: hidden; position: relative; transition: max-height 0.5s ease-in-out; } +.collapsible-text.expanded { max-height: 10000px; } +.collapsible-text::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to bottom, rgba(68, 68, 68, 0), rgba(68, 68, 68, 1) 100%); pointer-events: none; transition: opacity 0.3s; } +.collapsible-text.expanded::after { opacity: 0; } +.read-more-container { text-align: right; margin-top: 5px; } +a.read-more-link { font-size: 0.9em; font-weight: bold; } /* Schließen-Button (X) */ -.close-button-container { - position: absolute; - top: 15px; - right: 15px; - z-index: 10; -} -.close-button { - display: inline-flex; - justify-content: center; - align-items: center; - width: 30px; - height: 30px; - background-color: #555; - color: #ddd; - border: 1px solid #666; - border-radius: 50%; - text-decoration: none; - font-size: 1em; - line-height: 1; -} -.close-button:hover { - background-color: #C0392B; - color: white; - text-decoration: none; -} +.close-button-container { position: absolute; top: 15px; right: 15px; z-index: 10; } +.close-button { display: inline-flex; justify-content: center; align-items: center; width: 30px; height: 30px; background-color: #555; color: #ddd; border: 1px solid #666; border-radius: 50%; text-decoration: none; font-size: 1em; line-height: 1; } +.close-button:hover { background-color: #C0392B; color: white; text-decoration: none; } /* Stile für "Nach oben"-Button */ -.scroll-to-top-btn { - display: none; - position: fixed; - bottom: 20px; - right: 20px; - z-index: 999; - background-color: #555; - color: #FFA500; - border: 1px solid #666; - border-radius: 50%; - width: 40px; - height: 40px; - text-align: center; - font-size: 24px; - line-height: 38px; - cursor: pointer; - transition: background-color 0.3s, opacity 0.3s; -} -.scroll-to-top-btn:hover { - background-color: #666; - text-decoration: none; -} -.scroll-to-top-btn.show { - display: block; -} - -@media (max-width: 768px) { - /* Responsive Stile für Burger-Menü */ - .responsive-nav { - height: 50px; - } - .burger-menu { - display: flex; - } - .page-nav-buttons { - display: none; - position: absolute; - top: 45px; - right: 0; - width: 200px; - background-color: #3a3a3a; - border: 1px solid #666; - border-radius: 5px; - z-index: 100; - padding: 5px; - box-shadow: 0 4px 8px rgba(0,0,0,0.5); - } - .page-nav-buttons.menu-open { - display: flex; - flex-direction: column; - align-items: stretch; - } - .page-nav-buttons .button { - text-align: left; - margin: 5px; - width: auto; - } - .player-box { width: calc((100% - 15px) / 2); } - .page-nav-buttons .button { margin-top: 5px;} - - .world-preview .world-preview-content { - flex-direction: column; - } - .world-preview img { - width: 100%; - max-width: 100%; - height: auto; - margin-right: 0; - margin-bottom: 15px; - } - .map-view-container { - height: auto; - aspect-ratio: 4 / 3; - max-height: 70vh; - } -} -@media (max-width: 480px) { - .player-box { width: 100%; } - .admin-box { flex-basis: 100%; } -} - +.scroll-to-top-btn { display: none; position: fixed; bottom: 20px; right: 20px; z-index: 999; background-color: #555; color: #FFA500; border: 1px solid #666; border-radius: 50%; width: 40px; height: 40px; text-align: center; font-size: 24px; line-height: 38px; cursor: pointer; transition: background-color 0.3s, opacity 0.3s; } +.scroll-to-top-btn:hover { background-color: #666; text-decoration: none; } +.scroll-to-top-btn.show { display: block; } /* Stile für OpenLayers Popups (Leaflet-Look-and-Feel) */ -.ol-popup { - position: absolute; - background-color: #303030; - box-shadow: 0 1px 4px rgba(0,0,0,0.4); - padding: 1px; - border-radius: 5px; - bottom: 12px; - left: -50px; - min-width: 280px; - border: 1px solid #555; - color: #ddd; -} -.ol-popup:after, .ol-popup:before { - top: 100%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; -} -.ol-popup:after { - border-top-color: #303030; - border-width: 10px; - left: 48px; - margin-left: -10px; -} -.ol-popup:before { - border-top-color: #555; - border-width: 11px; - left: 48px; - margin-left: -11px; -} -.ol-popup-closer { - text-decoration: none; - position: absolute; - top: 2px; - right: 8px; - color: #ddd; - font-size: 1.5em; - font-weight: bold; -} -.ol-popup-closer:hover { - color: #fff; -} -.ol-popup-content { - font-size: 13px; - line-height: 1.5; - padding: 10px; - margin: 0; -} -/* Stile für den Inhalt innerhalb des Popups */ +.ol-popup { position: absolute; background-color: #303030; box-shadow: 0 1px 4px rgba(0,0,0,0.4); padding: 1px; border-radius: 5px; bottom: 12px; left: -50px; min-width: 280px; border: 1px solid #555; color: #ddd; } +.ol-popup:after, .ol-popup:before { top: 100%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } +.ol-popup:after { border-top-color: #303030; border-width: 10px; left: 48px; margin-left: -10px; } +.ol-popup:before { border-top-color: #555; border-width: 11px; left: 48px; margin-left: -11px; } +.ol-popup-closer { text-decoration: none; position: absolute; top: 2px; right: 8px; color: #ddd; font-size: 1.5em; font-weight: bold; } +.ol-popup-closer:hover { color: #fff; } +.ol-popup-content { font-size: 13px; line-height: 1.5; padding: 10px; margin: 0; } .popup-player-box .player-header { display: flex; align-items: center; margin-bottom: 8px; } .popup-player-box .player-identity { display: flex; align-items: center; } .popup-player-box .player-icon { width: 40px; height: 40px; margin-right: 10px; border-radius: 3px; flex-shrink: 0;} @@ -554,3 +219,68 @@ a.read-more-link { .popup-player-box .popup-player-vitals { display: flex; justify-content: space-around; gap: 10px; align-items: center; margin-top: 8px; } .popup-player-box .popup-player-vitals .vital { display: flex; align-items: center; gap: 5px; } .popup-player-box .popup-player-vitals .icon { font-size: 1.2em; } + +@media (max-width: 768px) { + .responsive-nav { height: 50px; } + .burger-menu { display: flex; } + .page-nav-buttons { display: none; position: absolute; top: 45px; right: 0; width: 200px; background-color: #3a3a3a; border: 1px solid #666; border-radius: 5px; z-index: 100; padding: 5px; box-shadow: 0 4px 8px rgba(0,0,0,0.5); } + .page-nav-buttons.menu-open { display: flex; flex-direction: column; align-items: stretch; } + .page-nav-buttons .button { text-align: left; margin: 5px; width: auto; } + .player-box { width: calc((100% - 15px) / 2); } + .world-preview .world-preview-content { flex-direction: column; } + .world-preview img { width: 100%; max-width: 100%; height: auto; margin-right: 0; margin-bottom: 15px; } + .map-view-container { height: auto; aspect-ratio: 4 / 3; max-height: 70vh; } +} +@media (max-width: 480px) { + .player-box { width: 100%; } + .admin-box { flex-basis: 100%; } +} + +/* KORREKTUR: Stile für OpenLayers Layer Switcher */ + +.layer-switcher.shown { + background-color: #303030 !important; +} + +.layer-switcher button { + font-size: 1.2em !important; +} + +.layer-switcher button:hover, .layer-switcher button:focus { + background-color: #666 !important; +} + +.layer-switcher .panel { + background-color: #303030 !important; + border: 1px solid #555 !important; + color: #ddd !important; + margin: 0; +} + +.layer-switcher ul { + padding-left: 1em; +} + +.layer-switcher label { + color: #ddd !important; +} + +/* NEU: Styling für Checkboxen im Layer-Switcher */ +.layer-switcher input[type=checkbox], +.layer-switcher input[type=radio] { + accent-color: #FFA500; + vertical-align: middle; + margin-right: 5px; +} + +/* NEU: Vergrößerung der Standard-Karten-Buttons */ +.ol-zoom .ol-zoom-in, +.ol-zoom .ol-zoom-out, +.ol-rotate .ol-rotate-reset, +.ol-full-screen button { + width: 25px !important; + height: 25px !important; + font-size: 1.5em; + line-height: 1; + padding: 0; +} diff --git a/site_generator/templates/html_header.template b/site_generator/templates/html_header.template index 458fad9..25888a5 100755 --- a/site_generator/templates/html_header.template +++ b/site_generator/templates/html_header.template @@ -9,6 +9,9 @@ + + + diff --git a/sync_areas.sh b/sync_areas.sh new file mode 100755 index 0000000..b80451b --- /dev/null +++ b/sync_areas.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# Lade globale Konfiguration +CONFIG_FILE_PATH="$(dirname "$0")/config.sh" +if [ -f "$CONFIG_FILE_PATH" ]; then + source "$CONFIG_FILE_PATH" +else + echo "FEHLER: Globale config.sh nicht unter ${CONFIG_FILE_PATH} gefunden!" + exit 1 +fi + +# === Logging Funktion (früh definieren für Wrapper-Logik) === +LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)" +log_message() { + local key="${1:-main}" + local msg="$2" + local log_target="${LOG_FILE_BASE}.log" + [ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log" + + local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}" + echo "${message_to_log}" | tee -a "$log_target" +} + +# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird --- +if [ -z "$1" ]; then + log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..." + shopt -s nullglob + for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do + if [ -f "${world_dir}web.conf" ]; then + world_key_to_process=$(basename "$world_dir") + log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---" + # Rufe das Skript für die gefundene Welt rekursiv auf + bash "$0" "$world_key_to_process" + fi + done + shopt -u nullglob + log_message "main" "Alle Welten verarbeitet." + exit 0 +fi + +# ############################################################################# +# Ab hier beginnt die Logik für eine EINZELNE Welt +# ############################################################################# + +# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert +/opt/luweb/check_dependencies.sh || exit 1 + +WORLD_KEY=$1 +CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" + +# === Abgeleitete Variablen === +SCRIPT_BASENAME=$(basename "$0" .sh) +LOG_FILE="${LOG_DIR_BASE}/${SCRIPT_BASENAME}_${WORLD_KEY}.log" +LOCK_FILE="${LOCK_FILE_BASE_DIR}/${SCRIPT_BASENAME}_${WORLD_KEY}.lock" + +# Pfade zu den Quell- und Zieldateien +AREAS_DAT_SOURCE_PATH="${CURRENT_MINETEST_WORLD_DATA_PATH}areas.dat" +AREAS_JSON_TARGET_DIR="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${WORLD_KEY}" +AREAS_JSON_FILE_PATH="${AREAS_JSON_TARGET_DIR}/areas.json" +AREAS_JSON_TMP_FILE_PATH="${AREAS_JSON_FILE_PATH}.tmp" + +# === Hauptlogik für eine einzelne Welt === +exec 200>"$LOCK_FILE" +flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } +trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT +log_message "${WORLD_KEY}" "Script gestartet." + +# Prüfen, ob die Quelldatei existiert +if [ ! -f "$AREAS_DAT_SOURCE_PATH" ]; then + log_message "${WORLD_KEY}" "INFO: Quelldatei ${AREAS_DAT_SOURCE_PATH} nicht gefunden. Erstelle leere areas.json." + mkdir -p "$AREAS_JSON_TARGET_DIR" + echo "{}" > "$AREAS_JSON_FILE_PATH" + exit 0 +fi + +log_message "${WORLD_KEY}" "Lese ${AREAS_DAT_SOURCE_PATH} und transformiere zu hierarchischem JSON..." + +jq ' + (map(select(.id != null)) | map(.sub_areas = []) | INDEX(.id | tostring)) as $parents + | (map(select(.parent != null)) | group_by(.parent)) as $children_grouped + | reduce $children_grouped[] as $group ( + $parents; + .[($group[0].parent|tostring)].sub_areas = $group + ) +' "$AREAS_DAT_SOURCE_PATH" > "$AREAS_JSON_TMP_FILE_PATH" + +if [ $? -ne 0 ]; then + log_message "${WORLD_KEY}" "FEHLER: jq-Transformation fehlgeschlagen. Prüfen Sie die JSON-Struktur in areas.dat." + rm -f "$AREAS_JSON_TMP_FILE_PATH" + exit 1 +fi + +# Temporäre Datei an den Zielort verschieben +mkdir -p "$AREAS_JSON_TARGET_DIR" +mv "$AREAS_JSON_TMP_FILE_PATH" "$AREAS_JSON_FILE_PATH" + +log_message "${WORLD_KEY}" "areas.json erfolgreich synchronisiert nach ${AREAS_JSON_FILE_PATH}" + +exit 0 diff --git a/sync_players.sh b/sync_players.sh index 47da8e2..7b13467 100755 --- a/sync_players.sh +++ b/sync_players.sh @@ -9,11 +9,43 @@ else exit 1 fi +# === Logging Funktion (früh definieren für Wrapper-Logik) === +LOG_FILE_BASE="${LOG_DIR_BASE}/$(basename "$0" .sh)" +log_message() { + local key="${1:-main}" + local msg="$2" + local log_target="${LOG_FILE_BASE}.log" + [ "$key" != "main" ] && log_target="${LOG_FILE_BASE}_${key}.log" + + local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${key}] - ${msg}" + echo "${message_to_log}" | tee -a "$log_target" +} + +# --- Wrapper-Logik zur Verarbeitung aller Welten, wenn kein Argument übergeben wird --- +if [ -z "$1" ]; then + log_message "main" "Kein spezifischer Welt-Schlüssel angegeben. Verarbeite alle Welten mit web.conf..." + shopt -s nullglob + for world_dir in "${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/; do + if [ -f "${world_dir}web.conf" ]; then + world_key_to_process=$(basename "$world_dir") + log_message "main" "--- Starte Durchlauf für '${world_key_to_process}' ---" + # Rufe das Skript für die gefundene Welt rekursiv auf + bash "$0" "$world_key_to_process" + fi + done + shopt -u nullglob + log_message "main" "Alle Welten verarbeitet." + exit 0 +fi + +# ############################################################################# +# Ab hier beginnt die Logik für eine EINZELNE Welt +# ############################################################################# + # Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert /opt/luweb/check_dependencies.sh || exit 1 -# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert -WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" +WORLD_KEY=$1 CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" # === Abgeleitete Variablen === @@ -31,44 +63,31 @@ PLAYERS_JSON_TMP_FILE_PATH="${PLAYERS_JSON_FILE_PATH}.tmp" # SQLite-Befehl SQLITE_CMD="sqlite3 -readonly" -# === Logging Funktion === -log_message() { - local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - $1" - echo "${message_to_log}" | tee -a "$LOG_FILE" -} - -# === Hauptlogik === +# === Hauptlogik für eine einzelne Welt === exec 200>"$LOCK_FILE" -flock -n 200 || { log_message "Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } -trap 'rm -f "$LOCK_FILE"; log_message "Skript beendet."' EXIT +flock -n 200 || { log_message "${WORLD_KEY}" "Script ${SCRIPT_BASENAME}.sh ist bereits für diese Welt aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; } +trap 'rm -f "$LOCK_FILE"; log_message "${WORLD_KEY}" "Skript beendet."' EXIT +log_message "${WORLD_KEY}" "Script gestartet." -mkdir -p "$LOG_DIR_BASE" -log_message "Script gestartet für Welt: ${WORLD_KEY}" - -# Prüfen, ob die Datenbanken existieren if [ ! -f "$PLAYERS_DB_PATH" ] || [ ! -f "$AUTH_DB_PATH" ]; then - log_message "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:" - log_message "-> ${PLAYERS_DB_PATH}" - log_message "-> ${AUTH_DB_PATH}" + log_message "${WORLD_KEY}" "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:" + log_message "${WORLD_KEY}" "-> ${PLAYERS_DB_PATH}" + log_message "${WORLD_KEY}" "-> ${AUTH_DB_PATH}" exit 1 fi -# Beginne mit dem Schreiben der temporären JSON-Datei echo "{" > "$PLAYERS_JSON_TMP_FILE_PATH" - first_entry=true -# Lese alle Spieler aus der auth-Datenbank (ID, Name, Letzter Login) player_list_query="SELECT id, name, last_login FROM auth;" $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_id name last_login; do - log_message "Verarbeite Spieler: ${name} (ID: ${player_id})" - + log_message "${WORLD_KEY}" "Verarbeite Spieler: ${name} (ID: ${player_id})" player_data_query="SELECT posX, posY, posZ, hp, breath, pitch, yaw, creation_date FROM player WHERE name = '${name}';" player_data=$($SQLITE_CMD "$PLAYERS_DB_PATH" "$player_data_query") if [ -z "$player_data" ]; then - log_message "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." + log_message "${WORLD_KEY}" "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." continue fi @@ -87,14 +106,12 @@ $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_ privs_query="SELECT privilege FROM user_privileges WHERE id = ${player_id};" privs_string=$($SQLITE_CMD "$AUTH_DB_PATH" "$privs_query" | tr '\n' ',' | sed 's/,$//') - # Komma vor dem Eintrag hinzufügen, außer beim ersten if [ "$first_entry" = true ]; then first_entry=false else echo "," >> "$PLAYERS_JSON_TMP_FILE_PATH" fi - # JSON-Objekt für den Spieler erstellen und in die Datei schreiben printf ' "%s": {\n' "$player_id" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "name": "%s",\n' "$name" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "pitch": %d,\n' "$pitch_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH" @@ -107,21 +124,17 @@ $SQLITE_CMD "$AUTH_DB_PATH" "$player_list_query" | while IFS='|' read -r player_ printf ' "stamina": %d,\n' "$stamina_rounded" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "creation_date": "%s",\n' "$(echo "$creation_date" | sed 's/ /T/')Z" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' "last_login": %s,\n' "$last_login" >> "$PLAYERS_JSON_TMP_FILE_PATH" - # KORREKTUR: Kein Komma nach dem letzten Element printf ' "privilege": "%s"\n' "$privs_string" >> "$PLAYERS_JSON_TMP_FILE_PATH" printf ' }' >> "$PLAYERS_JSON_TMP_FILE_PATH" done -# JSON-Datei abschließen -# KORREKTUR: Die überflüssige schließende Klammer wird hier entfernt echo "" >> "$PLAYERS_JSON_TMP_FILE_PATH" echo "}" >> "$PLAYERS_JSON_TMP_FILE_PATH" -# Temporäre Datei an den Zielort verschieben mkdir -p "$PLAYERS_JSON_TARGET_DIR" mv "$PLAYERS_JSON_TMP_FILE_PATH" "$PLAYERS_JSON_FILE_PATH" -log_message "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" +log_message "${WORLD_KEY}" "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" exit 0