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.
This commit is contained in:
TDSTOS
2026-04-04 18:51:14 +02:00
parent 1382de63fc
commit 1f9a43fb89
4 changed files with 853 additions and 13 deletions

View File

@@ -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()

View File

@@ -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 <welt>/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/<biome_name>.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<SpawnerEntry> = DEFAULT_MONSTER_SPAWNERS,
/** Tier-Spawner-Einträge. */
val creatureSpawners: List<SpawnerEntry> = 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<BiomeOverride> = 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}"""
}

View File

@@ -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<BlockPopulator> =
* 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 24 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<Biome, SurfaceConfig> = 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
}

View File

@@ -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 )