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.
This commit is contained in:
TDSTOS
2026-04-04 02:48:17 +02:00
parent 5be2ae2674
commit 88b0ba8b97
11 changed files with 75 additions and 50 deletions

View File

@@ -32,7 +32,7 @@ class RecraftManager(
return return
} }
Bukkit.getOnlinePlayers().stream() Bukkit.getOnlinePlayers()
.filter { plugin.gameManager.alivePlayers.contains( it.uniqueId ) } .filter { plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach { .forEach {
val recraft = Recraft() val recraft = Recraft()

View File

@@ -48,6 +48,7 @@ abstract class ActiveAbility(
* und dann O(1) gelesen. Initialisiert mit dem Hardcoded-Default * und dann O(1) gelesen. Initialisiert mit dem Hardcoded-Default
* als Safety-Net falls cacheHitsRequired() nie aufgerufen wird. * als Safety-Net falls cacheHitsRequired() nie aufgerufen wird.
*/ */
@Volatile
private var _hitsRequired: Int = -1 private var _hitsRequired: Int = -1
val hitsRequired: Int val hitsRequired: Int

View File

@@ -62,18 +62,17 @@ class BlackPantherKit : Kit()
companion object companion object
{ {
private val kitOverride get() = private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"]
SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"] ?: CustomGameSettings.KitOverride()
?: CustomGameSettings.KitOverride()
/** PDC key string shared with [KitEventDispatcher] for push-projectiles. */ /** PDC key string shared with [KitEventDispatcher] for push-projectiles. */
const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile" const val PUSH_PROJECTILE_KEY = "blackpanther_push_projectile"
private val FIST_MODE_MS = kitOverride.fistModeDurationMs // 12 seconds private val FIST_MODE_MS = override().fistModeDurationMs // 12 seconds
private val PUSH_RADIUS = kitOverride.pushRadius private val PUSH_RADIUS = override().pushRadius
private val POUNCE_MIN_FALL = kitOverride.pounceMinFall private val POUNCE_MIN_FALL = override().pounceMinFall
private val POUNCE_RADIUS = kitOverride.pounceRadius private val POUNCE_RADIUS = override().pounceRadius
private val POUNCE_DAMAGE = kitOverride.pounceDamage // 3 hearts = 6 HP private val POUNCE_DAMAGE = override().pounceDamage // 3 hearts = 6 HP
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────

View File

@@ -42,9 +42,10 @@ class GladiatorKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.IRON_BARS get() = Material.IRON_BARS
private val kitOverride get() = private val kitOverride: CustomGameSettings.KitOverride by lazy {
plugin.customGameManager.settings.kits.kits["gladiator"] plugin.customGameManager.settings.kits.kits["gladiator"]
?: CustomGameSettings.KitOverride() ?: CustomGameSettings.KitOverride()
}
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AllActive( Playstyle.AGGRESSIVE ) 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 )) enemy.teleport(Location( world, center.x - radius / 2, center.y + 1, center.z, -90f, 0f ))
} }
private var ended = false
override fun run() override fun run()
{ {
if ( !gladiator.isOnline || !enemy.isOnline ) if ( !gladiator.isOnline || !enemy.isOnline )
@@ -272,6 +275,9 @@ class GladiatorKit : Kit() {
private fun endFight() private fun endFight()
{ {
if ( ended ) return
ended = true
gladiator.apply { gladiator.apply {
removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin ) removeMetadata( KitMetaData.IN_GLADIATOR.getKey(), plugin )
removePotionEffect( PotionEffectType.WITHER ) removePotionEffect( PotionEffectType.WITHER )

View File

@@ -2,6 +2,7 @@ package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -36,9 +37,10 @@ class GoblinKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.MOSSY_COBBLESTONE get() = Material.MOSSY_COBBLESTONE
private val kitOverride get() = private val kitOverride: CustomGameSettings.KitOverride by lazy {
plugin.customGameManager.settings.kits.kits["goblin"] plugin.customGameManager.settings.kits.kits["goblin"]
?: CustomGameSettings.KitOverride() ?: CustomGameSettings.KitOverride()
}
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
@@ -152,7 +154,8 @@ class GoblinKit : Kit() {
val task = Bukkit.getScheduler().runTaskLater( plugin, { -> val task = Bukkit.getScheduler().runTaskLater( plugin, { ->
activeStealTasks.remove( player.uniqueId ) activeStealTasks.remove( player.uniqueId )
// Nur wiederherstellen, wenn Spieler noch alive und Spiel läuft // 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.removeKit( player )
plugin.kitManager.selectKit( player, currentKit ) plugin.kitManager.selectKit( player, currentKit )

View File

@@ -57,15 +57,14 @@ class RattlesnakeKit : Kit() {
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap() internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object { companion object {
private val kitOverride get() = private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"]
SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"] ?: CustomGameSettings.KitOverride()
?: CustomGameSettings.KitOverride()
private val POUNCE_COOLDOWN_MS = kitOverride.pounceCooldownMs private val POUNCE_COOLDOWN_MS = override().pounceCooldownMs
private val MAX_SNEAK_MS = kitOverride.pounceMaxSneakMs private val MAX_SNEAK_MS = override().pounceMaxSneakMs
private val MIN_RANGE = kitOverride.pounceMinRange private val MIN_RANGE = override().pounceMinRange
private val MAX_RANGE = kitOverride.pounceMaxRange private val MAX_RANGE = override().pounceMaxRange
private val POUNCE_TIMEOUT_TICKS = kitOverride.pounceTimeoutTicks private val POUNCE_TIMEOUT_TICKS = override().pounceTimeoutTicks
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
@@ -171,6 +170,7 @@ class RattlesnakeKit : Kit() {
// ── Miss timeout ────────────────────────────────────────────────── // ── Miss timeout ──────────────────────────────────────────────────
Bukkit.getScheduler().runTaskLater(plugin, { -> Bukkit.getScheduler().runTaskLater(plugin, { ->
if (!pouncingPlayers.remove(player.uniqueId)) return@runTaskLater // already hit 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) player.world.getNearbyEntities(player.location, 5.0, 5.0, 5.0)
.filterIsInstance<Player>() .filterIsInstance<Player>()
@@ -179,8 +179,7 @@ class RattlesnakeKit : Kit() {
enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0)) enemy.addPotionEffect(PotionEffect(PotionEffectType.NAUSEA, 3 * 20, 0))
enemy.addPotionEffect(PotionEffect(PotionEffectType.SLOWNESS, 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) }, POUNCE_TIMEOUT_TICKS)
return AbilityResult.Success return AbilityResult.Success

View File

@@ -2,6 +2,7 @@ package club.mcscrims.speedhg.kit.impl
import club.mcscrims.speedhg.SpeedHG import club.mcscrims.speedhg.SpeedHG
import club.mcscrims.speedhg.config.CustomGameSettings import club.mcscrims.speedhg.config.CustomGameSettings
import club.mcscrims.speedhg.game.GameState
import club.mcscrims.speedhg.kit.Kit import club.mcscrims.speedhg.kit.Kit
import club.mcscrims.speedhg.kit.Playstyle import club.mcscrims.speedhg.kit.Playstyle
import club.mcscrims.speedhg.kit.ability.AbilityResult import club.mcscrims.speedhg.kit.ability.AbilityResult
@@ -83,16 +84,15 @@ class TheWorldKit : Kit() {
) )
companion object { companion object {
private val kitOverride get() = private fun override() = SpeedHG.instance.customGameManager.settings.kits.kits["theworld"]
SpeedHG.instance.customGameManager.settings.kits.kits["theworld"] ?: CustomGameSettings.KitOverride()
?: CustomGameSettings.KitOverride()
private val ABILITY_COOLDOWN_MS = kitOverride.abilityCooldownMs private val ABILITY_COOLDOWN_MS = override().abilityCooldownMs
private val SHOCKWAVE_RADIUS = kitOverride.shockwaveRadius private val SHOCKWAVE_RADIUS = override().shockwaveRadius
private val TELEPORT_RANGE = kitOverride.teleportRange private val TELEPORT_RANGE = override().teleportRange
private val MAX_TELEPORT_CHARGES = kitOverride.maxTeleportCharges private val MAX_TELEPORT_CHARGES = override().maxTeleportCharges
private val FREEZE_DURATION_TICKS = kitOverride.freezeDurationTicks private val FREEZE_DURATION_TICKS = override().freezeDurationTicks
private val MAX_HITS_ON_FROZEN = kitOverride.maxHitsOnFrozen private val MAX_HITS_ON_FROZEN = override().maxHitsOnFrozen
} }
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
@@ -225,10 +225,10 @@ class TheWorldKit : Kit() {
override fun run() { override fun run() {
ticks++ ticks++
if (ticks >= FREEZE_DURATION_TICKS || if (ticks >= FREEZE_DURATION_TICKS || !target.isOnline ||
!target.isOnline ||
!plugin.gameManager.alivePlayers.contains(target.uniqueId) || !plugin.gameManager.alivePlayers.contains(target.uniqueId) ||
!frozenEnemies.containsKey(target.uniqueId)) !frozenEnemies.containsKey(target.uniqueId) ||
plugin.gameManager.currentState == GameState.ENDING) // ← neu
{ {
doUnfreeze(target) doUnfreeze(target)
cancel() cancel()

View File

@@ -49,9 +49,10 @@ class VenomKit : Kit() {
override val icon: Material override val icon: Material
get() = Material.SPIDER_EYE get() = Material.SPIDER_EYE
private val kitOverride get() = private val kitOverride: CustomGameSettings.KitOverride by lazy {
plugin.customGameManager.settings.kits.kits["venom"] plugin.customGameManager.settings.kits.kits["venom"]
?: CustomGameSettings.KitOverride() ?: CustomGameSettings.KitOverride()
}
// ── Cached ability instances (avoid allocating per event call) ──────────── // ── Cached ability instances (avoid allocating per event call) ────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
@@ -116,6 +117,11 @@ class VenomKit : Kit() {
override fun onRemove( override fun onRemove(
player: Player player: Player
) { ) {
activeShields[ player.uniqueId ]?.let { shield ->
shield.expireTask.cancel()
shield.particleTask.cancel()
activeShields.remove( player.uniqueId )
}
val items = cachedItems.remove( player.uniqueId ) ?: return val items = cachedItems.remove( player.uniqueId ) ?: return
items.forEach { player.inventory.remove( it ) } items.forEach { player.inventory.remove( it ) }
} }
@@ -275,11 +281,11 @@ class VenomKit : Kit() {
attacker: Player, attacker: Player,
event: EntityDamageByEntityEvent event: EntityDamageByEntityEvent
) { ) {
val shield = activeShields[ victim.uniqueId ] ?: return activeShields[victim.uniqueId]?.apply {
shield.remainingCapacity -= event.damage remainingCapacity -= event.damage
val isCrit = event.isCritical event.damage = if (event.isCritical) 3.0 else 2.0
event.damage = if ( isCrit ) 3.0 else 2.0 if (remainingCapacity <= 0) breakShield(victim)
if ( shield.remainingCapacity <= 0 ) breakShield( victim ) } ?: return
} }
} }

View File

@@ -58,9 +58,10 @@ class VoodooKit : Kit() {
/** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */ /** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */
internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap() internal val cursedExpiry: MutableMap<UUID, Long> = ConcurrentHashMap()
private val kitOverride get() = private val kitOverride: CustomGameSettings.KitOverride by lazy {
plugin.customGameManager.settings.kits.kits["voodoo"] plugin.customGameManager.settings.kits.kits["voodoo"]
?: CustomGameSettings.KitOverride() ?: CustomGameSettings.KitOverride()
}
// ── Cached ability instances ────────────────────────────────────────────── // ── Cached ability instances ──────────────────────────────────────────────
private val aggressiveActive = AggressiveActive() private val aggressiveActive = AggressiveActive()
@@ -258,7 +259,8 @@ class VoodooKit : Kit() {
if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) { if (!player.isOnline || !plugin.gameManager.alivePlayers.contains(player.uniqueId)) {
cancel(); return cancel(); return
} }
tickPassive(player) runCatching { tickPassive( player ) }
.onFailure { plugin.logger.severe( "[VoodooKit] tickPassive error: ${it.message}" ) }
} }
}.runTaskTimer(plugin, 0L, 20L) }.runTaskTimer(plugin, 0L, 20L)
tasks[player.uniqueId] = task tasks[player.uniqueId] = task

View File

@@ -83,6 +83,7 @@ class KitEventDispatcher(
val victim = event.entity as? Player ?: return val victim = event.entity as? Player ?: return
if ( !isIngame() ) return if ( !isIngame() ) return
if (!isAlive( victim )) return
val attackerKit = kitManager.getSelectedKit( attacker ) ?: return val attackerKit = kitManager.getSelectedKit( attacker ) ?: return
val attackerPlaystyle = kitManager.getSelectedPlaystyle( attacker ) val attackerPlaystyle = kitManager.getSelectedPlaystyle( attacker )
@@ -114,7 +115,7 @@ class KitEventDispatcher(
*/ */
@EventHandler( @EventHandler(
priority = EventPriority.HIGH, priority = EventPriority.HIGH,
ignoreCancelled = true ignoreCancelled = false
) )
fun onInteract( fun onInteract(
event: PlayerInteractEvent event: PlayerInteractEvent
@@ -397,4 +398,11 @@ class KitEventDispatcher(
else -> false else -> false
} }
private fun isAlive(
player: Player
): Boolean
{
return plugin.gameManager.alivePlayers.contains( player.uniqueId )
}
} }

View File

@@ -1,6 +1,8 @@
package club.mcscrims.speedhg.util package club.mcscrims.speedhg.util
import net.kyori.adventure.text.Component 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.ChatColor
import org.bukkit.Material import org.bukkit.Material
import org.bukkit.enchantments.Enchantment import org.bukkit.enchantments.Enchantment
@@ -11,6 +13,8 @@ class ItemBuilder(
private val itemStack: ItemStack private val itemStack: ItemStack
) { ) {
private val mm = MiniMessage.miniMessage()
constructor( constructor(
type: Material type: Material
) : this( ) : this(
@@ -48,13 +52,10 @@ class ItemBuilder(
lore: List<String> lore: List<String>
): ItemBuilder ): ItemBuilder
{ {
itemStack.editMeta { itemStack.editMeta { meta ->
val cLore = lore.stream() meta.lore(lore.map { line ->
.map( this::color ) mm.deserialize( line ).decoration( TextDecoration.ITALIC, false )
.map( Component::text ) })
.toList()
it.lore( cLore as List<Component> )
} }
return this return this
} }