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.ranking.RankingManager
import club.mcscrims.speedhg.scoreboard.ScoreboardManager import club.mcscrims.speedhg.scoreboard.ScoreboardManager
import club.mcscrims.speedhg.webhook.DiscordWebhookManager 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 club.mcscrims.speedhg.world.WorldManager
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.NamespacedKey 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.ItemStack
import org.bukkit.inventory.ShapelessRecipe import org.bukkit.inventory.ShapelessRecipe
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
@@ -99,10 +104,26 @@ class SpeedHG : JavaPlugin() {
val worldManager = WorldManager( this ) val worldManager = WorldManager( this )
worldManager.prepareRandomWorld() worldManager.prepareRandomWorld()
val dataPackManager = DataPackManager( this )
dataPackManager.install()
} }
override fun onEnable() 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 ) databaseManager = DatabaseManager( this )
try { try {
databaseManager.connect() 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 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 * Wird in onLoad() aufgerufen, um die Welt VOR dem Server-Start auszutauschen
*/ */
fun prepareRandomWorld() fun prepareRandomWorld()
{ {
val targetWorldFolder = deleteWorld()
if (!plugin.config.getBoolean( "map-system.enabled", false )) if (!plugin.config.getBoolean( "map-system.enabled", false ))
return return
@@ -37,19 +57,6 @@ class WorldManager(
plugin.logger.info( "[WorldManager] Ausgewählte Map: $randomMapName. Entpacke..." ) 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() targetWorldFolder.mkdirs()
unzip( zipFile, targetWorldFolder ) unzip( zipFile, targetWorldFolder )