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
}
Bukkit.getOnlinePlayers().stream()
Bukkit.getOnlinePlayers()
.filter { plugin.gameManager.alivePlayers.contains( it.uniqueId ) }
.forEach {
val recraft = Recraft()

View File

@@ -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

View File

@@ -62,18 +62,17 @@ class BlackPantherKit : Kit()
companion object
{
private val kitOverride get() =
SpeedHG.instance.customGameManager.settings.kits.kits["blackpanther"]
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 ──────────────────────────────────────────────

View File

@@ -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 )

View File

@@ -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 )

View File

@@ -57,15 +57,14 @@ class RattlesnakeKit : Kit() {
internal val lastPounceUse: MutableMap<UUID, Long> = ConcurrentHashMap()
companion object {
private val kitOverride get() =
SpeedHG.instance.customGameManager.settings.kits.kits["rattlesnake"]
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<Player>()
@@ -179,7 +179,6 @@ 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"))
}, POUNCE_TIMEOUT_TICKS)

View File

@@ -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"]
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()

View File

@@ -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
}
}

View File

@@ -58,9 +58,10 @@ class VoodooKit : Kit() {
/** Tracks active curses: victim UUID → System.currentTimeMillis() expiry. */
internal val cursedExpiry: MutableMap<UUID, Long> = 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

View File

@@ -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 )
}
}

View File

@@ -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<String>
): ItemBuilder
{
itemStack.editMeta {
val cLore = lore.stream()
.map( this::color )
.map( Component::text )
.toList()
it.lore( cLore as List<Component> )
itemStack.editMeta { meta ->
meta.lore(lore.map { line ->
mm.deserialize( line ).decoration( TextDecoration.ITALIC, false )
})
}
return this
}