From 88b0ba8b97b93faf2f825772cb00b85fd4b2f8ba Mon Sep 17 00:00:00 2001 From: TDSTOS Date: Sat, 4 Apr 2026 02:48:17 +0200 Subject: [PATCH] Refactor kits, add safety checks and fixes Multiple refactors and defensive fixes across kits and event handling: - RecraftManager: use Bukkit.getOnlinePlayers() collection API instead of stream. - ActiveAbility: mark backing _hitsRequired as @Volatile for thread-safety. - BlackPanther, Rattlesnake, TheWorld, Gladiator, Goblin, Venom, Voodoo: change how kit overrides are accessed (lazy or helper) to avoid stale initialization and improve readability. - Gladiator: add an ended flag to avoid double-ending fights. - Goblin & TheWorld: add game state checks to avoid restoring kits or operating on players after game end. - Rattlesnake: guard scheduled miss task with player.isOnline check and simplify action bar call. - Venom: clean up active shield tasks on kit removal and make damage handling null-safe with apply. - Voodoo: wrap passive tick in runCatching and log failures to prevent uncaught exceptions from killing tasks. - KitEventDispatcher: skip handling if victim is not alive, change interact handler to ignoreCancelled = false, and add isAlive helper. - ItemBuilder: switch lore serialization to MiniMessage, disable default italic decoration, and reuse a MiniMessage instance. These changes improve robustness, avoid race conditions, and add defensive guards against invalid state during scheduled tasks and event handling. --- .../speedhg/game/modules/RecraftManager.kt | 2 +- .../speedhg/kit/ability/ActiveAbility.kt | 1 + .../speedhg/kit/impl/BlackPantherKit.kt | 15 ++++++------ .../mcscrims/speedhg/kit/impl/GladiatorKit.kt | 8 ++++++- .../mcscrims/speedhg/kit/impl/GoblinKit.kt | 7 ++++-- .../speedhg/kit/impl/RattlesnakeKit.kt | 19 +++++++-------- .../mcscrims/speedhg/kit/impl/TheWorldKit.kt | 24 +++++++++---------- .../mcscrims/speedhg/kit/impl/VenomKit.kt | 18 +++++++++----- .../mcscrims/speedhg/kit/impl/VoodooKit.kt | 6 +++-- .../kit/listener/KitEventDispatcher.kt | 10 +++++++- .../club/mcscrims/speedhg/util/ItemBuilder.kt | 15 ++++++------ 11 files changed, 75 insertions(+), 50 deletions(-) diff --git a/src/main/kotlin/club/mcscrims/speedhg/game/modules/RecraftManager.kt b/src/main/kotlin/club/mcscrims/speedhg/game/modules/RecraftManager.kt index f418c4f..fbafbee 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/game/modules/RecraftManager.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/game/modules/RecraftManager.kt @@ -32,7 +32,7 @@ class RecraftManager( return } - Bukkit.getOnlinePlayers().stream() + Bukkit.getOnlinePlayers() .filter { plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .forEach { val recraft = Recraft() diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt index 52f0b0e..2795e90 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/ability/ActiveAbility.kt @@ -48,6 +48,7 @@ abstract class ActiveAbility( * und dann O(1) gelesen. Initialisiert mit dem Hardcoded-Default * als Safety-Net falls cacheHitsRequired() nie aufgerufen wird. */ + @Volatile private var _hitsRequired: Int = -1 val hitsRequired: Int diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt index 6e4cf1d..b775f8f 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/BlackPantherKit.kt @@ -62,18 +62,17 @@ class BlackPantherKit : Kit() companion object { - private val kitOverride get() = - SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] - ?: CustomGameSettings.KitOverride() + private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] + ?: CustomGameSettings.KitOverride() /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" - private val FIST_MODE_MS = kitOverride.fistModeDurationMs // 12 seconds - private val PUSH_RADIUS = kitOverride.pushRadius - private val POUNCE_MIN_FALL = kitOverride.pounceMinFall - private val POUNCE_RADIUS = kitOverride.pounceRadius - private val POUNCE_DAMAGE = kitOverride.pounceDamage // 3 hearts = 6 HP + private val FIST_MODE_MS = override().fistModeDurationMs // 12 seconds + private val PUSH_RADIUS = override().pushRadius + private val POUNCE_MIN_FALL = override().pounceMinFall + private val POUNCE_RADIUS = override().pounceRadius + private val POUNCE_DAMAGE = override().pounceDamage // 3 hearts = 6 HP } // ── Cached ability instances ────────────────────────────────────────────── diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt index 93025e6..b14c6ce 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GladiatorKit.kt @@ -42,9 +42,10 @@ class GladiatorKit : Kit() { override val icon: Material get() = Material.IRON_BARS - private val kitOverride get() = + private val kitOverride: CustomGameSettings.KitOverride by lazy { plugin.customGameManager.settings.kits.kits["gladiator"] ?: CustomGameSettings.KitOverride() + } // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE ) @@ -243,6 +244,8 @@ class GladiatorKit : Kit() { enemy.teleport(Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f )) } + private var ended = false + override fun run() { if ( !gladiator.isOnline || !enemy.isOnline ) @@ -272,6 +275,9 @@ class GladiatorKit : Kit() { private fun endFight() { + if ( ended ) return + ended = true + gladiator.apply { removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin ) removePotionEffect( PotionEffectType.WITHER ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt index a9e102c..36b1965 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/GoblinKit.kt @@ -2,6 +2,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.config.CustomGameSettings +import club.mcscrims.speedhg.game.GameState import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -36,9 +37,10 @@ class GoblinKit : Kit() { override val icon: Material get() = Material.MOSSY_COBBLESTONE - private val kitOverride get() = + private val kitOverride: CustomGameSettings.KitOverride by lazy { plugin.customGameManager.settings.kits.kits["goblin"] ?: CustomGameSettings.KitOverride() + } // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AggressiveActive() @@ -152,7 +154,8 @@ class GoblinKit : Kit() { val task = Bukkit.getScheduler().runTaskLater( plugin, { -> activeStealTasks.remove( player.uniqueId ) // Nur wiederherstellen, wenn Spieler noch alive und Spiel läuft - if (plugin.gameManager.alivePlayers.contains( player.uniqueId )) + if (plugin.gameManager.alivePlayers.contains( player.uniqueId ) && + plugin.gameManager.currentState == GameState.INGAME ) { plugin.kitManager.removeKit( player ) plugin.kitManager.selectKit( player, currentKit ) diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt index dcfc248..60e35ef 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/RattlesnakeKit.kt @@ -57,15 +57,14 @@ class RattlesnakeKit : Kit() { internal val lastPounceUse: MutableMap = ConcurrentHashMap() companion object { - private val kitOverride get() = - SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"] - ?: CustomGameSettings.KitOverride() + private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"] + ?: CustomGameSettings.KitOverride() - private val POUNCE_COOLDOWN_MS = kitOverride.pounceCooldownMs - private val MAX_SNEAK_MS = kitOverride.pounceMaxSneakMs - private val MIN_RANGE = kitOverride.pounceMinRange - private val MAX_RANGE = kitOverride.pounceMaxRange - private val POUNCE_TIMEOUT_TICKS = kitOverride.pounceTimeoutTicks + private val POUNCE_COOLDOWN_MS = override().pounceCooldownMs + private val MAX_SNEAK_MS = override().pounceMaxSneakMs + private val MIN_RANGE = override().pounceMinRange + private val MAX_RANGE = override().pounceMaxRange + private val POUNCE_TIMEOUT_TICKS = override().pounceTimeoutTicks } // ── Cached ability instances ────────────────────────────────────────────── @@ -171,6 +170,7 @@ class RattlesnakeKit : Kit() { // ── Miss timeout ────────────────────────────────────────────────── Bukkit.getScheduler().runTaskLater(plugin, { -> if (!pouncingPlayers.remove(player.uniqueId)) return@runTaskLater // already hit + if ( !player.isOnline ) return@runTaskLater player.world.getNearbyEntities(player.location, 5.0, 5.0, 5.0) .filterIsInstance() @@ -179,8 +179,7 @@ class RattlesnakeKit : Kit() { enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0)) enemy.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 3 * 20, 0)) } - if (player.isOnline) - player.sendActionBar(player.trans("kits.rattlesnake.messages.pounce_miss")) + player.sendActionBar(player.trans("kits.rattlesnake.messages.pounce_miss")) }, POUNCE_TIMEOUT_TICKS) return AbilityResult.Success diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt index f38f9aa..bba0b84 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/TheWorldKit.kt @@ -2,6 +2,7 @@ package club.mcscrims.speedhg.kit.impl import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.config.CustomGameSettings +import club.mcscrims.speedhg.game.GameState import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.ability.AbilityResult @@ -83,16 +84,15 @@ class TheWorldKit : Kit() { ) companion object { - private val kitOverride get() = - SpeedHG.instance.customGameManager.settings.kits.kits["theworld"] - ?: CustomGameSettings.KitOverride() + private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["theworld"] + ?: CustomGameSettings.KitOverride() - private val ABILITY_COOLDOWN_MS = kitOverride.abilityCooldownMs - private val SHOCKWAVE_RADIUS = kitOverride.shockwaveRadius - private val TELEPORT_RANGE = kitOverride.teleportRange - private val MAX_TELEPORT_CHARGES = kitOverride.maxTeleportCharges - private val FREEZE_DURATION_TICKS = kitOverride.freezeDurationTicks - private val MAX_HITS_ON_FROZEN = kitOverride.maxHitsOnFrozen + private val ABILITY_COOLDOWN_MS = override().abilityCooldownMs + private val SHOCKWAVE_RADIUS = override().shockwaveRadius + private val TELEPORT_RANGE = override().teleportRange + private val MAX_TELEPORT_CHARGES = override().maxTeleportCharges + private val FREEZE_DURATION_TICKS = override().freezeDurationTicks + private val MAX_HITS_ON_FROZEN = override().maxHitsOnFrozen } // ── Cached ability instances ────────────────────────────────────────────── @@ -225,10 +225,10 @@ class TheWorldKit : Kit() { override fun run() { ticks++ - if (ticks >= FREEZE_DURATION_TICKS || - !target.isOnline || + if (ticks >= FREEZE_DURATION_TICKS || !target.isOnline || !plugin.gameManager.alivePlayers.contains(target.uniqueId) || - !frozenEnemies.containsKey(target.uniqueId)) + !frozenEnemies.containsKey(target.uniqueId) || + plugin.gameManager.currentState == GameState.ENDING) // ← neu { doUnfreeze(target) cancel() diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt index 8328679..03b4424 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VenomKit.kt @@ -49,9 +49,10 @@ class VenomKit : Kit() { override val icon: Material get() = Material.SPIDER_EYE - private val kitOverride get() = + private val kitOverride: CustomGameSettings.KitOverride by lazy { plugin.customGameManager.settings.kits.kits["venom"] ?: CustomGameSettings.KitOverride() + } // ── Cached ability instances (avoid allocating per event call) ──────────── private val aggressiveActive = AggressiveActive() @@ -116,6 +117,11 @@ class VenomKit : Kit() { override fun onRemove( player: Player ) { + activeShields[ player.uniqueId ]?.let { shield -> + shield.expireTask.cancel() + shield.particleTask.cancel() + activeShields.remove( player.uniqueId ) + } val items = cachedItems.remove( player.uniqueId ) ?: return items.forEach { player.inventory.remove( it ) } } @@ -275,11 +281,11 @@ class VenomKit : Kit() { attacker: Player, event: EntityDamageByEntityEvent ) { - val shield = activeShields[ victim.uniqueId ] ?: return - shield.remainingCapacity -= event.damage - val isCrit = event.isCritical - event.damage = if ( isCrit ) 3.0 else 2.0 - if ( shield.remainingCapacity <= 0 ) breakShield( victim ) + activeShields[victim.uniqueId]?.apply { + remainingCapacity -= event.damage + event.damage = if (event.isCritical) 3.0 else 2.0 + if (remainingCapacity <= 0) breakShield(victim) + } ?: return } } diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt index 47133e0..531b3bc 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/impl/VoodooKit.kt @@ -58,9 +58,10 @@ class VoodooKit : Kit() { /** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ internal val cursedExpiry: MutableMap = ConcurrentHashMap() - private val kitOverride get() = + private val kitOverride: CustomGameSettings.KitOverride by lazy { plugin.customGameManager.settings.kits.kits["voodoo"] ?: CustomGameSettings.KitOverride() + } // ── Cached ability instances ────────────────────────────────────────────── private val aggressiveActive = AggressiveActive() @@ -258,7 +259,8 @@ class VoodooKit : Kit() { if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) { cancel(); return } - tickPassive(player) + runCatching { tickPassive( player ) } + .onFailure { plugin.logger.severe( "[VoodooKit] tickPassive error: ${it.message}" ) } } }.runTaskTimer(plugin, 0L, 20L) tasks[player.uniqueId] = task diff --git a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt index 0c876ce..edf124b 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/kit/listener/KitEventDispatcher.kt @@ -83,6 +83,7 @@ class KitEventDispatcher( val victim = event.entity as? Player ?: return if ( !isIngame() ) return + if (!isAlive( victim )) return val attackerKit = kitManager.getSelectedKit( attacker ) ?: return val attackerPlaystyle = kitManager.getSelectedPlaystyle( attacker ) @@ -114,7 +115,7 @@ class KitEventDispatcher( */ @EventHandler( priority = EventPriority.HIGH, - ignoreCancelled = true + ignoreCancelled = false ) fun onInteract( event: PlayerInteractEvent @@ -397,4 +398,11 @@ class KitEventDispatcher( else -> false } + private fun isAlive( + player: Player + ): Boolean + { + return plugin.gameManager.alivePlayers.contains( player.uniqueId ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt index 3bbe954..cbc04ad 100644 --- a/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt +++ b/src/main/kotlin/club/mcscrims/speedhg/util/ItemBuilder.kt @@ -1,6 +1,8 @@ package club.mcscrims.speedhg.util import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.TextDecoration +import net.kyori.adventure.text.minimessage.MiniMessage import org.bukkit.ChatColor import org.bukkit.Material import org.bukkit.enchantments.Enchantment @@ -11,6 +13,8 @@ class ItemBuilder( private val itemStack: ItemStack ) { + private val mm = MiniMessage.miniMessage() + constructor( type: Material ) : this( @@ -48,13 +52,10 @@ class ItemBuilder( lore: List ): ItemBuilder { - itemStack.editMeta { - val cLore = lore.stream() - .map( this::color ) - .map( Component::text ) - .toList() - - it.lore( cLore as List ) + itemStack.editMeta { meta -> + meta.lore(lore.map { line -> + mm.deserialize( line ).decoration( TextDecoration.ITALIC, false ) + }) } return this }