diff --git a/README.md b/README.md index 7ef16d3..9a56c3a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ No installation is needed on the client β€” just drop it into your server's `mod - πŸ“ˆ **Dynamic Health Scaling:** The Ender Dragon's max health increases with each eligible player in The End. - ⏱️ **Initial Spawn Delay:** Optionally delay the very first dragon spawn and show a countdown on players’ XP bars. - βš™οΈ **Server-Side Only:** No client installation required β€” perfect for modded or vanilla-compatible SMPs. -- πŸ”§ **Configurable:** Customize how scaling and spawn delays work via a simple `.properties` config file. +- πŸ”§ **Configurable:** Customize how scaling and spawn delays work via a structured `.toml` config file. - πŸ“’ **Broadcast Messages:** Optional announcements when a scaled dragon appears. - πŸ› οΈ **In-Game Reload:** `/scaleddragonfight reload` command to reload configuration without restarting the server. @@ -22,23 +22,49 @@ No installation is needed on the client β€” just drop it into your server's `mod After first launch, a config file will be generated at: ``` -/config/scaleddragonfight.properties +/config/scaleddragonfight.toml ``` You can configure: ``` -enableMod=true # Enable or disable the mod -scaleWithOnePlayer=false # Whether scaling starts with the first player -countCreativeModePlayers=false # Include creative players in scaling -baseDragonHealth=200.0 # Base health of the Ender Dragon -additionalHealthPerPlayer=100.0 # Health added per eligible player -enableBroadcast=true # Enable broadcast when dragon spawns - +# ───────────────────────────── +# General Settings +# ───────────────────────────── +[general] +enableMod = true # Master enable/disable switch for the mod +scaleWithOnePlayer = false # Whether scaling should start with the first player +countCreativeModePlayers = false # Include players in Creative mode when scaling + +# ───────────────────────────── +# Dragon Health Scaling +# ───────────────────────────── +[healthScaling] +baseDragonHealth = 200.0 # Base health of the Ender Dragon (vanilla default = 200) +additionalHealthPerPlayer = 100.0 # Extra health added per eligible player + +# ───────────────────────────── +# Broadcast Messages +# ───────────────────────────── +[broadcastMessages] +enable = true # Enable or disable broadcast messages +onSpawn = true # Send message when a scaled dragon spawns +onDeath = false # Send message when the dragon is defeated +showHealthInMessage = true # Include dragon’s health value in broadcast + +# ───────────────────────────── # Initial Spawn Delay -enableInitialSpawnDelay=true # Delay the first dragon spawn in The End -initialSpawnDelaySeconds=60 # Time (in seconds) before first dragon spawns -showSpawnDelayCountdown=true # Show countdown above XP bar during delay +# ───────────────────────────── +[initialSpawnDelay] +enable = true # Delay the very first dragon spawn in The End +delaySeconds = 60 # Time (in seconds) before first dragon spawns + +# ───────────────────────────── +# Miscellaneous +# ───────────────────────────── +[misc] +logDebugInfo = false # Enable extra debug logging in console + ``` --- @@ -53,6 +79,16 @@ Requires permission level 2 (OP status). --- +## ❓ FAQ + +**1. Why did my configuration reset after updating to v2.0.0?** + +Version 2.0.0 switched the config file from `.properties` to the new `.toml` format. Your old settings were automatically backed up to `config/scaleddragonfight.properties.bak`. + +Please copy your settings to the new `scaleddragonfight.toml` file. You can then run `/scaleddragonfight reload` to apply changes without a server restart. For more details, see the v2.0.0 Changelog. + +--- + ## πŸ”Œ Compatibility - Minecraft: `1.21.5, 1.21.7 - 1.21.9` diff --git a/build.gradle.kts b/build.gradle.kts index ed4d493..13d7932 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,9 @@ dependencies { modImplementation("net.fabricmc:fabric-loader:${project.property("loader_version")}") modImplementation("net.fabricmc:fabric-language-kotlin:${project.property("kotlin_loader_version")}") modImplementation("net.fabricmc.fabric-api:fabric-api:${project.property("fabric_version")}") + + // Config + modImplementation("com.electronwill.night-config:toml:${project.property("night_config_version")}") } tasks.processResources { diff --git a/gradle.properties b/gradle.properties index 72f3b78..c6e435d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,9 +7,12 @@ yarn_mappings=1.21.10+build.2 loader_version=0.17.3 kotlin_loader_version=1.13.6+kotlin.2.2.20 # Mod Properties -mod_version=v1.0.1+1.21.10 +mod_version=2.0.0+1.21.10 maven_group=com.zephbyte archives_base_name=ScaledDragonFight # Dependencies # check this on https://modmuss50.me/fabric.html fabric_version=0.136.0+1.21.10 + +# Night Config +night_config_version=3.8.1 diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/ConfigManager.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/ConfigManager.kt index aed45aa..82481fe 100644 --- a/src/main/kotlin/com/zephbyte/scaleddragonfight/ConfigManager.kt +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/ConfigManager.kt @@ -1,162 +1,173 @@ package com.zephbyte.scaleddragonfight +import com.electronwill.nightconfig.core.file.CommentedFileConfig +import com.electronwill.nightconfig.core.io.WritingMode import net.fabricmc.loader.api.FabricLoader -import java.nio.file.Files -import java.nio.file.Path -import java.util.* - -object ConfigManager { - - /*---- Default configuration values ----*/ - private const val DEFAULT_ENABLE_MOD = true - private const val DEFAULT_SCALE_WITH_ONE_PLAYER = false - private const val DEFAULT_COUNT_CREATIVE_MODE_PLAYERS = false - private const val DEFAULT_BASE_DRAGON_HEALTH = 200.0f - private const val DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER = 100.0f - private const val DEFAULT_ENABLE_BROADCAST = true - - // Values for delay dragon spawn - private const val DEFAULT_ENABLE_INITIAL_SPAWN_DELAY = true - private const val DEFAULT_INITIAL_SPAWN_DELAY_SECONDS = 60 - private const val DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN = true - - /*---- Configurable values ----*/ - var enableMod: Boolean = DEFAULT_ENABLE_MOD - var scaleWithOnePlayer: Boolean = DEFAULT_SCALE_WITH_ONE_PLAYER - var countCreativeModePlayers: Boolean = DEFAULT_COUNT_CREATIVE_MODE_PLAYERS - var baseDragonHealth: Float = DEFAULT_BASE_DRAGON_HEALTH - var additionalHealthPerPlayer: Float = DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER - var enableBroadcast: Boolean = DEFAULT_ENABLE_BROADCAST - - // Values for delay dragon - var enableInitialSpawnDelay: Boolean = DEFAULT_ENABLE_INITIAL_SPAWN_DELAY - var initialSpawnDelaySeconds: Int = DEFAULT_INITIAL_SPAWN_DELAY_SECONDS - var showSpawnDelayCountdown: Boolean = DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN - - private val configFilePath: Path = FabricLoader.getInstance().configDir.resolve("$MOD_ID.properties") - - fun loadConfig() { - LOGGER.info("Loading Scaled Dragon Fight configuration...") - val properties = Properties() - - if (Files.exists(configFilePath)) { - try { - Files.newInputStream(configFilePath).use { inputStream -> - properties.load(inputStream) - } - - /*---- Load configuration values ----*/ - enableMod = properties.getProperty("enableMod", DEFAULT_ENABLE_MOD.toString()).toBooleanStrictOrNull() - ?: DEFAULT_ENABLE_MOD - scaleWithOnePlayer = - properties.getProperty("scaleWithOnePlayer", DEFAULT_SCALE_WITH_ONE_PLAYER.toString()) - .toBooleanStrictOrNull() ?: DEFAULT_SCALE_WITH_ONE_PLAYER - countCreativeModePlayers = - properties.getProperty("countCreativeModePlayers", DEFAULT_COUNT_CREATIVE_MODE_PLAYERS.toString()) - .toBooleanStrictOrNull() ?: DEFAULT_COUNT_CREATIVE_MODE_PLAYERS - baseDragonHealth = - properties.getProperty("baseDragonHealth", DEFAULT_BASE_DRAGON_HEALTH.toString()).toFloatOrNull() - ?: DEFAULT_BASE_DRAGON_HEALTH - additionalHealthPerPlayer = - properties.getProperty("additionalHealthPerPlayer", DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER.toString()) - .toFloatOrNull() ?: DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER - enableBroadcast = properties.getProperty("enableBroadcast", DEFAULT_ENABLE_BROADCAST.toString()) - .toBooleanStrictOrNull() ?: DEFAULT_ENABLE_BROADCAST - - // Values for delay dragon spawn - enableInitialSpawnDelay = - properties.getProperty("enableInitialSpawnDelay", DEFAULT_ENABLE_INITIAL_SPAWN_DELAY.toString()) - .toBooleanStrictOrNull() ?: DEFAULT_ENABLE_INITIAL_SPAWN_DELAY - initialSpawnDelaySeconds = - properties.getProperty("initialSpawnDelaySeconds", DEFAULT_INITIAL_SPAWN_DELAY_SECONDS.toString()) - .toIntOrNull() ?: DEFAULT_INITIAL_SPAWN_DELAY_SECONDS - showSpawnDelayCountdown = - properties.getProperty("showSpawnDelayCountdown", DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN.toString()) - .toBooleanStrictOrNull() ?: DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN - - - LOGGER.info("Configuration loaded: Mod Enabled = $enableMod, Scale w/ 1 Player = $scaleWithOnePlayer, Count Creative = $countCreativeModePlayers, Base Health = $baseDragonHealth, Additional Health/Player = $additionalHealthPerPlayer, Enable Broadcast = $enableBroadcast") - // Ensure config file is up-to-date with current or default values if parsing failed for some - saveConfig() - } catch (e: Exception) { - LOGGER.error( - "Failed to load configuration for $MOD_ID. Using default values and attempting to save a new config file.", - e - ) - resetToDefaultsAndSave() - } - } else { - LOGGER.info("No configuration file found for $MOD_ID. Creating with default values.") - resetToDefaultsAndSave() - } +import kotlin.properties.ReadOnlyProperty + +private object ConfigManager { + + lateinit var config: ConfigData + private set + + private val builder = CommentedFileConfig.builder(FabricLoader.getInstance().configDir.resolve("$MOD_ID.toml")) + .autosave() + .writingMode(WritingMode.REPLACE) + + /** + * Loads or reloads the configuration from the file on disk. + * This function is called by the public-facing SDFConfig object. + */ + fun reloadConfig() { + val fileConfig = builder.build() + fileConfig.load() + config = ConfigData(fileConfig) + fileConfig.save() + fileConfig.close() + } +} + +/** + * A data class to hold all configuration values in a structured way. + * This is a public class so that its properties can be exposed by the public SDFConfig object. + */ +class ConfigData(fileConfig: CommentedFileConfig) { + val general = General(fileConfig) + val healthScaling = HealthScaling(fileConfig) + val broadcastMessages = BroadcastMessages(fileConfig) + val countdown = Countdown(fileConfig) + val spawnDelay = SpawnDelay(fileConfig) + + class General(config: CommentedFileConfig) { + val enableMod by define( + config, + "general.enableMod", + true, + "Enable or disable the entire mod.") + val scaleWithOnePlayer by define( + config, + "general.scaleWithOnePlayer", + false, + "If true, dragon's health will be scaled even if there is only one player in The End.") + val countCreativeModePlayers by define( + config, + "general.countCreativeModePlayers", + false, + "If true, players in creative mode will be counted for health scaling.") + } + + class HealthScaling(config: CommentedFileConfig) { + val baseDragonHealth by define( + config, + "healthScaling.baseDragonHealth", + 200.0f, + "The base health of the Ender Dragon.") + val additionalHealthPerPlayer by define( + config, + "healthScaling.additionalHealthPerPlayer", + 100.0f, + "How much health to add to the Ender Dragon for each additional player.") + } + + class BroadcastMessages(config: CommentedFileConfig) { + val enableBroadcast by define( + config, + "broadcastMessages.enableBroadcast", + true, + "Enable or disable broadcast messages (e.g., when the dragon's health is scaled).") + } + + class Countdown(config: CommentedFileConfig) { + val enableCountdownOverworld by define( + config, + "countdown.enableCountdownOverworld", + true, + "Show the countdown to players in the Overworld.") + val enableCountdownTheEnd by define( + config, + "countdown.enableCountdownTheEnd", + true, + "Show the countdown to players in The End.") + val enableCountdownNether by define( + config, + "countdown.enableCountdownNether", + false, + "Show the countdown to players in the Nether.") } - fun saveConfig() { - LOGGER.info("Saving Scaled Dragon Fight configuration...") - val properties = Properties() - - /*---- Save configuration values ----*/ - properties.setProperty("enableMod", enableMod.toString()) - properties.setProperty("scaleWithOnePlayer", scaleWithOnePlayer.toString()) - properties.setProperty("countCreativeModePlayers", countCreativeModePlayers.toString()) - properties.setProperty("baseDragonHealth", baseDragonHealth.toString()) - properties.setProperty("additionalHealthPerPlayer", additionalHealthPerPlayer.toString()) - properties.setProperty("enableBroadcast", enableBroadcast.toString()) - - // Values for delay dragon - properties.setProperty("enableInitialSpawnDelay", enableInitialSpawnDelay.toString()) - properties.setProperty("initialSpawnDelaySeconds", initialSpawnDelaySeconds.toString()) - properties.setProperty("showSpawnDelayCountdown", showSpawnDelayCountdown.toString()) - - val comments = """ - Scaled Dragon Fight Configuration - - enableMod: If true, the mod will be active. (Default: $DEFAULT_ENABLE_MOD) - baseDragonHealth: Base health of the Ender Dragon. (Default: $DEFAULT_BASE_DRAGON_HEALTH) - additionalHealthPerPlayer: Extra health added for each eligible player. (Default: $DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER) - scaleWithOnePlayer: If true, the dragon's health will increase counting the first eligible player. - If false, scaling only starts with the second eligible player. - Example (assuming 100 additional health per player): - True: - 1 Eligible Player = Base Health + 100 - 2 Eligible Players = Base Health + 200 - False: - 1 Eligible Player = Base Health - 2 Eligible Players = Base Health + 100 - (Default: $DEFAULT_SCALE_WITH_ONE_PLAYER) - countCreativeModePlayers: If true, players in creative mode will be counted when scaling health. (Default: $DEFAULT_COUNT_CREATIVE_MODE_PLAYERS) - enableBroadcast: If true, a message will be broadcast when the scaled dragon spawns. (Default: $DEFAULT_ENABLE_BROADCAST) - - --- Initial Spawn Delay --- - enableInitialSpawnDelay: If true, the very first Ender Dragon spawn in The End will be delayed. (Default: $DEFAULT_ENABLE_INITIAL_SPAWN_DELAY) - initialSpawnDelaySeconds: How many seconds to delay the initial dragon spawn. (Default: $DEFAULT_INITIAL_SPAWN_DELAY_SECONDS) - showSpawnDelayCountdown: If true, a countdown will be shown on players' XP bars in The End during the delay. (Default: $DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN) - \n - """.trimIndent() - - try { - Files.newOutputStream(configFilePath).use { outputStream -> - properties.store(outputStream, comments) - } - LOGGER.info("Configuration saved to $configFilePath") - } catch (e: Exception) { - LOGGER.error("Failed to save configuration for $MOD_ID.", e) + class SpawnDelay(config: CommentedFileConfig) { + val enableInitialSpawnDelay by define( + config, + "spawnDelay.enableInitialSpawnDelay", + true, + "Enable a delay before the Ender Dragon initially spawns.") + val initialSpawnDelaySeconds by define( + config, + "spawnDelay.initialSpawnDelaySeconds", + 60, + "The duration of the initial spawn delay in seconds.") + } +} + +/** + * A generic helper function that creates a delegated property for a config value. + * This is now a top-level private function, accessible within this file. + */ +@Suppress("UNCHECKED_CAST") +private fun define(config: CommentedFileConfig, path: String, defaultValue: T, comment: String): ReadOnlyProperty { + // Set the comment. This will be written to the file when config.save() is called. + config.setComment(path, " [Default: $defaultValue]\n $comment") + + // Get the raw value from the config, which could be of any type. + val rawValue = config.get(path) + val value: T + + when { + // Case 1: The value from the config is null or missing. + rawValue == null -> { + config.set(path, defaultValue) + value = defaultValue + } + + // Case 2: We want a Float but got a Number (like a Double) that can be converted. + defaultValue is Float && rawValue is Number -> { + value = rawValue.toFloat() as T + } + + // Case 3: The value exists and is already the correct type. + defaultValue::class.java.isInstance(rawValue) -> { + value = rawValue as T + } + + // Case 4: The value is the wrong type and can't be handled automatically. + else -> { + config.set(path, defaultValue) + value = defaultValue + LOGGER.info("ERROR: $path was set to $rawValue, which is NOT of type ${defaultValue::class.java}! Setting to default") } } - private fun resetToDefaultsAndSave() { - /*---- Reset configuration values to defaults ----*/ - enableMod = DEFAULT_ENABLE_MOD - scaleWithOnePlayer = DEFAULT_SCALE_WITH_ONE_PLAYER - countCreativeModePlayers = DEFAULT_COUNT_CREATIVE_MODE_PLAYERS - baseDragonHealth = DEFAULT_BASE_DRAGON_HEALTH - additionalHealthPerPlayer = DEFAULT_ADDITIONAL_HEALTH_PER_PLAYER - enableBroadcast = DEFAULT_ENABLE_BROADCAST - - // Values for delay dragon - enableInitialSpawnDelay = DEFAULT_ENABLE_INITIAL_SPAWN_DELAY - initialSpawnDelaySeconds = DEFAULT_INITIAL_SPAWN_DELAY_SECONDS - showSpawnDelayCountdown = DEFAULT_SHOW_SPAWN_DELAY_COUNTDOWN - saveConfig() + // Return a property delegate that simply provides the value loaded at creation time. + return ReadOnlyProperty { _, _ -> value } +} + +/** + * The public-facing API for accessing configuration values. + * + * To reload the config, call `SDFConfig.reload()`. + * To access a value, use `SDFConfig.general.enableMod`. + */ +object SDFConfig { + val general get() = ConfigManager.config.general + val healthScaling get() = ConfigManager.config.healthScaling + val broadcastMessages get() = ConfigManager.config.broadcastMessages + val countdown get() = ConfigManager.config.countdown + val spawnDelay get() = ConfigManager.config.spawnDelay + + /** + * Public function to trigger a reload of the configuration from the file. + */ + fun reload() { + ConfigManager.reloadConfig() } } \ No newline at end of file diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonEventHandler.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonEventHandler.kt index eb56340..ee65a80 100644 --- a/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonEventHandler.kt +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonEventHandler.kt @@ -1,6 +1,5 @@ package com.zephbyte.scaleddragonfight -import com.zephbyte.scaleddragonfight.ConfigManager.enableMod // Direct access to config import com.zephbyte.scaleddragonfight.mixin.EnderDragonFightAccessor import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents @@ -25,8 +24,8 @@ object DragonEventHandler { fun register() { ServerEntityEvents.ENTITY_LOAD.register { entity, world -> - // Check enableMod from ConfigManager - if (!enableMod) { + // Check if enabled in config + if (!SDFConfig.general.enableMod) { return@register // If mod is disabled, do nothing } @@ -53,7 +52,7 @@ object DragonEventHandler { val state = worldDelayStates.computeIfAbsent(world) { DelayState() } // Case 1: Feature is disabled in config - if (!ConfigManager.enableInitialSpawnDelay) { + if (!SDFConfig.spawnDelay.enableInitialSpawnDelay) { state.initialSpawnAttemptProcessed = true // Mark as processed, feature disabled if (state.delayActive) { // If a delay was somehow active and then config got disabled state.delayActive = false @@ -80,9 +79,9 @@ object DragonEventHandler { // Case 4: This is the first time for this world, feature enabled, not yet processed, not active. // This is where we initiate the delay. // (state.initialSpawnAttemptProcessed is false here, or it would have hit Case 2 or 3) - LOGGER.info("Initial Ender Dragon spawn in ${world.registryKey.value} will be delayed by ${ConfigManager.initialSpawnDelaySeconds} seconds.") + LOGGER.info("Initial Ender Dragon spawn in ${world.registryKey.value} will be delayed by ${SDFConfig.spawnDelay.initialSpawnDelaySeconds} seconds.") state.delayActive = true - state.ticksRemaining = ConfigManager.initialSpawnDelaySeconds * 20 // 20 ticks per second + state.ticksRemaining = SDFConfig.spawnDelay.initialSpawnDelaySeconds * 20 // 20 ticks per second state.initialSpawnAttemptProcessed = true // Mark that we've handled the first attempt state.fightInstance = fight // Store the fight instance to trigger spawn later @@ -97,7 +96,7 @@ object DragonEventHandler { val state = worldDelayStates[world] ?: return // No state for this world, or delay not initiated // Handle if feature gets disabled mid-countdown (e.g., via /reload and config change) - if (!ConfigManager.enableInitialSpawnDelay && state.delayActive) { + if (!SDFConfig.spawnDelay.enableInitialSpawnDelay && state.delayActive) { LOGGER.info("Initial spawn delay feature disabled mid-countdown for ${world.registryKey.value}. Triggering dragon spawn now.") state.delayActive = false state.ticksRemaining = 0 // This will trigger the spawn logic below immediately @@ -106,12 +105,29 @@ object DragonEventHandler { if (state.delayActive && state.ticksRemaining > 0) { state.ticksRemaining-- - if (ConfigManager.showSpawnDelayCountdown) { + // Check if any countdown is enabled before doing calculations + val countdownConfig = SDFConfig.countdown + if (countdownConfig.enableCountdownTheEnd || + countdownConfig.enableCountdownOverworld || + countdownConfig.enableCountdownNether) { val remainingSeconds = ceil(state.ticksRemaining / 20.0).toInt() - if (remainingSeconds > 0) { // Only show if time is actually remaining + if (remainingSeconds > 0) { val message = Text.literal("Dragon spawning in: $remainingSeconds...") - world.players.forEach { player -> - player.sendMessage(message, true) // 'true' sends to action bar + val server = world.server + + // Iterate through all players on the server + server.playerManager.playerList.forEach { player -> + val playerWorldKey = player.entityWorld.registryKey + val shouldSendMessage = when (playerWorldKey) { + World.END -> countdownConfig.enableCountdownTheEnd + World.OVERWORLD -> countdownConfig.enableCountdownOverworld + World.NETHER -> countdownConfig.enableCountdownNether + else -> false // Don't send to custom dimensions by default + } + + if (shouldSendMessage) { + player.sendMessage(message, true) + } } } } @@ -121,11 +137,6 @@ object DragonEventHandler { state.delayActive = false // Stop the delay state state.fightInstance?.let { fight -> - // This call will go through the EnderDragonFightMixin again. - // onInitialDragonPreSpawn will see: - // initialSpawnAttemptProcessed = true, delayActive = false. - // So it returns false (don't cancel), allowing respawnDragon to proceed. - // Cast to the accessor and call the invoker method if (fight is EnderDragonFightAccessor) { fight.callRespawnDragon(emptyList()) // Use the accessor LOGGER.info("Ender Dragon respawn initiated by the mod after delay via accessor.") diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonScaler.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonScaler.kt index 14526fa..9b8c9b2 100644 --- a/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonScaler.kt +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/DragonScaler.kt @@ -1,11 +1,6 @@ package com.zephbyte.scaleddragonfight // Import specific config values needed, or the whole ConfigManager -import com.zephbyte.scaleddragonfight.ConfigManager.additionalHealthPerPlayer -import com.zephbyte.scaleddragonfight.ConfigManager.baseDragonHealth -import com.zephbyte.scaleddragonfight.ConfigManager.countCreativeModePlayers -import com.zephbyte.scaleddragonfight.ConfigManager.enableBroadcast -import com.zephbyte.scaleddragonfight.ConfigManager.scaleWithOnePlayer import net.minecraft.entity.attribute.EntityAttributes import net.minecraft.entity.boss.dragon.EnderDragonEntity import net.minecraft.server.network.ServerPlayerEntity @@ -20,29 +15,29 @@ object DragonScaler { // Filter players in The End and consider creative mode setting val presentPlayers = world.players.filterIsInstance() val eligiblePlayers = presentPlayers.filter { player -> - countCreativeModePlayers || !player.isCreative + SDFConfig.general.countCreativeModePlayers || !player.isCreative } val scaleEligiblePlayerCount = eligiblePlayers.size // Determine the number of players that contribute to *additional* health val playersContributingToAdditionalHealth = when { - !scaleWithOnePlayer && scaleEligiblePlayerCount <= 1 -> 0 - scaleWithOnePlayer && scaleEligiblePlayerCount >= 1 -> scaleEligiblePlayerCount + !SDFConfig.general.scaleWithOnePlayer && scaleEligiblePlayerCount <= 1 -> 0 + SDFConfig.general.scaleWithOnePlayer && scaleEligiblePlayerCount >= 1 -> scaleEligiblePlayerCount else -> scaleEligiblePlayerCount - 1 } - val newMaxHealth = baseDragonHealth + (additionalHealthPerPlayer * playersContributingToAdditionalHealth) + val newMaxHealth = SDFConfig.healthScaling.baseDragonHealth + (SDFConfig.healthScaling.additionalHealthPerPlayer * playersContributingToAdditionalHealth) // Ensure health doesn't go below the configured base - val finalMaxHealth = newMaxHealth.coerceAtLeast(baseDragonHealth) + val finalMaxHealth = newMaxHealth.coerceAtLeast(SDFConfig.healthScaling.baseDragonHealth) dragon.getAttributeInstance(EntityAttributes.MAX_HEALTH)?.let { attributeInstance -> attributeInstance.baseValue = finalMaxHealth.toDouble() dragon.health = finalMaxHealth // Set current health to new max health - LOGGER.info("Scaled Ender Dragon health. Eligible Players: $scaleEligiblePlayerCount, Players Adding Health: $playersContributingToAdditionalHealth, New Max Health: $finalMaxHealth (Base: $baseDragonHealth, Per Player Factor: $additionalHealthPerPlayer)") + LOGGER.info("Scaled Ender Dragon health. Eligible Players: $scaleEligiblePlayerCount, Players Adding Health: $playersContributingToAdditionalHealth, New Max Health: $finalMaxHealth (Base: ${SDFConfig.healthScaling.baseDragonHealth}, Per Player Factor: ${SDFConfig.healthScaling.additionalHealthPerPlayer})") } ?: LOGGER.warn("Could not find GENERIC_MAX_HEALTH attribute for Ender Dragon. Health not scaled.") - if (enableBroadcast) { + if (SDFConfig.broadcastMessages.enableBroadcast) { broadcastScalingMessage(world, scaleEligiblePlayerCount, finalMaxHealth) } } @@ -50,7 +45,7 @@ object DragonScaler { private fun broadcastScalingMessage(world: ServerWorld, scaleEligiblePlayerCount: Int, finalMaxHealth: Float) { val server = world.server val broadcastMessageText = - if (scaleEligiblePlayerCount > 0 && (scaleWithOnePlayer || scaleEligiblePlayerCount > 1)) { + if (scaleEligiblePlayerCount > 0 && (SDFConfig.general.scaleWithOnePlayer || scaleEligiblePlayerCount > 1)) { // Announce scaling if it happened and health is above vanilla default if (finalMaxHealth > VANILLA_DRAGON_HEALTH) { "An ENDER DRAGON has appeared in THE END! Its power scales with $scaleEligiblePlayerCount brave warrior(s)!" diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/LegacyConfigMigrator.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/LegacyConfigMigrator.kt new file mode 100644 index 0000000..05b8889 --- /dev/null +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/LegacyConfigMigrator.kt @@ -0,0 +1,81 @@ +package com.zephbyte.scaleddragonfight + +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents +import net.fabricmc.loader.api.FabricLoader +import net.minecraft.text.ClickEvent +import net.minecraft.text.HoverEvent +import net.minecraft.text.Style +import net.minecraft.text.Text +import net.minecraft.util.Formatting +import java.net.URI +import java.nio.file.Files + +// TODO Zeph: Remove this legacy config check in v3.0.0 +// This code exists to help users migrate from v1.x.x `.properties` config +// to the v2.x.x `.toml` config. By v3.0.0 we can assume most have migrated. +object LegacyConfigMigrator { + + private var updateNotificationSent = false + + fun runCheck() { + val configDir = FabricLoader.getInstance().configDir + val legacyConfigFile = configDir.resolve("$MOD_ID.properties") + + if (Files.exists(legacyConfigFile)) { + // Log a big warning to the server console + LOGGER.warn("=====================================================================================") + LOGGER.warn("LEGACY CONFIG DETECTED! Scaled Dragon Fight has updated to a new '.toml' format.") + LOGGER.warn("Your old 'scaleddragonfight.properties' file is no longer used.") + LOGGER.warn("Your settings must be manually migrated to the new 'scaleddragonfight.toml' file.") + LOGGER.warn("=====================================================================================") + + // Register an event to notify the first OP that joins + ServerPlayConnectionEvents.JOIN.register { handler, _, _ -> + if (!updateNotificationSent && handler.player.hasPermissionLevel(2)) { + val changelogUrl = URI("https://modrinth.com/mod/scaled-dragon-fight/version/v1.0.1+1.21.10") // TODO Zeph: Update this link when releasing + val changelogText = Text.literal("[Changelog for v2.0.0]") + .setStyle( + Style.EMPTY + .withClickEvent(ClickEvent.OpenUrl(changelogUrl)) + .withHoverEvent(HoverEvent.ShowText(Text.literal("Click to open changelog"))) + .withColor(Formatting.BLUE) + .withFormatting(Formatting.UNDERLINE, Formatting.ITALIC) + ) + + val reloadCommandText = Text.literal("/scaleddragonfight reload") + .setStyle( + Style.EMPTY + .withColor(Formatting.AQUA) + .withClickEvent(ClickEvent.SuggestCommand("/scaleddragonfight reload")) + .withHoverEvent(HoverEvent.ShowText(Text.literal("Click to suggest command"))) + ) + + val fullMessage = Text.literal("") + .append(Text.literal("IMPORTANT CONFIG UPDATE\n").formatted(Formatting.RED, Formatting.BOLD)) + .append(Text.literal("Scaled Dragon Fight").formatted(Formatting.GOLD)) + .append(Text.literal(" ALL CONFIG SETTINGS have been reset to default for v2.0.0.\n").formatted(Formatting.YELLOW)) + .append(Text.literal("Your old settings were saved to ").formatted(Formatting.GRAY)) + .append(Text.literal("config/scaleddragonfight.properties.bak\n").formatted(Formatting.AQUA)) + .append(Text.literal("After migrating settings, run ").formatted(Formatting.GRAY)) + .append(reloadCommandText) + .append(Text.literal(" to apply them without a restart.\n").formatted(Formatting.GRAY)) + .append(Text.literal("See the ").formatted(Formatting.GRAY)) + .append(changelogText) + .append(Text.literal(" for more info.").formatted(Formatting.GRAY)) + + handler.player.sendMessage(fullMessage, false) + updateNotificationSent = true // Ensure we only send this once per server start + } + } + + // Rename the old file to .bak to prevent confusion and preserve user's settings + try { + val backupFile = configDir.resolve("$MOD_ID.properties.bak") + Files.move(legacyConfigFile, backupFile) + LOGGER.info("Renamed legacy config to 'scaleddragonfight.properties.bak'.") + } catch (e: Exception) { + LOGGER.error("Failed to rename legacy config file.", e) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/ModCommands.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/ModCommands.kt index 3d40ab4..d72e848 100644 --- a/src/main/kotlin/com/zephbyte/scaleddragonfight/ModCommands.kt +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/ModCommands.kt @@ -1,7 +1,6 @@ package com.zephbyte.scaleddragonfight import com.mojang.brigadier.CommandDispatcher -import com.zephbyte.scaleddragonfight.ConfigManager.enableMod // Direct access to config import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback import net.minecraft.server.command.CommandManager import net.minecraft.server.command.ServerCommandSource @@ -10,7 +9,7 @@ import net.minecraft.text.Text object ModCommands { fun register() { - CommandRegistrationCallback.EVENT.register { dispatcher, registryAccess, environment -> + CommandRegistrationCallback.EVENT.register { dispatcher, _, _ -> registerReloadCommand(dispatcher) } } @@ -23,8 +22,8 @@ object ModCommands { .then(CommandManager.literal("reload") .requires { source -> source.hasPermissionLevel(2) } // Only OPs (level 2) can reload .executes { context -> - ConfigManager.loadConfig() // Reload config from ConfigManager - val feedbackMsg = if (enableMod) { // Check enableMod from ConfigManager + SDFConfig.reload() // Reload config from ConfigManager + val feedbackMsg = if (SDFConfig.general.enableMod) { // Check enableMod from ConfigManager "Scaled Dragon Fight configuration reloaded. Mod is ENABLED." } else { "Scaled Dragon Fight configuration reloaded. Mod is DISABLED." diff --git a/src/main/kotlin/com/zephbyte/scaleddragonfight/ScaledDragonFight.kt b/src/main/kotlin/com/zephbyte/scaleddragonfight/ScaledDragonFight.kt index 3cc6f6f..8af94c7 100644 --- a/src/main/kotlin/com/zephbyte/scaleddragonfight/ScaledDragonFight.kt +++ b/src/main/kotlin/com/zephbyte/scaleddragonfight/ScaledDragonFight.kt @@ -3,11 +3,11 @@ package com.zephbyte.scaleddragonfight import net.fabricmc.api.ModInitializer class ScaledDragonFight : ModInitializer { - override fun onInitialize() { LOGGER.info("Scaled Dragon Fight mod initializing...") - ConfigManager.loadConfig() + LegacyConfigMigrator.runCheck() + SDFConfig.reload() DragonEventHandler.register() ModCommands.register()