From a225feef98a365b2cd94e2c9e5fd762f7b590076 Mon Sep 17 00:00:00 2001 From: rainer Date: Mon, 23 Jun 2025 01:41:17 +0200 Subject: [PATCH] feat(map): Implement interactive area overlays This major update introduces a complete system for visualizing protected areas on the OpenLayers map. It adds a new sync script to process area data and heavily modifies the frontend to support interactive, multi-layer display with custom popups. Additionally, data-sync scripts were refactored to run globally on all configured worlds, simplifying cron automation. --- README.md | 116 ++-- check_dependencies.sh | 7 +- colors.txt | 41 ++ generate_map.sh | 247 ++++----- site_generator/examples/web.conf.template | 9 - .../generators/world_detail_generator.sh | 3 +- site_generator/templates/css.template | 496 ++++-------------- site_generator/templates/html_header.template | 3 + .../templates/world_detail_radar.template | 159 ++++-- sync_areas.sh | 99 ++++ sync_players.sh | 75 +-- 11 files changed, 568 insertions(+), 687 deletions(-) create mode 100755 sync_areas.sh diff --git a/README.md b/README.md index e2c73a4..f1e0551 100644 --- a/README.md +++ b/README.md @@ -7,51 +7,40 @@ This system is designed from the ground up to be modular, easily configurable, a ## ✨ Features * **Automated Map Generation:** Leverages `minetestmapper` to create high-resolution PNG maps of your game worlds. -* **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. -* **Map Archive:** Automatically saves a daily snapshot of the map and makes it available through a toggle on the world detail page. +* **Dynamic Map Viewer:** Implements an interactive map viewer using [Leaflet.js](https://leafletjs.com/), powered by the generated map tiles. +* **Live Player Tracking:** Dynamically fetches and displays player locations as markers on the live map. +* **Map Archive:** Automatically saves a daily snapshot of the map and makes it available through a dropdown viewer 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. -* **Automation-Ready:** Designed for unattended execution via scheduling tools like `cron`. +* **Flexible Configuration:** Configuration is easy with a central global config and a sub-config file for every of your worlds. +* **Automation-Ready:** Designed for unattended execution via scheduling tools like cron. ## 🔧 Prerequisites 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). - * *Debian/Ubuntu Install:* `sudo apt-get install libvips-tools` -* **ImageMagick:** Currently still required for the `identify` command (to read image dimensions). +* **ImageMagick:** Required for `identify` (to read image dimensions) and `convert` (to resize images). * *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`. - * *Debian/Ubuntu Install:* `sudo apt-get install sqlite3` -* **bc:** The "basic calculator" command-line tool, required for calculations in `sync_players.sh`. - * *Debian/Ubuntu Install:* `sudo apt-get install bc` -* **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). +* **minetestmapper:** The executable used to render maps from world data. This must be placed within the project directory. * **Web Server:** A web server like Nginx or Apache is needed to serve the generated static files. -An included script (`check_dependencies.sh`) can automatically verify all important dependencies. - ## ⚙️ Installation & Setup -### 1. Project Files +### 1. Clone the Repository -Download the **latest build** from the [Releases-Page](https://git.geigernet.eu/rainer/luanti-web/releases) and extract it to a base directory on your server, such as `/opt/luweb/`. +Download the **latest build** from the [Releases-Page](https://git.geigernet.eu/rainer/luanti-web/releases) and save it to your server's base directory, such as `/opt/luweb/`. OR -Clone the Git repository to a base directory. +Clone all project files to a base directory on your server. -```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 +chmod +x generate_map.sh generate_site.sh ``` ### 2. Global Configuration @@ -61,47 +50,53 @@ The main configuration file is `config.sh`. You must edit this file to match you **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. +* `MINETESTMAPPER_WORLD_DATA_BASE_PATH`: The path to your Minetest/Luanti worlds' data directory (e.g., `/worlds/`), docker compatible. * `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. +For each world you want to feature on the website, a `web.conf` file must exist within that world's data directory (e.g., `/worlds/my_world/web.conf`). This file allows you to override global defaults with world-specific settings. -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. +A minimal `web.conf` could look like this and is automatically created for every detected world in your `MINETESTMAPPER_WORLD_DATA_BASE_PATH`: + +```bash +# Display name for the world +WORLD_DISPLAY_NAME="My Creative World" + +# Server connection details +SERVER_ADDRESS="your-server.com" +SERVER_PORT="30001" + +# A short description for the world overview page +WORLD_SHORT_DESCRIPTION="A brief, catchy description of this world." + +# A detailed HTML description for the world's detail page +WORLD_LONG_DESCRIPTION="

A longer description with HTML support.

" +``` ## 📂 Directory Structure -The system now uses a modular structure to improve maintainability: +The system expects the following directory structure: ```md /opt/luweb/ ├── config.sh ├── generate_map.sh ├── generate_site.sh -├── check_server_status.sh -├── check_dependencies.sh -├── sync_players.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 +│ │ ├── world_detail_archive.template │ │ └── ... │ └── examples/ │ └── web.conf.template ├── web_content/ │ ├── images/ +│ │ └── players/ │ └── static/ +│ ├── startseite_content.html +│ └── ... └── worldmaps_output/ └── / ├── map.png @@ -110,37 +105,38 @@ The system now uses a modular structure to improve maintainability: ## 🚀 Usage -### 1. Map Generation +The scripts are designed to be run from the command line, either manually or via automated jobs. + +### 1. Map and Tile Generation + +The `generate_map.sh` script creates the map, tiles, and archive images for a specific world. It must be called with the "world key" (the name of the world's directory) as an argument. + ```bash -./generate_map.sh +# Generate map assets for the world located in the 'world' directory +./generate_map.sh world ``` ### 2. Website Generation + +The `generate_site.sh` script builds the entire website (overview, detail pages, etc.). It scans all configured world directories and creates a detail page for each one. + ```bash +# Generate the complete website ./generate_site.sh ``` -### 3. Status & Player Sync -```bash -./check_server_status.sh -./sync_players.sh -``` +### 3. Automation (Cronjob) -### 4. Automation (Cronjob) +For fully automatic operation, setting up cronjobs is recommended. **Example for `crontab -e`:** + ```bash -# Update player data every minute -* * * * * /opt/luweb/sync_players.sh world >> /var/log/luweb/cron.log 2>&1 +# Generate map assets for the 'world' every 30 minutes +*/30 * * * * /opt/luweb/generate_map.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 +# Re-build the website every 12 hours +* */12 * * * /opt/luweb/generate_site.sh >> /var/log/luweb/cron.log 2>&1 ``` ## 📄 License @@ -165,5 +161,5 @@ 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) +## 👤 Authors +* **Rage87** (Main-Developer) \ No newline at end of file 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