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:
@@ -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()
|
||||
|
||||
559
src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt
Normal file
559
src/main/kotlin/club/mcscrims/speedhg/world/DataPackManager.kt
Normal 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}"""
|
||||
}
|
||||
@@ -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 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<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
|
||||
}
|
||||
@@ -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 )
|
||||
|
||||
|
||||
Reference in New Issue
Block a user