first alpha (v0.1)

This commit is contained in:
Rainer 2025-06-07 13:48:21 +02:00
commit 21d58c29f7
26 changed files with 15935 additions and 0 deletions

103
check_server_status.sh Executable file
View file

@ -0,0 +1,103 @@
#!/bin/bash
# Lade globale Konfiguration
# Annahme: config.sh liegt im selben Verzeichnis wie dieses Skript
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ -f "$CONFIG_FILE_PATH" ]; then
source "$CONFIG_FILE_PATH"
else
# Minimales Logging, falls Haupt-Log-Funktion nicht verfügbar oder config nicht geladen werden kann
echo "$(date '+%Y-%m-%d %H:%M:%S') - FEHLER: Globale config.sh nicht unter ${CONFIG_FILE_PATH} gefunden! Status-Skript kann nicht laufen." >> "/tmp/check_server_status_bootstrap_error.log"
exit 1
fi
# === Abgeleitete Variablen für dieses Skript ===
SCRIPT_BASENAME=$(basename "$0" .sh)
LOG_FILE="${LOG_DIR_BASE}/${SCRIPT_BASENAME}.log" # Eigene Log-Datei (z.B. /var/log/luweb/check_server_status.log)
LOCK_FILE="${LOCK_FILE_BASE_DIR}/${SCRIPT_BASENAME}.lock" # Eigene Lock-Datei
# === Logging Funktion ===
log_message() {
local message_to_log
message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - $1" # Weltname nicht im Prefix, da es alle Welten prüft
if [ -t 1 ]; then # Prüft, ob stdout ein Terminal ist
echo "${message_to_log}" | tee -a "$LOG_FILE"
else
echo "${message_to_log}" >> "$LOG_FILE"
fi
}
# === Hauptlogik ===
exec 200>"$LOCK_FILE"
flock -n 200 || { log_message "Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende."; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "Script ${SCRIPT_BASENAME}.sh beendet."' EXIT
mkdir -p "$LOG_DIR_BASE" # Sicherstellen, dass Log-Verzeichnis existiert
log_message "Script ${SCRIPT_BASENAME}.sh gestartet."
if ! command -v ss &> /dev/null; then
log_message "FEHLER: 'ss' (Socket Statistics) nicht gefunden. Port-Status kann nicht geprüft werden."
exit 1
fi
# === NEU: Welt-Entdeckung aus dem Minetest-Datenverzeichnis ===
log_message "Suche nach Welten in ${MINETESTMAPPER_WORLD_DATA_BASE_PATH}..."
shopt -s nullglob
world_key_dirs=("${MINETESTMAPPER_WORLD_DATA_BASE_PATH}"/*/) # Scannt Unterverzeichnisse
shopt -u nullglob
if [ ${#world_key_dirs[@]} -eq 0 ]; then
log_message "Keine Welt-Verzeichnisse in ${MINETESTMAPPER_WORLD_DATA_BASE_PATH} gefunden. Beende."
exit 0
fi
for world_data_dir in "${world_key_dirs[@]}"; do
current_world_key=$(basename "$world_data_dir")
world_mt_file="${world_data_dir}world.mt"
web_conf_file="${world_data_dir}web.conf"
# Eine Welt wird nur geprüft, wenn sie eine world.mt und eine web.conf hat
if [ ! -f "$world_mt_file" ] || [ ! -f "$web_conf_file" ]; then
log_message "WARNUNG: Für Welt-Schlüssel '${current_world_key}' fehlt world.mt oder web.conf. Überspringe Status-Check."
continue
fi
# Lade Standardwerte und überschreibe mit welt-spezifischer web.conf
# Für dieses Skript sind nur SERVER_ADDRESS und SERVER_PORT relevant
SERVER_ADDRESS="$DEFAULT_SERVER_ADDRESS"
SERVER_PORT="$DEFAULT_SERVER_PORT"
source "$web_conf_file" # Lädt u.a. SERVER_ADDRESS und SERVER_PORT
if [ -z "$SERVER_PORT" ] || [ -z "$SERVER_ADDRESS" ]; then
log_message "WARNUNG: Port ('${SERVER_PORT}') oder Adresse ('${SERVER_ADDRESS}') für Welt '${current_world_key}' nicht konfiguriert. Überspringe."
continue
fi
# Pfad zur online_status.txt Datei im Webverzeichnis der Welt
status_file_dir="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}"
status_file_path="${status_file_dir}/online_status.txt"
mkdir -p "$status_file_dir" # Stelle sicher, dass das Verzeichnis existiert
if [ ! -d "$status_file_dir" ]; then
log_message "FEHLER: Konnte Verzeichnis ${status_file_dir} nicht erstellen. Überspringe Welt ${current_world_key}."
continue
fi
log_message "Prüfe lokalen Port für Welt '${current_world_key}': ${SERVER_PORT}"
current_status="offline" # Standardmäßig offline setzen
# Prüfe mit 'ss -tulpn', ob der Port in der Ausgabe der lauschenden Sockets vorkommt.
# Dies prüft, ob ein Dienst (TCP oder UDP) auf dem Port auf dem lokalen System lauscht.
if ss -tulpn | grep -qE ":${SERVER_PORT}([[:space:]]|$)"; then
current_status="online"
fi
last_update_time=$(date '+%Y-%m-%d %H:%M:%S %Z')
echo "${current_status} - ${last_update_time}" > "$status_file_path"
log_message "Status für ${current_world_key} (Port ${SERVER_PORT}): ${current_status}. Datei aktualisiert: ${status_file_path}"
done
log_message "Alle konfigurierten Welten geprüft."
exit 0

791
colors.txt Normal file
View file

@ -0,0 +1,791 @@
# beds
beds:bed_bottom 130 3 3
beds:bed_top 185 162 163
beds:fancy_bed_bottom 136 49 28
beds:fancy_bed_top 179 153 148
# bones
bones:bones 117 117 117
# butterflies
# carts
carts:brakerail 150 121 102
carts:powerrail 160 145 102
carts:rail 146 128 108
# default
default:acacia_bush_leaves 109 133 87
default:acacia_bush_sapling 85 121 61
default:acacia_bush_stem 84 77 70
default:acacia_leaves 126 153 101
default:acacia_sapling 87 120 64
default:acacia_tree 195 119 97
default:acacia_wood 150 61 39
default:apple 161 34 19
default:aspen_leaves 72 105 29
default:aspen_sapling 85 123 45
default:aspen_tree 218 198 168
default:aspen_wood 210 199 170
default:blueberry_bush_leaves 63 99 22
default:blueberry_bush_leaves_with_berries 63 99 22
default:blueberry_bush_sapling 81 112 33
default:bookshelf 131 102 57
default:brick 123 99 95
default:bronzeblock 186 111 15
default:bush_leaves 35 55 29
default:bush_sapling 66 64 40
default:bush_stem 46 34 24
default:cactus 70 119 52
default:cave_ice 168 206 247
default:chest 149 115 69
default:chest_locked 149 115 69
default:chest_locked_open 149 115 69
default:chest_open 149 115 69
default:clay 183 183 183
default:cloud 255 255 255
default:coalblock 58 58 58
default:cobble 89 86 84
default:copperblock 193 126 65
default:coral_brown 146 113 77
default:coral_cyan 235 230 215
default:coral_green 235 230 215
default:coral_orange 197 68 17
default:coral_pink 235 230 215
default:coral_skeleton 235 230 215
default:desert_cobble 110 67 50
default:desert_sand 206 165 98
default:desert_sandstone 195 152 92
default:desert_sandstone_block 193 152 94
default:desert_sandstone_brick 191 151 95
default:desert_stone 130 79 61
default:desert_stone_block 131 80 61
default:desert_stonebrick 131 80 61
default:diamondblock 140 218 223
default:dirt 97 67 43
default:dirt_with_coniferous_litter 109 90 71
default:dirt_with_dry_grass 187 148 78
default:dirt_with_grass 64 111 26
default:dirt_with_grass_footsteps 64 111 26
default:dirt_with_rainforest_litter 76 39 10
default:dirt_with_snow 225 225 238
default:dry_dirt 178 136 90
default:dry_dirt_with_dry_grass 187 148 78
default:dry_grass_1 208 172 87
default:dry_grass_2 210 174 87
default:dry_grass_3 210 174 87
default:dry_grass_4 211 175 88
default:dry_grass_5 214 178 92
default:dry_shrub 103 67 18
default:emergent_jungle_sapling 51 40 16
default:fence_acacia_wood 151 62 39
default:fence_aspen_wood 210 199 170
default:fence_junglewood 57 39 14
default:fence_pine_wood 221 185 131
default:fence_rail_acacia_wood 150 61 39
default:fence_rail_aspen_wood 209 198 170
default:fence_rail_junglewood 56 39 14
default:fence_rail_pine_wood 221 184 130
default:fence_rail_wood 131 102 57
default:fence_wood 132 103 57
default:fern_1 85 118 51
default:fern_2 90 123 53
default:fern_3 91 125 54
default:furnace 101 98 96
default:furnace_active 101 98 96
default:glass 247 247 247 64 16
default:goldblock 231 203 35
default:grass_1 100 140 54
default:grass_2 98 139 55
default:grass_3 94 136 53
default:grass_4 89 133 48
default:grass_5 86 126 48
default:gravel 132 132 132
default:ice 168 206 247
default:junglegrass 67 110 28
default:jungleleaves 22 31 16
default:junglesapling 51 39 15
default:jungletree 121 97 62
default:junglewood 56 39 14
default:ladder_steel 132 132 132
default:ladder_wood 125 93 43
default:large_cactus_seedling 67 107 52
default:lava_flowing 255 100 0
default:lava_source 255 100 0
default:leaves 36 55 29
default:marram_grass_1 113 139 96
default:marram_grass_2 102 131 90
default:marram_grass_3 99 130 88
default:mese 222 222 0
default:mese_post_light 132 103 57
default:mese_post_light_acacia_wood 151 62 39
default:mese_post_light_aspen_wood 210 199 170
default:mese_post_light_junglewood 57 39 14
default:mese_post_light_pine_wood 221 185 131
default:meselamp 213 215 143
default:mossycobble 88 91 73
default:obsidian 21 24 29
default:obsidian_block 23 25 30
default:obsidian_glass 20 23 27 64 16
default:obsidianbrick 23 25 29
default:papyrus 97 134 38
default:permafrost 71 66 61
default:permafrost_with_moss 108 150 51
default:permafrost_with_stones 71 66 61
default:pine_bush_needles 16 50 19
default:pine_bush_sapling 58 51 40
default:pine_bush_stem 73 62 53
default:pine_needles 16 50 19
default:pine_sapling 41 48 26
default:pine_tree 191 165 132
default:pine_wood 221 185 130
default:river_water_flowing 39 66 106 128 224
default:river_water_source 39 66 106 128 224
default:sand 214 207 158
default:sand_with_kelp 214 207 158
default:sandstone 198 193 143
default:sandstone_block 195 191 142
default:sandstonebrick 194 190 141
default:sapling 67 63 41
default:sign_wall_steel 147 147 147
default:sign_wall_wood 148 103 66
default:silver_sand 193 191 179
default:silver_sandstone 195 192 181
default:silver_sandstone_block 192 190 180
default:silver_sandstone_brick 191 189 179
default:snow 225 225 238
default:snowblock 225 225 238
default:steelblock 195 195 195
# default stone and ores
default:stone 97 94 93
default:stone_block 100 97 96
default:stone_with_coal 77 75 74 # Stein mit einem deutlichen Graustich von Kohle (dunkler)
default:stone_with_copper 128 102 86 # Stein mit einem bräunlich-orangen Kupferstich
default:stone_with_diamond 119 135 142 # Stein mit einem hellen, leicht bläulichen Diamantstich
default:stone_with_gold 144 130 80 # Stein mit einem gelblichen Goldstich
default:stone_with_iron 119 96 86 # Stein mit einem rostbraunen/gräulichen Eisenstich
default:stone_with_mese 144 120 71 # Stein mit einem kräftigen orangegelben Mesestich
default:stone_with_tin 125 126 128
default:stonebrick 102 99 98
default:tinblock 150 150 150
default:torch 141 123 93
default:torch_ceiling 141 123 93
default:torch_wall 141 123 93
default:tree 179 145 99
default:water_flowing 39 66 106 128 224
default:water_source 39 66 106 128 224
default:wood 131 102 57
# doors
doors:door_glass_a 245 245 245 64 16
doors:door_glass_b 245 245 245 64 16
doors:door_glass_c 245 245 245 64 16
doors:door_glass_d 245 245 245 64 16
doors:door_obsidian_glass_a 48 49 50 64 16
doors:door_obsidian_glass_b 48 49 50 64 16
doors:door_obsidian_glass_c 48 49 50 64 16
doors:door_obsidian_glass_d 48 49 50 64 16
doors:door_steel_a 203 203 203
doors:door_steel_b 203 203 203
doors:door_steel_c 203 203 203
doors:door_steel_d 203 203 203
doors:door_wood_a 89 68 37
doors:door_wood_b 89 68 37
doors:door_wood_c 89 68 37
doors:door_wood_d 89 68 37
doors:gate_acacia_wood_closed 150 61 39
doors:gate_acacia_wood_open 150 61 39
doors:gate_aspen_wood_closed 210 199 170
doors:gate_aspen_wood_open 210 199 170
doors:gate_junglewood_closed 56 39 14
doors:gate_junglewood_open 56 39 14
doors:gate_pine_wood_closed 221 185 130
doors:gate_pine_wood_open 221 185 130
doors:gate_wood_closed 131 102 57
doors:gate_wood_open 131 102 57
doors:trapdoor 130 100 51
doors:trapdoor_open 68 53 30
doors:trapdoor_steel 200 200 200
doors:trapdoor_steel_open 97 97 97
# farming
farming:cotton_1 89 117 39
farming:cotton_2 89 116 38
farming:cotton_3 99 121 41
farming:cotton_4 108 114 47
farming:cotton_5 116 105 53
farming:cotton_6 121 95 59
farming:cotton_7 94 70 37
farming:cotton_8 122 108 93
farming:cotton_wild 111 111 101
farming:desert_sand_soil 161 132 72
farming:desert_sand_soil_wet 120 99 53
farming:dry_soil 178 136 90
farming:dry_soil_wet 178 136 90
farming:seed_cotton 92 87 60
farming:seed_wheat 177 161 96
farming:soil 97 67 43
farming:soil_wet 97 67 43
farming:straw 212 184 68
farming:wheat_1 110 175 36
farming:wheat_2 136 177 53
farming:wheat_3 163 182 84
farming:wheat_4 170 188 95
farming:wheat_5 171 179 97
farming:wheat_6 173 177 87
farming:wheat_7 193 181 83
farming:wheat_8 187 162 40
# fire
fire:basic_flame 223 136 44
fire:permanent_flame 223 136 44
# fireflies
fireflies:firefly_bottle 191 194 202
# flowers
flowers:chrysanthemum_green 118 152 44
flowers:dandelion_white 199 191 176
flowers:dandelion_yellow 212 167 31
flowers:geranium 77 91 168
flowers:mushroom_brown 109 84 78
flowers:mushroom_red 195 102 102
flowers:rose 130 68 33
flowers:tulip 156 101 44
flowers:tulip_black 78 120 72
flowers:viola 115 69 184
flowers:waterlily 107 160 68
flowers:waterlily_waving 107 160 68
# stairs
stairs:slab_acacia_wood 150 61 39
stairs:slab_aspen_wood 210 199 170
stairs:slab_brick 123 99 95
stairs:slab_bronzeblock 186 111 15
stairs:slab_cobble 89 86 84
stairs:slab_copperblock 193 126 65
stairs:slab_desert_cobble 110 67 50
stairs:slab_desert_sandstone 195 152 92
stairs:slab_desert_sandstone_block 193 152 94
stairs:slab_desert_sandstone_brick 191 151 95
stairs:slab_desert_stone 130 79 61
stairs:slab_desert_stone_block 131 80 61
stairs:slab_desert_stonebrick 131 80 61
stairs:slab_glass 247 247 247
stairs:slab_goldblock 231 203 35
stairs:slab_ice 168 206 247
stairs:slab_junglewood 56 39 14
stairs:slab_mossycobble 88 91 73
stairs:slab_obsidian 21 24 29
stairs:slab_obsidian_block 23 25 30
stairs:slab_obsidian_glass 20 23 27
stairs:slab_obsidianbrick 23 25 29
stairs:slab_pine_wood 221 185 130
stairs:slab_sandstone 198 193 143
stairs:slab_sandstone_block 195 191 142
stairs:slab_sandstonebrick 194 190 141
stairs:slab_silver_sandstone 195 192 181
stairs:slab_silver_sandstone_block 192 190 180
stairs:slab_silver_sandstone_brick 191 189 179
stairs:slab_snowblock 225 225 238
stairs:slab_steelblock 195 195 195
stairs:slab_stone 97 94 93
stairs:slab_stone_block 100 97 96
stairs:slab_stonebrick 102 99 98
stairs:slab_straw 212 184 68
stairs:slab_tinblock 150 150 150
stairs:slab_wood 131 102 57
stairs:stair_acacia_wood 150 61 39
stairs:stair_aspen_wood 210 199 170
stairs:stair_brick 123 99 95
stairs:stair_bronzeblock 186 111 15
stairs:stair_cobble 89 86 84
stairs:stair_copperblock 193 126 65
stairs:stair_desert_cobble 110 67 50
stairs:stair_desert_sandstone 195 152 92
stairs:stair_desert_sandstone_block 193 152 94
stairs:stair_desert_sandstone_brick 191 151 95
stairs:stair_desert_stone 130 79 61
stairs:stair_desert_stone_block 131 80 61
stairs:stair_desert_stonebrick 131 80 61
stairs:stair_glass 249 249 249
stairs:stair_goldblock 231 203 35
stairs:stair_ice 168 206 247
stairs:stair_inner_acacia_wood 150 61 39
stairs:stair_inner_aspen_wood 210 199 170
stairs:stair_inner_brick 123 99 95
stairs:stair_inner_bronzeblock 186 111 15
stairs:stair_inner_cobble 89 86 84
stairs:stair_inner_copperblock 193 126 65
stairs:stair_inner_desert_cobble 110 67 50
stairs:stair_inner_desert_sandstone 195 152 92
stairs:stair_inner_desert_sandstone_block 193 152 94
stairs:stair_inner_desert_sandstone_brick 191 151 95
stairs:stair_inner_desert_stone 130 79 61
stairs:stair_inner_desert_stone_block 131 80 61
stairs:stair_inner_desert_stonebrick 131 80 61
stairs:stair_inner_glass 250 250 250
stairs:stair_inner_goldblock 231 203 35
stairs:stair_inner_ice 168 206 247
stairs:stair_inner_junglewood 56 39 14
stairs:stair_inner_mossycobble 88 91 73
stairs:stair_inner_obsidian 21 24 29
stairs:stair_inner_obsidian_block 23 25 30
stairs:stair_inner_obsidian_glass 20 22 27
stairs:stair_inner_obsidianbrick 23 25 29
stairs:stair_inner_pine_wood 221 185 130
stairs:stair_inner_sandstone 198 193 143
stairs:stair_inner_sandstone_block 195 191 142
stairs:stair_inner_sandstonebrick 194 190 141
stairs:stair_inner_silver_sandstone 195 192 181
stairs:stair_inner_silver_sandstone_block 192 190 180
stairs:stair_inner_silver_sandstone_brick 191 189 179
stairs:stair_inner_snowblock 225 225 238
stairs:stair_inner_steelblock 195 195 195
stairs:stair_inner_stone 97 94 93
stairs:stair_inner_stone_block 100 97 96
stairs:stair_inner_stonebrick 102 99 98
stairs:stair_inner_straw 212 184 68
stairs:stair_inner_tinblock 150 150 150
stairs:stair_inner_wood 131 102 57
stairs:stair_junglewood 56 39 14
stairs:stair_mossycobble 88 91 73
stairs:stair_obsidian 21 24 29
stairs:stair_obsidian_block 23 25 30
stairs:stair_obsidian_glass 20 22 27
stairs:stair_obsidianbrick 23 25 29
stairs:stair_outer_acacia_wood 150 61 39
stairs:stair_outer_aspen_wood 210 199 170
stairs:stair_outer_brick 123 99 95
stairs:stair_outer_bronzeblock 186 111 15
stairs:stair_outer_cobble 89 86 84
stairs:stair_outer_copperblock 193 126 65
stairs:stair_outer_desert_cobble 110 67 50
stairs:stair_outer_desert_sandstone 195 152 92
stairs:stair_outer_desert_sandstone_block 193 152 94
stairs:stair_outer_desert_sandstone_brick 191 151 95
stairs:stair_outer_desert_stone 130 79 61
stairs:stair_outer_desert_stone_block 131 80 61
stairs:stair_outer_desert_stonebrick 131 80 61
stairs:stair_outer_glass 250 250 250
stairs:stair_outer_goldblock 231 203 35
stairs:stair_outer_ice 168 206 247
stairs:stair_outer_junglewood 56 39 14
stairs:stair_outer_mossycobble 88 91 73
stairs:stair_outer_obsidian 21 24 29
stairs:stair_outer_obsidian_block 23 25 30
stairs:stair_outer_obsidian_glass 20 22 27
stairs:stair_outer_obsidianbrick 23 25 29
stairs:stair_outer_pine_wood 221 185 130
stairs:stair_outer_sandstone 198 193 143
stairs:stair_outer_sandstone_block 195 191 142
stairs:stair_outer_sandstonebrick 194 190 141
stairs:stair_outer_silver_sandstone 195 192 181
stairs:stair_outer_silver_sandstone_block 192 190 180
stairs:stair_outer_silver_sandstone_brick 191 189 179
stairs:stair_outer_snowblock 225 225 238
stairs:stair_outer_steelblock 195 195 195
stairs:stair_outer_stone 97 94 93
stairs:stair_outer_stone_block 100 97 96
stairs:stair_outer_stonebrick 102 99 98
stairs:stair_outer_straw 212 184 68
stairs:stair_outer_tinblock 150 150 150
stairs:stair_outer_wood 131 102 57
stairs:stair_pine_wood 221 185 130
stairs:stair_sandstone 198 193 143
stairs:stair_sandstone_block 195 191 142
stairs:stair_sandstonebrick 194 190 141
stairs:stair_silver_sandstone 195 192 181
stairs:stair_silver_sandstone_block 192 190 180
stairs:stair_silver_sandstone_brick 191 189 179
stairs:stair_snowblock 225 225 238
stairs:stair_steelblock 195 195 195
stairs:stair_stone 97 94 93
stairs:stair_stone_block 100 97 96
stairs:stair_stonebrick 102 99 98
stairs:stair_straw 212 184 68
stairs:stair_tinblock 150 150 150
stairs:stair_wood 131 102 57
# tnt
tnt:gunpowder 12 12 12
tnt:gunpowder_burning 156 143 7
tnt:tnt 196 0 0
tnt:tnt_burning 201 41 0
# vessels
vessels:drinking_glass 207 214 228
vessels:glass_bottle 189 192 204
vessels:shelf 131 102 57
vessels:steel_bottle 194 193 193
# walls
walls:cobble 89 86 84
walls:desertcobble 110 67 50
walls:mossycobble 88 91 73
# wool
wool:black 30 30 30
wool:blue 0 73 146
wool:brown 88 44 0
wool:cyan 0 132 140
wool:dark_green 33 103 0
wool:dark_grey 60 60 60
wool:green 93 218 28
wool:grey 133 133 133
wool:magenta 201 3 112
wool:orange 214 83 22
wool:pink 255 133 133
wool:red 170 18 18
wool:violet 93 5 169
wool:white 220 220 220
wool:yellow 254 226 16
# xpanes
xpanes:bar 114 114 114 64 16
xpanes:bar_flat 114 114 114 64 16
xpanes:door_steel_bar_a 133 133 133 64 16
xpanes:door_steel_bar_b 133 133 133 64 16
xpanes:door_steel_bar_c 133 133 133 64 16
xpanes:door_steel_bar_d 133 133 133 64 16
xpanes:obsidian_pane 16 17 18 64 16
xpanes:obsidian_pane_flat 16 17 18 64 16
xpanes:pane 249 249 249 64 16
xpanes:pane_flat 249 249 249 64 16
xpanes:trapdoor_steel_bar 127 127 127 64 16
xpanes:trapdoor_steel_bar_open 77 77 77 64 16
# animalworld
animalworld:animalworld_tundrashrub1 102 128 102
animalworld:animalworld_tundrashrub2 95 120 95
animalworld:animalworld_tundrashrub3 110 135 110
animalworld:animalworld_tundrashrub4 100 125 100
animalworld:animalworld_tundrashrub5 115 140 115
animalworld:anthill 139 115 85 # Earthy brown
animalworld:termitemould 160 130 100 # Lighter earthy brown
animalworld:waspnest 210 180 140 # Papery light brown/yellowish
# beautiflowers
beautiflowers:ada 255 105 180 # Hot Pink
beautiflowers:agnes 255 20 147 # Deep Pink
beautiflowers:alicia 238 130 238 # Violet
beautiflowers:alma 255 255 0 # Yellow
beautiflowers:amaia 255 165 0 # Orange
beautiflowers:anastasia 173 216 230 # Light Blue
beautiflowers:any 220 220 220 # Light Grey (as a neutral "any" color)
beautiflowers:arcoiris 255 0 255 # Magenta (for rainbow)
beautiflowers:arleth 255 182 193 # Light Pink
beautiflowers:astrid 218 112 214 # Orchid
beautiflowers:azalea 255 127 170 # Pink/Red like Azaleas
beautiflowers:beatriz 255 105 180 # Hot Pink
beautiflowers:belen 255 20 147 # Deep Pink
beautiflowers:berta 238 130 238 # Violet
beautiflowers:blanca 250 250 250 # White
beautiflowers:bonsai_1 34 139 34 # Forest Green (leaves)
beautiflowers:bonsai_2 85 107 47 # Dark Olive Green (leaves)
beautiflowers:bonsai_3 0 100 0 # Dark Green (leaves)
beautiflowers:bonsai_4 139 69 19 # Saddle Brown (trunk/pot for variety)
beautiflowers:bonsai_5 50 150 50 # Medium Green (leaves)
beautiflowers:carla 255 255 0 # Yellow
beautiflowers:casandra 255 165 0 # Orange
beautiflowers:clara 173 216 230 # Light Blue
beautiflowers:claudia 255 182 193 # Light Pink
beautiflowers:cloe 218 112 214 # Orchid
beautiflowers:cristina 255 105 180 # Hot Pink
beautiflowers:dafne 255 20 147 # Deep Pink
beautiflowers:dana 238 130 238 # Violet
beautiflowers:delia 255 255 0 # Yellow
beautiflowers:diana 255 165 0 # Orange
beautiflowers:elena 173 216 230 # Light Blue
beautiflowers:erica 255 182 193 # Light Pink (Heather/Erica flowers are often pink/purple)
beautiflowers:estela 218 112 214 # Orchid
beautiflowers:eva 255 105 180 # Hot Pink
beautiflowers:fabiola 255 20 147 # Deep Pink
beautiflowers:fiona 238 130 238 # Violet
beautiflowers:gala 255 255 0 # Yellow
beautiflowers:genesis 255 165 0 # Orange
beautiflowers:gisela 173 216 230 # Light Blue
beautiflowers:gloria 255 182 193 # Light Pink
beautiflowers:hadassa 218 112 214 # Orchid
beautiflowers:ingrid 255 105 180 # Hot Pink
beautiflowers:irene 255 20 147 # Deep Pink
beautiflowers:iris 138 43 226 # BlueViolet (common Iris color)
beautiflowers:ivette 238 130 238 # Violet
beautiflowers:jennifer 255 255 0 # Yellow
beautiflowers:lara 255 165 0 # Orange
beautiflowers:laura 173 216 230 # Light Blue
beautiflowers:lidia 255 182 193 # Light Pink
beautiflowers:lucia 218 112 214 # Orchid
beautiflowers:mara 255 105 180 # Hot Pink
beautiflowers:martina 255 20 147 # Deep Pink
beautiflowers:melania 238 130 238 # Violet
beautiflowers:michelle 255 255 0 # Yellow
beautiflowers:minerva 255 165 0 # Orange
beautiflowers:mireia 173 216 230 # Light Blue
beautiflowers:miriam 255 182 193 # Light Pink
beautiflowers:nadia 218 112 214 # Orchid
beautiflowers:nazareth 255 105 180 # Hot Pink
beautiflowers:nerea 255 20 147 # Deep Pink
beautiflowers:noelia 238 130 238 # Violet
beautiflowers:noemi 255 255 0 # Yellow
beautiflowers:olga 255 165 0 # Orange
beautiflowers:olimpia 173 216 230 # Light Blue
beautiflowers:oriana 255 182 193 # Light Pink
beautiflowers:pasto_1 50 205 50 # Lime Green
beautiflowers:pasto_10 34 139 34 # Forest Green
beautiflowers:pasto_2 124 252 0 # Lawn Green
beautiflowers:pasto_3 0 128 0 # Green
beautiflowers:pasto_4 60 179 113 # Medium Sea Green
beautiflowers:pasto_5 152 251 152 # Pale Green
beautiflowers:pasto_6 46 139 87 # Sea Green
beautiflowers:pasto_7 0 100 0 # Dark Green
beautiflowers:pasto_8 107 142 35 # Olive Drab
beautiflowers:pasto_9 127 255 0 # Chartreuse
beautiflowers:paula 218 112 214 # Orchid
beautiflowers:pia 255 105 180 # Hot Pink
beautiflowers:raquel 255 20 147 # Deep Pink
beautiflowers:regina 238 130 238 # Violet
beautiflowers:rocio 255 255 0 # Yellow
beautiflowers:ruth 255 165 0 # Orange
beautiflowers:sabrina 173 216 230 # Light Blue
beautiflowers:sandra 255 182 193 # Light Pink
beautiflowers:sara 218 112 214 # Orchid
beautiflowers:silvia 255 105 180 # Hot Pink
beautiflowers:sofia 255 20 147 # Deep Pink
beautiflowers:sonia 238 130 238 # Violet
beautiflowers:suri 255 255 0 # Yellow
beautiflowers:talia 255 165 0 # Orange
beautiflowers:thais 173 216 230 # Light Blue
beautiflowers:valentina 255 182 193 # Light Pink
beautiflowers:valeria 218 112 214 # Orchid
beautiflowers:vanesa 255 105 180 # Hot Pink
beautiflowers:vera 255 20 147 # Deep Pink
beautiflowers:victoria 238 130 238 # Violet
beautiflowers:virginia 255 255 0 # Yellow
beautiflowers:xena 255 165 0 # Orange
beautiflowers:xenia 173 216 230 # Light Blue
beautiflowers:zaida 255 182 193 # Light Pink
# butterflies
butterflies:butterfly_red 255 0 0 # Red
butterflies:butterfly_violet 138 43 226 # Violet
butterflies:butterfly_white 255 250 250 # White
# doors
doors:hidden 192 192 192 # Light Grey (for hidden things, to be inconspicuous)
# drawers
drawers:pine_wood1 222 184 135 # BurlyWood (Pine color)
drawers:pine_wood2 210 180 140 # Tan (Slightly different Pine color)
# ethereal
ethereal:strawberry_7 255 0 0 # Red (ripe strawberry)
# farming
farming:asparagus_4 143 188 143 # Dark Sea Green (Asparagus spears)
farming:beanbush 50 150 50 # Green (Bean plant leaves)
farming:beetroot_5 139 0 0 # Dark Red (Beetroot)
farming:blackberry_4 40 0 50 # Dark Purple/Black (Blackberry)
farming:blueberry_4 70 0 130 # Indigo (Blueberry)
farming:cabbage_6 127 255 0 # Chartreuse (Cabbage green)
farming:carrot_7 255 140 0 # Dark Orange (Carrot)
farming:coffee_5 0 100 0 # Dark Green (Coffee plant)
farming:corn_7 154 205 50 # YellowGreen (Corn plant, some yellow)
farming:cucumber_4 0 128 0 # Green (Cucumber)
farming:eggplant_3 75 0 130 # Indigo (Eggplant purple)
farming:ginger_3 210 180 140 # Tan (Ginger root)
farming:grapebush 60 80 20 # Dark Green (Grape leaves)
farming:lettuce_5 144 238 144 # Light Green (Lettuce)
farming:melon_8 152 251 152 # Pale Green (Melon rind)
farming:mint_4 60 179 113 # Medium Sea Green (Mint leaves)
farming:onion_5 255 224 180 # Light Brown/Yellow (Onion skin)
farming:parsley_3 34 139 34 # Forest Green (Parsley)
farming:pea_5 0 128 0 # Green (Pea pod/plant)
farming:pineapple_8 255 215 0 # Gold (Pineapple body)
farming:potato_3 139 69 19 # Saddle Brown (Potato)
farming:pumpkin_8 255 165 0 # Orange (Pumpkin)
farming:raspberry_4 227 23 67 # Raspberry Red
farming:rhubarb_3 220 20 60 # Crimson (Rhubarb stalks)
farming:soy_6 107 142 35 # Olive Drab (Soy plant)
farming:spinach_3 0 100 0 # Dark Green (Spinach)
farming:sunflower_8 255 215 0 # Gold (Sunflower petals)
farming:tomato_7 255 0 0 # Red (Tomato)
farming:vanilla_7 143 188 143 # Dark Sea Green (Vanilla plant/bean pod)
# fireflies
fireflies:hidden_firefly 40 40 40 # Dark Grey (for hidden, night context)
# gates_long
gates_long:fence_gate_closed_wood 160 82 45 # Sienna (Wood color)
# marinara
marinara:coastrock 169 169 169 # Dark Gray (Coast rock)
marinara:coastrock_alage 102 139 102 # Grayish Green (Rock with algae)
marinara:coastrock_with_brownalage 139 125 107 # Grayish Brown (Rock with brown algae)
marinara:hardcoral 245 245 220 # Beige (Base hard coral)
marinara:hardcoral_blue 100 149 237 # Cornflower Blue
marinara:hardcoral_brown 160 82 45 # Sienna
marinara:hardcoral_green 60 179 113 # Medium Sea Green
marinara:hardcoral_pink 255 182 193 # Light Pink
marinara:hardcoral_yellow 255 255 102 # Light Yellow
marinara:mussels 47 79 79 # Dark Slate Gray (Mussel shells)
marinara:oisterbank 220 220 220 # Gainsboro (Oyster bank)
marinara:sand_with_seashells 238 221 180 # Light Sand with whitish hint
marinara:sand_with_seashells_broken 220 200 160 # Sand color
marinara:sand_with_seashells_brown 210 190 150 # Brownish Sand
marinara:sand_with_seashells_orange 240 200 150 # Orangey Sand
marinara:sand_with_seashells_pink 240 210 200 # Pinkish Sand
marinara:sand_with_seashells_white 245 235 215 # Very Light Sand
marinara:sand_with_seashells_yellow 240 230 180 # Yellowish Sand
marinara:seaanemone_tentacle3 255 127 80 # Coral (Anemone color)
marinara:seapocks 240 240 240 # Whitish Grey (Sea Pocks/Barnacles)
marinara:seaworm2 255 170 170 # Light Fleshy Pink (Seaworm)
marinara:seaworm3 240 160 160 # Darker Fleshy Pink (Seaworm)
marinara:softcoral_white 250 250 250 # White
marinara:softcoral_yellow 255 255 224 # Light Yellow
marinara:hardcoral_red 220 40 40 # Ein kräftiges Rot für rote Hartkoralle
marinara:reed_root 100 80 60 # Dunkles, schlammiges Braun für Schilfwurzeln
marinara:sand_with_alage 175 193 140 # Sandfarbe mit einem deutlichen Grünstich durch Algen
marinara:sand_with_kelp 171 185 144 # Sandfarbe mit einem dunkleren Grün/Braun-Ton von Kelp
marinara:sand_with_seagrass 183 205 148 # Sandfarbe mit hellem Seegrasgrün
marinara:sand_with_seagrass2 191 209 156 # Eine leicht variierende Seegras-Sand-Farbe
marinara:seaanemone_tentacle 255 182 193 # Helles Rosa für Seeanemonen-Tentakel
marinara:seaanemone_tentacle2 173 216 230 # Helles Blau für Seeanemonen-Tentakel
marinara:seaanemone_tentacle4 255 255 150 # Blasses Gelb für Seeanemonen-Tentakel
marinara:seaworm 230 150 140 # Ein fleischiges Rosa/Braun für Seewürmer
marinara:softcoral 255 240 220 # Cremefarben/hellbeige für generische Weichkoralle
marinara:softcoral_brown 160 120 80 # Ein gedämpftes Braun für braune Weichkoralle
marinara:softcoral_green 140 190 150 # Ein blasses Meerschaumgrün für grüne Weichkoralle
marinara:softcoral_red 240 100 100 # Ein gedämpftes Rot für rote Weichkoralle
# markers
markers:stone 128 128 128 # Grey (Stone marker)
# moreblocks
moreblocks:stair_cobble 130 130 130 # Cobblestone Grey
moreblocks:stair_aspen_wood 245 228 190 # Sehr helles, fast weißliches Gelb-Braun für Espenholz
# nether
nether:airlike_darkness 10 10 10 # Very Dark Grey / Near Black
nether:basalt 50 50 50 # Dark Grey (Basalt)
nether:brick 112 28 28 # Nether Brick Red
nether:brick_cracked 100 25 25 # Slightly darker/muted Nether Brick
nether:fumarole 150 150 120 # Greyish Yellow (Sulfurous)
nether:fumarole_corner 150 150 120 # Greyish Yellow
nether:fumarole_slab 150 150 120 # Greyish Yellow
nether:geode 100 80 120 # Purplish Grey (Geode exterior/hint of crystal)
nether:glowstone 255 223 128 # Light Yellowish Glow
nether:glowstone_deep 255 200 100 # Deeper Yellow/Orange Glow
nether:lava_crust 60 30 30 # Dark Reddish Brown (Cooled Lava)
nether:lava_source 255 100 0 # Bright Orange/Red (Lava)
nether:rack 120 50 50 # Netherrack Red/Maroon
nether:rack_deep 100 40 40 # Darker Netherrack
nether:sand 80 65 60 # Soul Sand Brownish Grey
# ontime_clocks
ontime_clocks:frameless_gold 255 215 0 # Gold
ontime_clocks:green_digital 0 255 0 # Bright Green (Digital LED)
ontime_clocks:white 250 250 250 # White
# signs
signs:wooden_sign 160 82 45 # Sienna (Wood color)
# signs_road
signs_road:blue_sign 0 0 205 # Medium Blue
signs_road:blue_street_sign 70 130 180 # Steel Blue
# stairs
stairs:stair_netherrack 120 50 50 # Netherrack Red/Maroon
# tpad
tpad:tpad 70 70 100 # Dark Bluish Grey (Teleporter Pad)
# xnether
xnether:blue_leaves 0 0 200 # Blue
xnether:blue_tree 80 60 40 # Dark Brown/Grey (Nether wood trunk)
xnether:fruit 128 0 128 # Purple (Generic xnether fruit)
xnether:grass_blue_1 100 100 255 # Light Blue Grass
xnether:grass_blue_2 80 80 220 # Medium Blue Grass
xnether:grass_blue_3 60 60 190 # Dark Blue Grass
xnether:grass_purple_1 180 100 220 # Light Purple Grass
xnether:grass_purple_2 150 80 190 # Medium Purple Grass
xnether:grass_purple_3 120 60 160 # Dark Purple Grass
xnether:purple_leaves 128 0 128 # Purple
xnether:purple_tree 70 50 30 # Dark Brown/Grey (Nether wood trunk)
xnether:rack_deep_with_mese 100 60 50 # Netherrack with Mese Tint
xnether:rack_with_gold 120 80 50 # Netherrack with Gold Tint
# butterflies
butterflies:hidden_butterfly_white 220 220 220 # Ein sehr helles Grau, fast weiß, für einen "versteckten" weißen Schmetterling
# carpet
carpet:wool_black 30 30 30
carpet:wool_blue 0 0 205
carpet:wool_brown 139 69 19
carpet:wool_cyan 0 255 255
carpet:wool_dark_green 0 100 0
carpet:wool_dark_grey 70 70 70
carpet:wool_green 0 128 0
carpet:wool_grey 128 128 128
carpet:wool_magenta 255 0 255
carpet:wool_orange 255 165 0
carpet:wool_pink 255 192 203
carpet:wool_red 255 0 0
carpet:wool_violet 138 43 226
carpet:wool_white 250 250 250
# castle_gates
castle_gates:steel_gate_hinge 170 170 180 # Stahlgrau
castle_gates:steel_gate_panel 170 170 180 # Stahlgrau
castle_gates:steel_portcullis_bars 170 170 180 # Stahlgrau für Gitterstäbe
castle_gates:steel_portcullis_bars_bottom 160 160 170 # Etwas dunkleres Stahlgrau für den unteren Teil
castle_gates:stonebrick_gate_slot 120 120 120 # Steinziegelgrau
castle_gates:stonebrick_gate_slot_reverse 120 120 120 # Steinziegelgrau
castle_gates:wood_gate_edge 139 90 43 # Allgemeines Holzbraun
castle_gates:wood_gate_edge_handle 120 70 30 # Dunkleres Holzbraun für den Griff
# doors
doors:trapdoor_oak 200 160 100 # Eichenholzfarbe
# fake_fire
fake_fire:embers 255 100 0 # Tiefes, glühendes Orange für Glut
# fireflies
fireflies:firefly 220 255 100 # Helles, gelb-grünliches Leuchten für Glühwürmchen
# gates_long
gates_long:fence_gate_closed_acacia 205 92 92 # Rötlich-oranges Akazienholz
gates_long:fence_gate_closed_junglewood 139 76 57 # Sattes Dschungelholzbraun
gates_long:fence_gate_closed_pine 222 184 135 # Helles Kiefernholz
gates_long:fence_gate_closed_tree 160 82 45 # Standard Holzfarbe (wie default wood)
# moreblocks
moreblocks:all_faces_tree 101 67 33 # Rindenbraun
moreblocks:cactus_brick 120 150 100 # Gedämpftes Kaktusgrün für Ziegel
moreblocks:circle_stone_bricks 115 115 115 # Kreissteinziegel, ähnlich Steinziegelgrau
moreblocks:coal_stone 50 50 50 # Sehr dunkles Grau, fast schwarz (Kohlegestein)
moreblocks:coal_stone_bricks 60 60 60 # Ziegel aus Kohlegestein, sehr dunkelgrau
moreblocks:cobble_compressed 110 110 110 # Verdichteter Bruchstein, etwas dunkler
moreblocks:desert_cobble_compressed 190 170 130 # Verdichteter Wüstenbruchstein, sandig-grau
moreblocks:dirt_compressed 100 70 50 # Verdichtete Erde, dunkleres Braun
moreblocks:grey_bricks 140 140 140 # Standard graue Ziegel
moreblocks:iron_stone 100 90 85 # Eisengestein, dunkel-metallisch mit Rosthauch
moreblocks:iron_stone_bricks 105 95 90 # Ziegel aus Eisengestein
# morelights_extras
morelights_extras:stone_block 100 97 96 # Standard Steinblockfarbe
# steles
steles:desert_stone_stele 210 180 140 # Wüstenstein (Tan)
steles:sandstone_stele 230 190 150 # Sandstein (helles Tan/Sandorange)
steles:stone_stele 97 94 93 # Standard Steingrau
# xnether
xnether:fence_blue 60 50 150 # Blaues Nether-Holz für Zaun
xnether:fence_purple 100 50 100 # Lilanes Nether-Holz für Zaun
# xpanes
xpanes:jailbars_flat 80 80 90 # Dunkles Metallgrau für Gitterstäbe
xpanes:nether_crystal_pane_flat 120 80 150 # Lilafarbene Kristallglasscheibe

61
config.sh Executable file
View file

@ -0,0 +1,61 @@
#!/bin/bash
# config.sh - Globale Konfiguration für Karten- und Webseitengenerierung
# === Globale Skript- und Pfad-Einstellungen ===
BASE_SCRIPT_DIR="/opt/luweb"
SITE_GENERATOR_BASE_PATH="${BASE_SCRIPT_DIR}/site_generator" # Basis für Generator-Komponenten
TEMPLATE_DIR_PATH="${SITE_GENERATOR_BASE_PATH}/templates" # Unterordner für Templates
EXAMPLE_TEMPLATE_DIR_PATH="${SITE_GENERATOR_BASE_PATH}/examples" # NEU: Für Beispiel-Konfigs
LOG_DIR_BASE="/var/log/luweb"
TEMP_MARKER_DIR="/tmp"
LOCK_FILE_BASE_DIR="/tmp"
DEFAULT_WORLD_NAME_KEY="world"
# === Globale Minetestmapper Standard-Einstellungen ===
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_CFG_DRAWALPHA="true"; DEFAULT_MM_CFG_DRAWORIGIN="true"
DEFAULT_MM_CFG_DRAWPLAYERS="true"; DEFAULT_MM_CFG_DRAWSCALE="true"
# --- Dateinamen und relative Pfade (innerhalb BASE_SCRIPT_DIR) ---
RAW_MAP_BASE_SUBDIR="worldmaps_output"
RAW_MAP_FILENAME="map.png"
# --- Globale Webserver Haupt-Einstellungen ---
WEB_ROOT_PATH="/var/www/luanti.geigernet.eu/web"
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_WEB_MAP_PNG_FILENAME="map.png"
DEFAULT_RESIZED_MAX_DIMENSION=4096
DEFAULT_ARCHIVE_SUBDIR_NAME="archive"
# === Globale Webseiten-Einstellungen ===
SITE_TITLE="Luanti.GEIGERnet"
SITE_OWNER_NAME="Rage87"
SITE_OWNER_EMAIL="rage87@geigernet.eu"
DEFAULT_PLAYER_SKIN_URL="images/user_icon.png"
# Statisches Bannerbild
STATIC_BANNER_FILENAME="luanti_main_banner.png"
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"
DEFAULT_WORLD_SHORT_DESCRIPTION="Keine Kurzbeschreibung vorhanden."
DEFAULT_WORLD_LONG_DESCRIPTION="<p>Für diese Welt wurde noch keine detaillierte Beschreibung hinterlegt.</p>"
# Basisverzeichnis für Webseiten-Quellinhalte (Texte, zu kopierende Bilder)
WEB_CONTENT_DIR_NAME="web_content"
WEB_CONTENT_BASE_PATH="${BASE_SCRIPT_DIR}/${WEB_CONTENT_DIR_NAME}"
WEB_CONTENT_IMAGES_SOURCE_SUBDIR="images"
WEB_CONTENT_STATIC_SUBDIR="static"

204
generate_map.sh Executable file
View file

@ -0,0 +1,204 @@
#!/bin/bash
# Lade globale Konfiguration
GLOBAL_CONFIG_FILE="$(dirname "$0")/config.sh"
if [ -f "$GLOBAL_CONFIG_FILE" ]; then
source "$GLOBAL_CONFIG_FILE"
else
echo "FEHLER: Globale Konfigurationsdatei config.sh nicht gefunden!"
exit 1
fi
# Welt-Schlüssel (Verzeichnisname) aus Argument oder Standardwert
WORLD_KEY="${1:-$DEFAULT_WORLD_NAME_KEY}" # Nutzt DEFAULT_WORLD_NAME_KEY aus config.sh
# 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
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!"
exit 1
fi
if [ ! -f "${CURRENT_MINETEST_WORLD_DATA_PATH}world.mt" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - FEHLER: Die Datei 'world.mt' wurde im Verzeichnis '${CURRENT_MINETEST_WORLD_DATA_PATH}' nicht gefunden!"
exit 1
fi
# === Lade Standardwerte und überschreibe mit welt-spezifischer web.conf ===
MM_OPT_ZOOM_LEVEL="$DEFAULT_MM_OPT_ZOOM_LEVEL"; MM_OPT_MIN_Y="$DEFAULT_MM_OPT_MIN_Y"
MM_OPT_ORIGINCOLOR="$DEFAULT_MM_OPT_ORIGINCOLOR"; MM_OPT_PLAYERCOLOR="$DEFAULT_MM_OPT_PLAYERCOLOR"
MM_OPT_SCALECOLOR="$DEFAULT_MM_OPT_SCALECOLOR"; MM_OPT_BGCOLOR="$DEFAULT_MM_OPT_BGCOLOR"
MM_CFG_DRAWALPHA="$DEFAULT_MM_CFG_DRAWALPHA"; MM_CFG_DRAWORIGIN="$DEFAULT_MM_CFG_DRAWORIGIN"
MM_CFG_DRAWPLAYERS="$DEFAULT_MM_CFG_DRAWPLAYERS"; MM_CFG_DRAWSCALE="$DEFAULT_MM_CFG_DRAWSCALE"
TILES_SUBDIR_NAME="$DEFAULT_TILES_SUBDIR_NAME"; GDAL2TILES_ZOOM_LEVELS="$DEFAULT_GDAL2TILES_ZOOM_LEVELS"
WEB_MAP_PNG_FILENAME="$DEFAULT_WEB_MAP_PNG_FILENAME"; RESIZED_MAX_DIMENSION="$DEFAULT_RESIZED_MAX_DIMENSION"
ARCHIVE_SUBDIR_NAME="$DEFAULT_ARCHIVE_SUBDIR_NAME"
# RAW_MAP_FILENAME ist global aus config.sh
# RAW_MAP_BASE_SUBDIR ist global aus config.sh (wird für den Output im BASE_SCRIPT_DIR verwendet)
WORLD_WEB_CONFIG_FILE="${CURRENT_MINETEST_WORLD_DATA_PATH}web.conf" # web.conf im Minetest Weltordner
if [ -f "$WORLD_WEB_CONFIG_FILE" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - Lade Web-Konfiguration aus ${WORLD_WEB_CONFIG_FILE}"
source "$WORLD_WEB_CONFIG_FILE"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - WARNUNG: Keine web.conf (${WORLD_WEB_CONFIG_FILE}) gefunden. Verwende globale/Default-Einstellungen."
fi
# === Abgeleitete Variablen ===
SCRIPT_BASENAME=$(basename "$0" .sh)
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"
# Wichtige Prüfungen für Pfadkomponenten für den Output von minetestmapper
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
# Ausgabepfade für rohe Kartendaten (im BASE_SCRIPT_DIR)
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"
# Web-Pfade (nutzen den WORLD_KEY für die Verzeichnisstruktur)
WEB_CURRENT_WORLD_DIR="${WEB_ROOT_PATH}/${WEB_MAPS_BASE_SUBDIR}/${WORLD_KEY}"
TILES_FULL_OUTPUT_PATH="${WEB_CURRENT_WORLD_DIR}/${TILES_SUBDIR_NAME}"
WEB_MAP_PNG_FULL_PATH="${WEB_CURRENT_WORLD_DIR}/${WEB_MAP_PNG_FILENAME}"
ARCHIVE_BASE_WEB_PATH="${WEB_CURRENT_WORLD_DIR}/${ARCHIVE_SUBDIR_NAME}"
# === Logging Funktion ===
log_message() {
local message_to_log; message_to_log="$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] - $1"
echo "${message_to_log}" | tee -a "$LOG_FILE"
}
# === Funktion zur Archivbereinigung (unverändert) ===
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
local today_seconds=$(date +%s); local cutoff_date_14_days=$(date -d "today - 14 days" +%Y-%m-%d)
local cutoff_seconds_14_days=$(date -d "$cutoff_date_14_days" +%s)
log_message "Archivbereinigung: Behalte tägliche Bilder bis einschl. ${cutoff_date_14_days}. Ältere nur Montage."
local images_processed=0; local images_deleted=0
find "$ARCHIVE_BASE_WEB_PATH" -type f -name "*.png" | while IFS= read -r archive_file_path; do
images_processed=$((images_processed + 1))
if [[ "$archive_file_path" =~ /([0-9]{4})/([0-9]{2})/([0-9]{2})\.png$ ]]; then
local year="${BASH_REMATCH[1]}"; local month="${BASH_REMATCH[2]}"; local day="${BASH_REMATCH[3]}"
local img_date_str="${year}-${month}-${day}"; local img_date_seconds; local day_of_week
if ! date -d "$img_date_str" "+%s" >/dev/null 2>&1; then log_message "WARNUNG: Ungültiges Datum: '${img_date_str}' ('${archive_file_path}')."; continue; fi
img_date_seconds=$(date -d "$img_date_str" +%s)
if [ "$img_date_seconds" -ge "$cutoff_seconds_14_days" ]; then log_message "BEHALTE (<=14 Tage): ${archive_file_path}"
else day_of_week=$(date -d "$img_date_str" +%u); if [ "$day_of_week" -eq 1 ]; then log_message "BEHALTE (>14 Tage, Montag): ${archive_file_path}"; else log_message "LÖSCHE (>14 Tage, kein Montag): ${archive_file_path}"; if rm -f "$archive_file_path"; then images_deleted=$((images_deleted + 1)); else log_message "FEHLER Löschen: ${archive_file_path}"; fi; fi; fi
else log_message "WARNUNG: Pfad '${archive_file_path}' passt nicht zu JJJJ/MM/TT.png."; fi
done
log_message "Archivbereinigung: ${images_processed} geprüft, ${images_deleted} gelöscht."
log_message "Räume leere Archiv-Unterverzeichnisse auf..."; find "$ARCHIVE_BASE_WEB_PATH" -mindepth 2 -maxdepth 2 -type d -empty -print -delete >> "$LOG_FILE" 2>&1
find "$ARCHIVE_BASE_WEB_PATH" -mindepth 1 -maxdepth 1 -type d -empty -print -delete >> "$LOG_FILE" 2>&1; log_message "Aufräumen leerer Archiv-Unterverzeichnisse abgeschlossen."
}
# === Hauptlogik ===
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "$(date '+%Y-%m-%d %H:%M:%S') - [${WORLD_KEY}] Script ${SCRIPT_BASENAME}.sh ist bereits aktiv (Lock: ${LOCK_FILE}). Beende." | tee -a "$LOG_FILE"; exit 1; }
trap 'rm -f "$LOCK_FILE"; log_message "Skript ${SCRIPT_BASENAME}.sh beendet."' EXIT
mkdir -p "$LOG_DIR_BASE"; log_message "Skript ${SCRIPT_BASENAME}.sh gestartet für Welt-Schlüssel: ${WORLD_KEY}"
log_message "DEBUG: MINETESTMAPPER_WORLD_DATA_BASE_PATH = '${MINETESTMAPPER_WORLD_DATA_BASE_PATH}'" # Neuer Debug-Log
log_message "DEBUG: CURRENT_MINETEST_WORLD_DATA_PATH (für -i Option) = '${CURRENT_MINETEST_WORLD_DATA_PATH}'" # Neuer Debug-Log
log_message "DEBUG: RAW_MAP_ABSOLUTE_PATH (für -o Option) = '${RAW_MAP_ABSOLUTE_PATH}'"
mkdir -p "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}"
if [ ! -d "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" ]; then log_message "FEHLER: Rohkarten-Ausgabeverz. (${RAW_MAP_OUTPUT_DIR_ABSOLUTE}) nicht erstellt."; exit 1; fi
log_message "DEBUG: Rohkarten-Ausgabeverzeichnis (${RAW_MAP_OUTPUT_DIR_ABSOLUTE}) existiert."
ls -ld "${RAW_MAP_OUTPUT_DIR_ABSOLUTE}" >> "$LOG_FILE" 2>/dev/null
# 1. Generiere die map.png
if [ ! -x "$MINETESTMAPPER_PATH" ]; then log_message "FEHLER: minetestmapper (${MINETESTMAPPER_PATH}) nicht ausführbar."; exit 1; fi
MM_ALL_OPTIONS_STR="--zoom ${MM_OPT_ZOOM_LEVEL}"; if [ "${MM_CFG_DRAWALPHA}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawalpha"; fi
if [ "${MM_CFG_DRAWORIGIN}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --draworigin"; fi
if [ "${MM_CFG_DRAWPLAYERS}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawplayers"; fi
if [ "${MM_CFG_DRAWSCALE}" = "true" ]; then MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --drawscale"; fi
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --origincolor '${MM_OPT_ORIGINCOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --playercolor '${MM_OPT_PLAYERCOLOR}'"
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --scalecolor '${MM_OPT_SCALECOLOR}'"; MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --bgcolor '${MM_OPT_BGCOLOR}'"
MM_ALL_OPTIONS_STR="${MM_ALL_OPTIONS_STR} --min-y ${MM_OPT_MIN_Y}"
# Verwendet CURRENT_MINETEST_WORLD_DATA_PATH für -i (korrekt mit / am Ende aus config.sh und WORLD_KEY)
MAP_GENERATION_COMMAND_TO_EVAL="'${MINETESTMAPPER_PATH}' -i '${CURRENT_MINETEST_WORLD_DATA_PATH}' -o '${RAW_MAP_ABSOLUTE_PATH}' ${MM_ALL_OPTIONS_STR}"
log_message "DEBUG: Eval-Befehl: ${MAP_GENERATION_COMMAND_TO_EVAL}"
log_message "Starte minetestmapper (Optionen: ${MM_ALL_OPTIONS_STR}). Ausgaben folgen:"
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 [ ! -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}."
# 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"
if [ -s "$TEMP_NEW_UNKNOWN_NODES_FILE" ]; then log_message "Neue 'Unknown nodes' gefunden."; touch "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; cat "$TEMP_NEW_UNKNOWN_NODES_FILE" >> "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; sort -u "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}" -o "${UNKNOWN_NODES_FILE_ABSOLUTE_PATH}"; log_message "$(wc -l < "$TEMP_NEW_UNKNOWN_NODES_FILE") 'Unknown nodes' verarbeitet.";
else log_message "Keine neuen 'Unknown nodes' gefunden."; fi; rm -f "$TEMP_NEW_UNKNOWN_NODES_FILE"
else log_message "WARNUNG: minetestmapper-Ausgabe nicht verarbeitbar."; fi
rm -f "$MAPPER_RUN_OUTPUT_CAPTURE_FILE"
# 2. Web-Vorschaukarte (verkleinert) erstellen (wie zuvor)
# ...
log_message "Erzeuge Web-Version von ${RAW_MAP_FILENAME} (max ${RESIZED_MAX_DIMENSION}px) 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
fi
fi
# 3. Tiles generieren (wie zuvor)
# ...
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")
if [ $? -ne 0 ]; then log_message "FEHLER: gdal2tiles.py fehlgeschlagen."; rm -rf "$TEMP_TILES_DIR"; exit 1; fi
log_message "Kacheln in ${TEMP_TILES_DIR} generiert."
log_message "Entferne altes Kachel-Verzeichnis: ${TILES_FULL_OUTPUT_PATH}"; rm -rf "$TILES_FULL_OUTPUT_PATH"
log_message "Verschiebe neue Kacheln nach ${TILES_FULL_OUTPUT_PATH}"
if ! mv "$TEMP_TILES_DIR" "$TILES_FULL_OUTPUT_PATH"; then log_message "FEHLER: Verschieben der Kacheln fehlgeschlagen."; exit 1; fi
log_message "Neue Kacheln erfolgreich verschoben."
fi
# === Archivbereinigung ===
prune_archives
# 4. Tägliches Archivbild (wie zuvor)
# ...
ARCHIVE_YEAR=$(date '+%Y'); ARCHIVE_MONTH=$(date '+%m'); ARCHIVE_DAY=$(date '+%d')
ARCHIVE_DAILY_TARGET_DIR="${ARCHIVE_BASE_WEB_PATH}/${ARCHIVE_YEAR}/${ARCHIVE_MONTH}"
ARCHIVE_DAILY_FILE_PATH="${ARCHIVE_DAILY_TARGET_DIR}/${ARCHIVE_DAY}.png"
log_message "Prüfe Notwendigkeit für Archivbild für ${ARCHIVE_YEAR}-${ARCHIVE_MONTH}-${ARCHIVE_DAY}..."
if [ ! -f "$ARCHIVE_DAILY_FILE_PATH" ]; then
log_message "Archivbild ${ARCHIVE_DAILY_FILE_PATH} existiert noch nicht. Versuche Erstellung..."
if [ ! -f "$RAW_MAP_ABSOLUTE_PATH" ]; then log_message "FEHLER: Quelldatei (${RAW_MAP_ABSOLUTE_PATH}) nicht gefunden! Archiv nicht erstellt.";
else
mkdir -p "$ARCHIVE_DAILY_TARGET_DIR"
if [ ! -d "$ARCHIVE_DAILY_TARGET_DIR" ]; then log_message "FEHLER: Archiv-Zielverzeichnis ${ARCHIVE_DAILY_TARGET_DIR} nicht erstellt.";
else
log_message "Erzeuge Archivbild (max ${RESIZED_MAX_DIMENSION}px) 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
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
# 5. Status- und Info-Dateien im Webverzeichnis (wie zuvor)
# ...
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."
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
exit 0

268
generate_site.sh Executable file
View file

@ -0,0 +1,268 @@
#!/bin/bash
# Lade globale Konfiguration
CONFIG_FILE_PATH="$(dirname "$0")/config.sh"
if [ -f "$CONFIG_FILE_PATH" ]; then
source "$CONFIG_FILE_PATH"
else
echo "FEHLER: Konfigurationsdatei config.sh nicht unter ${CONFIG_FILE_PATH} gefunden!"
exit 1
fi
# === Abgeleitete 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)
ACTUAL_BANNER_IMG_URL_PATH="${FALLBACK_BANNER_IMG_URL}"
# === Logging Funktion ===
log_message() { local msg; msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"; echo "${msg}" | tee -a "$LOG_FILE"; }
# === 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"
fi
}
# === Template Rendering Funktion ===
render_template() {
local template_path="$1"
if [ ! -f "$template_path" ]; then log_message "FEHLER: Template-Datei nicht gefunden: ${template_path}"; return 1; fi
local template_content; template_content=$(<"$template_path")
eval "cat <<EOF
${template_content}
EOF"
}
# === HTML/CSS Erzeugungsfunktionen ===
generate_css() {
local css_file_path="${WEB_ROOT_PATH}/style.css"
log_message "Erzeuge/Aktualisiere ${css_file_path} aus Template..."
local css_banner_image_path="${ACTUAL_BANNER_IMG_URL_PATH}"
render_template "${TEMPLATE_DIR_PATH}/css.template" > "$css_file_path"
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
render_template "${TEMPLATE_DIR_PATH}/html_header.template"
}
# --- ANGEPASST: Nutzt jetzt die neue Template-Datei ---
generate_html_footer() {
render_template "${TEMPLATE_DIR_PATH}/html_footer.template"
}
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
for world_data_dir_loop_overview in "${world_key_dirs[@]}"; do
local 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")
local admin_name_ov; unset ADMIN_NAME; source "$web_conf_file" &>/dev/null; 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="${DEFAULT_SERVER_DISPLAY_NAME_PREFIX}${current_world_key}"; if [[ "$world_display_name_ov" == "${DEFAULT_SERVER_DISPLAY_NAME_PREFIX}"* && "$DEFAULT_SERVER_DISPLAY_NAME_PREFIX" == "n/a" ]]; then world_display_name_ov="Welt ${current_world_key}"; fi; 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
discovered_worlds_count=$((discovered_worlds_count + 1))
render_template "${TEMPLATE_DIR_PATH}/worlds_overview_entry.template" >> "$overview_file"
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 WORLD_DISPLAY_NAME_PAGE=$(get_config_value_from_file "$web_conf_file" "WORLD_DISPLAY_NAME")
local SERVER_ADDRESS_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_ADDRESS" "$DEFAULT_SERVER_ADDRESS")
local SERVER_PORT_PAGE=$(get_config_value_from_file "$web_conf_file" "SERVER_PORT" "$DEFAULT_SERVER_PORT")
local 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_TILES_SUBDIR_NAME=$(get_config_value_from_file "$web_conf_file" "TILES_SUBDIR_NAME" "$DEFAULT_TILES_SUBDIR_NAME")
local CURRENT_GDAL2TILES_ZOOM_LEVELS=$(get_config_value_from_file "$web_conf_file" "GDAL2TILES_ZOOM_LEVELS" "$DEFAULT_GDAL2TILES_ZOOM_LEVELS")
local 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=$(get_config_value_from_file "$web_conf_file" "ARCHIVE_SUBDIR_NAME" "$DEFAULT_ARCHIVE_SUBDIR_NAME")
local LEAFLET_BOUNDS_SOUTH=$(get_config_value_from_file "$web_conf_file" "LEAFLET_BOUNDS_SOUTH" "$DEFAULT_LEAFLET_BOUNDS_SOUTH")
local LEAFLET_BOUNDS_WEST=$(get_config_value_from_file "$web_conf_file" "LEAFLET_BOUNDS_WEST" "$DEFAULT_LEAFLET_BOUNDS_WEST")
local LEAFLET_BOUNDS_NORTH=$(get_config_value_from_file "$web_conf_file" "LEAFLET_BOUNDS_NORTH" "$DEFAULT_LEAFLET_BOUNDS_NORTH")
local LEAFLET_BOUNDS_EAST=$(get_config_value_from_file "$web_conf_file" "LEAFLET_BOUNDS_EAST" "$DEFAULT_LEAFLET_BOUNDS_EAST")
local LEAFLET_ZOOM_AFTER_FIT=$(get_config_value_from_file "$web_conf_file" "LEAFLET_ZOOM_AFTER_FIT" "$DEFAULT_LEAFLET_ZOOM_AFTER_FIT")
local MT_WORLD_NAME_FROM_MT="$current_world_key"; 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
world_name) MT_WORLD_NAME_FROM_MT="$value" ;; 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"
if [ -z "$WORLD_DISPLAY_NAME_PAGE" ]; then if [ -n "$MT_WORLD_NAME_FROM_MT" ] && [ "$MT_WORLD_NAME_FROM_MT" != "$current_world_key" ]; then WORLD_DISPLAY_NAME_PAGE="$MT_WORLD_NAME_FROM_MT"; else WORLD_DISPLAY_NAME_PAGE="${DEFAULT_SERVER_DISPLAY_NAME_PREFIX}${current_world_key}"; if [[ "$WORLD_DISPLAY_NAME_PAGE" == "${DEFAULT_SERVER_DISPLAY_NAME_PREFIX}"* && "$DEFAULT_SERVER_DISPLAY_NAME_PREFIX" == "n/a" ]]; then WORLD_DISPLAY_NAME_PAGE="Welt ${current_world_key}"; fi;fi;fi
local web_map_preview_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_WEB_MAP_PNG_FILENAME}"
local web_tiles_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_TILES_SUBDIR_NAME}"
local web_last_update_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/last_update.txt"
local web_unknown_nodes_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/unknown_nodes.txt"
local web_online_status_rel_path="${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/online_status.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 archive_world_rel_to_webroot="/${WEB_MAPS_BASE_SUBDIR}/${current_world_key}/${CURRENT_ARCHIVE_SUBDIR_NAME}"
local map_preview_abs_path="${WEB_ROOT_PATH}/${web_map_preview_rel_path}"; local last_update_abs_path="${WEB_ROOT_PATH}/${web_last_update_rel_path}"
local unknown_nodes_abs_path="${WEB_ROOT_PATH}/${web_unknown_nodes_rel_path}"; local min_zoom="${CURRENT_GDAL2TILES_ZOOM_LEVELS%%-*}"; local max_zoom="${CURRENT_GDAL2TILES_ZOOM_LEVELS##*-}"
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}?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_HTML=""; if [ -d "${WEB_ROOT_PATH}/${web_tiles_rel_path}" ]; then 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; MAP_HTML=$(render_template "${TEMPLATE_DIR_PATH}/world_detail_map.template"); else MAP_HTML="<p>Kartenkacheln nicht verfügbar.</p>"; [ -f "$map_preview_abs_path" ] && MAP_HTML+="<p>Statische Vorschau:</p><img src='/${web_map_preview_rel_path}?v=${CACHE_BUSTER}' alt='Vorschau ${WORLD_DISPLAY_NAME_PAGE}' style='max-width:100%;'>"; fi
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}]"; ARCHIVE_HTML=$(render_template "${TEMPLATE_DIR_PATH}/world_detail_archive.template"); else ARCHIVE_HTML="<p>Keine Archivbilder für diese Welt verfügbar.</p>"; fi
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
generate_html_header "Welt: ${WORLD_DISPLAY_NAME_PAGE}" "" "" > "$detail_page_file"
render_template "${TEMPLATE_DIR_PATH}/world_detail_page.template" >> "$detail_page_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
}
# === 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}"
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>"
log_message "Starte Generierung der statischen Webseiten-Dateien..."
# --- Asset Copying ---
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_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
# --- Ende Asset Copying ---
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
generate_css; generate_homepage; generate_impressum; generate_downloads_page; generate_datenschutz_page; generate_worlds_overview
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

File diff suppressed because it is too large Load diff

127
logs/generate_site_cron.log Normal file
View file

@ -0,0 +1,127 @@
2025-06-05 04:01:01 - Script generate_site.sh gestartet.
2025-06-05 04:01:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-05 04:01:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-05 04:01:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-05 04:01:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-05 04:01:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-05 04:01:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-05 04:01:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css...
2025-06-05 04:01:01 - CSS-Datei erfolgreich erstellt.
2025-06-05 04:01:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-05 04:01:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-05 04:01:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-05 04:01:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-05 04:01:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-05 04:01:01 - Weltenübersicht erstellt.
2025-06-05 04:01:02 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-05 04:01:02 - Parse /opt/luanti/data/worlds/world/world.mt für Mods, GameID etc. (Welt 'world')...
2025-06-05 04:01:06 - Erzeuge Detailseite für Welt 'geigernet-world' (Key: world): /var/www/luanti.geigernet.eu/web/world_world.html...
2025-06-05 04:01:06 - Detailseite geigernet-world erstellt.
2025-06-05 04:01:06 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-05 04:01:06 - Webseiten-Generierung abgeschlossen.
2025-06-05 04:01:07 - Script generate_site.sh beendet.
2025-06-05 06:10:01 - Script generate_site.sh gestartet.
2025-06-05 06:10:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-05 06:10:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-05 06:10:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-05 06:10:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-05 06:10:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-05 06:10:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-05 06:10:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css...
2025-06-05 06:10:01 - CSS-Datei erfolgreich erstellt.
2025-06-05 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-05 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-05 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-05 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-05 06:10:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-05 06:10:02 - Weltenübersicht erstellt.
2025-06-05 06:10:02 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-05 06:10:02 - Parse /opt/luanti/data/worlds/world/world.mt für Mods, GameID etc. (Welt 'world')...
2025-06-05 06:10:05 - Erzeuge Detailseite für Welt 'geigernet-world' (Key: world): /var/www/luanti.geigernet.eu/web/world_world.html...
2025-06-05 06:10:05 - Detailseite geigernet-world erstellt.
2025-06-05 06:10:05 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-05 06:10:05 - Webseiten-Generierung abgeschlossen.
2025-06-05 06:10:05 - Script generate_site.sh beendet.
2025-06-05 12:10:01 - Script generate_site.sh gestartet.
2025-06-05 12:10:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-05 12:10:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-05 12:10:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-05 12:10:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-05 12:10:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-05 12:10:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-05 12:10:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css...
2025-06-05 12:10:01 - CSS-Datei erfolgreich erstellt.
2025-06-05 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-05 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-05 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-05 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-05 12:10:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-05 12:10:02 - Weltenübersicht erstellt.
2025-06-05 12:10:02 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-05 12:10:02 - Parse /opt/luanti/data/worlds/world/world.mt für Mods, GameID etc. (Welt 'world')...
2025-06-05 12:10:05 - Erzeuge Detailseite für Welt 'geigernet-world' (Key: world): /var/www/luanti.geigernet.eu/web/world_world.html...
2025-06-05 12:10:05 - Detailseite geigernet-world erstellt.
2025-06-05 12:10:05 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-05 12:10:05 - Webseiten-Generierung abgeschlossen.
2025-06-05 12:10:05 - Script generate_site.sh beendet.
2025-06-06 00:10:01 - Script generate_site.sh gestartet.
2025-06-06 00:10:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-06 00:10:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-06 00:10:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-06 00:10:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-06 00:10:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-06 00:10:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-06 00:10:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css aus Template...
2025-06-06 00:10:01 - CSS-Datei erfolgreich erstellt.
2025-06-06 00:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-06 00:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-06 00:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-06 00:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-06 00:10:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-06 00:10:01 - Weltenübersicht erstellt.
2025-06-06 00:10:01 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-06 00:10:05 - Erzeuge Detailseite für Welt 'geigernet-world' (Key: world): /var/www/luanti.geigernet.eu/web/world_world.html...
2025-06-06 00:10:05 - Detailseite geigernet-world erstellt.
2025-06-06 00:10:05 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-06 00:10:05 - Webseiten-Generierung abgeschlossen.
2025-06-06 00:10:05 - Script generate_site.sh beendet.
2025-06-06 06:10:01 - Script generate_site.sh gestartet.
2025-06-06 06:10:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-06 06:10:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-06 06:10:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-06 06:10:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-06 06:10:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-06 06:10:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-06 06:10:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css aus Template...
2025-06-06 06:10:01 - CSS-Datei erfolgreich erstellt.
2025-06-06 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-06 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-06 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-06 06:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-06 06:10:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-06 06:10:01 - Weltenübersicht erstellt.
2025-06-06 06:10:01 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-06 06:10:06 - Detailseite geigernet-world erstellt.
2025-06-06 06:10:06 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-06 06:10:06 - Webseiten-Generierung abgeschlossen.
2025-06-06 06:10:06 - Script generate_site.sh beendet.
2025-06-06 12:10:01 - Script generate_site.sh gestartet.
2025-06-06 12:10:01 - Prüfe und erstelle Webseiten-Inhaltsverzeichnisse und Platzhalter...
2025-06-06 12:10:01 - Starte Generierung der statischen Webseiten-Dateien...
2025-06-06 12:10:01 - Kopiere Webseiten-Assets (Bilder, Icons)...
2025-06-06 12:10:01 - Allgemeine Bilder kopiert/aktualisiert.
2025-06-06 12:10:01 - Spieler-Skins kopiert/aktualisiert.
2025-06-06 12:10:01 - Verwende statisches Banner: /images/luanti_main_banner.png
2025-06-06 12:10:01 - Erzeuge/Aktualisiere /var/www/luanti.geigernet.eu/web/style.css aus Template...
2025-06-06 12:10:01 - CSS-Datei erfolgreich erstellt.
2025-06-06 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/index.html...
2025-06-06 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/impressum.html...
2025-06-06 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/downloads.html...
2025-06-06 12:10:01 - Erzeuge /var/www/luanti.geigernet.eu/web/datenschutz.html...
2025-06-06 12:10:01 - Erzeuge Weltenübersicht: /var/www/luanti.geigernet.eu/web/worlds.html...
2025-06-06 12:10:01 - Weltenübersicht erstellt.
2025-06-06 12:10:01 - Platzhalter areas.txt, players.txt, weather.txt für Welt 'world' sichergestellt.
2025-06-06 12:10:04 - Detailseite geigernet-world erstellt.
2025-06-06 12:10:04 - 1 Welt-Detailseiten generiert/aktualisiert.
2025-06-06 12:10:04 - Webseiten-Generierung abgeschlossen.
2025-06-06 12:10:04 - Script generate_site.sh beendet.

BIN
minetestmapper Executable file

Binary file not shown.

View file

@ -0,0 +1,78 @@
# ================================================================
# Beispiel-Konfiguration für eine neue Welt (web.conf)
# ================================================================
# Passen Sie die folgenden Werte für Ihre neue Welt an.
# --- Allgemeine Welt-Informationen ---
WORLD_DISPLAY_NAME="Beispiel-Welt"
SERVER_ADDRESS="deine-server-adresse.de"
SERVER_PORT="30000"
SERVER_ACCESS_INFO="Kein Passwort"
# --- Welt-Admins (ARRAY-STRUKTUR) ---
# Jeder Admin erhält einen Index, startend bei 0 (0, 1, 2, ...).
# Lassen Sie Felder entsprechende leer, wenn die Info nicht angezeigt werden soll.
# Erster Admin
ADMIN_NAME[0]="Admin1"
ADMIN_SKIN_URL[0]="images/user_icon.png" # Pfad relativ zum Web-Root
ADMIN_EMAIL[0]="admin1@example.com"
ADMIN_DISCORD[0]="" # Discord Benutzer-ID oder Name
ADMIN_MATRIX[0]="@admin1:matrix.org"
ADMIN_STEAM[0]="" # Komplette URL zum Steam-Profil
ADMIN_TEAMSPEAK[0]=""
ADMIN_MUMBLE[0]=""
# Zweiter Admin (optional)
# ADMIN_NAME[1]="Admin2"
# ADMIN_SKIN_URL[1]="images/user_icon.png"
# ADMIN_EMAIL[1]="admin2@example.com"
# --- Beschreibungen (erlaubt mehrzeiliges HTML) ---
WORLD_SHORT_DESCRIPTION="Eine kurze, knackige Beschreibung dieser neuen Welt, die in der Weltenübersicht angezeigt wird."
WORLD_LONG_DESCRIPTION=$(cat <<'EOD'
<p><strong>Willkommen in der Beispiel-Welt!</strong></p>
<p>Dies ist eine ausführliche Beschreibung, die auf der Detailseite der Welt erscheint. Sie können hier <strong>beliebiges HTML</strong> verwenden, um den Text zu formatieren.</p>
<ul>
<li>Listenpunkte sind möglich.</li>
<li>Genau wie Absätze und <strong>fetter Text</strong>.</li>
</ul>
<p>Beschreiben Sie hier die Besonderheiten Ihrer Welt, die Ziele oder die Community.</p>
EOD
)
# Spielregeln (erlaubt mehrzeiliges HTML)
WORLD_GAME_RULES=$(cat <<'EOD'
<p>Bitte halten Sie sich an folgende Regeln:</p>
<ol>
<li>Seien Sie respektvoll zu anderen Spielern.</li>
<li>Zerstören Sie keine fremden Bauwerke.</li>
<li>Keine Cheats oder Hacks.</li>
</ol>
EOD
)
# ================================================================
# OPTIONALE ÜBERSCHREIBUNGEN (Kommentare entfernen zum Aktivieren)
# ================================================================
# --- Leaflet Kartengrenzen ---
# Diese Werte aus der gdal2tiles Ausgabe (leaflet.html) entnehmen!
# Wichtig für die korrekte Positionierung der Karte.
#LEAFLET_BOUNDS_SOUTH="-85.0511"
#LEAFLET_BOUNDS_WEST="-170"
#LEAFLET_BOUNDS_NORTH="85.0511"
#LEAFLET_BOUNDS_EAST="170"
#LEAFLET_ZOOM_AFTER_FIT="1"
# --- Minetestmapper-Optionen für DIESE WELT ---
#MM_OPT_ZOOM_LEVEL="3"
#MM_CFG_DRAWPLAYERS="false"
#MM_OPT_BGCOLOR="#000000"
# --- Kachel-Optionen für DIESE WELT ---
#GDAL2TILES_ZOOM_LEVELS="0-6"
#TILES_SUBDIR_NAME="map_tiles_hd"

View file

@ -0,0 +1,474 @@
body { font-family: sans-serif; margin: 0; background-color: #333; color: #ddd; line-height: 1.6; }
header {
background-color: #222;
padding: 1em;
text-align: center;
border-bottom: 2px solid #4A2E0A;
}
header h1 { margin: 0; color: #FFD700; }
.header-separator {
width: 100px;
height: 2px;
background-color: #FFD700;
border: 0;
border-radius: 1px;
margin: 15px auto 10px auto;
}
nav {
margin-top: 15px;
}
nav ul {
list-style-type: none;
padding: 0;
margin: 0;
text-align: center;
}
nav ul li {
display: inline-block;
margin: 0 2px;
}
nav ul li a {
display: block;
padding: 8px 15px;
color: #ddd;
text-decoration: none;
font-weight: bold;
border-radius: 5px;
transition: background-color 0.3s, color 0.3s;
}
nav ul li a:hover {
background-color: #555;
color: #FFC500;
text-decoration: none;
}
nav ul li.active a {
background-color: #FFA500;
color: #222;
}
a { color: #FFA500; text-decoration: none; }
a:hover { text-decoration: underline; color: #FFC500; }
.header-banner {
width: 100%;
height: 240px;
background-image: url('${css_banner_image_path}?v=${CACHE_BUSTER}');
background-size: cover;
background-position: center 45%;
border-bottom: 2px solid #4A2E0A;
}
.container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
background-color: #444;
border: 1px solid #555;
border-radius: 8px;
position: relative;
}
h2 { color: #FFD700; border-bottom: 1px solid #555; padding-bottom: 0.5em; }
h2.world-detail-title { text-align: center; font-size: 1.8em; }
h3 { color: #FFD700; margin-top: 1.5em; padding-bottom: 0.3em; border-bottom: 1px dotted #555;}
/* Welt-Übersicht */
a.world-preview {
display: block;
border: 1px solid #555;
margin-bottom: 20px;
padding: 15px;
background-color: #3a3a3a;
border-radius: 5px;
text-decoration: none;
color: #ddd;
}
a.world-preview:hover {
background-color: #4f4f4f;
text-decoration: none;
}
.world-preview .world-preview-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #666; padding-bottom: 10px; margin-bottom: 10px;}
.world-preview .world-preview-header h3 { border-bottom: none; margin: 0; padding: 0; font-size: 1.5em; color: #FFD700 !important; text-decoration: none !important; }
.world-preview .world-preview-header h3:hover { text-decoration: none !important; }
.world-preview .world-preview-header .online-status-badge { font-size: 0.9em; }
.world-preview .world-preview-content { display: flex; align-items: flex-start; }
.world-preview img { max-width: 180px; max-height: 135px; object-fit: cover; height: auto; margin-right: 15px; border: 1px solid #666; flex-shrink: 0; }
.world-preview-text p { color: #ccc !important; margin-top: 0; text-decoration: none !important; }
.world-preview-footer { font-size: 0.85em; color: #aaa; border-top: 1px solid #666; margin-top: 10px; padding-top: 10px; }
.online-status-badge { font-size: 0.8em; padding: 3px 8px; border-radius: 10px; color: white !important; margin-left: 10px; }
.online-status-badge.online { background-color: #3E8E41; } .online-status-badge.offline { background-color: #C0392B; }
/* Welt-Detailseite */
.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: 500px; 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; }
.info-box { background-color: #3a3a3a; border: 1px solid #555; padding: 15px; margin-bottom: 20px; border-left: 5px solid #FFD700; border-radius: 3px;}
.info-box h3 { margin-top: 0; border-bottom: none; padding-bottom: 0.2em; }
/* Admin Sektion */
.admin-section h3 { margin-top: 2.5em !important; }
.admin-grid {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.admin-box {
background-color: #303030;
padding: 15px;
border-radius: 5px;
border: 1px solid #4a4a4a;
flex: 1;
min-width: 280px;
}
.admin-contact-block { display: flex; align-items: center; }
.admin-skin-icon { width: 48px; height: 48px; margin-right: 15px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;}
.admin-text-details p { margin: 0.2em 0; }
.admin-player-name { font-weight: bold; color: #FFD700; font-size: 1.1em; }
.contact-links {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #555;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.contact-button {
background-color: #555;
color: #ddd;
border: 1px solid #666;
padding: 3px 8px;
font-size: 0.85em;
border-radius: 3px;
text-decoration: none;
position: relative;
}
.contact-button:hover {
background-color: #666;
color: #FFA500;
text-decoration: none;
}
.contact-links a[data-title]:hover::after {
content: attr(data-title);
position: absolute;
background-color: #222;
color: #fff;
padding: 3px 7px;
border-radius: 3px;
font-size: 0.7rem;
white-space: nowrap;
z-index: 100;
left: 50%;
bottom: 125%;
transform: translateX(-50%);
border: 1px solid #555;
}
.server-details p { margin: 0.5em 0; display: flex; align-items: center;} .server-details strong { min-width: 120px; display: inline-block; flex-shrink: 0;}
.status-text-colored.online { color: #7CFC00; font-weight: bold; }
.status-text-colored.offline { color: #FF4500; font-weight: bold; }
.info-separator { border: 0; height: 1px; background-color: #FFA500; margin: 15px 0; }
.copy-button { background: #555; border: 1px solid #666; color: #FFA500; padding: 1px 5px; margin-left: 8px; cursor: pointer; border-radius: 3px; font-size: 0.8em; vertical-align: middle;}
.copy-button:hover { background: #666; }
.player-list-container { margin-top: 1em; }
.player-list-grid { display: flex; flex-wrap: wrap; gap: 15px; box-sizing: border-box; }
.player-box { background-color: #303030; border: 1px solid #555; border-radius: 5px; padding: 10px; width: calc(33.333% - 10px); box-sizing: border-box; display: flex; flex-direction: column;}
.player-box.has-server-priv { border-left: 5px solid #FFD700; }
.player-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.player-identity { display: flex; align-items: center; }
.player-vitals { display: flex; gap: 10px; }
.player-icon { width: 40px; height: 40px; margin-right: 10px; border: 1px solid #666; border-radius: 3px; flex-shrink: 0;}
.player-name-status { display: flex; align-items: center; }
.player-name { font-weight: bold; margin-left: 5px; color: #FFD700; }
.privilege-separator {
height: 1px;
background-color: #555;
border: 0;
margin: 8px 0;
}
.player-attributes { font-size: 0.9em; line-height: 1.5; }
.player-attributes .icon { margin-right: 8px; cursor: help; font-size: 1.2em; }
.player-attributes .icon.privilege { opacity: 0.15; }
.player-attributes .icon.privilege.active { opacity: 1; }
.player-attributes .icon[data-title]:hover::after, .status-dot[data-title]:hover::after, .player-vitals .icon[data-title]:hover::after {
content: attr(data-title); position: absolute; background-color: #222; color: #fff;
padding: 3px 7px; border-radius: 3px; font-size: 0.85em; white-space: nowrap;
z-index: 100; margin-left: 5px; transform: translateY(-28px);
border: 1px solid #555;
}
footer { text-align: center; margin-top: 30px; padding: 20px; background-color: #222; color: #aaa; border-top: 2px solid #4A2E0A;}
.scrollable-mod-list { max-height: 20em; overflow-y: auto; border: 1px solid #555; padding: 10px; background-color: #303030; margin-top: 0.5em; border-radius: 3px; }
ul.mod-list, ul.mod-list ul { list-style-type: none; padding-left: 20px; } ul.mod-list > li { margin-bottom: 0.5em; }
ul.mod-list > li > ul > li { font-size: 0.9em; }
.status-dot { height: 10px; width: 10px; border-radius: 50%; display: inline-block; margin-right: 5px; vertical-align: middle; background-color: #bbb; cursor: help;}
.status-dot.online { background-color: #7CFC00; }
.status-dot.offline { background-color: #FF4500; }
.status-dot.unknown { background-color: #FFA500; }
.status-text { vertical-align: middle; }
.status-text.status-loading { color: #ccc; font-style: italic; }
.status-text.status-online { color: #7CFC00; font-weight: bold; }
.status-text.status-offline { color: #FF4500; font-weight: bold; }
.status-text.status-unknown { color: #FFA500; font-weight: bold; }
.archive-controls { margin-bottom: 15px; } .archive-controls label { margin-right: 5px; }
.archive-controls select { padding: 5px; background-color: #555; color: #ddd; border: 1px solid #666; border-radius: 3px;}
.archive-image-container { margin-top: 10px; text-align: center; }
.archive-image-container img { max-width: 100%; border: 1px solid #666; background-color: #222; }
.archive-image-container p { color: #aaa; }
/* Stile für responsive Navigation & Burger-Menü */
.responsive-nav {
position: relative;
margin-bottom: 15px;
}
.burger-menu {
display: none;
width: 40px;
height: 40px;
background-color: #555;
border: 1px solid #666;
border-radius: 5px;
cursor: pointer;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 8px;
box-sizing: border-box;
position: absolute;
top: 0;
right: 0;
z-index: 101;
}
.burger-menu:hover {
background-color: #666;
}
.burger-menu span {
display: block;
width: 100%;
height: 3px;
background-color: #FFA500;
border-radius: 3px;
}
/* Stile für Live/Archiv Umschalter */
.section-header-flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.map-toggle-controls {
display: flex;
gap: 5px;
}
.map-toggle-button {
background-color: #555;
color: #ddd;
border: 1px solid #666;
padding: 3px 8px;
font-size: 0.85em;
border-radius: 3px;
cursor: pointer;
}
.map-toggle-button:hover {
background-color: #666;
}
.map-toggle-button.active {
color: #FFA500;
border-color: #FFA500;
font-weight: bold;
}
/* Spielerliste Filter */
.filter-container {
position: relative;
}
.filter-dropdown-btn {
background-color: #555;
color: #ddd;
border: 1px solid #666;
padding: 3px 8px;
font-size: 0.85em;
border-radius: 3px;
cursor: pointer;
}
.filter-dropdown-btn:hover {
background-color: #666;
}
.filter-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
background-color: #3a3a3a;
border: 1px solid #666;
border-radius: 5px;
padding: 10px;
z-index: 100;
min-width: 220px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.filter-dropdown.show {
display: block;
}
.filter-option {
display: flex;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
font-size: 0.9em;
font-weight: normal;
color: #ddd;
}
.filter-option:last-child {
margin-bottom: 0;
}
.filter-option input[type="checkbox"] {
margin-right: 8px;
}
.filter-option .icon {
font-size: 1.2em;
width: 20px;
text-align: center;
margin-right: 8px;
}
.filter-option .status-dot {
margin-left: 0;
margin-right: 8px;
}
/* Stile für "Weiterlesen"-Funktion */
.collapsible-text {
max-height: 250px;
overflow: hidden;
position: relative;
transition: max-height 0.5s ease-in-out;
}
.collapsible-text.expanded {
max-height: 10000px;
}
.collapsible-text::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 80px;
background: linear-gradient(to bottom, rgba(68, 68, 68, 0), rgba(68, 68, 68, 1) 100%);
pointer-events: none;
transition: opacity 0.3s;
}
.collapsible-text.expanded::after {
opacity: 0;
}
.read-more-container {
text-align: right;
margin-top: 5px;
}
a.read-more-link {
font-size: 0.9em;
font-weight: bold;
}
/* Schließen-Button (X) */
.close-button-container {
position: absolute;
top: 15px;
right: 15px;
z-index: 10;
}
.close-button {
display: inline-flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
background-color: #555;
color: #ddd;
border: 1px solid #666;
border-radius: 50%;
text-decoration: none;
font-size: 1em;
line-height: 1;
}
.close-button:hover {
background-color: #C0392B;
color: white;
text-decoration: none;
}
/* Stile für "Nach oben"-Button */
.scroll-to-top-btn {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 999;
background-color: #555;
color: #FFA500;
border: 1px solid #666;
border-radius: 50%;
width: 40px;
height: 40px;
text-align: center;
font-size: 24px;
line-height: 38px;
cursor: pointer;
transition: background-color 0.3s, opacity 0.3s;
}
.scroll-to-top-btn:hover {
background-color: #666;
text-decoration: none;
}
.scroll-to-top-btn.show {
display: block;
}
.leaflet-control-fullscreen a { background:#fff url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"%3E%3Cpath d="M4 14h6v6h4v-4h6v-4h-6v-4h-4v4H4zm17 2h12v-2H21zm0 4h12v-2H21zm5 1h7v-2h-7zM10 23h12v4h-4v-4h-4v4H10zm15 0h7v-2h-7z"/%3E%3C/svg%3E') no-repeat 0 0 !important; background-size: 16px 16px !important; }
.leaflet-control-fullscreen a.leaflet-fullscreen-on { background-image: url('data:image/svg+xml;charset=utf8,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"%3E%3Cpath d="M7 13h2V7h6V5H7zm11-2h7v7h-2v-5h-5z"/%3E%3C/svg%3E') no-repeat 0 0 !important; background-size: 16px 16px !important; }
@media (max-width: 768px) {
/* Responsive Stile für Burger-Menü */
.responsive-nav {
height: 50px;
}
.burger-menu {
display: flex;
}
.page-nav-buttons {
display: none;
position: absolute;
top: 45px;
right: 0;
width: 200px;
background-color: #3a3a3a;
border: 1px solid #666;
border-radius: 5px;
z-index: 100;
padding: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.5);
}
.page-nav-buttons.menu-open {
display: flex;
flex-direction: column;
align-items: stretch;
}
.page-nav-buttons .button {
text-align: left;
margin: 5px;
width: auto;
}
.player-box { width: calc(50% - 10px); }
.page-nav-buttons .button { margin-top: 5px;}
/* Responsive Stile für Welt-Vorschau */
.world-preview .world-preview-content {
flex-direction: column;
}
.world-preview img {
width: 100%;
max-width: 100%;
height: auto;
margin-right: 0;
margin-bottom: 15px;
}
/* NEU: Responsive Höhe für Leaflet-Karte */
.leaflet-map {
height: auto; /* Deaktiviert die feste Höhe von 500px */
aspect-ratio: 1 / 1; /* Erzwingt ein quadratisches Seitenverhältnis */
max-height: 70vh; /* Stellt sicher, dass die Karte nicht den ganzen Bildschirm füllt */
}
}
@media (max-width: 480px) { .player-box { width: 100%; } }

View file

@ -0,0 +1,9 @@
</div>
<footer>
<p>&copy; $(date '+%Y') ${SITE_TITLE} | Betreiber: ${SITE_OWNER_NAME} | <a href="impressum.html">Impressum</a> | <a href="datenschutz.html">Datenschutz</a></p>
</footer>
<a href="#" id="scrollToTopBtn" class="scroll-to-top-btn" title="Nach oben scrollen">▲</a>
</body>
</html>

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<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>
<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() {
if (window.scrollY > 300) {
scrollToTopBtn.classList.add('show');
} else {
scrollToTopBtn.classList.remove('show');
}
});
});
// Klick-Event für den Scroll-To-Top Button
scrollToTopBtn.addEventListener('click', function(e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
</script>
</head>
<body>
<header>
<h1>${SITE_TITLE}</h1>
<div class="header-separator"></div>
<nav>
<ul>
<li class="${active_class_home}"><a href="${relative_path_prefix}/index.html">Startseite</a></li>
<li class="${active_class_worlds}"><a href="${relative_path_prefix}/worlds.html">Welten</a></li>
<li class="${active_class_downloads}"><a href="${relative_path_prefix}/downloads.html">Downloads</a></li>
</ul>
</nav>
</header>
<div class="header-banner"></div>
<div class="container">

View file

@ -0,0 +1,3 @@
<div class="archive-controls"><label for="archive-date-select-${current_world_key}">Archivdatum:</label><select id="archive-date-select-${current_world_key}"><option value="">-- Bitte wählen --</option></select><span id="archive-loading-error-${current_world_key}" style="color:red;margin-left:10px;"></span></div>
<div class="archive-image-container"><img id="archive-image-${current_world_key}" src="#" alt="Archivkarte" style="max-width:100%;border:1px solid #666;display:none;"><p id="archive-no-image-${current_world_key}" style="display:none;">Kein Bild für Auswahl.</p></div>
<script>(function(){const e=document.getElementById('archive-date-select-${current_world_key}'),t=document.getElementById('archive-image-${current_world_key}'),n=document.getElementById('archive-no-image-${current_world_key}'),o=document.getElementById('archive-loading-error-${current_world_key}'),a=${available_archive_dates_js_array},i='${archive_world_rel_to_webroot}';if(!e||!a||a.length===0)return void(e&&(e.disabled=!0,e.innerHTML='<option value="">Keine Archivdaten</option>'));a.forEach(a_date=>{const r=document.createElement('option');r.value=a_date,r.textContent=a_date,e.appendChild(r)}),e.addEventListener('change',function(){t.style.display='none',n.style.display='none',o.textContent='',this.value&&(t.onload=function(){t.style.display='block'},t.onerror=function(){n.style.display='block',o.textContent='Bild nicht geladen.'},t.src=i+'/'+this.value.replace(/-/g,'/')+'.png?v=${CACHE_BUSTER}&t='+new Date().getTime())})})();</script>

View file

@ -0,0 +1,13 @@
<div id='mapid_${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>
<script>
var map_${current_world_key}=L.map('mapid_${current_world_key}',{fullscreenControl:true,fullscreenControlOptions:{position:'topleft',title:'Vollbild',titleCancel:'Vollbild verlassen'}});
var worldBounds=L.latLngBounds(L.latLng(${LEAFLET_BOUNDS_SOUTH},${LEAFLET_BOUNDS_WEST}),L.latLng(${LEAFLET_BOUNDS_NORTH},${LEAFLET_BOUNDS_EAST}));
map_${current_world_key}.fitBounds(worldBounds);
map_${current_world_key}.setMaxBounds(worldBounds);
map_${current_world_key}.setZoom(${LEAFLET_ZOOM_AFTER_FIT});
L.tileLayer('/${web_tiles_rel_path}/{z}/{x}/{y}.png?v=${CACHE_BUSTER}',{attribution:'&copy; ${SITE_TITLE} - ${WORLD_DISPLAY_NAME_PAGE}',minZoom:${min_zoom:-0},maxZoom:${max_zoom:-5},noWrap:true,bounds:worldBounds,tms:false}).addTo(map_${current_world_key});
</script>

View file

@ -0,0 +1,374 @@
<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>
<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>
<a href='#welt-admin' class='button'>Admin</a>
<a href='#beschreibung' class='button'>Beschreibung</a>
<a href='#spielregeln' class='button'>Spielregeln</a>
<a href='#weltradar' class='button'>Radar</a>
<a href='#mods' class='button'>Mods</a>
<a href='#spielerliste' class='button'>Spielerliste</a>
</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>
<div id='server-verbindung' class='info-box server-details'>
<h3>Server-Verbindung</h3>
<p><strong>Adresse:</strong> <span id='addr-${current_world_key}'>${SERVER_ADDRESS_PAGE}</span><button title='Adresse kopieren' class='copy-button' onclick='copyToClipboard("addr-${current_world_key}")'>📋</button></p>
<p><strong>Port:</strong> <span id='port-${current_world_key}'>${SERVER_PORT_PAGE}</span><button title='Port kopieren' class='copy-button' onclick='copyToClipboard("port-${current_world_key}")'>📋</button></p>
<p><strong>Passwort:</strong> <span id='pass-${current_world_key}'>${SERVER_ACCESS_INFO_PAGE}</span><button title='Info kopieren' class='copy-button' onclick='copyToClipboard("pass-${current_world_key}")'>📋</button></p>
</div>
<div id='welt-admin' class='admin-section'>
<h3>Welt-Admin</h3>
<div class='admin-grid'>
${ADMIN_BOXES_HTML}
</div>
</div>
<script>
// === Globale Variablen für Spielerliste ===
let masterPlayerData_${current_world_key} = {};
let playerFilters_${current_world_key} = {
activeOnly: false,
privileges: new Set()
};
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));}}
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';}
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]) => {
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 + ')';
return;
}
const onlineStatusParts = onlineStatusContent.split(' - ');
const currentStatus = onlineStatusParts[0].trim();
if (currentStatus === 'online') {
statusDotEl.className = 'status-dot online';
statusTextEl.className = 'status-text status-online';
statusTextEl.textContent = 'Online';
} else {
const offlineSince = onlineStatusParts.length > 1 ? onlineStatusParts.slice(1).join(' - ').trim() : 'unbekannt';
statusDotEl.className = 'status-dot offline';
statusTextEl.className = 'status-text status-offline';
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';
});
}
function fetchDataForElement(elementId, filePath, isRawText, prefixText, suffixText, isJsonPlayerList) {
const el = document.getElementById(elementId);
if (!el) { 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} = {};
applyPlayerFiltersAndRender_${current_world_key}();
}
} else {
if (isJsonPlayerList) {
masterPlayerData_${current_world_key} = JSON.parse(t);
applyPlayerFiltersAndRender_${current_world_key}();
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>');
}
}
el.innerHTML = (prefixText ? prefixText : '') + content + (suffixText ? suffixText : '');
}).catch(e => {
console.error('Fehler Laden ' + filePath + ':', e);
el.innerHTML = (prefixText ? prefixText : '') + '<em>nicht abrufbar</em>' + (suffixText ? suffixText : '');
});
}
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() {
// === Burger-Menü Logik ===
const burgerToggle = document.getElementById('burger-menu-toggle');
const navContainer = document.getElementById('page-nav-buttons-container-${current_world_key}');
if (burgerToggle && navContainer) {
burgerToggle.addEventListener('click', () => navContainer.classList.toggle('menu-open'));
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}');
const leafletMapExists = (typeof map_${current_world_key} !== 'undefined');
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 (leafletMapExists) { 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 ===
document.querySelectorAll('.read-more-link').forEach(link => {
const targetId = link.dataset.target;
const targetElement = document.getElementById(targetId);
if (targetElement) {
if (targetElement.scrollHeight <= targetElement.clientHeight) {
link.parentElement.style.display = 'none';
} else {
link.addEventListener('click', function(e) {
e.preventDefault();
targetElement.classList.add('expanded');
link.parentElement.style.display = 'none';
});
}
}
});
// === 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 ===
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}">${MAP_HTML}</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,15 @@
<a href='${detail_page_filename}' class='world-preview'>
<div class='world-preview-header'>
<h3>${world_display_name_ov}</h3>
<span class='online-status-badge ${online_status_class}'>${online_status_text}</span>
</div>
<div class='world-preview-content'>
${preview_img_html}
<div class='world-preview-text'>
<p>${world_short_desc_ov}</p>
</div>
</div>
<div class='world-preview-footer'>
Welt-Admin: ${admin_name_ov}
</div>
</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 582 B

View file

@ -0,0 +1 @@
<h2>Datenschutzerklärung</h2><p>Hier deinen Datenschutztext einfügen.</p>

View file

@ -0,0 +1,3 @@
<h2>Luanti Downloads</h2><p>Hier findest du Links zu verschiedenen Versionen von Luanti (Minetest-Variante) und offizielle Download-Quellen.</p>
<h3>Offizielle Luanti Download-Seite</h3><p><a href="https://www.luanti.org/downloads/" target="_blank" rel="noopener noreferrer">https://www.luanti.org/downloads/</a></p>
<p>Auf der offiziellen Seite findest du immer die aktuellsten Versionen und weitere Informationen.</p>

View file

@ -0,0 +1 @@
<h2>Impressum</h2><p>Impressum hier einfügen. Betreiber: Rage87, Kontakt: deine-globale-kontakt-email@example.com</p>

View file

@ -0,0 +1,29 @@
<h2>Herzlich Willkommen auf dem Luanti.GEIGERnet Server!</h2>
<p>
Tritt ein in eine Welt voller Kreativität, Entdeckungen und gemeinsamer Abenteuer! Unser Luanti-Server (vorher minetest) ist ein privates Hobbyprojekt, das mit viel Liebe gestaltet wurde, um Freunden, Familien und besonders Kindern einen sicheren und inspirierenden Ort zum Spielen und Bauen zu bieten.
</p>
<p>
Luanti ist ein fantastisches, <strong>komplett kostenloses Open-Source-Spiel</strong> sehr ähnlich zu Minecraft, das auf nahezu jeder Plattform (Windows, Linux, macOS, Android, FreeBSD) und auch auf älteren oder schwächeren PCs flüssig läuft. Die wahre Stärke von Luanti liegt in seiner unglaublichen Flexibilität und den unzähligen <strong>Modifikationsmöglichkeiten</strong>, die das Spielerlebnis einzigartig machen.
</p>
<h3>Was erwartet dich bei uns?</h3>
<ul>
<li><strong>Kreatives Paradies:</strong> Lasse deiner Fantasie freien Lauf! Baue beeindruckende Strukturen, gemütliche Heime oder weitläufige Landschaften.</li>
<li><strong>Gemeinsam Entdecken:</strong> Erkunde eine liebevoll gestaltete Welt voller Geheimnisse und versteckter Orte ganz ohne Stress und Hektik.</li>
<li><strong>Sicher und Friedlich:</strong> Unsere Welt ist ein Ort ohne Monster und Gewalt. Hier stehen das Miteinander und der Spaß am Spiel im Vordergrund.</li>
<li><strong>Dein eigenes Reich:</strong> Jeder Spieler erhält auf Wunsch sein eigenes, geschütztes Grundstück, um seine Ideen ungestört verwirklichen zu können. Auch größere Flächen für gemeinsame Projekte sind möglich!</li>
<li><strong>Vielfalt durch Mods:</strong> Entdecke eine Vielzahl an Mods, die das Spiel um neue Blöcke, Werkzeuge, Pflanzen, Tiere und viele andere spannende Elemente erweitern und für lang anhaltenden Spielspaß sorgen.</li>
</ul>
<h3>Bereit für das Abenteuer?</h3>
<p>
Wir laden dich und deine Familie herzlich ein, Teil unserer kleinen Gemeinschaft zu werden. Schau dir unsere <a href="worlds.html">Weltenübersicht</a> an, um mehr über unsere aktuelle Spielwelt zu erfahren und wie du beitreten kannst.
</p>
<p>
Wir freuen uns auf dich!
</p>
<p>
<em>Dein Luanti.GEIGERnet Server-Team</em>
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

View file

@ -0,0 +1,25 @@
anvil:anvil
butterflies:hidden_butterfly_red
butterflies:hidden_butterfly_violet
crops:melon_plant_4
crops:melon_plant_5
crops:pumpkin_plant_4
flowerpot:flowers_dandelion_yellow
flowerpot:flowers_geranium
flowerpot:flowers_tulip
itemshelf:half_depth_shelf_small
moreblocks:slab_aspen_wood
moreblocks:slab_coal_stone_quarter
moreblocks:slab_cobble
moreblocks:slab_desert_stone_block
moreblocks:slab_obsidianbrick
moreblocks:slab_stone
moreblocks:slab_stonebrick
morelights_modern:pathlight_d
morelights_modern:tablelamp_l
morelights_vintage:lantern_w
morelights_vintage:smallblock
pillars:silver_sandstone_bot
pillars:silver_sandstone_mid
pillars:silver_sandstone_top
tables_chairs:wood_stool