diff --git a/README.md b/README.md index 41e4d39..e2c73a4 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,10 @@ 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 & 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). +* **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map, including custom-styled popups and permanent name labels. * **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.sh` and a `web.conf` file for every one of your worlds. +* **Flexible Configuration:** Configuration is easy with a central global config and a `web.conf`-file for every of your worlds. * **Automation-Ready:** Designed for unattended execution via scheduling tools like `cron`. ## 🔧 Prerequisites @@ -22,18 +21,16 @@ 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. +* **vips:** A high-performance image processing library that replaces `convert` (ImageMagick). * *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 to query the game databases (`players.sqlite`, `auth.sqlite`). +* **SQLite3:** The command-line tool for reading SQLite databases, required by `sync_players.sh`. * *Debian/Ubuntu Install:* `sudo apt-get install sqlite3` -* **bc:** The "basic calculator" command-line tool, required for calculations in scripts. +* **bc:** The "basic calculator" command-line tool, required for calculations in `sync_players.sh`. * *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. @@ -50,24 +47,34 @@ OR Clone the Git repository to a base directory. --```bash -git clone [https://git.geigernet.eu/rainer/luanti-web.git](https://git.geigernet.eu/rainer/luanti-web.git) /opt/luweb +```bash +git clone 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 sync_areas.sh --``` +chmod +x generate_map.sh generate_site.sh check_server_status.sh check_dependencies.sh sync_players.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 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 (e.g., `/worlds/my_world/web.conf`) and adjust the values. ## 📂 Directory Structure --```md +The system now uses a modular structure to improve maintainability: +```md /opt/luweb/ ├── config.sh ├── generate_map.sh @@ -75,43 +82,88 @@ The system is designed so that **only worlds with a `web.conf` file** will be di ├── 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 & Automation (Cronjob) +## 🚀 Usage -The scripts are designed for automated execution. Set them up using `crontab -e`. +### 1. Map Generation +```bash +./generate_map.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 +### 2. Website Generation +```bash +./generate_site.sh +``` -# (Hourly) Generate the base map and tiles -0 * * * * cd /opt/luweb && ./generate_map.sh >> /var/log/luweb/cron.log 2>&1 +### 3. Status & Player Sync +```bash +./check_server_status.sh +./sync_players.sh +``` -# (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 --``` +### 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 +``` ## 📄 License **MIT License** Copyright (c) 2025 Rage87 - rage87@geigernet.eu -(License text as before) +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: -## 👤 Authors +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 * **Rage87** (Main-Developer) diff --git a/check_dependencies.sh b/check_dependencies.sh index 61154d0..b126722 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 -# HINZUGEFÜGT: sqlite3 und jq -declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss" "bc" "sqlite3" "jq") +# KORREKTUR: 'convert' und 'identify' entfernt, 'vips' hinzugefügt +declare -a runtime_deps_in_path=("gdal2tiles.py" "vips" "ss") 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,9 +79,6 @@ 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 16fbe68..fc25ca0 100644 --- a/colors.txt +++ b/colors.txt @@ -1402,44 +1402,3 @@ 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 fcc4775..0708dd1 100755 --- a/generate_map.sh +++ b/generate_map.sh @@ -1,62 +1,30 @@ #!/bin/bash # Lade globale Konfiguration -CONFIG_FILE_PATH="$(dirname "$0")/config.sh" -if [ -f "$CONFIG_FILE_PATH" ]; then - source "$CONFIG_FILE_PATH" +GLOBAL_CONFIG_FILE="$(dirname "$0")/config.sh" +if [ -f "$GLOBAL_CONFIG_FILE" ]; then + source "$GLOBAL_CONFIG_FILE" 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 -WORLD_KEY=$1 +# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert +WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" # 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 - log_message "${WORLD_KEY}" "FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!" + echo "$(date '+%Y-%m-%d %H:%M:%S') - [${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 - log_message "${WORLD_KEY}" "FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!" + 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!" exit 1 fi @@ -64,16 +32,18 @@ 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="false" -MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="false" +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" 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 - log_message "${WORLD_KEY}" "Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}" + echo "$(date '+%Y-%m-%d %H:%M:%S') - [${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 === @@ -92,107 +62,176 @@ 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 "${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 + 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 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 "${WORLD_KEY}" "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage." + log_message "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=$((images_processed + 1)) 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 "${WORLD_KEY}" "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 "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 "${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 + 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 done - 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 + 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." } -# === Hauptlogik für eine einzelne Welt === +# === Hauptlogik === 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}" "Skript gestartet." +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}" 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 -log_message "${WORLD_KEY}" "Starte minetestmapper zur Kartengenerierung..." +if [ ! -x "$MINETESTMAPPER_PATH" ]; then log_message "FEHLER: minetestmapper (${MINETESTMAPPER_PATH}) nicht ausführbar."; exit 1; fi 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}' --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} --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} --min-y ${MM_OPT_MIN_Y}" -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." +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}." -# 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 "${WORLD_KEY}" "map_info.txt erstellt: Dim=${MAP_DIMENSIONS}, Extent=${MAP_EXTENT}" +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 - log_message "${WORLD_KEY}" "FEHLER: map_info.txt konnte nicht erstellt werden." + MAP_DIMENSIONS=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null) fi -# 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}" +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 -rm -f "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}.new" "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" -# 4. Erzeuge Web-Vorschaukarte mit vips -log_message "${WORLD_KEY}" "Erzeuge Web-Version von map.png (max ${RESIZED_MAX_DIMENSION}px) mit 'vips'..." +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}" +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." +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" + +# === 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}..." mkdir -p "$(dirname "$WEB_MAP_PNG_FULL_PATH")" -(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 +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 -# 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." +# === 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 -# 6. Archiv-Management +# === Archivbereinigung === 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 "${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 + 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." fi -# 7. Finale Info-Dateien kopieren -log_message "${WORLD_KEY}" "Kopiere finale Info-Dateien ins Web-Verzeichnis..." +# === Status- und Info-Dateien im Webverzeichnis === +log_message "Erstelle Status- und Info-Dateien im Webverzeichnis ${WEB_CURRENT_WORLD_DIR}..." mkdir -p "$WEB_CURRENT_WORLD_DIR" -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" +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 exit 0 diff --git a/site_generator/examples/web.conf.template b/site_generator/examples/web.conf.template index 9cdb9ae..d35c7f9 100755 --- a/site_generator/examples/web.conf.template +++ b/site_generator/examples/web.conf.template @@ -59,6 +59,15 @@ 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 b54c8a9..9489634 100644 --- a/site_generator/functions/generators/world_detail_generator.sh +++ b/site_generator/functions/generators/world_detail_generator.sh @@ -30,6 +30,7 @@ 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") @@ -171,7 +172,6 @@ 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,7 +208,6 @@ 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 cc2624e..d852780 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; } +.online-status-badge.unknown { background-color: #808080; } /* HINZUGEFÜGT */ /* Welt-Detailseite */ .page-nav-buttons { text-align: right; margin-top: 10px; margin-bottom: 15px; } @@ -114,17 +114,68 @@ 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; } @@ -133,7 +184,16 @@ 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; } @@ -141,12 +201,22 @@ 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; } @@ -166,50 +236,315 @@ 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; } +.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%; } +} + /* 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; } +.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 */ .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;} @@ -219,68 +554,3 @@ a.read-more-link { font-size: 0.9em; font-weight: bold; } .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 25888a5..458fad9 100755 --- a/site_generator/templates/html_header.template +++ b/site_generator/templates/html_header.template @@ -9,9 +9,6 @@ - - - diff --git a/sync_areas.sh b/sync_areas.sh deleted file mode 100755 index b80451b..0000000 --- a/sync_areas.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/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 7b13467..47da8e2 100755 --- a/sync_players.sh +++ b/sync_players.sh @@ -9,43 +9,11 @@ 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 -WORLD_KEY=$1 +# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert +WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" # === Abgeleitete Variablen === @@ -63,31 +31,44 @@ PLAYERS_JSON_TMP_FILE_PATH="${PLAYERS_JSON_FILE_PATH}.tmp" # SQLite-Befehl SQLITE_CMD="sqlite3 -readonly" -# === 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." +# === 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 === +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 + +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 "${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}" + log_message "FEHLER: players.sqlite oder auth.sqlite nicht gefunden. Pfade prüfen:" + log_message "-> ${PLAYERS_DB_PATH}" + log_message "-> ${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 "${WORLD_KEY}" "Verarbeite Spieler: ${name} (ID: ${player_id})" + log_message "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 "${WORLD_KEY}" "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." + log_message "WARNUNG: Spieler '${name}' hat keine Positionsdaten in players.sqlite. Wird übersprungen." continue fi @@ -106,12 +87,14 @@ $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" @@ -124,17 +107,21 @@ $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 "${WORLD_KEY}" "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" +log_message "players.txt erfolgreich synchronisiert nach ${PLAYERS_JSON_FILE_PATH}" exit 0