second alpha (v0.2)

This commit is contained in:
Rainer 2025-06-21 15:58:52 +02:00
parent 7a343254e2
commit 7a1094c733
22 changed files with 2343 additions and 1511 deletions

90
check_dependencies.sh Executable file
View file

@ -0,0 +1,90 @@
#!/bin/bash
# check_dependencies.sh - Prüft, ob alle benötigten Programme und Build-Abhängigkeiten installiert sind.
# Lade die Projekt-Konfiguration, um den Pfad zu minetestmapper zu kennen
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ ! -f "$CONFIG_FILE_PATH" ]; then
echo "FEHLER: Konfigurationsdatei config.sh nicht unter ${CONFIG_FILE_PATH} gefunden!"
exit 1
fi
source "$CONFIG_FILE_PATH"
echo "Starte Überprüfung der System-Abhängigkeiten..."
echo ""
declare -i missing_count=0
# --- Teil 1: Kritische Laufzeit-Programme ---
echo "--- Prüfe Laufzeit-Programme ---"
# Spezieller Check für minetestmapper am Projekt-Pfad
MINETESTMAPPER_FULL_PATH="${BASE_SCRIPT_DIR}/${MINETESTMAPPER_EXEC_NAME}"
if [ -f "$MINETESTMAPPER_FULL_PATH" ]; then
if [ -x "$MINETESTMAPPER_FULL_PATH" ]; then
echo "[+] OK: '${MINETESTMAPPER_EXEC_NAME}' ist verfügbar und ausführbar unter: ${MINETESTMAPPER_FULL_PATH}"
else
echo "[-] FEHLER: '${MINETESTMAPPER_EXEC_NAME}' gefunden, aber NICHT AUSFÜHRBAR unter: ${MINETESTMAPPER_FULL_PATH}"
echo " -> Bitte mit 'chmod +x ${MINETESTMAPPER_FULL_PATH}' korrigieren."
missing_count=$((missing_count + 1))
fi
else
echo "[-] FEHLER: Das Hauptprogramm '${MINETESTMAPPER_EXEC_NAME}' wurde nicht unter dem erwarteten Pfad ${MINETESTMAPPER_FULL_PATH} gefunden."
missing_count=$((missing_count + 1))
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")
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."
missing_count=$((missing_count + 1))
else
found_path=$(command -v "$dep")
echo "[+] OK: '$dep' ist verfügbar unter: ${found_path}"
fi
done
echo ""
# --- Teil 2: Build-Abhängigkeiten für minetestmapper ---
echo "--- Prüfe Build-Abhängigkeiten (für Debian/Ubuntu-basierte Systeme) ---"
declare -a build_deps=("cmake" "libgd-dev" "libhiredis-dev" "libleveldb-dev" "libpq-dev" "libsqlite3-dev" "zlib1g-dev" "libzstd-dev")
# Prüfen, ob dpkg verfügbar ist, sonst diesen Teil überspringen
if ! command -v "dpkg" &> /dev/null; then
echo "[!] WARNUNG: 'dpkg' Kommando nicht gefunden. Kann Build-Abhängigkeiten nicht prüfen. (Dies ist normal auf nicht-Debian-Systemen)"
else
for pkg in "${build_deps[@]}"; do
# dpkg -s gibt einen Fehlercode zurück, wenn das Paket nicht installiert ist
if ! dpkg -s "$pkg" &> /dev/null; then
echo "[-] FEHLER: Das benötigte Build-Paket '$pkg' scheint nicht installiert zu sein."
missing_count=$((missing_count + 1))
else
echo "[+] OK: Build-Paket '$pkg' ist installiert."
fi
done
fi
echo "-----------------------------------------------------"
# --- Fazit ---
if [ "$missing_count" -gt 0 ]; then
echo ""
echo "Prüfung fehlgeschlagen. Es fehlen ${missing_count} Abhängigkeit(en)."
echo "Bitte die oben als FEHLER markierten Programme und/oder Pakete installieren bzw. korrigieren."
echo ""
echo "Hinweise zur Installation (Beispiele für Debian/Ubuntu):"
echo " Laufzeit-Programme:"
echo " - minetestmapper: Muss manuell kompiliert oder aus einer anderen Quelle bezogen werden."
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 ""
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"
exit 1
else
echo ""
echo "Alle geprüften Abhängigkeiten sind vorhanden. Das System ist startklar."
exit 0
fi

View file

@ -1,5 +1,8 @@
#!/bin/bash
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
./check_dependencies.sh || exit 1
# Lade globale Konfiguration
# Annahme: config.sh liegt im selben Verzeichnis wie dieses Skript
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"

1530
colors.txt

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ MINETESTMAPPER_EXEC_NAME="minetestmapper"
MINETESTMAPPER_WORLD_DATA_BASE_PATH="/opt/luanti/data/worlds/"
DEFAULT_MM_OPT_ZOOM_LEVEL="2"; DEFAULT_MM_OPT_MIN_Y="-25"
DEFAULT_MM_OPT_ORIGINCOLOR="#ff0000"; DEFAULT_MM_OPT_PLAYERCOLOR="#ff0000"
DEFAULT_MM_OPT_SCALECOLOR="#ff0000"; DEFAULT_MM_OPT_BGCOLOR="#111111"
DEFAULT_MM_OPT_SCALECOLOR="#ff0000"; DEFAULT_MM_OPT_BGCOLOR="#dddddd"
DEFAULT_MM_CFG_DRAWALPHA="true"; DEFAULT_MM_CFG_DRAWORIGIN="true"
DEFAULT_MM_CFG_DRAWPLAYERS="true"; DEFAULT_MM_CFG_DRAWSCALE="true"
@ -30,7 +30,7 @@ WEB_MAPS_BASE_SUBDIR="worldmaps"
# --- Standard-Einstellungen für generierte Dateien (überschreibbar in web.conf) ---
DEFAULT_TILES_SUBDIR_NAME="map_tiles"
DEFAULT_GDAL2TILES_ZOOM_LEVELS="1-6"
DEFAULT_GDAL2TILES_ZOOM_LEVELS="1-7"
DEFAULT_WEB_MAP_PNG_FILENAME="map.png"
DEFAULT_RESIZED_MAX_DIMENSION=4096
DEFAULT_ARCHIVE_SUBDIR_NAME="archive"
@ -48,9 +48,8 @@ FALLBACK_BANNER_IMG_URL="/images/default_banner.png"
# Fallback-Werte für Serverdetails (falls in web.conf nicht spezifiziert)
DEFAULT_SERVER_DISPLAY_NAME_PREFIX="unbekannt"; DEFAULT_SERVER_ADDRESS="unbekannt"; DEFAULT_SERVER_PORT="30000"
DEFAULT_SERVER_ACCESS_INFO="Keine Angaben"; DEFAULT_SERVER_STATUS_TEXT_FALLBACK="Status wird ermittelt..."
DEFAULT_LEAFLET_BOUNDS_SOUTH="-85.05112878"; DEFAULT_LEAFLET_BOUNDS_WEST="-180"
DEFAULT_LEAFLET_BOUNDS_NORTH="85.05112878"; DEFAULT_LEAFLET_BOUNDS_EAST="180"
DEFAULT_LEAFLET_ZOOM_AFTER_FIT="2"; DEFAULT_GAMEID="minetest"
# Veraltete Leaflet-Variablen wurden entfernt.
DEFAULT_GAMEID="minetest"
DEFAULT_WORLD_SHORT_DESCRIPTION="Keine Kurzbeschreibung vorhanden."
DEFAULT_WORLD_LONG_DESCRIPTION="<p>Für diese Welt wurde noch keine detaillierte Beschreibung hinterlegt.</p>"

View file

@ -9,11 +9,15 @@ else
exit 1
fi
# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert
WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" # Nutzt DEFAULT_WORLD_NAME_KEY aus config.sh
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
# Annahme: check_dependencies.sh wurde um 'vips' erweitert
/opt/luweb/check_dependencies.sh || exit 1
# Pfad zum Verzeichnis der aktuellen Welt (verwendet jetzt MINETESTMAPPER_WORLD_DATA_BASE_PATH)
CURRENT_MINETEST_WORLD_DATA_PATH="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${WORLD_KEY}/" # Für -i Option und world.mt/web.conf
# 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
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Das Welt-Datenverzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' wurde nicht gefunden!"
@ -48,12 +52,10 @@ MINETESTMAPPER_PATH="${BASE_SCRIPT_DIR}/${MINETESTMAPPER_EXEC_NAME}"
LOG_FILE="${LOG_DIR_BASE}/${SCRIPT_BASENAME}_${WORLD_KEY}.log"
LOCK_FILE="${LOCK_FILE_BASE_DIR}/${SCRIPT_BASENAME}_${WORLD_KEY}.lock"
if [ -z "$RAW_MAP_BASE_SUBDIR" ]; then echo "FEHLER: RAW_MAP_BASE_SUBDIR ist in config.sh nicht gesetzt oder leer!"; exit 1; fi
if [ -z "$RAW_MAP_FILENAME" ]; then echo "FEHLER: RAW_MAP_FILENAME ist in config.sh nicht gesetzt oder leer!"; exit 1; fi
RAW_MAP_OUTPUT_DIR_ABSOLUTE="${BASE_SCRIPT_DIR}/${RAW_MAP_BASE_SUBDIR}/${WORLD_KEY}"
RAW_MAP_ABSOLUTE_PATH="${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/${RAW_MAP_FILENAME}"
UNKNOWN_NODES_FILE_ABSOLUTE_PATH="${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/unknown_nodes.txt"
MAP_INFO_FILE_ABSOLUTE_PATH="${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/map_info.txt"
WEB_CURRENT_WORLD_DIR="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${WORLD_KEY}"
TILES_FULL_OUTPUT_PATH="${WEB_CURRENT_WORLD_DIR}/${TILES_SUBDIR_NAME}"
@ -66,7 +68,7 @@ log_message() {
echo "${message_to_log}" | tee -a "$LOG_FILE"
}
# === Funktion zur Archivbereinigung (unverändert) ===
# === 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
@ -110,29 +112,40 @@ MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}'";
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 (Optionen: ${MM_ALL_OPTIONS_STR}). Ausgaben folgen:"
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 (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 [ ${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}."
# === Karten-Dimensionen auslesen und in map_info.txt speichern ===
log_message "Ermittle Dimensionen von ${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_info.txt kann nicht erstellt werden."
log_message "WARNUNG: 'identify' (Teil von ImageMagick) nicht gefunden. map_dimension kann nicht ermittelt werden."
else
dimensions=$(identify -format "%wx%h" "$RAW_MAP_ABSOLUTE_PATH" 2>/dev/null)
if [ -n "$dimensions" ]; then
map_info_file="${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/map_info.txt"
echo "$dimensions" > "$map_info_file"
log_message "Dimensionen (${dimensions}) erfolgreich in ${map_info_file} gespeichert."
else
log_message "FEHLER: Konnte Dimensionen von ${RAW_MAP_ABSOLUTE_PATH} nicht ermitteln."
fi
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
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
# Unknown Nodes Verarbeitung
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"
@ -141,25 +154,28 @@ if [ -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE" ]; then
else log_message "WARNUNG: minetestmapper-Ausgabe nicht verarbeitbar."; fi
rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"
# 2. Web-Vorschaukarte (verkleinert) erstellen
log_message "Erzeuge Web-Version von ${RAW_MAP_FILENAME} (max ${RESIZED_MAX_DIMENSION}px) nach ${WEB_MAP_PNG_FULL_PATH}..."
# === 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")"
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: Quelldatei ${RAW_MAP_ABSOLUTE_PATH} für Web-Vorschau nicht gefunden!"; else
if ! command -v convert &> /dev/null; then log_message "WARNUNG: convert nicht gefunden. Kopiere Karte."
(set -o pipefail; cp "$RAW_MAP_ABSOLUTE_PATH" "$WEB_MAP_PNG_FULL_PATH" 2>&1 | tee -a "$LOG_FILE" )
if [ $? -ne 0 ]; then log_message "FEHLER: Kopieren fehlgeschlagen."; else log_message "Rohkarte kopiert."; fi
else log_message "Führe convert aus..."
(set -o pipefail; convert "$RAW_MAP_ABSOLUTE_PATH" -resize "${RESIZED_MAX_DIMENSION}x${RESIZED_MAX_DIMENSION}>" "$WEB_MAP_PNG_FULL_PATH" 2>&1 | tee -a "$LOG_FILE")
if [ $? -ne 0 ]; then log_message "FEHLER: Verkleinern fehlgeschlagen."; else log_message "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
# 3. Tiles generieren
# === 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..."
(set -o pipefail; gdal2tiles.py --profile=raster --xyz --zoom="${GDAL2TILES_ZOOM_LEVELS}" "${RAW_MAP_ABSOLUTE_PATH}" "${TEMP_TILES_DIR}" 2>&1 | tee -a "$LOG_FILE")
# 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"
@ -171,29 +187,36 @@ fi
# === Archivbereinigung ===
prune_archives
# 4. Tägliches Archivbild
# === 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.";
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.";
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) für ${ARCHIVE_DAILY_FILE_PATH}..."
if ! command -v convert &> /dev/null; then log_message "WARNUNG: convert nicht gefunden. Kopiere Originalgröße."
if cp "$RAW_MAP_ABSOLUTE_PATH" "$ARCHIVE_DAILY_FILE_PATH"; then log_message "Archivbild (Originalgröße) erstellt."; else log_message "FEHLER: Archivbild (cp) nicht erstellt.";fi
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
(set -o pipefail; convert "$RAW_MAP_ABSOLUTE_PATH" -resize "${RESIZED_MAX_DIMENSION}x${RESIZED_MAX_DIMENSION}>" "$ARCHIVE_DAILY_FILE_PATH" 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 (convert).";fi
fi; fi; fi
else log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert bereits."; fi
log_message "FEHLER: Verkleinertes Archivbild nicht erstellt (vips)."
fi
fi
fi
else
log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert bereits."
fi
# 5. Status- und Info-Dateien im Webverzeichnis
# === 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" && log_message "last_update.txt erstellt." || log_message "FEHLER: last_update.txt nicht erstellt."
@ -201,9 +224,8 @@ 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
# NEU: Kopiere map_info.txt in das Web-Verzeichnis
if [ -f "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/map_info.txt" ]; then
if cp "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}/map_info.txt" "${WEB_CURRENT_WORLD_DIR}/map_info.txt"; then
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."

View file

@ -1,5 +1,8 @@
#!/bin/bash
# Prüfe Abhängigkeiten, bevor irgendetwas anderes passiert
./check_dependencies.sh || exit 1
# Lade globale Konfiguration
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ -f "$CONFIG_FILE_PATH" ]; then
@ -9,473 +12,49 @@ else
exit 1
fi
# === Abgeleitete Variablen ===
# === Abgeleitete Haupt-Variablen ===
SCRIPT_BASENAME=$(basename "$0" .sh)
LOG_FILE="${LOG_DIR_BASE}/${SCRIPT_BASENAME}.log"
LOCK_FILE="${LOCK_FILE_BASE_DIR}/${SCRIPT_BASENAME}.lock"
WEB_CONTENT_STATIC_PATH="${WEB_CONTENT_BASE_PATH}/${WEB_CONTENT_STATIC_SUBDIR}"
CACHE_BUSTER=$(date +%s)
FUNCTIONS_DIR="${SITE_GENERATOR_BASE_PATH}/functions"
GENERATORS_DIR="${FUNCTIONS_DIR}/generators"
CURRENT_YEAR=$(date '+%Y')
ACTUAL_BANNER_IMG_URL_PATH="${FALLBACK_BANNER_IMG_URL}"
CACHE_BUSTER=$(date +%s)
WEB_CONTENT_STATIC_PATH="${WEB_CONTENT_BASE_PATH}/${WEB_CONTENT_STATIC_SUBDIR}"
# === Logging Funktion ===
log_message() { local msg; msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"; echo "${msg}" | tee -a "$LOG_FILE"; }
# --- Lade alle Funktions-Skripte aus den Unterverzeichnissen ---
if [ ! -d "$FUNCTIONS_DIR" ] || [ ! -d "$GENERATORS_DIR" ]; then
echo "FEHLER: Funktions-Verzeichnisse (${FUNCTIONS_DIR}, ${GENERATORS_DIR}) nicht gefunden!"
exit 1
fi
# === Hilfsfunktionen für Platzhalter ===
create_placeholder_file() { local fp="$1"; local dc="$2"; if [ ! -f "$fp" ]; then mkdir -p "$(dirname "$fp")"; echo -e "$dc" > "$fp"; log_message "Platzhalterdatei: $fp"; fi; }
get_config_value_from_file() { local cf="$1"; local k="$2"; local dv="${3:-}"; local v; if [ ! -f "$cf" ]; then echo "$dv"; return; fi; v=$(grep -E "^\s*${k}\s*=" "$cf"|tail -n 1|sed -e 's/\s*#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'|cut -d'=' -f2-|sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//"); if [ -n "$v" ]; then echo "$v"; else echo "$dv"; fi;}
create_placeholder_web_conf() {
local target_path="$1"
local template_file="${EXAMPLE_TEMPLATE_DIR_PATH}/web.conf.template"
if [ -f "$template_file" ]; then
cp "$template_file" "$target_path"
log_message "Beispiel-Konfiguration web.conf von Template nach ${target_path} kopiert."
else
log_message "FEHLER: Beispiel-Konfigurations-Template nicht gefunden unter ${template_file}!"
local world_key=$(basename "$(dirname "$target_path")")
local content="# Minimale Konfiguration für die Welt '${world_key}'\n\nWORLD_DISPLAY_NAME=\"${world_key}\""
create_placeholder_file "$target_path" "$content"
# Lade Hilfsfunktionen und dann die Generatoren
for func_file in "$FUNCTIONS_DIR"/*.sh "$GENERATORS_DIR"/*.sh; do
if [ -f "$func_file" ]; then
source "$func_file"
fi
}
# === Template Rendering Funktion (FINAL, ROBUST) ===
render_template() {
local template_path="$1"
local output_path="$2"
shift 2
local replacements=("$@")
if [ ! -f "$template_path" ]; then
log_message "FEHLER: Template-Datei nicht gefunden: ${template_path}"
return 1
fi
local template_content
template_content=$(<"$template_path")
# Wende jede Ersetzung nacheinander an.
# Diese Methode ist robuster als eine einzelne große Ersetzung.
for ((i=0; i<${#replacements[@]}; i+=2)); do
local key_name="${replacements[i]}"
local key="%%${key_name}%%"
local value="${replacements[i+1]}"
local new_content=""
# Wir zerlegen den String manuell, um mehrzeilige Ersetzungen zu ermöglichen.
# Dies ersetzt alle Vorkommen des Schlüssels.
while [[ "$template_content" == *"$key"* ]]; do
# Füge den Teil vor dem Schlüssel und den Ersatzwert hinzu
new_content+="${template_content%%$key*}${value}"
# Entferne den Teil, der bereits verarbeitet wurde
template_content="${template_content#*$key}"
done
# Füge den verbleibenden Rest des Inhalts hinzu
template_content="${new_content}${template_content}"
done
echo "$template_content" > "$output_path"
}
# === HTML/CSS Erzeugungsfunktionen ===
generate_css() {
local css_file_path="${WEB_ROOT_PATH}/style.css"
log_message "Erzeuge/Aktualisiere ${css_file_path} aus Template..."
render_template "${TEMPLATE_DIR_PATH}/css.template" "$css_file_path" \
"css_banner_image_path" "$ACTUAL_BANNER_IMG_URL_PATH" \
"CACHE_BUSTER" "$CACHE_BUSTER"
if [ $? -eq 0 ]; then log_message "CSS-Datei erfolgreich erstellt."; else log_message "FEHLER CSS."; fi
}
generate_html_header() {
local current_page_title="$1"
local relative_path_prefix="${2:-.}"
local active_page_id="${3:-}"
local active_class_home=""; local active_class_worlds=""; local active_class_downloads=""
case "$active_page_id" in
home) active_class_home="active" ;;
worlds) active_class_worlds="active" ;;
downloads) active_class_downloads="active" ;;
esac
local header_file
header_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/html_header.template" "$header_file" \
"current_page_title" "$current_page_title" \
"relative_path_prefix" "$relative_path_prefix" \
"active_class_home" "$active_class_home" \
"active_class_worlds" "$active_class_worlds" \
"active_class_downloads" "$active_class_downloads" \
"CACHE_BUSTER" "$CACHE_BUSTER" \
"SITE_TITLE" "$SITE_TITLE"
cat "$header_file"
rm "$header_file"
}
generate_html_footer() {
local footer_file
footer_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/html_footer.template" "$footer_file" \
"CURRENT_YEAR" "$CURRENT_YEAR" \
"SITE_TITLE" "$SITE_TITLE" \
"SITE_OWNER_NAME" "$SITE_OWNER_NAME"
cat "$footer_file"
rm "$footer_file"
}
generate_homepage() { local p="${WEB_ROOT_PATH}/index.html"; local c="${WEB_CONTENT_STATIC_PATH}/startseite_content.html"; log_message "Erzeuge ${p}..."; generate_html_header "Willkommen" "." "home" > "$p"; if [ -f "$c" ]; then cat "$c" >> "$p"; else echo "<h2>Willkommen!</h2>" >> "$p"; fi; generate_html_footer >> "$p"; }
generate_impressum() { local p="${WEB_ROOT_PATH}/impressum.html"; local c="${WEB_CONTENT_STATIC_PATH}/impressum_content.html"; log_message "Erzeuge ${p}..."; generate_html_header "Impressum" "" > "$p"; if [ -f "$c" ]; then cat "$c" >> "$p"; else echo "<h2>Impressum</h2><p>Betreiber: ${SITE_OWNER_NAME}<br>Kontakt: <a href='mailto:${SITE_OWNER_EMAIL}'>${SITE_OWNER_EMAIL}</a></p>" >> "$p"; fi; generate_html_footer >> "$p"; }
generate_downloads_page() { local p="${WEB_ROOT_PATH}/downloads.html"; local c="${WEB_CONTENT_STATIC_PATH}/downloads_content.html"; log_message "Erzeuge ${p}..."; generate_html_header "Downloads" "." "downloads" > "$p"; if [ -f "$c" ]; then cat "$c" >> "$p"; else cat >>"$p" <<EOF
<h2>Downloads</h2><p>Offizielle Seite: <a href="https://www.luanti.org/downloads/" target="_blank">luanti.org</a></p>
EOF
fi; generate_html_footer >> "$p"; }
generate_datenschutz_page() { local p="${WEB_ROOT_PATH}/datenschutz.html"; local c="${WEB_CONTENT_STATIC_PATH}/datenschutz_content.html"; log_message "Erzeuge ${p}..."; generate_html_header "Datenschutz" "" > "$p"; if [ -f "$c" ]; then cat "$c" >> "$p"; else echo "<h2>Datenschutzerklärung</h2><p>Platzhalter.</p>" >> "$p"; fi; generate_html_footer >> "$p"; }
generate_worlds_overview() {
local overview_file="${WEB_ROOT_PATH}/worlds.html"
log_message "Erzeuge Weltenübersicht: ${overview_file}..."
generate_html_header "Weltenübersicht" "." "worlds" > "$overview_file"
echo "<h2>Unsere Welten</h2>" >> "$overview_file"
local discovered_worlds_count=0; shopt -s nullglob
local world_key_dirs=("${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/)
shopt -u nullglob
if [ ${#world_key_dirs[@]} -eq 0 ]; then
log_message "WARNUNG: Keine Welt-Verzeichnisse in ${MINETESTMAPPER_WORLD_DATA_BASE_PATH}."
echo "<p>Keine Welten verfügbar.</p>" >> "$overview_file"
else
local overview_entry_template_path="${TEMPLATE_DIR_PATH}/worlds_overview_entry.template"
for world_data_dir_loop_overview in "${world_key_dirs[@]}"; do
local current_world_key
current_world_key=$(basename "$world_data_dir_loop_overview")
local world_mt_file="${world_data_dir_loop_overview}world.mt"
local web_conf_file="${world_data_dir_loop_overview}web.conf"
if [ ! -f "$world_mt_file" ] || [ ! -f "$web_conf_file" ]; then continue; fi
local world_display_name_ov
world_display_name_ov=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME")
local world_short_desc_ov
world_short_desc_ov=$(get_config_value_from_file "$web_conf_file" "WORLD_SHORT_DESCRIPTION" "$DEFAULT_WORLD_SHORT_DESCRIPTION")
unset ADMIN_NAME; source "$web_conf_file" &>/dev/null
local admin_name_ov
if [ ${#ADMIN_NAME[@]} -gt 0 ]; then admin_name_ov=$(IFS=', '; echo "${ADMIN_NAME[*]}"); else admin_name_ov="$SITE_OWNER_NAME"; fi
local current_map_png_filename_for_preview_ov
current_map_png_filename_for_preview_ov=$(get_config_value_from_file "$web_conf_file" "WEB_MAP_PNG_FILENAME" "$DEFAULT_WEB_MAP_PNG_FILENAME")
if [ -z "$world_display_name_ov" ]; then
local mt_world_name_from_mt_ov
mt_world_name_from_mt_ov=$(grep -E "^\s*world_name\s*=" "$world_mt_file" | tail -n 1 | cut -d'=' -f2 | xargs)
if [ -n "$mt_world_name_from_mt_ov" ]; then world_display_name_ov="$mt_world_name_from_mt_ov";
else world_display_name_ov="Welt ${current_world_key}"; fi
fi
local detail_page_filename="world_${current_world_key}.html"
local preview_img_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${current_map_png_filename_for_preview_ov}"
local preview_img_abs_path="${WEB_ROOT_PATH}/${preview_img_rel_path}"
local online_status_text="offline"; local online_status_class="offline"
local status_file_for_overview="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/online_status.txt"
if [ -f "$status_file_for_overview" ]; then
local status_line_overview
status_line_overview=$(cat "$status_file_for_overview")
if [[ "$status_line_overview" == "online"* ]]; then online_status_text="online"; online_status_class="online"; fi
fi
local preview_img_html
if [ -f "$preview_img_abs_path" ]; then
preview_img_html="<img src='${preview_img_rel_path}?v=${CACHE_BUSTER}' alt='Vorschau ${world_display_name_ov}'>"
else
preview_img_html="<img src='images/placeholder_map.png?v=${CACHE_BUSTER}' alt='Keine Vorschau für ${world_display_name_ov}'>"
fi
local temp_overview_entry_file
temp_overview_entry_file=$(mktemp)
render_template "$overview_entry_template_path" "$temp_overview_entry_file" \
"detail_page_filename" "$detail_page_filename" \
"preview_img_html" "$preview_img_html" \
"world_display_name_ov" "$world_display_name_ov" \
"online_status_class" "$online_status_class" \
"online_status_text" "$online_status_text" \
"admin_name_ov" "$admin_name_ov" \
"world_short_desc_ov" "$world_short_desc_ov"
cat "$temp_overview_entry_file" >> "$overview_file"
rm "$temp_overview_entry_file"
discovered_worlds_count=$((discovered_worlds_count + 1))
done
if [ "$discovered_worlds_count" -eq 0 ]; then echo "<p>Keine Welten mit gültiger Konfiguration gefunden.</p>" >> "$overview_file"; fi
fi
generate_html_footer >> "$overview_file"
if [ $? -eq 0 ]; then log_message "Weltenübersicht erstellt."; else log_message "FEHLER Weltenübersicht."; fi
}
generate_world_detail_page() {
local current_world_key="$1"; local detail_page_file="${WEB_ROOT_PATH}/world_${current_world_key}.html"
local current_minetest_world_path="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${current_world_key}"
local world_mt_file="${current_minetest_world_path}/world.mt"; local web_conf_file="${current_minetest_world_path}/web.conf"
if [ ! -f "$world_mt_file" ] || [ ! -f "$web_conf_file" ]; then log_message "FEHLER: world.mt oder web.conf für ${current_world_key} fehlt."; return 1; fi
local map_info_file="${BASE_SCRIPT_DIR}/${RAW_MAP_BASE_SUBDIR}/${current_world_key}/map_info.txt"
local MAP_WIDTH=0; local MAP_HEIGHT=0; local MAP_EXTENT_MIN_X=0; local MAP_EXTENT_MIN_Z=0; local MAP_EXTENT_WIDTH=0; local MAP_EXTENT_HEIGHT=0
if [ -f "$map_info_file" ]; then
local map_dim_val; map_dim_val=$(get_config_value_from_file "$map_info_file" "map_dimension")
if [[ "$map_dim_val" == *"x"* ]]; then
MAP_WIDTH=$(echo "$map_dim_val" | cut -d'x' -f1)
MAP_HEIGHT=$(echo "$map_dim_val" | cut -d'x' -f2)
fi
local map_ext_val; map_ext_val=$(get_config_value_from_file "$map_info_file" "map_extent")
if [[ "$map_ext_val" == *":"* && "$map_ext_val" == *"+"* ]]; then
local min_coords; min_coords=$(echo "$map_ext_val" | cut -d'+' -f1)
local extent_dims; extent_dims=$(echo "$map_ext_val" | cut -d'+' -f2,3)
MAP_EXTENT_MIN_X=$(echo "$min_coords" | cut -d':' -f1)
MAP_EXTENT_MIN_Z=$(echo "$min_coords" | cut -d':' -f2)
MAP_EXTENT_WIDTH=$(echo "$extent_dims" | cut -d'+' -f1)
MAP_EXTENT_HEIGHT=$(echo "$extent_dims" | cut -d'+' -f2)
fi
else
log_message "WARNUNG: ${map_info_file} für '${current_world_key}' nicht gefunden."
fi
local WORLD_DISPLAY_NAME_PAGE; WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME")
if [ -z "$WORLD_DISPLAY_NAME_PAGE" ]; then
WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$world_mt_file" "world_name" "Welt ${current_world_key}")
fi
local SERVER_ADDRESS_PAGE; SERVER_ADDRESS_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_ADDRESS" "$DEFAULT_SERVER_ADDRESS")
local SERVER_PORT_PAGE; SERVER_PORT_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_PORT" "$DEFAULT_SERVER_PORT")
local SERVER_ACCESS_INFO_PAGE; SERVER_ACCESS_INFO_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_ACCESS_INFO" "$DEFAULT_SERVER_ACCESS_INFO")
unset ADMIN_NAME ADMIN_SKIN_URL ADMIN_EMAIL ADMIN_DISCORD ADMIN_MATRIX ADMIN_STEAM ADMIN_TEAMSPEAK ADMIN_MUMBLE WORLD_LONG_DESCRIPTION WORLD_GAME_RULES
source "$web_conf_file"
local WORLD_LONG_DESCRIPTION_PAGE="${WORLD_LONG_DESCRIPTION:-$DEFAULT_WORLD_LONG_DESCRIPTION}"
local WORLD_GAME_RULES_PAGE="${WORLD_GAME_RULES:-}"
local STATUS_TEXT_FALLBACK_PAGE="$DEFAULT_SERVER_STATUS_TEXT_FALLBACK"
local CURRENT_WEB_MAP_PNG_FILENAME; CURRENT_WEB_MAP_PNG_FILENAME=$(get_config_value_from_file "$web_conf_file" "WEB_MAP_PNG_FILENAME" "$DEFAULT_WEB_MAP_PNG_FILENAME")
local CURRENT_ARCHIVE_SUBDIR_NAME; CURRENT_ARCHIVE_SUBDIR_NAME=$(get_config_value_from_file "$web_conf_file" "ARCHIVE_SUBDIR_NAME" "$DEFAULT_ARCHIVE_SUBDIR_NAME")
local MT_GAMEID="$DEFAULT_GAMEID"; local MT_ENABLE_DAMAGE="false"; local MT_CREATIVE_MODE="false"
declare -A parsed_mod_packs; declare -a parsed_standalone_mods
while IFS='=' read -r key value || [ -n "$key" ]; do
key=$(echo "$key"|xargs); value=$(echo "$value"|xargs)
case "$key" in
gameid) MT_GAMEID="$value" ;;
enable_damage) MT_ENABLE_DAMAGE="$value" ;;
creative_mode) MT_CREATIVE_MODE="$value" ;;
load_mod_*) local mod_id="${key#load_mod_}"; mod_id=$(echo "$mod_id"|xargs)
if [[ "$value" =~ ^mods/([^/]+)/.+ ]]; then local pack_n="${BASH_REMATCH[1]}"; parsed_mod_packs["$pack_n"]="${parsed_mod_packs[$pack_n]} ${mod_id}";
elif [[ "$value" =~ ^mods/([^/]+)$ ]]; then parsed_standalone_mods+=("$mod_id");
elif [ -n "$value" ]; then parsed_standalone_mods+=("$mod_id"); fi ;;
esac;
done < "$world_mt_file"
local MODS_HTML=""
if [ ${#parsed_mod_packs[@]} -gt 0 ] || [ ${#parsed_standalone_mods[@]} -gt 0 ]; then
MODS_HTML+="<ul class='mod-list'>"
local sorted_pack_names=(); if [ ${#parsed_mod_packs[@]} -gt 0 ]; then mapfile -t sorted_pack_names < <(printf '%s\n' "${!parsed_mod_packs[@]}" | sort); fi
for pack_name in "${sorted_pack_names[@]}"; do
MODS_HTML+="<li>+ <a href='https://content.luanti.org/packages/?q=${pack_name}' target='_blank' rel='noopener noreferrer'>${pack_name}</a><ul>"
local mods_in_pack_str="${parsed_mod_packs[$pack_name]}"
local sorted_mods_in_pack=(); if [ -n "$mods_in_pack_str" ]; then mapfile -t sorted_mods_in_pack < <(echo "$mods_in_pack_str" | tr ' ' '\n' | grep -v '^\s*$' | sort); fi
for mod_in_pack in "${sorted_mods_in_pack[@]}"; do
[ -n "$mod_in_pack" ] && MODS_HTML+="<li>- <a href='https://content.luanti.org/packages/?q=${mod_in_pack}' target='_blank' rel='noopener noreferrer'>${mod_in_pack}</a></li>"
done
MODS_HTML+="</ul></li>"
done
local sorted_standalone_mods=(); if [ ${#parsed_standalone_mods[@]} -gt 0 ]; then mapfile -t sorted_standalone_mods < <(printf '%s\n' "${parsed_standalone_mods[@]}" | sort); fi
for solo_mod in "${sorted_standalone_mods[@]}"; do
[ -n "$solo_mod" ] && MODS_HTML+="<li>- <a href='https://content.luanti.org/packages/?q=${solo_mod}' target='_blank' rel='noopener noreferrer'>${solo_mod}</a></li>"
done
MODS_HTML+="</ul>"
else
MODS_HTML="<p>Keine Mod-Informationen in world.mt gefunden.</p>"
fi
# KORREKTUR: Logik zur Archiv-Suche wiederhergestellt (wie im Original-Skript)
local ARCHIVE_HTML=""; local available_archive_dates_js_array="[]"; local -a available_archive_dates_bash=();
local archive_scan_base_path="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_ARCHIVE_SUBDIR_NAME}";
if [ -d "$archive_scan_base_path" ]; then
shopt -s nullglob;
for y_d in "${archive_scan_base_path}"/*/; do
if [ -d "$y_d" ]; then
local y=$(basename "$y_d");
for m_d in "${y_d}"*/; do
if [ -d "$m_d" ]; then
local m=$(basename "$m_d");
for d_f in "${m_d}"*.png; do
if [ -f "$d_f" ]; then
local d=$(basename "$d_f" .png);
if [[ "$y" =~ ^[0-9]{4}$ && "$m" =~ ^[0-9]{2}$ && "$d" =~ ^[0-9]{2}$ ]]; then
available_archive_dates_bash+=("${y}-${m}-${d}");
fi;
fi;
done;
fi;
done;
fi;
done;
shopt -u nullglob;
fi;
if [ ${#available_archive_dates_bash[@]} -gt 0 ]; then
local sorted_dates_str; sorted_dates_str=$(printf '%s\n' "${available_archive_dates_bash[@]}" | sort -r);
local js_dates_temp_array=(); if [ -n "$sorted_dates_str" ]; then mapfile -t js_dates_temp_array < <(echo "$sorted_dates_str"); fi;
local js_array_content="";
if [ ${#js_dates_temp_array[@]} -gt 0 ]; then
for date_str_loop in "${js_dates_temp_array[@]}"; do
if [ -n "$date_str_loop" ]; then js_array_content+="\"${date_str_loop}\","; fi;
done;
js_array_content=${js_array_content%,};
fi;
available_archive_dates_js_array="[${js_array_content}]";
local temp_archive_html_file; temp_archive_html_file=$(mktemp)
local archive_world_rel_to_webroot="/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_ARCHIVE_SUBDIR_NAME}"
render_template "${TEMPLATE_DIR_PATH}/world_detail_archive.template" "$temp_archive_html_file" \
"current_world_key" "$current_world_key" \
"available_archive_dates_js_array" "$available_archive_dates_js_array" \
"archive_world_rel_to_webroot" "$archive_world_rel_to_webroot" \
"CACHE_BUSTER" "$CACHE_BUSTER"
ARCHIVE_HTML=$(<"$temp_archive_html_file")
rm "$temp_archive_html_file"
else
ARCHIVE_HTML="<p>Keine Archivbilder für diese Welt verfügbar.</p>";
fi
local web_map_preview_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_WEB_MAP_PNG_FILENAME}"
local web_online_status_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/online_status.txt"
local web_last_update_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt"
local web_players_txt_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/players.txt"
local web_weather_txt_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/weather.txt"
local web_unknown_nodes_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/unknown_nodes.txt"
local unknown_nodes_abs_path="${WEB_ROOT_PATH}/${web_unknown_nodes_rel_path}"
local creative_text="Aus"; local creative_text_class="offline"; if [ "$MT_CREATIVE_MODE" = "true" ]; then creative_text="An"; creative_text_class="online"; fi
local damage_text="Aus"; local damage_text_class="online"; if [ "$MT_ENABLE_DAMAGE" = "true" ]; then damage_text="An"; damage_text_class="offline"; fi
local ADMIN_BOXES_HTML=""
local num_admins=${#ADMIN_NAME[@]}
if [ $num_admins -eq 0 ]; then
ADMIN_BOXES_HTML="<div class='admin-box'><div class='admin-contact-block'><img src='/${SITE_OWNER_ADMIN_SKIN_URL:-$DEFAULT_PLAYER_SKIN_URL}?v=${CACHE_BUSTER}' alt='Admin Skin' class='admin-skin-icon'><div class='admin-text-details'><p><strong class='admin-player-name'>${SITE_OWNER_NAME}</strong></p><div class='contact-links'><a href='mailto:${SITE_OWNER_EMAIL}' class='contact-button' data-title='E-Mail: ${SITE_OWNER_EMAIL}'>Email</a></div></div></div></div>"
else
for i in "${!ADMIN_NAME[@]}"; do
local name="${ADMIN_NAME[$i]}"; local skin_url="${ADMIN_SKIN_URL[$i]:-$DEFAULT_PLAYER_SKIN_URL}"; local email="${ADMIN_EMAIL[$i]}"; local discord="${ADMIN_DISCORD[$i]}"; local matrix="${ADMIN_MATRIX[$i]}"; local steam="${ADMIN_STEAM[$i]}"; local teamspeak="${ADMIN_TEAMSPEAK[$i]}"; local mumble="${ADMIN_MUMBLE[$i]}"
local contact_links_html=""
[ -n "$email" ] && contact_links_html+="<a href='mailto:${email}' class='contact-button' data-title='E-Mail: ${email}'>Email</a>"
[ -n "$discord" ] && contact_links_html+="<a href='https://discordapp.com/users/${discord}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Discord: ${discord}'>Discord</a>"
[ -n "$matrix" ] && contact_links_html+="<a href='https://matrix.to/#/${matrix}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Matrix: ${matrix}'>Matrix</a>"
[ -n "$steam" ] && contact_links_html+="<a href='${steam}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Steam Profil'>Steam</a>"
[ -n "$teamspeak" ] && contact_links_html+="<a href='ts3server://${teamspeak}' class='contact-button' data-title='Teamspeak: ${teamspeak}'>Teamspeak</a>"
[ -n "$mumble" ] && contact_links_html+="<a href='mumble://${mumble}' class='contact-button' data-title='Mumble: ${mumble}'>Mumble</a>"
ADMIN_BOXES_HTML+="<div class='admin-box'><div class='admin-contact-block'><img src='/${skin_url}?v=${CACHE_BUSTER}' alt='Admin Skin ${name}' class='admin-skin-icon' onerror=\"this.onerror=null;this.src='/${DEFAULT_PLAYER_SKIN_URL}?v=${CACHE_BUSTER}';\"><div class='admin-text-details'><p><strong class='admin-player-name'>${name}</strong></p></div></div>"; if [ -n "$contact_links_html" ]; then ADMIN_BOXES_HTML+="<div class='contact-links'>${contact_links_html}</div>"; fi; ADMIN_BOXES_HTML+="</div>"
done
fi
local map_sub_info_link=""
if [ -f "$unknown_nodes_abs_path" ]; then
map_sub_info_link="<span class='map-file-link'><a href='/${web_unknown_nodes_rel_path}?v=${CACHE_BUSTER}' target='_blank'>Fehlende Map-Nodes</a></span>"
else
map_sub_info_link="<span class='map-file-link'>&nbsp;</span>"
fi
{
generate_html_header "Welt: ${WORLD_DISPLAY_NAME_PAGE}" ""
local temp_body_file
temp_body_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/world_detail_page.template" "$temp_body_file" \
"WORLD_DISPLAY_NAME_PAGE" "$WORLD_DISPLAY_NAME_PAGE" \
"current_world_key" "$current_world_key" \
"STATUS_TEXT_FALLBACK_PAGE" "$STATUS_TEXT_FALLBACK_PAGE" \
"MT_GAMEID" "$MT_GAMEID" \
"creative_text_class" "$creative_text_class" \
"creative_text" "$creative_text" \
"damage_text_class" "$damage_text_class" \
"damage_text" "$damage_text" \
"SERVER_ADDRESS_PAGE" "$SERVER_ADDRESS_PAGE" \
"SERVER_PORT_PAGE" "$SERVER_PORT_PAGE" \
"SERVER_ACCESS_INFO_PAGE" "$SERVER_ACCESS_INFO_PAGE" \
"ADMIN_BOXES_HTML" "$ADMIN_BOXES_HTML" \
"web_online_status_rel_path" "$web_online_status_rel_path" \
"web_last_update_rel_path" "$web_last_update_rel_path" \
"CACHE_BUSTER" "$CACHE_BUSTER" \
"web_players_txt_rel_path" "$web_players_txt_rel_path" \
"MAP_WIDTH" "$MAP_WIDTH" \
"MAP_HEIGHT" "$MAP_HEIGHT" \
"MAP_EXTENT_MIN_X" "$MAP_EXTENT_MIN_X" \
"MAP_EXTENT_MIN_Z" "$MAP_EXTENT_MIN_Z" \
"MAP_EXTENT_WIDTH" "$MAP_EXTENT_WIDTH" \
"MAP_EXTENT_HEIGHT" "$MAP_EXTENT_HEIGHT" \
"DEFAULT_PLAYER_SKIN_URL" "$DEFAULT_PLAYER_SKIN_URL" \
"web_map_preview_rel_path" "$web_map_preview_rel_path" \
"web_weather_txt_rel_path" "$web_weather_txt_rel_path" \
"WORLD_LONG_DESCRIPTION_PAGE" "$WORLD_LONG_DESCRIPTION_PAGE" \
"WORLD_GAME_RULES_PAGE" "$WORLD_GAME_RULES_PAGE" \
"web_unknown_nodes_rel_path" "$web_unknown_nodes_rel_path" \
"ARCHIVE_HTML" "$ARCHIVE_HTML" \
"MODS_HTML" "$MODS_HTML" \
"map_sub_info_link" "$map_sub_info_link"
cat "$temp_body_file"
rm "$temp_body_file"
generate_html_footer
} > "$detail_page_file"
if [ $? -eq 0 ]; then log_message "Detailseite ${WORLD_DISPLAY_NAME_PAGE} erstellt."; else log_message "FEHLER Detailseite ${WORLD_DISPLAY_NAME_PAGE}."; fi
}
done
# === Hauptlogik für Webseitengenerierung ===
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "$(date '+%Y-%m-%d %H:%M:%S') - Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende." | tee -a "$LOG_FILE"; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "Script ${SCRIPT_BASENAME}.sh beendet."' EXIT
mkdir -p "$LOG_DIR_BASE"; mkdir -p "${WEB_ROOT_PATH}"
log_message "Script ${SCRIPT_BASENAME}.sh gestartet."
log_message "Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter..."
mkdir -p "${WEB_CONTENT_BASE_PATH}"; mkdir -p "${WEB_CONTENT_STATIC_PATH}"; mkdir -p "${TEMPLATE_DIR_PATH}"; mkdir -p "${EXAMPLE_TEMPLATE_DIR_PATH}"
# --- Führe die ausgelagerten Funktionen in der richtigen Reihenfolge aus ---
DEFAULT_MINETEST_WORLD_DATA_DIR_FOR_CONF="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${DEFAULT_WORLD_NAME_KEY}"
if [ -d "$DEFAULT_MINETEST_WORLD_DATA_DIR_FOR_CONF" ] && [ -f "${DEFAULT_MINETEST_WORLD_DATA_DIR_FOR_CONF}/world.mt" ]; then
if [ ! -f "${DEFAULT_MINETEST_WORLD_DATA_DIR_FOR_CONF}/web.conf" ]; then log_message "Erstelle Beispiel web.conf für '${DEFAULT_WORLD_NAME_KEY}'..."; create_placeholder_web_conf "${DEFAULT_MINETEST_WORLD_DATA_DIR_FOR_CONF}/web.conf"; fi; fi
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/startseite_content.html" "<h2>Willkommen!</h2><p>Inhalt hier.</p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/impressum_content.html" "<h2>Impressum</h2><p>Impressum. Betreiber: ${SITE_OWNER_NAME}, Kontakt: ${SITE_OWNER_EMAIL}</p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/downloads_content.html" "<h2>Downloads</h2><p>Infos. Offiziell: <a href='https://www.luanti.org/downloads/'>luanti.org</a></p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/datenschutz_content.html" "<h2>Datenschutzerklärung</h2><p>Hier deinen Datenschutztext einfügen.</p>"
# 1. Initialisierung: Verzeichnisse und Assets vorbereiten
initialize_site_structure_and_assets
log_message "Starte Generierung der statischen Webseiten-Dateien..."
log_message "Kopiere Webseiten-Assets (Bilder, Icons)..."
WEB_IMAGES_TARGET_DIR="${WEB_ROOT_PATH}/images"
WEB_PLAYER_IMAGES_TARGET_DIR="${WEB_IMAGES_TARGET_DIR}/players"
SOURCE_IMAGES_DIR="${WEB_CONTENT_BASE_PATH}/${WEB_CONTENT_IMAGES_SOURCE_SUBDIR:-images_source}"
SOURCE_PLAYER_IMAGES_TARGET_DIR="${SOURCE_IMAGES_DIR}/players"
mkdir -p "$WEB_IMAGES_TARGET_DIR"; mkdir -p "$WEB_PLAYER_IMAGES_TARGET_DIR"
if [ -d "$SOURCE_IMAGES_DIR" ]; then shopt -s nullglob; for src_file in "$SOURCE_IMAGES_DIR"/* ; do [ -f "$src_file" ] && cp -u "$src_file" "$WEB_IMAGES_TARGET_DIR/"; done; shopt -u nullglob; log_message "Allgemeine Bilder kopiert/aktualisiert.";
else log_message "WARNUNG: Quellverz. allgemeine Bilder nicht gefunden: ${SOURCE_IMAGES_DIR}"; fi
if [ -d "$SOURCE_PLAYER_IMAGES_DIR" ]; then shopt -s nullglob; for src_file in "$SOURCE_PLAYER_IMAGES_DIR"/*.png ; do [ -f "$src_file" ] && cp -u "$src_file" "$WEB_PLAYER_IMAGES_TARGET_DIR/"; done; shopt -u nullglob; log_message "Spieler-Skins kopiert/aktualisiert.";
else log_message "WARNUNG: Quellverz. Spieler-Skins nicht gefunden: ${SOURCE_PLAYER_IMAGES_DIR}"; fi
# 2. Globale CSS-Datei generieren
generate_css
if [ -n "$STATIC_BANNER_FILENAME" ]; then ACTUAL_BANNER_IMG_URL_PATH="/images/${STATIC_BANNER_FILENAME}"; if [ ! -f "${WEB_ROOT_PATH}${ACTUAL_BANNER_IMG_URL_PATH}" ]; then log_message "WARNUNG: Statisches Banner '${STATIC_BANNER_FILENAME}' nicht in '${WEB_ROOT_PATH}/images/' gefunden. Fallback: ${FALLBACK_BANNER_IMG_URL}"; ACTUAL_BANNER_IMG_URL_PATH="${FALLBACK_BANNER_IMG_URL}"; else log_message "Verwende statisches Banner: ${ACTUAL_BANNER_IMG_URL_PATH}"; fi
else log_message "Kein STATIC_BANNER_FILENAME, verwende Fallback: ${FALLBACK_BANNER_IMG_URL}"; ACTUAL_BANNER_IMG_URL_PATH="${FALLBACK_BANNER_IMG_URL}"; fi
# 3. Statische Seiten generieren
generate_static_pages
generate_css; generate_homepage; generate_impressum; generate_downloads_page; generate_datenschutz_page; generate_worlds_overview
# 4. Welt-spezifische Seiten generieren
generate_all_world_pages
processed_world_count=0; shopt -s nullglob
site_world_key_dirs_detail_loop=("${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/)
shopt -u nullglob
if [ ${#site_world_key_dirs_detail_loop[@]} -gt 0 ]; then
for site_world_data_dir_item in "${site_world_key_dirs_detail_loop[@]}"; do
site_current_world_key_item=$(basename "$site_world_data_dir_item")
if [ -z "$site_current_world_key_item" ]; then log_message "WARNUNG: Leerer Welt-Schlüssel '${site_world_data_dir_item}'."; continue; fi
if [ -f "${site_world_data_dir_item}world.mt" ] && [ -f "${site_world_data_dir_item}web.conf" ]; then
target_web_world_dir_item="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${site_current_world_key_item}"
mkdir -p "$target_web_world_dir_item"
touch "${target_web_world_dir_item}/areas.txt"; touch "${target_web_world_dir_item}/players.txt"; touch "${target_web_world_dir_item}/weather.txt"
log_message "Platzhalter areas.txt, players.txt, weather.txt für Welt '${site_current_world_key_item}' sichergestellt."
generate_world_detail_page "$site_current_world_key_item"; processed_world_count=$((processed_world_count + 1))
else log_message "WARNUNG: Für ${site_world_data_dir_item} (Key: ${site_current_world_key_item}) fehlt world.mt oder web.conf.";fi
done
log_message "${processed_world_count} Welt-Detailseiten generiert/aktualisiert."
else log_message "Keine Weltverzeichnisse in ${MINETESTMAPPER_WORLD_DATA_BASE_PATH} gefunden."; fi
log_message "Webseiten-Generierung abgeschlossen."
exit 0

View file

@ -0,0 +1,69 @@
#!/bin/bash
# 01_utils.sh - Grundlegende Hilfsfunktionen
# === Logging Funktion ===
log_message() {
local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
# Prüft, ob stdout ein Terminal ist, um "tee" nur bei interaktiver Ausführung zu nutzen
if [ -t 1 ]; then
echo "${msg}" | tee -a "$LOG_FILE"
else
echo "${msg}" >> "$LOG_FILE"
fi
}
# === Hilfsfunktionen für Platzhalter ===
create_placeholder_file() {
local fp="$1"
local dc="$2"
if [ ! -f "$fp" ]; then
mkdir -p "$(dirname "$fp")"
echo -e "$dc" > "$fp"
log_message "Platzhalterdatei erstellt: $fp"
fi
}
get_config_value_from_file() {
local cf="$1"
local k="$2"
local dv="${3:-}"
local v
if [ ! -f "$cf" ]; then echo "$dv"; return; fi
# Robuste Extraktion, die Kommentare, Leerzeichen und Anführungszeichen berücksichtigt
v=$(grep -E "^\s*${k}\s*=" "$cf" | tail -n 1 | sed -e 's/\s*#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' | cut -d'=' -f2- | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")
if [ -n "$v" ]; then echo "$v"; else echo "$dv"; fi
}
# === Template Rendering Funktion ===
render_template() {
local template_path="$1"
local output_path="$2"
shift 2
local replacements=("$@")
if [ ! -f "$template_path" ]; then
log_message "FEHLER: Template-Datei nicht gefunden: ${template_path}"
return 1
fi
local template_content
template_content=$(<"$template_path")
for ((i=0; i<${#replacements[@]}; i+=2)); do
local key_name="${replacements[i]}"
local key="%%${key_name}%%"
local value="${replacements[i+1]}"
# Manuelle Ersetzung in einer Schleife, um mehrzeilige Inhalte und mehrfache Vorkommen zu unterstützen
local new_content=""
while [[ "$template_content" == *"$key"* ]]; do
new_content+="${template_content%%$key*}${value}"
template_content="${template_content#*$key}"
done
template_content="${new_content}${template_content}"
done
echo "$template_content" > "$output_path"
return $?
}

View file

@ -0,0 +1,54 @@
#!/bin/bash
# 02_init.sh - Initialisierungsfunktionen für Verzeichnisse und Assets
create_placeholder_web_conf() {
local target_path="$1"
local template_file="${EXAMPLE_TEMPLATE_DIR_PATH}/web.conf.template"
if [ -f "$template_file" ]; then
cp "$template_file" "$target_path"
log_message "Beispiel-Konfiguration web.conf von Template nach ${target_path} kopiert."
else
log_message "WARNUNG: Beispiel-Konfigurations-Template nicht gefunden unter ${template_file}!"
local world_key=$(basename "$(dirname "$target_path")")
local content="# Minimale Konfiguration für die Welt '${world_key}'\n\nWORLD_DISPLAY_NAME=\"${world_key}\""
create_placeholder_file "$target_path" "$content"
fi
}
initialize_site_structure_and_assets() {
log_message "Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter..."
mkdir -p "${WEB_ROOT_PATH}"
mkdir -p "${WEB_CONTENT_BASE_PATH}"
mkdir -p "${WEB_CONTENT_STATIC_PATH}"
mkdir -p "${TEMPLATE_DIR_PATH}"
mkdir -p "${EXAMPLE_TEMPLATE_DIR_PATH}"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/startseite_content.html" "<h2>Willkommen!</h2><p>Inhalt hier.</p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/impressum_content.html" "<h2>Impressum</h2><p>Impressum. Betreiber: ${SITE_OWNER_NAME}, Kontakt: ${SITE_OWNER_EMAIL}</p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/downloads_content.html" "<h2>Downloads</h2><p>Infos. Offiziell: <a href='https://www.luanti.org/downloads/'>luanti.org</a></p>"
create_placeholder_file "${WEB_CONTENT_STATIC_PATH}/datenschutz_content.html" "<h2>Datenschutzerklärung</h2><p>Hier deinen Datenschutztext einfügen.</p>"
local default_world_data_dir="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${DEFAULT_WORLD_NAME_KEY}"
if [ -d "$default_world_data_dir" ] && [ -f "${default_world_data_dir}/world.mt" ]; then
if [ ! -f "${default_world_data_dir}/web.conf" ]; then
log_message "Erstelle Beispiel web.conf für '${DEFAULT_WORLD_NAME_KEY}'..."
create_placeholder_web_conf "${default_world_data_dir}/web.conf"
fi
fi
log_message "Kopiere Webseiten-Assets (Bilder, Icons)..."
local web_images_target_dir="${WEB_ROOT_PATH}/images"
local source_images_dir="${WEB_CONTENT_BASE_PATH}/${WEB_CONTENT_IMAGES_SOURCE_SUBDIR:-images_source}"
mkdir -p "$web_images_target_dir"
mkdir -p "${web_images_target_dir}/players"
if [ -d "$source_images_dir" ]; then
shopt -s nullglob
for src_file in "$source_images_dir"/* ; do [ -f "$src_file" ] && cp -u "$src_file" "$web_images_target_dir/"; done
for src_file in "$source_images_dir"/players/*.png ; do [ -f "$src_file" ] && cp -u "$src_file" "${web_images_target_dir}/players/"; done
shopt -u nullglob
log_message "Bilder und Spieler-Skins kopiert/aktualisiert."
else
log_message "WARNUNG: Quellverzeichnis für Bilder nicht gefunden: ${source_images_dir}"
fi
}

View file

@ -0,0 +1,38 @@
#!/bin/bash
# 03_html_helpers.sh - Funktionen zum Erstellen von Header und Footer
generate_html_header() {
local current_page_title="$1"
local relative_path_prefix="${2:-.}"
local active_page_id="${3:-}"
local active_class_home=""; local active_class_worlds=""; local active_class_downloads=""
case "$active_page_id" in
home) active_class_home="active" ;;
worlds) active_class_worlds="active" ;;
downloads) active_class_downloads="active" ;;
esac
local header_file
header_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/html_header.template" "$header_file" \
"current_page_title" "$current_page_title" \
"relative_path_prefix" "$relative_path_prefix" \
"active_class_home" "$active_class_home" \
"active_class_worlds" "$active_class_worlds" \
"active_class_downloads" "$active_class_downloads" \
"CACHE_BUSTER" "$CACHE_BUSTER" \
"SITE_TITLE" "$SITE_TITLE"
cat "$header_file"
rm "$header_file"
}
generate_html_footer() {
local footer_file
footer_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/html_footer.template" "$footer_file" \
"CURRENT_YEAR" "$CURRENT_YEAR" \
"SITE_TITLE" "$SITE_TITLE" \
"SITE_OWNER_NAME" "$SITE_OWNER_NAME"
cat "$footer_file"
rm "$footer_file"
}

View file

@ -0,0 +1,25 @@
#!/bin/bash
# css_generator.sh - Erzeugt die zentrale CSS-Datei
generate_css() {
local css_file_path="${WEB_ROOT_PATH}/style.css"
local actual_banner_img_url_path="${FALLBACK_BANNER_IMG_URL}"
if [ -n "$STATIC_BANNER_FILENAME" ]; then
local banner_web_path="/images/${STATIC_BANNER_FILENAME}"
if [ -f "${WEB_ROOT_PATH}${banner_web_path}" ]; then
actual_banner_img_url_path="$banner_web_path"
log_message "Verwende statisches Banner: ${actual_banner_img_url_path}"
else
log_message "WARNUNG: Statisches Banner '${STATIC_BANNER_FILENAME}' nicht gefunden. Fallback: ${FALLBACK_BANNER_IMG_URL}"
fi
else
log_message "Kein STATIC_BANNER_FILENAME definiert, verwende Fallback: ${FALLBACK_BANNER_IMG_URL}"
fi
log_message "Erzeuge/Aktualisiere ${css_file_path}..."
render_template "${TEMPLATE_DIR_PATH}/css.template" "$css_file_path" \
"css_banner_image_path" "$actual_banner_img_url_path" \
"CACHE_BUSTER" "$CACHE_BUSTER"
if [ $? -ne 0 ]; then log_message "FEHLER bei der CSS-Generierung."; fi
}

View file

@ -0,0 +1,37 @@
#!/bin/bash
# main_orchestrator.sh - Ruft die Generatoren für alle weltspezifischen Seiten auf
generate_all_world_pages() {
log_message "Starte Generierung der Weltenübersicht und aller Detailseiten..."
generate_worlds_overview
local processed_world_count=0
shopt -s nullglob
local world_key_dirs=("${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/)
shopt -u nullglob
if [ ${#world_key_dirs[@]} -eq 0 ]; then
log_message "Keine Weltverzeichnisse für Detailseiten gefunden."
return
fi
for world_data_dir in "${world_key_dirs[@]}"; do
local world_key=$(basename "$world_data_dir")
if [ -z "$world_key" ]; then continue; fi
if [ -f "${world_data_dir}world.mt" ] && [ -f "${world_data_dir}web.conf" ]; then
# Platzhalter für zukünftige sync-Skripte sicherstellen
local web_world_dir="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${world_key}"
mkdir -p "$web_world_dir"
touch "${web_world_dir}/areas.txt"
touch "${web_world_dir}/players.txt"
touch "${web_world_dir}/weather.txt"
generate_world_detail_page "$world_key"
processed_world_count=$((processed_world_count + 1))
else
log_message "[${world_key}] WARNUNG: world.mt oder web.conf fehlt. Detailseite nicht erstellt."
fi
done
log_message "${processed_world_count} Welt-Detailseiten generiert/aktualisiert."
}

View file

@ -0,0 +1,53 @@
#!/bin/bash
# static_pages_generator.sh - Erzeugt alle einfachen, statischen Seiten
generate_homepage() {
local p="${WEB_ROOT_PATH}/index.html"
local c="${WEB_CONTENT_STATIC_PATH}/startseite_content.html"
{
generate_html_header "Willkommen" "." "home"
if [ -f "$c" ]; then cat "$c"; else echo "<h2>Willkommen!</h2>"; fi
generate_html_footer
} > "$p"
}
generate_impressum() {
local p="${WEB_ROOT_PATH}/impressum.html"
local c="${WEB_CONTENT_STATIC_PATH}/impressum_content.html"
{
generate_html_header "Impressum" "."
if [ -f "$c" ]; then cat "$c"; else echo "<h2>Impressum</h2><p>Betreiber: ${SITE_OWNER_NAME}<br>Kontakt: <a href='mailto:${SITE_OWNER_EMAIL}'>${SITE_OWNER_EMAIL}</a></p>"; fi
generate_html_footer
} > "$p"
}
generate_downloads_page() {
local p="${WEB_ROOT_PATH}/downloads.html"
local c="${WEB_CONTENT_STATIC_PATH}/downloads_content.html"
{
generate_html_header "Downloads" "." "downloads"
if [ -f "$c" ]; then cat "$c"; else cat >&1 <<EOF
<h2>Downloads</h2><p>Offizielle Seite: <a href="https://www.luanti.org/downloads/" target="_blank">luanti.org</a></p>
EOF
fi
generate_html_footer
} > "$p"
}
generate_datenschutz_page() {
local p="${WEB_ROOT_PATH}/datenschutz.html"
local c="${WEB_CONTENT_STATIC_PATH}/datenschutz_content.html"
{
generate_html_header "Datenschutz" "."
if [ -f "$c" ]; then cat "$c"; else echo "<h2>Datenschutzerklärung</h2><p>Platzhalter.</p>"; fi
generate_html_footer
} > "$p"
}
generate_static_pages() {
log_message "Generiere statische Seiten (Homepage, Impressum etc.)..."
generate_homepage
generate_impressum
generate_downloads_page
generate_datenschutz_page
}

View file

@ -0,0 +1,221 @@
#!/bin/bash
# world_detail_generator.sh - Erzeugt die komplexe Detailseite für eine einzelne Welt
generate_world_detail_page() {
local current_world_key="$1"
local detail_page_file="${WEB_ROOT_PATH}/world_${current_world_key}.html"
local current_minetest_world_path="${MINETESTMAPPER_WORLD_DATA_BASE_PATH}${current_world_key}"
local world_mt_file="${current_minetest_world_path}/world.mt"
local web_conf_file="${current_minetest_world_path}/web.conf"
if [ ! -f "$world_mt_file" ] || [ ! -f "$web_conf_file" ]; then log_message "[${current_world_key}] FEHLER: world.mt oder web.conf fehlt."; return 1; fi
log_message "[${current_world_key}] Starte Generierung der Detailseite..."
# --- DATEN SAMMELN ---
local gdal_zoom_levels; gdal_zoom_levels=$(get_config_value_from_file "$web_conf_file" "GDAL2TILES_ZOOM_LEVELS" "$DEFAULT_GDAL2TILES_ZOOM_LEVELS")
local min_zoom_val; min_zoom_val=$(echo "$gdal_zoom_levels" | cut -d- -f1)
local max_zoom_val; max_zoom_val=$(echo "$gdal_zoom_levels" | cut -d- -f2)
if [ -z "$max_zoom_val" ]; then max_zoom_val="$min_zoom_val"; fi
local resolutions_array=""
local num_zooms=$((max_zoom_val - min_zoom_val + 1))
for (( i=0; i<num_zooms; i++ )); do
local current_zoom_level=$((min_zoom_val + i))
local res=$((2**(max_zoom_val - current_zoom_level)))
resolutions_array+="${res},"
done
resolutions_array="[${resolutions_array%,}]"
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}"
# HINZUGEFÜGT: Hintergrundfarbe auslesen
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")
if [ -z "$WORLD_DISPLAY_NAME_PAGE" ]; then
WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$world_mt_file" "world_name" "Welt ${current_world_key}")
fi
local SERVER_ADDRESS_PAGE; SERVER_ADDRESS_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_ADDRESS" "$DEFAULT_SERVER_ADDRESS")
local SERVER_PORT_PAGE; SERVER_PORT_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_PORT" "$DEFAULT_SERVER_PORT")
local SERVER_ACCESS_INFO_PAGE; SERVER_ACCESS_INFO_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_ACCESS_INFO" "$DEFAULT_SERVER_ACCESS_INFO")
unset ADMIN_NAME ADMIN_SKIN_URL ADMIN_EMAIL ADMIN_DISCORD ADMIN_MATRIX ADMIN_STEAM ADMIN_TEAMSPEAK ADMIN_MUMBLE WORLD_LONG_DESCRIPTION WORLD_GAME_RULES
source "$web_conf_file"
local WORLD_LONG_DESCRIPTION_PAGE="${WORLD_LONG_DESCRIPTION:-$DEFAULT_WORLD_LONG_DESCRIPTION}"
local WORLD_GAME_RULES_PAGE="${WORLD_GAME_RULES:-}"
local MT_GAMEID="$DEFAULT_GAMEID"; local MT_ENABLE_DAMAGE="false"; local MT_CREATIVE_MODE="false"
declare -A parsed_mod_packs; declare -a parsed_standalone_mods
while IFS='=' read -r key value || [ -n "$key" ]; do
key=$(echo "$key"|xargs); value=$(echo "$value"|xargs)
case "$key" in
gameid) MT_GAMEID="$value" ;;
enable_damage) MT_ENABLE_DAMAGE="$value" ;;
creative_mode) MT_CREATIVE_MODE="$value" ;;
load_mod_*) local mod_id="${key#load_mod_}"; mod_id=$(echo "$mod_id"|xargs)
if [[ "$value" =~ ^mods/([^/]+)/.+ ]]; then local pack_n="${BASH_REMATCH[1]}"; parsed_mod_packs["$pack_n"]="${parsed_mod_packs[$pack_n]} ${mod_id}";
elif [[ "$value" =~ ^mods/([^/]+)$ ]]; then parsed_standalone_mods+=("$mod_id");
elif [ -n "$value" ]; then parsed_standalone_mods+=("$mod_id"); fi ;;
esac;
done < "$world_mt_file"
local MODS_HTML=""
if [ ${#parsed_mod_packs[@]} -gt 0 ] || [ ${#parsed_standalone_mods[@]} -gt 0 ]; then
MODS_HTML+="<ul class='mod-list'>"
local sorted_pack_names=(); if [ ${#parsed_mod_packs[@]} -gt 0 ]; then mapfile -t sorted_pack_names < <(printf '%s\n' "${!parsed_mod_packs[@]}" | sort); fi
for pack_name in "${sorted_pack_names[@]}"; do
MODS_HTML+="<li>+ <a href='https://content.luanti.org/packages/?q=${pack_name}' target='_blank' rel='noopener noreferrer'>${pack_name}</a><ul>"
local mods_in_pack_str="${parsed_mod_packs[$pack_name]}"
local sorted_mods_in_pack=(); if [ -n "$mods_in_pack_str" ]; then mapfile -t sorted_mods_in_pack < <(echo "$mods_in_pack_str" | tr ' ' '\n' | grep -v '^\s*$' | sort); fi
for mod_in_pack in "${sorted_mods_in_pack[@]}"; do
[ -n "$mod_in_pack" ] && MODS_HTML+="<li>- <a href='https://content.luanti.org/packages/?q=${mod_in_pack}' target='_blank' rel='noopener noreferrer'>${mod_in_pack}</a></li>"
done
MODS_HTML+="</ul></li>"
done
local sorted_standalone_mods=(); if [ ${#parsed_standalone_mods[@]} -gt 0 ]; then mapfile -t sorted_standalone_mods < <(printf '%s\n' "${parsed_standalone_mods[@]}" | sort); fi
for solo_mod in "${sorted_standalone_mods[@]}"; do
[ -n "$solo_mod" ] && MODS_HTML+="<li>- <a href='https://content.luanti.org/packages/?q=${solo_mod}' target='_blank' rel='noopener noreferrer'>${solo_mod}</a></li>"
done
MODS_HTML+="</ul>"
else
MODS_HTML="<p>Keine Mod-Informationen in world.mt gefunden.</p>"
fi
local ARCHIVE_HTML=""; local available_archive_dates_js_array="[]"; local -a available_archive_dates_bash=();
local CURRENT_ARCHIVE_SUBDIR_NAME=$(get_config_value_from_file "$web_conf_file" "ARCHIVE_SUBDIR_NAME" "$DEFAULT_ARCHIVE_SUBDIR_NAME")
local archive_scan_base_path="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_ARCHIVE_SUBDIR_NAME}";
if [ -d "$archive_scan_base_path" ]; then
shopt -s nullglob;
for y_d in "${archive_scan_base_path}"/*/; do
if [ -d "$y_d" ]; then
local y=$(basename "$y_d");
for m_d in "${y_d}"*/; do
if [ -d "$m_d" ]; then
local m=$(basename "$m_d");
for d_f in "${m_d}"*.png; do
if [ -f "$d_f" ]; then
local d=$(basename "$d_f" .png);
if [[ "$y" =~ ^[0-9]{4}$ && "$m" =~ ^[0-9]{2}$ && "$d" =~ ^[0-9]{2}$ ]]; then
available_archive_dates_bash+=("${y}-${m}-${d}");
fi;
fi;
done;
fi;
done;
fi;
done;
shopt -u nullglob;
fi;
if [ ${#available_archive_dates_bash[@]} -gt 0 ]; then
local sorted_dates_str; sorted_dates_str=$(printf '%s\n' "${available_archive_dates_bash[@]}" | sort -r);
local js_dates_temp_array=(); if [ -n "$sorted_dates_str" ]; then mapfile -t js_dates_temp_array < <(echo "$sorted_dates_str"); fi;
local js_array_content="";
if [ ${#js_dates_temp_array[@]} -gt 0 ]; then
for date_str_loop in "${js_dates_temp_array[@]}"; do
if [ -n "$date_str_loop" ]; then js_array_content+="\"${date_str_loop}\","; fi;
done;
js_array_content=${js_array_content%,};
fi;
available_archive_dates_js_array="[${js_array_content}]";
fi
local temp_archive_html_file; temp_archive_html_file=$(mktemp)
local archive_world_rel_to_webroot="/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_ARCHIVE_SUBDIR_NAME}"
render_template "${TEMPLATE_DIR_PATH}/world_detail_archiv.template" "$temp_archive_html_file" \
"current_world_key" "$current_world_key" \
"available_archive_dates_js_array" "$available_archive_dates_js_array" \
"archive_world_rel_to_webroot" "$archive_world_rel_to_webroot" \
"CACHE_BUSTER" "$CACHE_BUSTER"
ARCHIVE_HTML=$(<"$temp_archive_html_file")
rm "$temp_archive_html_file"
local ADMIN_BOXES_HTML=""
local num_admins=${#ADMIN_NAME[@]}
if [ $num_admins -eq 0 ]; then
ADMIN_BOXES_HTML="<div class='admin-box'><div class='admin-contact-block'><img src='/${SITE_OWNER_ADMIN_SKIN_URL:-$DEFAULT_PLAYER_SKIN_URL}?v=${CACHE_BUSTER}' alt='Admin Skin' class='admin-skin-icon'><div class='admin-text-details'><p><strong class='admin-player-name'>${SITE_OWNER_NAME}</strong></p><div class='contact-links'><a href='mailto:${SITE_OWNER_EMAIL}' class='contact-button' data-title='E-Mail: ${SITE_OWNER_EMAIL}'>Email</a></div></div></div></div>"
else
for i in "${!ADMIN_NAME[@]}"; do
local name="${ADMIN_NAME[$i]}"; local skin_url="${ADMIN_SKIN_URL[$i]:-$DEFAULT_PLAYER_SKIN_URL}"; local email="${ADMIN_EMAIL[$i]}"; local discord="${ADMIN_DISCORD[$i]}"; local matrix="${ADMIN_MATRIX[$i]}"; local steam="${ADMIN_STEAM[$i]}"; local teamspeak="${ADMIN_TEAMSPEAK[$i]}"; local mumble="${ADMIN_MUMBLE[$i]}"
local contact_links_html=""
[ -n "$email" ] && contact_links_html+="<a href='mailto:${email}' class='contact-button' data-title='E-Mail: ${email}'>Email</a>"
[ -n "$discord" ] && contact_links_html+="<a href='https://discordapp.com/users/${discord}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Discord: ${discord}'>Discord</a>"
[ -n "$matrix" ] && contact_links_html+="<a href='https://matrix.to/#/${matrix}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Matrix: ${matrix}'>Matrix</a>"
[ -n "$steam" ] && contact_links_html+="<a href='${steam}' target='_blank' rel='noopener noreferrer' class='contact-button' data-title='Steam Profil'>Steam</a>"
[ -n "$teamspeak" ] && contact_links_html+="<a href='ts3server://${teamspeak}' class='contact-button' data-title='Teamspeak: ${teamspeak}'>Teamspeak</a>"
[ -n "$mumble" ] && contact_links_html+="<a href='mumble://${mumble}' class='contact-button' data-title='Mumble: ${mumble}'>Mumble</a>"
ADMIN_BOXES_HTML+="<div class='admin-box'><div class='admin-contact-block'><img src='/${skin_url}?v=${CACHE_BUSTER}' alt='Admin Skin ${name}' class='admin-skin-icon' onerror=\"this.onerror=null;this.src='/${DEFAULT_PLAYER_SKIN_URL}?v=${CACHE_BUSTER}';\"><div class='admin-text-details'><p><strong class='admin-player-name'>${name}</strong></p></div></div>"; if [ -n "$contact_links_html" ]; then ADMIN_BOXES_HTML+="<div class='contact-links'>${contact_links_html}</div>"; fi; ADMIN_BOXES_HTML+="</div>"
done
fi
local web_unknown_nodes_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/unknown_nodes.txt"
local unknown_nodes_abs_path="${WEB_ROOT_PATH}/${web_unknown_nodes_rel_path}"
local map_sub_info_link=""
if [ -f "$unknown_nodes_abs_path" ]; then
map_sub_info_link="<span class='map-file-link'><a href='/${web_unknown_nodes_rel_path}?v=${CACHE_BUSTER}' target='_blank'>Fehlende Map-Nodes</a></span>"
else
map_sub_info_link="<span class='map-file-link'>&nbsp;</span>"
fi
local creative_text="Aus"; local creative_text_class="offline"; if [ "$MT_CREATIVE_MODE" = "true" ]; then creative_text="An"; creative_text_class="online"; fi
local damage_text="Aus"; local damage_text_class="online"; if [ "$MT_ENABLE_DAMAGE" = "true" ]; then damage_text="An"; damage_text_class="offline"; fi
# --- TEMPLATES RENDERN ---
local temp_radar_file; temp_radar_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/world_detail_radar.template" "$temp_radar_file" \
"current_world_key" "$current_world_key" \
"map_sub_info_link" "$map_sub_info_link" \
"web_last_update_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt" \
"CACHE_BUSTER" "$CACHE_BUSTER" \
"ARCHIVE_HTML" "$ARCHIVE_HTML" \
"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" \
"map_background_color" "$map_background_color"
local WORLD_RADAR_HTML; WORLD_RADAR_HTML=$(<"$temp_radar_file")
rm "$temp_radar_file"
local temp_playerlist_file; temp_playerlist_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/world_detail_playerlist.template" "$temp_playerlist_file" \
"current_world_key" "$current_world_key" \
"DEFAULT_PLAYER_SKIN_URL" "$DEFAULT_PLAYER_SKIN_URL" \
"CACHE_BUSTER" "$CACHE_BUSTER"
local WORLD_PLAYERLIST_HTML; WORLD_PLAYERLIST_HTML=$(<"$temp_playerlist_file")
rm "$temp_playerlist_file"
local temp_body_file; temp_body_file=$(mktemp)
render_template "${TEMPLATE_DIR_PATH}/world_detail_page.template" "$temp_body_file" \
"WORLD_DISPLAY_NAME_PAGE" "$WORLD_DISPLAY_NAME_PAGE" \
"current_world_key" "$current_world_key" \
"STATUS_TEXT_FALLBACK_PAGE" "$DEFAULT_SERVER_STATUS_TEXT_FALLBACK" \
"MT_GAMEID" "$MT_GAMEID" \
"creative_text_class" "$creative_text_class" \
"creative_text" "$creative_text" \
"damage_text_class" "$damage_text_class" \
"damage_text" "$damage_text" \
"SERVER_ADDRESS_PAGE" "$SERVER_ADDRESS_PAGE" \
"SERVER_PORT_PAGE" "$SERVER_PORT_PAGE" \
"SERVER_ACCESS_INFO_PAGE" "$SERVER_ACCESS_INFO_PAGE" \
"ADMIN_BOXES_HTML" "$ADMIN_BOXES_HTML" \
"WORLD_LONG_DESCRIPTION_PAGE" "$WORLD_LONG_DESCRIPTION_PAGE" \
"WORLD_GAME_RULES_PAGE" "$WORLD_GAME_RULES_PAGE" \
"MODS_HTML" "$MODS_HTML" \
"WORLD_RADAR_HTML" "$WORLD_RADAR_HTML" \
"WORLD_PLAYERLIST_HTML" "$WORLD_PLAYERLIST_HTML" \
"web_online_status_rel_path" "${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/online_status.txt" \
"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" \
"CACHE_BUSTER" "$CACHE_BUSTER"
{
generate_html_header "Welt: ${WORLD_DISPLAY_NAME_PAGE}" "."
cat "$temp_body_file"
generate_html_footer
} > "$detail_page_file"
rm "$temp_body_file"
log_message "[${current_world_key}] Detailseite erstellt."
}

View file

@ -0,0 +1,85 @@
#!/bin/bash
# world_overview_generator.sh - Erzeugt die Welten-Übersichtsseite
generate_worlds_overview() {
local overview_file="${WEB_ROOT_PATH}/worlds.html"
log_message "Erzeuge Weltenübersicht: ${overview_file}..."
{
generate_html_header "Weltenübersicht" "." "worlds"
echo "<h2>Unsere Welten</h2>"
} > "$overview_file"
local discovered_worlds_count=0
shopt -s nullglob
local world_key_dirs=("${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/)
shopt -u nullglob
if [ ${#world_key_dirs[@]} -eq 0 ]; then
log_message "WARNUNG: Keine Welt-Verzeichnisse in ${MINETESTMAPPER_WORLD_DATA_BASE_PATH}."
echo "<p>Keine Welten verfügbar.</p>" >> "$overview_file"
else
local overview_entry_template_path="${TEMPLATE_DIR_PATH}/worlds_overview_entry.template"
for world_data_dir_loop_overview in "${world_key_dirs[@]}"; do
local current_world_key
current_world_key=$(basename "$world_data_dir_loop_overview")
local world_mt_file="${world_data_dir_loop_overview}world.mt"
local web_conf_file="${world_data_dir_loop_overview}web.conf"
if [ ! -f "$world_mt_file" ] || [ ! -f "$web_conf_file" ]; then continue; fi
local world_display_name_ov
world_display_name_ov=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME")
local world_short_desc_ov
world_short_desc_ov=$(get_config_value_from_file "$web_conf_file" "WORLD_SHORT_DESCRIPTION" "$DEFAULT_WORLD_SHORT_DESCRIPTION")
unset ADMIN_NAME
source "$web_conf_file" &>/dev/null
local admin_name_ov
if [ ${#ADMIN_NAME[@]} -gt 0 ]; then admin_name_ov=$(IFS=', '; echo "${ADMIN_NAME[*]}"); else admin_name_ov="$SITE_OWNER_NAME"; fi
local current_map_png_filename_for_preview_ov
current_map_png_filename_for_preview_ov=$(get_config_value_from_file "$web_conf_file" "WEB_MAP_PNG_FILENAME" "$DEFAULT_WEB_MAP_PNG_FILENAME")
if [ -z "$world_display_name_ov" ]; then
local mt_world_name_from_mt_ov
mt_world_name_from_mt_ov=$(grep -E "^\s*world_name\s*=" "$world_mt_file" | tail -n 1 | cut -d'=' -f2 | xargs)
if [ -n "$mt_world_name_from_mt_ov" ]; then world_display_name_ov="$mt_world_name_from_mt_ov";
else world_display_name_ov="Welt ${current_world_key}"; fi
fi
local detail_page_filename="world_${current_world_key}.html"
local preview_img_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${current_map_png_filename_for_preview_ov}"
local preview_img_abs_path="${WEB_ROOT_PATH}/${preview_img_rel_path}"
local online_status_text="offline"; local online_status_class="offline"
local status_file_for_overview="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/online_status.txt"
if [ -f "$status_file_for_overview" ]; then
local status_line_overview
status_line_overview=$(cat "$status_file_for_overview")
if [[ "$status_line_overview" == "online"* ]]; then online_status_text="online"; online_status_class="online"; fi
fi
local preview_img_html
if [ -f "$preview_img_abs_path" ]; then
preview_img_html="<img src='${preview_img_rel_path}?v=${CACHE_BUSTER}' alt='Vorschau ${world_display_name_ov}'>"
else
preview_img_html="<img src='images/placeholder_map.png?v=${CACHE_BUSTER}' alt='Keine Vorschau für ${world_display_name_ov}'>"
fi
local temp_overview_entry_file
temp_overview_entry_file=$(mktemp)
render_template "$overview_entry_template_path" "$temp_overview_entry_file" \
"detail_page_filename" "$detail_page_filename" \
"preview_img_html" "$preview_img_html" \
"world_display_name_ov" "$world_display_name_ov" \
"online_status_class" "$online_status_class" \
"online_status_text" "$online_status_text" \
"admin_name_ov" "$admin_name_ov" \
"world_short_desc_ov" "$world_short_desc_ov"
cat "$temp_overview_entry_file" >> "$overview_file"
rm "$temp_overview_entry_file"
discovered_worlds_count=$((discovered_worlds_count + 1))
done
if [ "$discovered_worlds_count" -eq 0 ]; then echo "<p>Keine Welten mit gültiger Konfiguration gefunden.</p>" >> "$overview_file"; fi
fi
generate_html_footer >> "$overview_file"
}

View file

@ -100,7 +100,14 @@ a.world-preview:hover {
.page-nav-buttons { text-align: right; margin-top: 10px; margin-bottom: 15px; }
.page-nav-buttons .button { background-color: #555; color: #FFA500; border: 1px solid #666; padding: 5px 10px; margin-left: 5px; margin-bottom: 5px; border-radius: 3px; text-decoration: none; font-size: 0.9em; display: inline-block; }
.page-nav-buttons .button:hover { background-color: #666; color: #FFC500; }
.leaflet-map { width: 100%; height: 600px; border: 1px solid #666; margin-bottom: 5px; border-radius: 3px; background-color: #111;}
/* KORREKTUR: Klasse umbenannt von .leaflet-map zu .map-view-container */
.map-view-container {
width: 100%;
height: 600px;
border: 1px solid #666;
margin-bottom: 5px;
border-radius: 3px;
}
.map-sub-info { display: flex; justify-content: space-between; align-items: center; font-size: 0.9em; color: #aaa; margin-bottom: 20px; padding: 5px 0; }
.map-sub-info .map-file-link { text-align: left; }
.map-sub-info .map-last-update { text-align: right; }
@ -119,7 +126,7 @@ a.world-preview:hover {
padding: 15px;
border-radius: 5px;
border: 1px solid #4a4a4a;
flex-grow: 0; /* KORREKTUR: Verhindert das Strecken der letzten Box */
flex-grow: 0;
flex-shrink: 1;
flex-basis: calc(50% - 8px);
box-sizing: border-box;
@ -181,7 +188,7 @@ a.world-preview:hover {
border: 1px solid #555;
border-radius: 5px;
padding: 10px;
width: calc((100% - 30px) / 3); /* KORREKTUR: Max 3 Boxen pro Zeile */
width: calc((100% - 30px) / 3);
box-sizing: border-box;
display: flex;
flex-direction: column;
@ -429,57 +436,6 @@ a.read-more-link {
display: block;
}
/* KORREKTUR: Leaflet Fullscreen Button Icon */
.leaflet-control-fullscreen a {
background: #fff url('data:image/svg+xml;charset=utf8,%3Csvg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M2 9H0v7h7v-2H2V9zM0 7h2V2h5V0H0v7zm14 2v5h-5v2h7V9h-2zm0-2V0h-7v2h5v5h2z" fill="%23333"/%3E%3C/svg%3E') no-repeat center center;
background-size: 16px 16px;
font-size: 0; /* Versteckt fehlerhafte Text-Icons */
}
.leaflet-control-fullscreen a.leaflet-fullscreen-on {
background-image: url('data:image/svg+xml;charset=utf8,%3Csvg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"%3E%3Cpath d="M0 11h5v5h2V9H0v2zm11 5h5V9h-2v5h-3v2zM5 0H0v7h2V2h3V0zm9 2V0h-2v7h7V5h-5z" fill="%23333"/%3E%3C/svg%3E');
}
/* KORREKTUR: Stile für Marker-Popup */
.leaflet-popup-content-wrapper, .leaflet-popup-content {
background-color: #303030 !important;
color: #ddd !important;
border-radius: 5px;
}
.leaflet-popup-content {
margin: 0 !important;
padding: 0 !important;
line-height: 1.5;
}
.leaflet-container a.leaflet-popup-close-button {
color: #ddd;
padding: 8px 8px 0 0;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #fff;
background-color: transparent;
}
.popup-player-box {
padding: 10px;
}
.popup-player-box .player-name {
font-size: 1.2em; /* Schriftgröße für Namen im Popup erhöht */
}
.popup-player-vitals {
display: flex;
justify-content: space-around;
gap: 10px;
align-items: center;
margin-top: 8px;
}
.popup-player-vitals .vital {
display: flex;
align-items: center;
gap: 5px;
}
.popup-player-vitals .icon {
font-size: 1.2em;
}
@media (max-width: 768px) {
/* Responsive Stile für Burger-Menü */
.responsive-nav {
@ -525,7 +481,7 @@ a.read-more-link {
margin-right: 0;
margin-bottom: 15px;
}
.leaflet-map {
.map-view-container { /* KORREKTUR: Klasse umbenannt */
height: auto;
aspect-ratio: 4 / 3;
max-height: 70vh;
@ -535,3 +491,66 @@ a.read-more-link {
.player-box { width: 100%; } /* 1 Spalte auf Handys */
.admin-box { flex-basis: 100%; } /* 1 Spalte auf Handys */
}
/* 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 */
.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;}
.popup-player-box .player-name-status { display: flex; align-items: center; }
.popup-player-box .player-name { font-weight: bold; font-size: 1.2em; color: #FFD700; margin-left: 5px; }
.popup-player-box .privilege-separator { height: 1px; background-color: #555; border: 0; margin: 8px 0; }
.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; }

View file

@ -5,16 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>%%current_page_title%% - %%SITE_TITLE%%</title>
<link rel="stylesheet" href="%%relative_path_prefix%%/style.css?v=%%CACHE_BUSTER%%">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link href='https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/leaflet.fullscreen.css' rel='stylesheet' />
<script src='https://api.mapbox.com/mapbox.js/plugins/leaflet-fullscreen/v1.0.1/Leaflet.fullscreen.min.js'></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.css" type="text/css">
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const scrollToTopBtn = document.getElementById('scrollToTopBtn');
// Scroll-Event nur für den "Nach oben"-Button
if (scrollToTopBtn) {
window.addEventListener('scroll', function() {
window.requestAnimationFrame(function() {
@ -25,14 +22,9 @@
}
});
});
// Klick-Event für den Scroll-To-Top Button
scrollToTopBtn.addEventListener('click', function(e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
window.scrollTo({ top: 0, behavior: 'smooth' });
});
}
});

View file

@ -0,0 +1,70 @@
<div class="archive-controls">
<label for="archive-date-selector-%%current_world_key%%">Archivdatum:</label>
<select id="archive-date-selector-%%current_world_key%%">
<option value="">-- Bitte wählen --</option>
</select>
<span id="archive-error-msg-%%current_world_key%%" class="archive-error"></span>
</div>
<div class="archive-image-container">
<img id="archive-image-%%current_world_key%%" alt="Archivierte Karte" style="display:none; max-width:100%;">
<p id="archive-placeholder-%%current_world_key%%">Bitte ein Datum auswählen, um die Karte anzuzeigen.</p>
</div>
<script>
// Um zu verhindern, dass das Skript mehrfach ausgeführt wird
if (!window.archiveScriptLoaded_%%current_world_key%%) {
window.archiveScriptLoaded_%%current_world_key%% = true;
const selector = document.getElementById('archive-date-selector-%%current_world_key%%');
const imageElement = document.getElementById('archive-image-%%current_world_key%%');
const placeholder = document.getElementById('archive-placeholder-%%current_world_key%%');
const errorMessage = document.getElementById('archive-error-msg-%%current_world_key%%');
const availableDates = %%available_archive_dates_js_array%%;
const imageBaseUrl = '%%archive_world_rel_to_webroot%%';
if (selector && availableDates && availableDates.length > 0) {
availableDates.forEach(dateStr => {
const option = document.createElement('option');
const parts = dateStr.split('-');
option.value = dateStr;
option.textContent = `${parts[2]}.${parts[1]}.${parts[0]}`;
selector.appendChild(option);
});
selector.addEventListener('change', function() {
imageElement.style.display = 'none';
errorMessage.textContent = '';
const selectedDate = this.value;
if (selectedDate) {
// WICHTIGE ÄNDERUNG: Platzhalter wird jetzt hier versteckt
placeholder.style.display = 'none';
const dateParts = selectedDate.split('-');
const imageUrl = `${imageBaseUrl}/${dateParts[0]}/${dateParts[1]}/${dateParts[2]}.png?v=%%CACHE_BUSTER%%`;
imageElement.src = imageUrl;
} else {
// Wenn "-- Bitte wählen --" ausgewählt wird, Platzhalter wieder anzeigen
placeholder.style.display = 'block';
}
});
imageElement.onload = function() {
this.style.display = 'block';
errorMessage.textContent = ''; // Sicherstellen, dass keine alte Fehlermeldung mehr da ist
};
imageElement.onerror = function() {
this.style.display = 'none';
// WICHTIGE ÄNDERUNG: Fehlermeldung ersetzt jetzt den Platzhalter
placeholder.style.display = 'none';
errorMessage.textContent = 'Fehler: Bild für dieses Datum konnte nicht geladen werden.';
};
} else if (selector) {
selector.disabled = true;
selector.querySelector('option').textContent = 'Keine Archive verfügbar';
}
}
</script>

View file

@ -1,18 +1,11 @@
<div class='page-title-container'>
<h2 class='world-detail-title'>%%WORLD_DISPLAY_NAME_PAGE%%</h2>
</div>
<div class="close-button-container">
<a href="worlds.html" class="close-button" title="Zurück zur Weltenübersicht"><b>X</b></a>
</div>
<div class="responsive-nav">
<button id="burger-menu-toggle" class="burger-menu" aria-label="Menü öffnen/schließen">
<span></span>
<span></span>
<span></span>
</button>
<button id="burger-menu-toggle" class="burger-menu" aria-label="Menü öffnen/schließen"><span></span><span></span><span></span></button>
<div class='page-nav-buttons' id="page-nav-buttons-container-%%current_world_key%%">
<a href='#server-info' class='button'>Server-Info</a>
<a href='#server-verbindung' class='button'>Verbindung</a>
@ -25,13 +18,13 @@
</div>
</div>
<div id='server-info' class='info-box server-details'>
<h3>Server-Info</h3>
<p><strong>Status:</strong> <span class='status-dot' id='status-dot-%%current_world_key%%'></span><span id='world-status-%%current_world_key%%' class='status-text status-loading'>%%STATUS_TEXT_FALLBACK_PAGE%%</span></p>
<p><strong>Spiel:</strong> <a href='https://content.luanti.org/packages/?type=game&q=%%MT_GAMEID%%' target='_blank' rel='noopener noreferrer'>%%MT_GAMEID%%</a></p>
<p><strong>Kreativmodus:</strong> <span class='status-text-colored %%creative_text_class%%'>%%creative_text%%</span></p>
<p><strong>Schaden:</strong> <span class='status-text-colored %%damage_text_class%%'>%%damage_text%%</span></p>
<div id="weather-info-%%current_world_key%%"></div>
</div>
<div id='server-verbindung' class='info-box server-details'>
@ -43,44 +36,50 @@
<div id='welt-admin' class='admin-section'>
<h3>Welt-Admin</h3>
<div class='admin-grid'>
%%ADMIN_BOXES_HTML%%
<div class='admin-grid'>%%ADMIN_BOXES_HTML%%</div>
</div>
<div id="description-text-wrapper">
<h3 id='beschreibung'>Beschreibung</h3>
<div id="description-text" class="collapsible-text">%%WORLD_LONG_DESCRIPTION_PAGE%%</div>
<div class="read-more-container"><a href="#" class="read-more-link" data-target="description-text">weiterlesen</a></div>
</div>
<div id="gamerules-section">
<h3 id='spielregeln'>Spielregeln</h3>
<div id="gamerules-text-wrapper">
<div id="gamerules-text" class="collapsible-text">%%WORLD_GAME_RULES_PAGE%%</div>
<div class="read-more-container"><a href="#" class="read-more-link" data-target="gamerules-text">weiterlesen</a></div>
</div>
</div>
<script>
// === Globale Variablen für Spielerliste und Karte ===
let map_%%current_world_key%%;
let playerMarkers_%%current_world_key%% = {};
let masterPlayerData_%%current_world_key%% = {};
let playerFilters_%%current_world_key%% = {
activeOnly: false,
privileges: new Set()
};
%%WORLD_RADAR_HTML%%
// === Hilfsfunktionen ===
function copyToClipboard(elementId) { const el = document.getElementById(elementId); if(el) { navigator.clipboard.writeText(el.innerText || el.textContent).then(() => { const btn = el.nextElementSibling; if(btn && btn.classList.contains('copy-button')) { const orig_btn_text = btn.innerHTML; btn.innerHTML = '✓'; setTimeout(() => { btn.innerHTML = orig_btn_text; }, 1500); } else { alert('Kopiert: ' + el.innerText); } }).catch(err => console.error('Fehler Kopieren: ', err));}}
<h3 id='mods'>Verwendete Mods</h3>
<div class='scrollable-mod-list'>%%MODS_HTML%%</div>
%%WORLD_PLAYERLIST_HTML%%
<script>
// === Globale Hilfsfunktionen & Datenabruf ===
function copyToClipboard(elementId) { const el = document.getElementById(elementId); if(el) { navigator.clipboard.writeText(el.innerText || el.textContent).then(() => { const btn = el.nextElementSibling; if(btn && btn.classList.contains('copy-button')) { const orig_btn_text = btn.innerHTML; btn.innerHTML = '✓'; setTimeout(() => { btn.innerHTML = orig_btn_text; }, 1500); } }).catch(err => console.error('Fehler Kopieren: ', err));}}
function formatTimestampForDisplay(epochSeconds) { if (!epochSeconds || epochSeconds == 0) return 'unbekannt'; const date = new Date(epochSeconds * 1000); return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit'}) + ' Uhr';}
// === Status- und Datenabruf ===
function fetchWorldStatus_%%current_world_key%%() {
const statusTextEl = document.getElementById('world-status-%%current_world_key%%');
const statusDotEl = document.getElementById('status-dot-%%current_world_key%%');
if (!statusTextEl || !statusDotEl) return;
const onlineStatusUrl = '/%%web_online_status_rel_path%%?v=%%CACHE_BUSTER%%&t=' + new Date().getTime();
const lastUpdateUrl = '/%%web_last_update_rel_path%%?v=%%CACHE_BUSTER%%&t=' + new Date().getTime();
Promise.all([
fetch(onlineStatusUrl).then(res => res.text()),
fetch(lastUpdateUrl).then(res => res.text())
]).then(([onlineStatusContent, lastUpdateContent]) => {
Promise.all([fetch(onlineStatusUrl).then(res => res.text()), fetch(lastUpdateUrl).then(res => res.text())])
.then(([onlineStatusContent, lastUpdateContent]) => {
const dateObject = new Date(lastUpdateContent.trim());
const lastUpdateEpoch = !isNaN(dateObject.getTime()) ? Math.floor(dateObject.getTime() / 1000) : NaN;
const nowInSeconds = Math.floor(Date.now() / 1000);
if (!isNaN(lastUpdateEpoch) && (nowInSeconds - lastUpdateEpoch > 900)) {
statusDotEl.className = 'status-dot unknown';
statusTextEl.className = 'status-text status-unknown';
const formattedTime = formatTimestampForDisplay(lastUpdateEpoch);
statusTextEl.textContent = 'Unbekannt (seit: ' + formattedTime + ')';
statusTextEl.textContent = 'Unbekannt (seit: ' + formatTimestampForDisplay(lastUpdateEpoch) + ')';
return;
}
const onlineStatusParts = onlineStatusContent.split(' - ');
@ -96,7 +95,6 @@ function fetchWorldStatus_%%current_world_key%%() {
statusTextEl.textContent = 'Offline (seit: ' + offlineSince + ')';
}
}).catch(error => {
console.error('Fehler beim Abrufen des Welt-Status (%%current_world_key%%):', error);
statusDotEl.className = 'status-dot offline';
statusTextEl.className = 'status-text status-offline';
statusTextEl.textContent = 'Status nicht abrufbar';
@ -105,242 +103,38 @@ function fetchWorldStatus_%%current_world_key%%() {
function fetchDataForElement(elementId, filePath, isRawText, prefixText, suffixText, isJsonPlayerList) {
const el = document.getElementById(elementId);
if (!el) { return; }
if (!el && !isJsonPlayerList) return;
fetch('/' + filePath + '?v=%%CACHE_BUSTER%%&t=' + new Date().getTime())
.then(r => { if (!r.ok) throw new Error('Datei ' + filePath + ' nicht erreichbar (' + r.status + ')'); return r.text(); })
.then(t => {
let content = "";
if (t.trim() === "") {
content = (elementId.startsWith("map-last-update") ? "<em>unbekannt</em>" : "<em>Keine Daten verfügbar.</em>");
if (isJsonPlayerList) {
masterPlayerData_%%current_world_key%% = {};
updatePlayerMarkers_%%current_world_key%%(masterPlayerData_%%current_world_key%%);
applyPlayerFiltersAndRender_%%current_world_key%%();
if (isJsonPlayerList && window.playerListLogic_%%current_world_key%%) {
window.playerListLogic_%%current_world_key%%.masterPlayerData = {};
window.playerListLogic_%%current_world_key%%.updatePlayerMarkers({});
window.playerListLogic_%%current_world_key%%.applyPlayerFiltersAndRender();
}
} else {
if (isJsonPlayerList) {
if (isJsonPlayerList && window.playerListLogic_%%current_world_key%%) {
try {
masterPlayerData_%%current_world_key%% = JSON.parse(t);
updatePlayerMarkers_%%current_world_key%%(masterPlayerData_%%current_world_key%%);
applyPlayerFiltersAndRender_%%current_world_key%%();
} catch (e) {
console.error("Fehler beim Parsen der Spielerdaten:", e);
}
const playerData = JSON.parse(t);
const logic = window.playerListLogic_%%current_world_key%%;
logic.masterPlayerData = playerData;
logic.updatePlayerMarkers(playerData);
logic.applyPlayerFiltersAndRender();
} catch (e) { console.error("Fehler beim Parsen der Spielerdaten:", e); }
return;
} else if (isRawText) {
const dv = document.createElement('div');
dv.textContent = t;
content = dv.innerHTML.replace(/\n|\r\n|\r/g, '<br>');
} else {
content = t.replace(/\n|\r\n|\r/g, '<br>');
} else { content = isRawText ? t.replace(/\n/g, '<br>') : t; }
}
}
el.innerHTML = (prefixText ? prefixText : '') + content + (suffixText ? suffixText : '');
if(el) { el.innerHTML = (prefixText || '') + content + (suffixText || ''); }
}).catch(e => {
console.error('Fehler Laden ' + filePath + ':', e);
el.innerHTML = (prefixText ? prefixText : '') + '<em>nicht abrufbar</em>' + (suffixText ? suffixText : '');
if(el) { el.innerHTML = (prefixText || '') + '<em>nicht abrufbar</em>' + (suffixText || ''); }
});
}
// === Spieler- und Marker-Logik ===
function convertMinetestToLeaflet_%%current_world_key%%(posX, posZ) {
const mapWidth = %%MAP_WIDTH%%;
const mapHeight = %%MAP_HEIGHT%%;
const minX = %%MAP_EXTENT_MIN_X%%;
const minZ = %%MAP_EXTENT_MIN_Z%%;
const extentWidth = %%MAP_EXTENT_WIDTH%%;
const extentHeight = %%MAP_EXTENT_HEIGHT%%;
if (mapWidth === 0 || extentWidth === 0) return null;
const percentX = (posX - minX) / extentWidth;
const percentZ = (posZ - minZ) / extentHeight;
const pixelX = percentX * mapWidth;
const pixelY_from_bottom = percentZ * mapHeight;
const pixelY_from_top = mapHeight - pixelY_from_bottom;
return L.latLng(-pixelY_from_top, pixelX);
}
function updatePlayerMarkers_%%current_world_key%%(playerData) {
if (!map_%%current_world_key%%) return;
const adminIcon = L.icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-yellow.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] });
const playerIcon = L.icon({ iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-grey.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] });
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
const playersOnMap = new Set();
for (const id in playerData) {
const p = playerData[id];
playersOnMap.add(p.name);
const latLng = convertMinetestToLeaflet_%%current_world_key%%(p.posX, p.posZ);
if (!latLng) continue;
const isAdmin = (p.privilege || '').includes('server');
const icon = isAdmin ? adminIcon : playerIcon;
let lastLoginFormatted = 'unbekannt';
let statusDotClass = 'offline';
if (p.last_login) {
if ((now_epoch - p.last_login < twentyFourHoursInSeconds)) { statusDotClass = 'online'; }
lastLoginFormatted = formatTimestampForDisplay(p.last_login);
}
const popupContent = `
<div class='popup-player-box'>
<div class="player-header">
<div class="player-identity">
<img src="/images/players/${encodeURIComponent(p.name)}.png?v=%%CACHE_BUSTER%%" class="player-icon" alt="${p.name}" onerror="this.onerror=null;this.src='/%%DEFAULT_PLAYER_SKIN_URL%%?v=%%CACHE_BUSTER%%';">
<span class="player-name-status">
<span class="status-dot ${statusDotClass}" title="Letzter Login: ${lastLoginFormatted}"></span>
<strong class="player-name">${p.name}</strong>
</span>
</div>
</div>
<hr class="privilege-separator">
<div class="popup-player-vitals">
<span class="vital"><span class="icon">❤️</span> ${p.hp !== undefined ? p.hp : '?'}</span>
<span class="vital"><span class="icon">💨</span> ${p.breath !== undefined ? p.breath : '?'}</span>
<span class="vital"><span class="icon">🍖</span> ${p.stamina !== undefined ? p.stamina : '?'}</span>
</div>
</div>`;
if (playerMarkers_%%current_world_key%%[p.name]) {
const marker = playerMarkers_%%current_world_key%%[p.name];
marker.setLatLng(latLng);
marker.setIcon(icon);
marker.setPopupContent(popupContent);
} else {
const newMarker = L.marker(latLng, { icon: icon }).bindPopup(popupContent, { minWidth: 240 });
newMarker.addTo(map_%%current_world_key%%);
playerMarkers_%%current_world_key%%[p.name] = newMarker;
}
}
for (const name in playerMarkers_%%current_world_key%%) {
if (!playersOnMap.has(name)) {
map_%%current_world_key%%.removeLayer(playerMarkers_%%current_world_key%%[name]);
delete playerMarkers_%%current_world_key%%[name];
}
}
}
function applyPlayerFiltersAndRender_%%current_world_key%%() {
const filteredData = {};
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
for (const id in masterPlayerData_%%current_world_key%%) {
const player = masterPlayerData_%%current_world_key%%[id];
let passesFilter = true;
if (playerFilters_%%current_world_key%%.activeOnly) {
const isActive = player.last_login && (now_epoch - player.last_login < twentyFourHoursInSeconds);
if (!isActive) { passesFilter = false; }
}
if (passesFilter && playerFilters_%%current_world_key%%.privileges.size > 0) {
const playerPrivs = new Set(player.privilege ? player.privilege.toLowerCase().split(',') : []);
for (const requiredPriv of playerFilters_%%current_world_key%%.privileges) {
if (!playerPrivs.has(requiredPriv)) {
passesFilter = false;
break;
}
}
}
if (passesFilter) { filteredData[id] = player; }
}
const playerListContainer = document.getElementById('player-info-%%current_world_key%%');
if(playerListContainer) { playerListContainer.innerHTML = renderPlayerListHTML_%%current_world_key%%(filteredData); }
}
function renderPlayerListHTML_%%current_world_key%%(playerData) {
if (!playerData || Object.keys(playerData).length === 0) {
return "<p><em>Keine Spieler entsprechen den aktuellen Filtereinstellungen.</em></p>";
}
let html = "<div class='player-list-grid'>";
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
for (const id in playerData) {
const p = playerData[id];
const privs = p.privilege ? p.privilege.toLowerCase().split(',') : [];
const hasServerPriv = privs.includes("server");
const playerBoxClass = "player-box" + (hasServerPriv ? " has-server-priv" : "");
let statusDotClass = "offline";
let lastLoginFormatted = 'unbekannt';
if (p.last_login) {
if ((now_epoch - p.last_login < twentyFourHoursInSeconds)) { statusDotClass = "online"; }
lastLoginFormatted = formatTimestampForDisplay(p.last_login);
}
const hasInteract = privs.includes('interact'); const hasShout = privs.includes('shout'); const hasFast = privs.includes('fast'); const hasFly = privs.includes('fly'); const hasGive = privs.includes('give'); const hasTeleport = privs.includes('teleport'); const hasAreas = privs.includes('areas'); const hasNoclip = privs.includes('noclip'); const hasSettime = privs.includes('settime'); const hasWeather = privs.includes('weather');
html += `
<div class="${playerBoxClass}">
<div class="player-header">
<div class="player-identity">
<img src="/images/players/${encodeURIComponent(p.name)}.png?v=%%CACHE_BUSTER%%" class="player-icon" alt="${p.name}" onerror="this.onerror=null;this.src='/%%DEFAULT_PLAYER_SKIN_URL%%?v=%%CACHE_BUSTER%%';">
<span class="player-name-status">
<span class="status-dot ${statusDotClass}" data-title="Letzter Login: ${lastLoginFormatted}"></span>
<strong class="player-name">${p.name}</strong>
</span>
</div>
<div class="player-vitals">
<span class="icon" data-title="HP: ${p.hp !== undefined ? p.hp : '?'}">❤️</span>
<span class="icon" data-title="Atem: ${p.breath !== undefined ? p.breath : '?'}">💨</span>
<span class="icon" data-title="Ausdauer: ${p.stamina !== undefined ? p.stamina : '?'}">🍖</span>
</div>
</div>
<hr class="privilege-separator">
<div class="player-attributes">
<span class="icon privilege ${hasInteract ? 'active' : ''}" data-title="Interagieren">🖐️</span>
<span class="icon privilege ${hasShout ? 'active' : ''}" data-title="Rufen">📢</span>
<span class="icon privilege ${hasFast ? 'active' : ''}" data-title="Schnelles Laufen">🏃</span>
<span class="icon privilege ${hasFly ? 'active' : ''}" data-title="Fliegen">🕊️</span>
<span class="icon privilege ${hasGive ? 'active' : ''}" data-title="Geben">🎁</span>
<span class="icon privilege ${hasTeleport ? 'active' : ''}" data-title="Teleportieren">✨</span>
<span class="icon privilege ${hasAreas ? 'active' : ''}" data-title="Bereiche">🗺️</span>
<span class="icon privilege ${hasNoclip ? 'active' : ''}" data-title="Noclip">👻</span>
<span class="icon privilege ${hasSettime ? 'active' : ''}" data-title="Zeit setzen">🕒</span>
<span class="icon privilege ${hasWeather ? 'active' : ''}" data-title="Wetter">🌦️</span>
<span class="icon privilege ${hasServerPriv ? 'active' : ''}" data-title="Server-Admin">🛠️</span>
</div>
</div>`;
}
html += "</div>";
return html;
}
document.addEventListener('DOMContentLoaded', function() {
// === Leaflet-Karten Initialisierung ===
const mapContainer = document.getElementById('leaflet-map-container-%%current_world_key%%');
if (mapContainer && typeof L !== 'undefined' && %%MAP_WIDTH%% > 0) {
map_%%current_world_key%% = L.map(mapContainer, {
crs: L.CRS.Simple,
minZoom: -4, // Erlaubt sehr weites Herauszoomen
fullscreenControl: true,
fullscreenControlOptions: {
position: 'topleft',
title: 'Vollbild',
titleCancel: 'Vollbild verlassen'
}
});
const mapWidth = %%MAP_WIDTH%%;
const mapHeight = %%MAP_HEIGHT%%;
const bounds = [[-mapHeight, 0], [0, mapWidth]];
const imageUrl = '/%%web_map_preview_rel_path%%?v=%%CACHE_BUSTER%%';
L.imageOverlay(imageUrl, bounds).addTo(map_%%current_world_key%%);
map_%%current_world_key%%.fitBounds(bounds);
map_%%current_world_key%%.setMaxBounds(bounds);
} else if (mapContainer) {
if (%%MAP_WIDTH%% <= 0) {
mapContainer.innerHTML = "<p><em>Dynamische Karte konnte nicht geladen werden (fehlende Kartendaten).</em></p>";
}
}
// === Burger-Menü Logik ===
// Burger-Menü Logik
const burgerToggle = document.getElementById('burger-menu-toggle');
const navContainer = document.getElementById('page-nav-buttons-container-%%current_world_key%%');
if (burgerToggle && navContainer) {
@ -348,43 +142,9 @@ document.addEventListener('DOMContentLoaded', function() {
navContainer.addEventListener('click', e => { if (e.target.classList.contains('button')) { navContainer.classList.remove('menu-open'); }});
}
// === Live/Archiv Umschalter Logik ===
const liveBtn = document.getElementById('toggle-live-btn-%%current_world_key%%');
const archiveBtn = document.getElementById('toggle-archive-btn-%%current_world_key%%');
const liveContainer = document.getElementById('live-map-container-%%current_world_key%%');
const archiveContainer = document.getElementById('archive-view-container-%%current_world_key%%');
if (liveBtn && archiveBtn && liveContainer && archiveContainer) {
liveBtn.addEventListener('click', () => {
liveBtn.classList.add('active'); archiveBtn.classList.remove('active');
liveContainer.style.display = 'block'; archiveContainer.style.display = 'none';
if (map_%%current_world_key%%) { setTimeout(() => map_%%current_world_key%%.invalidateSize(), 10); }
});
archiveBtn.addEventListener('click', () => {
archiveBtn.classList.add('active'); liveBtn.classList.remove('active');
archiveContainer.style.display = 'block'; liveContainer.style.display = 'none';
});
}
// === Spieler-Filter Logik ===
const filterBtn = document.getElementById('player-filter-toggle-btn-%%current_world_key%%');
const filterDropdown = document.getElementById('player-filter-dropdown-%%current_world_key%%');
if(filterBtn && filterDropdown) {
filterBtn.addEventListener('click', (e) => { e.stopPropagation(); filterDropdown.classList.toggle('show'); });
filterDropdown.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const filterType = e.target.dataset.filter; const filterValue = e.target.dataset.priv;
if(filterType === 'active') { playerFilters_%%current_world_key%%.activeOnly = e.target.checked; }
else if(filterType === 'privilege') { if(e.target.checked) { playerFilters_%%current_world_key%%.privileges.add(filterValue); } else { playerFilters_%%current_world_key%%.privileges.delete(filterValue); } }
applyPlayerFiltersAndRender_%%current_world_key%%();
}
});
}
window.addEventListener('click', (e) => { if (filterDropdown && !filterBtn.contains(e.target) && !filterDropdown.contains(e.target)) { filterDropdown.classList.remove('show'); }});
// === "Weiterlesen"-Logik ===
// "Weiterlesen"-Logik
document.querySelectorAll('.read-more-link').forEach(link => {
const targetId = link.dataset.target;
const targetElement = document.getElementById(targetId);
const targetElement = document.getElementById(link.dataset.target);
if (targetElement) {
if (targetElement.scrollHeight <= targetElement.clientHeight) {
link.parentElement.style.display = 'none';
@ -398,112 +158,16 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
// === Initiale Daten-Ladeaufrufe ===
// Initiale Daten-Ladeaufrufe
fetchWorldStatus_%%current_world_key%%();
fetchDataForElement('player-info-%%current_world_key%%', '%%web_players_txt_rel_path%%', false, '', '', true);
fetchDataForElement('weather-info-%%current_world_key%%', '%%web_weather_txt_rel_path%%', true, '<p><strong>Wetter:</strong> ', '</p>');
fetchDataForElement('map-last-update-text-%%current_world_key%%', '%%web_last_update_rel_path%%', true, '', '');
});
// === Periodische Updates ===
// Periodische Updates
setInterval(fetchWorldStatus_%%current_world_key%%, 60000);
setInterval(() => fetchDataForElement('map-last-update-text-%%current_world_key%%', '%%web_last_update_rel_path%%', true, '', ''), 60000);
setInterval(() => fetchDataForElement('player-info-%%current_world_key%%', '%%web_players_txt_rel_path%%', false, '', '', true), 70000);
setInterval(() => fetchDataForElement('weather-info-%%current_world_key%%', '%%web_weather_txt_rel_path%%', true, '<p><strong>Wetter:</strong> ', '</p>'), 300000);
</script>
<div id="description-text-wrapper">
<h3 id='beschreibung'>Beschreibung</h3>
<div id="description-text" class="collapsible-text">
%%WORLD_LONG_DESCRIPTION_PAGE%%
</div>
<div class="read-more-container">
<a href="#" class="read-more-link" data-target="description-text">weiterlesen</a>
</div>
</div>
<div id="gamerules-section">
<h3 id='spielregeln'>Spielregeln</h3>
<div id="gamerules-text-wrapper">
<div id="gamerules-text" class="collapsible-text">
%%WORLD_GAME_RULES_PAGE%%
</div>
<div class="read-more-container">
<a href="#" class="read-more-link" data-target="gamerules-text">weiterlesen</a>
</div>
</div>
</div>
<h3 id='weltradar' class="section-header-flex">
<span>Weltradar</span>
<div class="map-toggle-controls">
<button id="toggle-live-btn-%%current_world_key%%" class="map-toggle-button active">Live</button>
<button id="toggle-archive-btn-%%current_world_key%%" class="map-toggle-button">Archiv</button>
</div>
</h3>
<div id="live-map-container-%%current_world_key%%">
<div id="leaflet-map-container-%%current_world_key%%" class="leaflet-map">
</div>
<div class='map-sub-info'>
%%map_sub_info_link%%
<span class='map-last-update'>Letzte Kartenaktualisierung: <span id='map-last-update-text-%%current_world_key%%'><em>wird geladen...</em></span></span>
</div>
</div>
<div id="archive-view-container-%%current_world_key%%" style="display: none;">
%%ARCHIVE_HTML%%
</div>
<h3 id='mods'>Verwendete Mods</h3>
<div class='scrollable-mod-list'>%%MODS_HTML%%</div>
<h3 id='spielerliste' class="section-header-flex">
<span>Spielerliste</span>
<div class="filter-container">
<button id="player-filter-toggle-btn-%%current_world_key%%" class="filter-dropdown-btn">Filter ▾</button>
<div id="player-filter-dropdown-%%current_world_key%%" class="filter-dropdown">
<label class="filter-option">
<input type="checkbox" data-filter="active">
<span class="status-dot online"></span>
zuletzt aktiv
</label>
<hr class="privilege-separator">
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="interact"> <span class="icon">🖐️</span> Interagieren
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="shout"> <span class="icon">📢</span> Rufen
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="fast"> <span class="icon">🏃</span> Schnell
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="fly"> <span class="icon">🕊️</span> Fliegen
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="give"> <span class="icon">🎁</span> Geben
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="teleport"> <span class="icon">✨</span> Teleportieren
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="areas"> <span class="icon">🗺️</span> Bereiche
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="noclip"> <span class="icon">👻</span> Noclip
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="settime"> <span class="icon">🕒</span> Zeit
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="weather"> <span class="icon">🌦️</span> Wetter
</label>
<label class="filter-option">
<input type="checkbox" data-filter="privilege" data-priv="server"> <span class="icon">🛠️</span> Server
</label>
</div>
</div>
</h3>
<div id='player-info-%%current_world_key%%' class='player-list-container'>
<p><em>Spielerdaten werden geladen...</em></p>
</div>

View file

@ -0,0 +1,164 @@
<h3 id='spielerliste' class="section-header-flex">
<span>Spielerliste</span>
<div class="filter-container">
<button id="player-filter-toggle-btn-%%current_world_key%%" class="filter-dropdown-btn">Filter ▾</button>
<div id="player-filter-dropdown-%%current_world_key%%" class="filter-dropdown">
<label class="filter-option">
<input type="checkbox" data-filter="active">
<span class="status-dot online"></span>
zuletzt aktiv
</label>
<hr class="privilege-separator">
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="interact"> <span class="icon">🖐️</span> Interagieren</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="shout"> <span class="icon">📢</span> Rufen</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="fast"> <span class="icon">🏃</span> Schnell</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="fly"> <span class="icon">🕊️</span> Fliegen</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="give"> <span class="icon">🎁</span> Geben</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="teleport"> <span class="icon">✨</span> Teleportieren</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="areas"> <span class="icon">🗺️</span> Bereiche</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="noclip"> <span class="icon">👻</span> Noclip</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="settime"> <span class="icon">🕒</span> Zeit</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="weather"> <span class="icon">🌦️</span> Wetter</label>
<label class="filter-option"><input type="checkbox" data-filter="privilege" data-priv="server"> <span class="icon">🛠️</span> Server</label>
</div>
</div>
</h3>
<div id='player-info-%%current_world_key%%' class='player-list-container'>
<p><em>Spielerdaten werden geladen...</em></p>
</div>
<script>
if (!window.playerListLogic_%%current_world_key%%) {
window.playerListLogic_%%current_world_key%% = {
masterPlayerData: {},
playerFilters: { activeOnly: false, privileges: new Set() },
updatePlayerMarkers: function(playerData) {
const source = window.playerMarkerSource_%%current_world_key%%;
if (!source || typeof ol === 'undefined') return;
source.clear(); // Entferne alle alten Marker
// KORREKTUR: Marker-Stile auf Kreise umgestellt
const adminIconStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 7,
fill: new ol.style.Fill({ color: 'rgba(255, 255, 0, 0.9)' }), // Gelb
stroke: new ol.style.Stroke({ color: '#fff', width: 2 })
})
});
const playerIconStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 6,
fill: new ol.style.Fill({ color: 'rgba(255, 0, 0, 0.9)' }), // Rot
stroke: new ol.style.Stroke({ color: '#fff', width: 1 })
})
});
const features = [];
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
for (const id in playerData) {
const p = playerData[id];
const coords = convertMinetestToOpenLayers_%%current_world_key%%(p.posX, p.posZ);
if (!coords) continue;
const isAdmin = (p.privilege || '').includes('server');
let lastLoginFormatted = 'unbekannt';
let statusDotClass = 'offline';
if (p.last_login) {
if ((now_epoch - p.last_login < twentyFourHoursInSeconds)) { statusDotClass = 'online'; }
lastLoginFormatted = formatTimestampForDisplay(p.last_login);
}
const feature = new ol.Feature({
geometry: new ol.geom.Point(coords),
playerData: p,
statusDotClass: statusDotClass,
lastLoginFormatted: lastLoginFormatted
});
feature.set('style', isAdmin ? adminIconStyle : playerIconStyle);
features.push(feature);
}
source.addFeatures(features);
},
renderPlayerListHTML: function(playerData) {
if (!playerData || Object.keys(playerData).length === 0) {
return "<p><em>Keine Spieler entsprechen den aktuellen Filtereinstellungen.</em></p>";
}
let html = "<div class='player-list-grid'>";
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
for (const id in playerData) {
const p = playerData[id];
const privs = p.privilege ? p.privilege.toLowerCase().split(',') : [];
const hasServerPriv = privs.includes("server");
let statusDotClass = "offline";
let lastLoginFormatted = 'unbekannt';
if (p.last_login) {
if ((now_epoch - p.last_login < twentyFourHoursInSeconds)) { statusDotClass = "online"; }
lastLoginFormatted = formatTimestampForDisplay(p.last_login);
}
html += `<div class="player-box ${hasServerPriv ? "has-server-priv" : ""}">
<div class="player-header">
<div class="player-identity"><img src="/images/players/${encodeURIComponent(p.name)}.png?v=%%CACHE_BUSTER%%" class="player-icon" onerror="this.onerror=null;this.src='/%%DEFAULT_PLAYER_SKIN_URL%%?v=%%CACHE_BUSTER%%';"><span class="player-name-status"><span class="status-dot ${statusDotClass}" data-title="Letzter Login: ${lastLoginFormatted}"></span><strong class="player-name">${p.name}</strong></span></div>
<div class="player-vitals"><span class="icon" data-title="HP: ${p.hp ?? '?'}">❤️</span><span class="icon" data-title="Atem: ${p.breath ?? '?'}">💨</span><span class="icon" data-title="Ausdauer: ${p.stamina ?? '?'}">🍖</span></div>
</div>
<hr class="privilege-separator">
<div class="player-attributes">
<span class="icon privilege ${privs.includes('interact')?'active':''}" data-title="Interagieren">🖐️</span> <span class="icon privilege ${privs.includes('shout')?'active':''}" data-title="Rufen">📢</span>
<span class="icon privilege ${privs.includes('fast')?'active':''}" data-title="Schnelles Laufen">🏃</span> <span class="icon privilege ${privs.includes('fly')?'active':''}" data-title="Fliegen">🕊️</span>
<span class="icon privilege ${privs.includes('give')?'active':''}" data-title="Geben">🎁</span> <span class="icon privilege ${privs.includes('teleport')?'active':''}" data-title="Teleportieren">✨</span>
<span class="icon privilege ${privs.includes('areas')?'active':''}" data-title="Bereiche">🗺️</span> <span class="icon privilege ${privs.includes('noclip')?'active':''}" data-title="Noclip">👻</span>
<span class="icon privilege ${privs.includes('settime')?'active':''}" data-title="Zeit setzen">🕒</span> <span class="icon privilege ${privs.includes('weather')?'active':''}" data-title="Wetter">🌦️</span>
<span class="icon privilege ${hasServerPriv?'active':''}" data-title="Server-Admin">🛠️</span>
</div></div>`;
}
html += "</div>";
return html;
},
applyPlayerFiltersAndRender: function() {
const filteredData = {};
const now_epoch = Math.floor(Date.now() / 1000);
const twentyFourHoursInSeconds = 24 * 60 * 60;
for (const id in this.masterPlayerData) {
const player = this.masterPlayerData[id];
let passesFilter = true;
if (this.playerFilters.activeOnly) {
if (!player.last_login || !(now_epoch - player.last_login < twentyFourHoursInSeconds)) passesFilter = false;
}
if (passesFilter && this.playerFilters.privileges.size > 0) {
const playerPrivs = new Set(player.privilege ? player.privilege.toLowerCase().split(',') : []);
for (const requiredPriv of this.playerFilters.privileges) {
if (!playerPrivs.has(requiredPriv)) { passesFilter = false; break; }
}
}
if (passesFilter) { filteredData[id] = player; }
}
const playerListContainer = document.getElementById('player-info-%%current_world_key%%');
if(playerListContainer) { playerListContainer.innerHTML = this.renderPlayerListHTML(filteredData); }
},
init: function() {
const filterBtn = document.getElementById('player-filter-toggle-btn-%%current_world_key%%');
const filterDropdown = document.getElementById('player-filter-dropdown-%%current_world_key%%');
if(filterBtn && filterDropdown) {
filterBtn.addEventListener('click', (e) => { e.stopPropagation(); filterDropdown.classList.toggle('show'); });
filterDropdown.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
const filterType = e.target.dataset.filter; const filterValue = e.target.dataset.priv;
if(filterType === 'active') { this.playerFilters.activeOnly = e.target.checked; }
else if(filterType === 'privilege') { if(e.target.checked) { this.playerFilters.privileges.add(filterValue); } else { this.playerFilters.privileges.delete(filterValue); } }
this.applyPlayerFiltersAndRender();
}
});
}
window.addEventListener('click', (e) => { if (filterDropdown && filterDropdown.classList.contains('show') && !filterBtn.contains(e.target) && !filterDropdown.contains(e.target)) { filterDropdown.classList.remove('show'); }});
}
};
document.addEventListener('DOMContentLoaded', () => window.playerListLogic_%%current_world_key%%.init());
}
</script>

View file

@ -0,0 +1,202 @@
<h3 id='weltradar' class="section-header-flex">
<span>Weltradar</span>
<div class="map-toggle-controls">
<button id="toggle-live-btn-%%current_world_key%%" class="map-toggle-button active">Live</button>
<button id="toggle-archive-btn-%%current_world_key%%" class="map-toggle-button">Archiv</button>
</div>
</h3>
<div id="live-map-container-%%current_world_key%%">
<div id="ol-map-container-%%current_world_key%%" class="map-view-container"></div>
<div id="popup-%%current_world_key%%" class="ol-popup">
<a href="#" id="popup-closer-%%current_world_key%%" class="ol-popup-closer"></a>
<div id="popup-content-%%current_world_key%%" class="ol-popup-content"></div>
</div>
<div class='map-sub-info'>
%%map_sub_info_link%%
<span class='map-last-update'>Letzte Kartenaktualisierung: <span id='map-last-update-text-%%current_world_key%%'><em>wird geladen...</em></span></span>
</div>
</div>
<div id="archive-view-container-%%current_world_key%%" style="display: none;">
%%ARCHIVE_HTML%%
</div>
<script>
// Globale Referenzen
window.olMap_%%current_world_key%% = null;
window.playerMarkerSource_%%current_world_key%% = new ol.source.Vector();
window.mapData_%%current_world_key%% = {};
// Konvertiert Minetest-Koordinaten in OpenLayers-Pixel-Koordinaten
function convertMinetestToOpenLayers_%%current_world_key%%(posX, posZ) {
const mapData = window.mapData_%%current_world_key%%;
if (!mapData || !mapData.mapWidth) return null;
const percentX = (posX - mapData.minX) / mapData.extentWidth;
const percentZ = (posZ - mapData.minZ) / mapData.extentHeight;
const pixelX = percentX * mapData.mapWidth;
const pixelY = - (mapData.mapHeight - (percentZ * mapData.mapHeight));
return [pixelX, pixelY];
}
document.addEventListener('DOMContentLoaded', function() {
const mapContainer = document.getElementById('ol-map-container-%%current_world_key%%');
const mapInfoPath = '/%%web_map_info_rel_path%%?v=' + new Date().getTime();
fetch(mapInfoPath)
.then(response => {
if (!response.ok) throw new Error('map_info.txt nicht gefunden (Status: ' + response.status + ')');
return response.text();
})
.then(mapInfoText => {
const mapInfo = (text => {
const data = {};
text.split('\n').forEach(line => {
const parts = line.split('=');
if (parts.length === 2 && parts[0] && parts[1]) data[parts[0].trim()] = parts[1].trim();
});
return data;
})(mapInfoText);
if (!mapInfo.map_dimension || !mapInfo.map_extent) throw new Error('map_info.txt ist unvollständig.');
const mapData = window.mapData_%%current_world_key%%;
const mapDim = mapInfo.map_dimension.split('x');
mapData.mapWidth = parseInt(mapDim[0], 10);
mapData.mapHeight = parseInt(mapDim[1], 10);
const mapExt = mapInfo.map_extent.split(/[+:]/);
mapData.minX = parseInt(mapExt[0], 10);
mapData.minZ = parseInt(mapExt[1], 10);
mapData.extentWidth = parseInt(mapExt[2], 10);
mapData.extentHeight = parseInt(mapExt[3], 10);
if (mapContainer && typeof ol !== 'undefined' && mapData.mapWidth > 0) {
mapContainer.innerHTML = '';
const extent = [0, -mapData.mapHeight, mapData.mapWidth, 0];
const resolutions = %%RESOLUTIONS_JS_ARRAY%%;
const projection = new ol.proj.Projection({
code: 'pixel-map-%%current_world_key%%',
units: 'pixels',
extent: extent
});
const tileGrid = new ol.tilegrid.TileGrid({
origin: ol.extent.getTopLeft(extent),
resolutions: resolutions,
});
const tileSource = new ol.source.TileImage({
projection: projection,
tileGrid: tileGrid,
tileUrlFunction: function(tileCoord) {
if (!tileCoord) return '';
return ('/%%web_tiles_rel_path%%/' + tileCoord[0] + '/' + tileCoord[1] + '/' + tileCoord[2] + '.png?v=%%CACHE_BUSTER%%');
}
});
const tileLayer = new ol.layer.Tile({ source: tileSource });
const playerLayer = new ol.layer.Vector({ source: window.playerMarkerSource_%%current_world_key%%, style: f => f.get('style') });
const view = new ol.View({
projection: projection,
center: ol.extent.getCenter(extent),
resolutions: resolutions,
maxZoom: resolutions.length - 1
});
const popupContainer = document.getElementById('popup-%%current_world_key%%');
const popupContent = document.getElementById('popup-content-%%current_world_key%%');
const popupCloser = document.getElementById('popup-closer-%%current_world_key%%');
const overlay = new ol.Overlay({ element: popupContainer, autoPan: { animation: { duration: 250 } } });
popupCloser.onclick = () => { overlay.setPosition(undefined); popupCloser.blur(); return false; };
const mapControls = [ new ol.control.Zoom(), new ol.control.Rotate(), new ol.control.Attribution({ collapsible: false }), new ol.control.FullScreen() ];
window.olMap_%%current_world_key%% = new ol.Map({
controls: mapControls,
layers: [tileLayer, playerLayer],
overlays: [overlay],
target: mapContainer,
view: view
});
const map = window.olMap_%%current_world_key%%;
// KORREKTUR: Hintergrundfarbe wird hier per JavaScript gesetzt
map.getViewport().style.backgroundColor = '%%map_background_color%%';
map.getView().fit(extent, { size: map.getSize() });
map.on('click', function(evt) {
const feature = map.forEachFeatureAtPixel(evt.pixel, f => f);
overlay.setPosition(undefined);
popupCloser.blur();
if (feature) {
const playerData = feature.get('playerData');
if (playerData) {
const statusDotClass = feature.get('statusDotClass');
const lastLoginFormatted = feature.get('lastLoginFormatted');
const popupHTML = `
<div class='popup-player-box'>
<div class="player-header">
<div class="player-identity">
<img src="/images/players/${encodeURIComponent(playerData.name)}.png?v=%%CACHE_BUSTER%%" class="player-icon" onerror="this.onerror=null;this.src='/%%DEFAULT_PLAYER_SKIN_URL%%?v=%%CACHE_BUSTER%%';">
<span class="player-name-status">
<span class="status-dot ${statusDotClass}" title="Letzter Login: ${lastLoginFormatted}"></span>
<strong class="player-name">${playerData.name}</strong>
</span>
</div>
</div>
<hr class="privilege-separator">
<div class="popup-player-vitals">
<span class="vital"><span class="icon">❤️</span> ${playerData.hp ?? '?'}</span>
<span class="vital"><span class="icon">💨</span> ${playerData.breath ?? '?'}</span>
<span class="vital"><span class="icon">🍖</span> ${playerData.stamina ?? '?'}</span>
</div>
</div>`;
popupContent.innerHTML = popupHTML;
overlay.setPosition(feature.getGeometry().getCoordinates());
}
}
});
if (window.playerListLogic_%%current_world_key%% && window.playerListLogic_%%current_world_key%%.masterPlayerData) {
window.playerListLogic_%%current_world_key%%.updatePlayerMarkers(window.playerListLogic_%%current_world_key%%.masterPlayerData);
}
}
})
.catch(error => {
console.error("Fehler beim Initialisieren der Karte:", error);
mapContainer.innerHTML = "<p><em>Karte konnte nicht initialisiert werden: " + error.message + "</em></p>";
});
const liveBtn = document.getElementById('toggle-live-btn-%%current_world_key%%');
const archiveBtn = document.getElementById('toggle-archive-btn-%%current_world_key%%');
const liveContainer = document.getElementById('live-map-container-%%current_world_key%%');
const archiveContainer = document.getElementById('archive-view-container-%%current_world_key%%');
if (liveBtn && archiveBtn && liveContainer && archiveContainer) {
liveBtn.addEventListener('click', () => {
liveBtn.classList.add('active'); archiveBtn.classList.remove('active');
liveContainer.style.display = 'block'; archiveContainer.style.display = 'none';
if (window.olMap_%%current_world_key%%) {
setTimeout(() => window.olMap_%%current_world_key%%.updateSize(), 10);
}
});
archiveBtn.addEventListener('click', () => {
archiveBtn.classList.add('active'); liveBtn.classList.remove('active');
archiveContainer.style.display = 'block'; liveContainer.style.display = 'none';
});
}
});
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

View file

Before

Width:  |  Height:  |  Size: 582 B

After

Width:  |  Height:  |  Size: 582 B

Before After
Before After