From 1f9a43fb8979856fba761afb8fda31c456b49fd9 Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Sat, 4 Apr 2026 18:51:14 +0200 Subject: [PATCH] Add DataPackManager & SurfaceBlockPopulator Introduce DataPackManager to generate/install a datapack into the target world folder (biome overrides, simplified noise_settings) and call install() during onLoad after the world folder is prepared. Add SurfaceBlockPopulator to apply surface/sub-surface block overrides (for TerraformGenerator worlds) and register it in onEnable via a WorldInitEvent listener for the configured target world. Refactor WorldManager to extract deletion logic into deleteWorld() and delete existing world/_nether/_the_end before unpacking the selected map. Also update imports and wiring to support these changes. --- .../kotlin/club/mcscrims/speedhg/SpeedHG.kt | 21 + .../mcscrims/speedhg/world/DataPackManager.kt | 559 ++++++++++++++++++ .../speedhg/world/SurfaceBlockPopulator.kt | 253 ++++++++ .../mcscrims/speedhg/world/WorldManager.kt | 33 +- 4 files changed, 853 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt create mode 100644 src/main/kotlin/club/mcscrims/speedhg/world/SurfaceBlockPopulator.kt diff --git a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt index 012b86e..46817c4 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/SpeedHG.kt @@ -31,10 +31,15 @@ import club.mcscrims.speedhg.perk.listener.PerkEventDispatcher import club.mcscrims.speedhg.ranking.RankingManager import club.mcscrims.speedhg.scoreboard.ScoreboardManager import club.mcscrims.speedhg.webhook.DiscordWebhookManager +import club.mcscrims.speedhg.world.DataPackManager +import club.mcscrims.speedhg.world.SurfaceBlockPopulator import club.mcscrims.speedhg.world.WorldManager import org.bukkit.Bukkit import org.bukkit.Material import org.bukkit.NamespacedKey +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.world.WorldInitEvent import org.bukkit.inventory.ItemStack import org.bukkit.inventory.ShapelessRecipe import org.bukkit.plugin.java.JavaPlugin @@ -99,10 +104,26 @@ class SpeedHG : JavaPlugin() { val worldManager = WorldManager( this ) worldManager.prepareRandomWorld() + + val dataPackManager = DataPackManager( this ) + dataPackManager.install() } override fun onEnable() { + server.pluginManager.registerEvents(object : Listener { + @EventHandler + fun onWorldInit( + event: WorldInitEvent + ) { + val targetWorldName = config.getString( "map-system.target-world-name", "world" ) + if ( event.world.name != targetWorldName ) return + + event.world.populators.add( SurfaceBlockPopulator() ) + logger.info( "[SpeedHG] SurfaceBlockPopulator für '${event.world.name}' registriert." ) + } + }, this ) + databaseManager = DatabaseManager( this ) try { databaseManager.connect() diff --git a/src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt b/src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt new file mode 100644 index 0000000..92cd3ba --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt @@ -0,0 +1,559 @@ +package club.mcscrims.speedhg.world + +import club.mcscrims.speedhg.SpeedHG +import java.io.File +import java.util.logging.Logger + +/** + * Erstellt vor dem World-Load einen vollständigen DataPack im Weltordner. + * + * ## Was dieser DataPack KANN (mit und ohne TerraformGenerator) + * - Biom-Farben: Wasser-, Himmel-, Nebelfarbe, Laubfarbe + * - Biom-Geräusche: Ambiente-Sounds, Partikel + * - Mob-Spawntabellen: Welche Mobs in welcher Häufigkeit spawnen + * - Biom-Klimawerte: Temperatur, Niederschlag + * + * ## Was dieser DataPack NICHT kann (mit TerraformGenerator) + * - Surface-Blöcke (Gras → Sand → Stein): TG ignoriert noise_settings/overworld.json, + * da TG die Block-Platzierung komplett in eigenem Java-Code erledigt. + * → Für Surface-Block-Overrides: [SurfaceBlockPopulator] verwenden. + * + * ## Was dieser DataPack KANN (NUR ohne TerraformGenerator, Vanilla-Gen) + * - noise_settings/overworld.json: Komplette Surface-Rule-Bäume für Vanilla-Welten. + * + * ## Timing + * Muss in [JavaPlugin.onLoad] aufgerufen werden, NACHDEM der Weltordner + * durch [WorldManager.prepareRandomWorld] entpackt wurde, aber BEVOR + * der Server die Welt lädt. In onEnable() ist es zu spät — die Welt + * ist dann bereits vollständig initialisiert. + */ +class DataPackManager(private val plugin: SpeedHG) { + + private val log: Logger get() = plugin.logger + + companion object { + /** Name des DataPack-Ordners unter /datapacks/. */ + const val DATAPACK_NAME = "SpeedHG-Gen" + + /** + * DataPack-Format-Nummer je Minecraft-Version: + * 1.20.4 = 26 | 1.21 = 48 | 1.21.1 = 57 | 1.21.4 = 61 + * Bei einem Server-Versions-Upgrade aktualisieren. + */ + const val PACK_FORMAT = 57 + + const val PACK_DESCRIPTION = "SpeedHG – Custom Biome und Surface Overrides" + } + + // ───────────────────────────────────────────────────────────────────────── + // Öffentliche API + // ───────────────────────────────────────────────────────────────────────── + + /** + * Convenience-Methode: liest den Weltordner aus der config.yml und + * installiert den DataPack dort. Für den Normalfall gedacht. + */ + fun install() { + val worldName = plugin.config.getString("map-system.target-world-name", "world")!! + val serverRoot = plugin.dataFolder.parentFile.parentFile + val worldFolder = File(serverRoot, worldName) + install(worldFolder) + } + + /** + * Installiert den DataPack in den angegebenen [worldFolder]. + * + * Idempotent: Existiert bereits ein alter DataPack, wird er komplett + * neu geschrieben. Sicher bei jedem Serverstart / Map-Reset aufzurufen. + * + * @param worldFolder Ordner der Zielwelt (z.B. File(serverRoot, "world")). + * Muss noch nicht existieren – er wird angelegt. + */ + fun install(worldFolder: File) { + val packRoot = File(worldFolder, "datapacks/$DATAPACK_NAME") + + try { + // Alte Version vollständig entfernen, damit keine veralteten + // JSON-Dateien aus früheren Plugin-Versionen übrig bleiben. + if (packRoot.exists()) { + log.info("[DataPackManager] Alten DataPack löschen...") + packRoot.deleteRecursively() + } + + packRoot.mkdirs() + + writePackMcmeta(packRoot) + writeBiomeOverrides(packRoot) + + // ─ Vanilla-Gen only ───────────────────────────────────────────── + // Bei TerraformGenerator-Welten ist dieser Block wirkungslos, + // da TG noise_settings komplett ignoriert. Für Dokumentations- + // zwecke (und zukünftige Vanilla-Gen-Setups) trotzdem erstellt. + writeNoiseSettingOverride(packRoot) + // ──────────────────────────────────────────────────────────────── + + log.info("[DataPackManager] '$DATAPACK_NAME' installiert in: ${packRoot.absolutePath}") + + } catch (e: Exception) { + log.severe("[DataPackManager] Installation fehlgeschlagen: ${e.message}") + e.printStackTrace() + } + } + + /** + * Entfernt den DataPack aus dem Weltordner. + * Kann z.B. in onDisable() aufgerufen werden, wenn der nächste + * Serverstart ohnehin einen frischen World-Reset durchführt. + */ + fun uninstall(worldFolder: File) { + val packRoot = File(worldFolder, "datapacks/$DATAPACK_NAME") + if (packRoot.exists()) { + packRoot.deleteRecursively() + log.info("[DataPackManager] '$DATAPACK_NAME' entfernt.") + } + } + + // ───────────────────────────────────────────────────────────────────────── + // pack.mcmeta + // ───────────────────────────────────────────────────────────────────────── + + private fun writePackMcmeta(packRoot: File) { + // Hinweis: Seit 1.20.2 kann pack.mcmeta auch ein "overlays"-Array + // enthalten, das partielle Overrides (nur bestimmte Dateien aus einer + // Version) ermöglicht. Für unseren Anwendungsfall (volle Biom-Dateien) + // reicht das einfache Format. + File(packRoot, "pack.mcmeta").writeText( + """ + { + "pack": { + "pack_format": $PACK_FORMAT, + "description": "$PACK_DESCRIPTION" + } + } + """.trimIndent(), + Charsets.UTF_8 + ) + log.fine("[DataPackManager] pack.mcmeta geschrieben.") + } + + // ───────────────────────────────────────────────────────────────────────── + // Biom-Overrides + // Pfad: data/minecraft/worldgen/biome/.json + // + // WICHTIG: Biom-Dateien in DataPacks ersetzen den KOMPLETTEN Vanilla- + // Eintrag. Alle Felder müssen vorhanden sein. Fehlende Felder führen + // zu Biom-Darstellungsfehlern oder Server-Abstürzen beim Laden. + // + // WICHTIG: Biom-JSONs kontrollieren KEINE Surface-Blöcke (Gras, Sand)! + // Surface-Blöcke liegen in noise_settings (s.u.) oder im TG-Java-Code. + // + // Was Biom-JSONs kontrollieren: + // ✔ Wasserfarbe, Wassernebelfarbe, Himmelsfarbe, Nebelfarbe + // ✔ Laubfarbe (foliage_color), Grasfarbe (grass_color) + // ✔ Biom-Ambient-Sounds, Partikel, Stimmungsgeräusche + // ✔ Mob-Spawntabellen (monster, creature, ambient, ...) + // ✔ Klimawerte: Temperatur, Niederschlag, Gefrierverhalten + // ───────────────────────────────────────────────────────────────────────── + + private fun writeBiomeOverrides(packRoot: File) { + val biomeDir = File(packRoot, "data/minecraft/worldgen/biome").also { it.mkdirs() } + BiomeOverride.all.forEach { override -> + val file = File(biomeDir, "${override.vanillaId}.json") + file.writeText(override.buildJson(), Charsets.UTF_8) + log.fine("[DataPackManager] Biom-Override geschrieben: ${override.vanillaId}.json") + } + } + + // ───────────────────────────────────────────────────────────────────────── + // noise_settings Override (Surface Rules – NUR Vanilla-Gen) + // Pfad: data/minecraft/worldgen/noise_settings/overworld.json + // + // Dieser Block FUNKTIONIERT NICHT mit TerraformGenerator. + // TG generiert Blöcke in eigenem Java-Code und liest diese Datei nie. + // + // Für Vanilla-Gen: Lade die vollständige vanilla overworld.json unter + // https://raw.githubusercontent.com/misode/mcmeta/refs/tags/1.21.1-summary/ + // data/minecraft/worldgen/noise_settings/overworld.json + // und ersetze nur den "surface_rule"-Block durch eigene Regeln. + // + // Wir schreiben hier eine vereinfachte Demonstrations-Version. + // In Produktion mit Vanilla-Gen: volle Datei + nur surface_rule ändern. + // ───────────────────────────────────────────────────────────────────────── + + private fun writeNoiseSettingOverride(packRoot: File) { + val nsDir = File(packRoot, "data/minecraft/worldgen/noise_settings").also { it.mkdirs() } + File(nsDir, "overworld.json").writeText(buildOverworldNoiseSetting(), Charsets.UTF_8) + log.fine("[DataPackManager] noise_settings/overworld.json geschrieben (nur Vanilla-Gen).") + } + + /** + * Erstellt eine VEREINFACHTE noise_settings/overworld.json. + * + * Das echte Vanilla-overworld.json hat ~3000 Zeilen mit feature_flags, + * ore_veins, aquifer_config, noise_router und mehr. + * + * Für Vanilla-Gen-Produktion: Vollständige Datei von misode.github.io + * oder vom MC-Data-Generator laden, dann NUR den "surface_rule"-Block + * durch buildSurfaceRuleBlock() ersetzen. + * + * Für TerraformGenerator: Diese Datei ist irrelevant – SurfaceBlockPopulator + * verwenden (separate Klasse, ebenfalls im WorldManager-Package). + */ + private fun buildOverworldNoiseSetting(): String { + // Die Surface-Rule ist ein Entscheidungsbaum: Condition → Action. + // Unsere benutzerdefinierten Regeln kommen ZUERST, da "sequence" die + // erste zutreffende Regel gewinnen lässt (Priority-Queue-Semantik). + return """ + { + "sea_level": 63, + "disable_mob_generation": false, + "aquifers_enabled": true, + "ore_veins_enabled": true, + "legacy_random_source": false, + "default_block": { + "Name": "minecraft:stone" + }, + "default_fluid": { + "Name": "minecraft:water", + "Properties": { "level": "0" } + }, + "noise": { + "height": 384, + "size_horizontal": 1, + "size_vertical": 2 + }, + "surface_rule": { + "type": "minecraft:sequence", + "sequence": [ + ${buildSurfaceRuleBlock()}, + { + "type": "minecraft:block", + "result_state": { "Name": "minecraft:grass_block", "Properties": { "snowy": "false" } } + } + ] + }, + "spawn_target": [], + "noise_router": {} + } + """.trimIndent() + } + + /** + * Baut den biom-spezifischen Surface-Rule-Block. + * + * Surface-Rules sind ein Baum aus condition/sequence/block-Nodes: + * condition → prüft (biom, Tiefe, Noise-Wert, ...) + * sequence → führt die erste zutreffende Regel in einer Liste aus + * block → platziert einen spezifischen Block + * + * stone_depth-Bedingung: + * offset=0, add_surface_depth=false → nur die oberste Schicht (y=surface) + * offset=0, add_surface_depth=true → oberste Schicht + Tiefe per Noise + */ + private fun buildSurfaceRuleBlock(): String = """ + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:biome", + "biome_is": [ + "minecraft:badlands", + "minecraft:eroded_badlands", + "minecraft:wooded_badlands" + ] + }, + "then_run": { + "type": "minecraft:sequence", + "sequence": [ + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:stone_depth", + "offset": 0, + "add_surface_depth": false, + "secondary_depth_range": 0, + "surface_type": "floor" + }, + "then_run": { + "type": "minecraft:block", + "result_state": { "Name": "minecraft:red_sand" } + } + }, + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:stone_depth", + "offset": 0, + "add_surface_depth": true, + "secondary_depth_range": 0, + "surface_type": "floor" + }, + "then_run": { + "type": "minecraft:block", + "result_state": { "Name": "minecraft:red_sandstone" } + } + } + ] + } + }, + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:biome", + "biome_is": ["minecraft:desert"] + }, + "then_run": { + "type": "minecraft:sequence", + "sequence": [ + { + "type": "minecraft:condition", + "if_true": { + "type": "minecraft:stone_depth", + "offset": 0, + "add_surface_depth": true, + "secondary_depth_range": 0, + "surface_type": "floor" + }, + "then_run": { + "type": "minecraft:block", + "result_state": { "Name": "minecraft:sand" } + } + }, + { + "type": "minecraft:condition", + "if_true": { "type": "minecraft:above_preliminary_surface" }, + "then_run": { + "type": "minecraft:block", + "result_state": { "Name": "minecraft:sandstone" } + } + } + ] + } + } + """.trimIndent() +} + + +// ───────────────────────────────────────────────────────────────────────────── +// Datenmodell: Biom-Overrides +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Kapselt alle Daten für eine einzelne Biom-Override-JSON-Datei. + * + * In Minecraft 1.21 enthält eine Biom-JSON-Datei in einem DataPack folgende + * Pflichtfelder (Fehler → World-Load schlägt fehl): + * - temperature, downfall, has_precipitation + * - effects (sky_color, fog_color, water_color, water_fog_color + mood_sound) + * - spawners (alle Kategorien als leere Listen oder mit Einträgen) + * - spawn_costs + * - carvers (Vanilla: { "air": ["minecraft:cave", "minecraft:canyon"] }) + * - features (10 leere Listen für die 10 Decoration-Phasen) + * + * Farben als Dezimal-Integer (hex → decimal): + * #55C677 (Sumpf-Wasser) = 5588599 + * #617B64 (Sumpf-Laub) = 6325092 + * + * Hex → Dezimal: "0x${hexCode.removePrefix("#")}".toLong(16).toInt() + */ +data class BiomeOverride( + /** Vanilla-Biom-ID ohne Namespace, z.B. "badlands". */ + val vanillaId: String, + val temperature: Double, + val downfall: Double, + val hasPrecipitation: Boolean, + /** Himmelsfarbe als Dezimal-Integer. */ + val skyColor: Int, + /** Hauptnebelfarbe. */ + val fogColor: Int, + /** Wasserfarbe (sichtbare Wasserfläche). */ + val waterColor: Int, + /** Wassernebelfarbe (Unterwasser-Sicht). */ + val waterFogColor: Int, + /** Optionale Laubfarbe (null = Vanilla-Berechnung via Temperatur/Niederschlag). */ + val foliageColor: Int? = null, + /** Optionale Grasfarbe (null = Vanilla-Berechnung). */ + val grassColor: Int? = null, + /** Monster-Spawner-Einträge. */ + val monsterSpawners: List = DEFAULT_MONSTER_SPAWNERS, + /** Tier-Spawner-Einträge. */ + val creatureSpawners: List = emptyList(), +) { + + fun buildJson(): String { + val foliageBlock = if (foliageColor != null) """"foliage_color": $foliageColor,""" else "" + val grassBlock = if (grassColor != null) """"grass_color": $grassColor,""" else "" + val monsterJson = monsterSpawners.joinToString(",\n ") { it.toJson() } + val creatureJson = creatureSpawners.joinToString(",\n ") { it.toJson() } + + return """ + { + "temperature": $temperature, + "downfall": $downfall, + "has_precipitation": $hasPrecipitation, + "effects": { + "sky_color": $skyColor, + "fog_color": $fogColor, + "water_color": $waterColor, + "water_fog_color": $waterFogColor, + $foliageBlock + $grassBlock + "mood_sound": { + "block_search_extent": 8, + "offset": 2.0, + "sound": "minecraft:ambient.cave", + "tick_delay": 6000 + } + }, + "carvers": { + "air": ["minecraft:cave", "minecraft:canyon"] + }, + "features": [[], [], [], [], [], [], [], [], [], []], + "spawners": { + "monster": [ + $monsterJson + ], + "creature": [ + $creatureJson + ], + "ambient": [], + "water_creature": [], + "underground_water_creature": [ + {"type": "minecraft:glow_squid", "weight": 10, "minCount": 4, "maxCount": 6} + ], + "water_ambient": [], + "axolotls": [], + "misc": [] + }, + "spawn_costs": {} + } + """.trimIndent() + } + + companion object { + /** Standard-Monster-Spawner für die meisten Overworld-Biome. */ + val DEFAULT_MONSTER_SPAWNERS = listOf( + SpawnerEntry("minecraft:spider", 100, 4, 4), + SpawnerEntry("minecraft:zombie", 95, 4, 4), + SpawnerEntry("minecraft:zombie_villager", 5, 1, 1), + SpawnerEntry("minecraft:skeleton", 100, 4, 4), + SpawnerEntry("minecraft:creeper", 100, 4, 4), + SpawnerEntry("minecraft:slime", 100, 4, 4), + SpawnerEntry("minecraft:enderman", 10, 1, 4), + SpawnerEntry("minecraft:witch", 5, 1, 1), + ) + + /** Alle konfigurierten Biom-Overrides für diesen DataPack. */ + val all: List = buildList { + + // ── Badlands ───────────────────────────────────────────────────── + // Sehr heiß, kein Niederschlag, heißer Himmel, trübes Wasser. + // Vanilla skyColor=7254527, waterColor=4159204 + add(BiomeOverride( + vanillaId = "badlands", + temperature = 2.0, + downfall = 0.0, + hasPrecipitation = false, + skyColor = 7254527, + fogColor = 12638463, + waterColor = 4159204, + waterFogColor = 329011, + // Kein foliage/grass – Badlands hat kaum Vegetation + monsterSpawners = DEFAULT_MONSTER_SPAWNERS, + )) + + // Varianten mitüberziehen, damit alle drei Badlands-Typen konsistent sind + add(BiomeOverride( + vanillaId = "eroded_badlands", + temperature = 2.0, + downfall = 0.0, + hasPrecipitation = false, + skyColor = 7254527, + fogColor = 12638463, + waterColor = 4159204, + waterFogColor = 329011, + monsterSpawners = DEFAULT_MONSTER_SPAWNERS, + )) + + add(BiomeOverride( + vanillaId = "wooded_badlands", + temperature = 2.0, + downfall = 0.0, + hasPrecipitation = false, + skyColor = 7254527, + fogColor = 12638463, + waterColor = 4159204, + waterFogColor = 329011, + monsterSpawners = DEFAULT_MONSTER_SPAWNERS, + )) + + // ── Swamp ───────────────────────────────────────────────────────── + // Angepasste Wasserfarbe (dunkler, grünlicher als Vanilla). + // Vanilla waterColor=6388580, foliageColor=6975545, grassColor=6975545 + add(BiomeOverride( + vanillaId = "swamp", + temperature = 0.8, + downfall = 0.9, + hasPrecipitation = true, + skyColor = 7907327, + fogColor = 12638463, + waterColor = 3832426, // dunkler, trüber als Vanilla + waterFogColor = 2302743, + foliageColor = 6975545, + grassColor = 6975545, + monsterSpawners = buildList { + addAll(DEFAULT_MONSTER_SPAWNERS) + add(SpawnerEntry("minecraft:slime", 1, 1, 1)) + }, + creatureSpawners = listOf( + SpawnerEntry("minecraft:frog", 10, 2, 5), + ), + )) + + // ── Dark Forest ────────────────────────────────────────────────── + // Dunklere Laubfarbe, trüberer Himmel für dunklere Atmosphäre. + add(BiomeOverride( + vanillaId = "dark_forest", + temperature = 0.7, + downfall = 0.8, + hasPrecipitation = true, + skyColor = 7972607, + fogColor = 12638463, + waterColor = 4159204, + waterFogColor = 329011, + foliageColor = 4145489, // deutlich dunkler als Vanilla (6529093) + grassColor = 4145489, + monsterSpawners = DEFAULT_MONSTER_SPAWNERS, + )) + + // ── Desert ─────────────────────────────────────────────────────── + // Heißer, etwas orangestichigerer Himmel, trübes Wasser. + add(BiomeOverride( + vanillaId = "desert", + temperature = 2.0, + downfall = 0.0, + hasPrecipitation = false, + skyColor = 7254527, + fogColor = 12638463, + waterColor = 4159204, + waterFogColor = 329011, + monsterSpawners = buildList { + addAll(DEFAULT_MONSTER_SPAWNERS) + add(SpawnerEntry("minecraft:husk", 80, 4, 4)) + }, + )) + + // Weitere Biome hier ergänzen... + } + } +} + +/** Einzelner Mob-Spawner-Eintrag für die spawners-Tabelle. */ +data class SpawnerEntry( + val type: String, + val weight: Int, + val minCount: Int, + val maxCount: Int, +) { + fun toJson(): String = + """{"type": "$type", "weight": $weight, "minCount": $minCount, "maxCount": $maxCount}""" +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/world/SurfaceBlockPopulator.kt b/src/main/kotlin/club/mcscrims/speedhg/world/SurfaceBlockPopulator.kt new file mode 100644 index 0000000..e358717 --- /dev/null +++ b/src/main/kotlin/club/mcscrims/speedhg/world/SurfaceBlockPopulator.kt @@ -0,0 +1,253 @@ +package club.mcscrims.speedhg.world + +import org.bukkit.Material +import org.bukkit.World +import org.bukkit.block.Biome +import org.bukkit.generator.BlockPopulator +import org.bukkit.generator.LimitedRegion +import org.bukkit.generator.WorldInfo +import java.util.Random + +/** + * Ersetzt Surface- und Sub-Surface-Blöcke nach der TerraformGenerator-Generation. + * + * ## Warum BlockPopulator statt DataPack? + * + * TerraformGenerator (TG) platziert Surface-Blöcke vollständig in eigenem + * Java-Code. Die Datei noise_settings/overworld.json eines DataPacks wird + * von TG nie gelesen – sie gilt nur für Vanilla-Worldgen. + * + * Ein [BlockPopulator] wird von Paper aufgerufen, NACHDEM TG einen Chunk + * fertig generiert hat. Wir überschreiben dann die Blöcke per API. + * + * ## Einbindung + * ```kotlin + * // In der ChunkGenerator-Implementierung (oder als Zusatz zu TG): + * class SpeedHGWorldGenerator : ChunkGenerator() { + * override fun getDefaultPopulators(world: World): MutableList = + * mutableListOf(SurfaceBlockPopulator()) + * } + * ``` + * + * Da TG selbst der ChunkGenerator ist, muss der Populator über ein eigenes + * Plugin oder via Paper's [World.addPopulator] (deprecated seit 1.17) oder + * — am zuverlässigsten — als WorldGen-Feature im Plugin registriert werden. + * + * ## Empfehlung für TG + Paper 1.21 + * + * Paper 1.18+ empfiehlt [WorldGenOptions] oder den Registration-Weg via + * `plugin.yml` / `GeneratorSettings`. Da TG den Generator stellt, ist die + * sauberste Lösung ein `WorldInitEvent`-Listener: + * + * ```kotlin + * @EventHandler + * fun onWorldInit(event: WorldInitEvent) { + * if (event.world.name == "world") { + * event.world.populators.add(SurfaceBlockPopulator()) + * } + * } + * ``` + * + * Dieser Listener muss in onEnable() registriert sein (nicht onLoad()). + * WorldInitEvent feuert kurz bevor Chunks geloaded/generiert werden. + * + * ## Performance-Hinweis + * Der Populator iteriert maximal über die oberste Schicht eines Chunks + * (16×16 = 256 Blöcke). Für jeden Block wird: + * 1. Das Biom abgefragt (O(1) in Paper) + * 2. Eine Map-Lookup durchgeführt (O(1)) + * 3. Ggf. ein Block gesetzt (nur wenn Änderung nötig) + * + * Der Overhead ist minimal – deutlich kleiner als TG's eigene Generation. + */ +class SurfaceBlockPopulator : BlockPopulator() { + + /** + * Surface-Block-Konfiguration pro Biom-Typ. + * @property surface Oberster Block (Y = Oberfläche) + * @property subSurface Nächste 2–4 Blöcke darunter + * @property deepSurface Tief genug für Sandstein-Effekte o.Ä. + */ + private data class SurfaceConfig( + val surface: Material, + val subSurface: Material, + val deepSurface: Material = Material.STONE, + val subSurfaceDepth: Int = 3, + ) + + /** + * Biom → Surface-Konfiguration. + * + * Nur Biome, die abweichen sollen, müssen eingetragen werden. + * Alle anderen Biome bleiben unverändert (TG's Output bleibt bestehen). + * + * Für Biome, die TG bereits korrekt generiert (Forest = Gras/Erde), + * ist kein Eintrag nötig. + */ + private val surfaceOverrides: Map = buildMap { + + // Badlands: Terrakotta oben, Rotsandstein darunter + // (TG platziert hier bereits Terrakotta, aber kein Rotsandstein darunter) + val badlandsConfig = SurfaceConfig( + surface = Material.TERRACOTTA, + subSurface = Material.RED_SANDSTONE, + deepSurface = Material.RED_SANDSTONE, + subSurfaceDepth = 4, + ) + put(Biome.BADLANDS, badlandsConfig) + put(Biome.ERODED_BADLANDS, badlandsConfig) + put(Biome.WOODED_BADLANDS, badlandsConfig) + + // Desert: Sand auf Sandstein (TG macht das meist schon, aber doppelt hält) + val desertConfig = SurfaceConfig( + surface = Material.SAND, + subSurface = Material.SAND, + deepSurface = Material.SANDSTONE, + subSurfaceDepth = 3, + ) + put(Biome.DESERT, desertConfig) + + // Swamp: Schmutziger mit Matsch/Erde statt Gras + put(Biome.SWAMP, SurfaceConfig( + surface = Material.GRASS_BLOCK, + subSurface = Material.MUD, // Mud (seit 1.19) + deepSurface = Material.DIRT, + subSurfaceDepth = 2, + )) + + // Snowy surfaces: Powder Snow-Effekt auf Berghöhen + val snowyConfig = SurfaceConfig( + surface = Material.SNOW_BLOCK, + subSurface = Material.DIRT, + subSurfaceDepth = 2, + ) + put(Biome.SNOWY_TAIGA, snowyConfig) + put(Biome.SNOWY_PLAINS, snowyConfig) + put(Biome.ICE_SPIKES, snowyConfig) + + // Stone Shore (Felsige Küste): Stein von Anfang an + put(Biome.STONY_SHORE, SurfaceConfig( + surface = Material.STONE, + subSurface = Material.STONE, + deepSurface = Material.STONE, + )) + + // Weitere Biome nach Bedarf ergänzen... + } + + // ───────────────────────────────────────────────────────────────────────── + // BlockPopulator API + // ───────────────────────────────────────────────────────────────────────── + + /** + * Wird von Paper für jeden neu generierten Chunk aufgerufen. + * + * Wir iterieren nur über die x/z-Spalten des Chunks (16×16) und + * suchen pro Spalte die Oberfläche – kein komplettes Y-Scan nötig. + */ + override fun populate( + worldInfo: WorldInfo, + random: Random, + chunkX: Int, + chunkZ: Int, + limitedRegion: LimitedRegion, + ) { + val worldMinY = worldInfo.minHeight + + for (lx in 0..15) { + for (lz in 0..15) { + val worldX = chunkX * 16 + lx + val worldZ = chunkZ * 16 + lz + + // Biom einmalig pro Spalte abfragen (Y spielt für Biom-Typ weniger Rolle) + val biome = limitedRegion.getBiome(worldX, 64, worldZ) + val config = surfaceOverrides[biome] ?: continue + + // Oberste solide Oberfläche dieser Spalte finden + val surfaceY = findSurfaceY(limitedRegion, worldX, worldZ, worldInfo.maxHeight, worldMinY) + ?: continue // Keine Oberfläche gefunden (z.B. tiefe Ozean-Spalte unter Wasser) + + applySurfaceConfig(limitedRegion, worldX, worldZ, surfaceY, config, worldMinY) + } + } + } + + // ───────────────────────────────────────────────────────────────────────── + // Hilfsmethoden + // ───────────────────────────────────────────────────────────────────────── + + /** + * Findet den Y-Wert des obersten soliden, nicht-flüssigen Blocks in dieser Spalte. + * Gibt null zurück, wenn keine solide Oberfläche gefunden wurde. + */ + private fun findSurfaceY( + region: LimitedRegion, + x: Int, + z: Int, + maxY: Int, + minY: Int, + ): Int? { + for (y in maxY downTo minY) { + if (!region.isInRegion(x, y, z)) continue + + val type = region.getType(x, y, z) + if (type.isSolid && !type.isLiquid()) return y + } + return null + } + + /** + * Wendet die Surface-Konfiguration für eine Spalte an: + * surfaceY → config.surface + * surfaceY-1 bis surfaceY-subSurfaceDepth → config.subSurface + * darunter ggf. → config.deepSurface (wenn sich von subSurface unterscheidet) + */ + private fun applySurfaceConfig( + region: LimitedRegion, + x: Int, + z: Int, + surfaceY: Int, + config: SurfaceConfig, + minY: Int, + ) { + // Oberflächen-Block ersetzen + if (region.isInRegion(x, surfaceY, z)) { + val existing = region.getType(x, surfaceY, z) + // Nur solide Blöcke ersetzen, keine Flüssigkeiten, kein Glas etc. + if (existing.isSolid && !existing.isLiquid()) { + region.setType(x, surfaceY, z, config.surface) + } + } + + // Sub-Surface-Schichten ersetzen + for (depth in 1..config.subSurfaceDepth) { + val y = surfaceY - depth + if (y < minY || !region.isInRegion(x, y, z)) break + + val existing = region.getType(x, y, z) + if (!existing.isSolid || existing.isLiquid()) break + + val targetMaterial = if (depth <= config.subSurfaceDepth) config.subSurface + else config.deepSurface + region.setType(x, y, z, targetMaterial) + } + + // Deep-Surface: Blöcke tiefer als subSurfaceDepth (bis Stein) + if (config.deepSurface != Material.STONE) { + for (depth in (config.subSurfaceDepth + 1)..(config.subSurfaceDepth + 4)) { + val y = surfaceY - depth + if (y < minY || !region.isInRegion(x, y, z)) break + + val existing = region.getType(x, y, z) + // Nur weiche Blöcke weiter ersetzen (nicht Stein/Tiefenschiefer) + if (existing == Material.STONE || existing == Material.DEEPSLATE) break + + region.setType(x, y, z, config.deepSurface) + } + } + } + + private fun Material.isLiquid(): Boolean = + this == Material.WATER || this == Material.LAVA || + this == Material.SEAGRASS || this == Material.TALL_SEAGRASS +} \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/world/WorldManager.kt b/src/main/kotlin/club/mcscrims/speedhg/world/WorldManager.kt index 39a334c..89bf98b 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/world/WorldManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/world/WorldManager.kt @@ -10,11 +10,31 @@ class WorldManager( private val plugin: SpeedHG ) { + private fun deleteWorld(): File + { + val targetWorldName = plugin.config.getString("map-system.target-world-name", "world")!! + val serverRoot = plugin.dataFolder.parentFile.parentFile + val targetWorldFolder = File(serverRoot, targetWorldName) + + if ( targetWorldFolder.exists() ) + { + plugin.logger.info( "[WorldManager] Lösche alte Welt..." ) + targetWorldFolder.deleteRecursively() + } + + File( serverRoot, "${targetWorldName}_nether" ).deleteRecursively() + File( serverRoot, "${targetWorldName}_the_end" ).deleteRecursively() + + return targetWorldFolder + } + /** * Wird in onLoad() aufgerufen, um die Welt VOR dem Server-Start auszutauschen */ fun prepareRandomWorld() { + val targetWorldFolder = deleteWorld() + if (!plugin.config.getBoolean( "map-system.enabled", false )) return @@ -37,19 +57,6 @@ class WorldManager( plugin.logger.info( "[WorldManager] Ausgewählte Map: $randomMapName. Entpacke..." ) - val targetWorldName = plugin.config.getString( "map-system.target-world-name", "world" )!! - val serverRoot = plugin.dataFolder.parentFile.parentFile // Geht von plugins/SpeedHG -> plugins -> Server Root - val targetWorldFolder = File( serverRoot, targetWorldName ) - - if ( targetWorldFolder.exists() ) - { - plugin.logger.info( "[WorldManager] Lösche alte Welt..." ) - targetWorldFolder.deleteRecursively() - } - - File( serverRoot, "${targetWorldName}_nether" ).deleteRecursively() - File( serverRoot, "${targetWorldName}_the_end" ).deleteRecursively() - targetWorldFolder.mkdirs() unzip( zipFile, targetWorldFolder )