From 678d25dacd719c31db7cec2ff6099e75a99d83f8 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Mon, 14 Aug 2023 13:22:53 +0000 Subject: [PATCH] Timers can be flagged for removal on death (fixes deep wilderness threats attacking for brief period after respawn) Fixed an issue with the skill restore timer that caused overheals to be reset to normal hp --- .../wilderness/handlers/DeepWildyThreat.kt | 2 +- .../main/core/game/node/entity/Entity.java | 3 +- .../core/game/node/entity/player/Player.java | 2 - .../core/game/system/timer/PersistTimer.kt | 2 +- .../main/core/game/system/timer/RSTimer.kt | 8 +- .../main/core/game/system/timer/TimerFlag.kt | 5 + .../core/game/system/timer/TimerManager.kt | 30 ++++-- .../core/game/system/timer/impl/Disease.kt | 2 +- .../core/game/system/timer/impl/Frozen.kt | 2 +- .../game/system/timer/impl/FrozenImmunity.kt | 2 +- .../game/system/timer/impl/HealOverTime.kt | 2 +- .../core/game/system/timer/impl/Miasmic.kt | 13 ++- .../game/system/timer/impl/MiasmicImmunity.kt | 11 ++- .../core/game/system/timer/impl/Poison.kt | 2 +- .../game/system/timer/impl/PoisonImmunity.kt | 2 +- .../game/system/timer/impl/SkillRestore.kt | 2 +- .../core/game/system/timer/impl/Skulled.kt | 2 +- .../core/game/system/timer/impl/Teleblock.kt | 2 +- Server/src/test/kotlin/TestUtils.kt | 1 + Server/src/test/kotlin/core/TimerTests.kt | 99 +++++++++++++++++++ 20 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 Server/src/main/core/game/system/timer/TimerFlag.kt create mode 100644 Server/src/test/kotlin/core/TimerTests.kt diff --git a/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt index 1b0aa0d65..8bde765aa 100644 --- a/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt +++ b/Server/src/main/content/region/wilderness/handlers/DeepWildyThreat.kt @@ -99,7 +99,7 @@ class RevGuardianBehavior : NPCBehavior() { override fun tick(self: NPC): Boolean { val target = getAttribute(self, "dw-threat-target", null) ?: return true - if (!target.isActive) { + if (!target.isActive || DeathTask.isDead(target)) { self.clear() return true } diff --git a/Server/src/main/core/game/node/entity/Entity.java b/Server/src/main/core/game/node/entity/Entity.java index cd4ad0a0a..01f6ba2c7 100644 --- a/Server/src/main/core/game/node/entity/Entity.java +++ b/Server/src/main/core/game/node/entity/Entity.java @@ -33,8 +33,6 @@ import core.game.system.timer.TimerRegistry; import java.util.*; -import static core.api.ContentAPIKt.isStunned; - /** * An entity is a movable node, such as players and NPCs. * @author Emperor @@ -278,6 +276,7 @@ public abstract class Entity extends Node { skills.rechargePrayerPoints(); impactHandler.getImpactQueue().clear(); impactHandler.setDisabledTicks(10); + timers.onEntityDeath(); removeAttribute("combat-time"); face(null); //Check if it's a Loar shade and transform back into the shadow version. diff --git a/Server/src/main/core/game/node/entity/player/Player.java b/Server/src/main/core/game/node/entity/player/Player.java index c0d498e8f..53c59f883 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -700,8 +700,6 @@ public class Player extends Entity { getPrayer().reset(); super.finalizeDeath(killer); appearance.sync(); - timers.removeTimer("poison"); - timers.removeTimer("poison:immunity"); if (!getSavedData().getGlobalData().isDeathScreenDisabled()) { getInterfaceManager().open(new Component(153)); } diff --git a/Server/src/main/core/game/system/timer/PersistTimer.kt b/Server/src/main/core/game/system/timer/PersistTimer.kt index 784ca04fb..977c57438 100644 --- a/Server/src/main/core/game/system/timer/PersistTimer.kt +++ b/Server/src/main/core/game/system/timer/PersistTimer.kt @@ -8,7 +8,7 @@ import kotlin.reflect.full.createInstance /** * A timer implementation with support for saving and loading arbitrary data. See `RSTimer` for more info on timers themselves. **/ -abstract class PersistTimer (runInterval: Int, identifier: String, isSoft: Boolean = false, isAuto: Boolean = false) : RSTimer (runInterval, identifier, isSoft, isAuto) { +abstract class PersistTimer (runInterval: Int, identifier: String, isSoft: Boolean = false, isAuto: Boolean = false, flags: Array = arrayOf()) : RSTimer (runInterval, identifier, isSoft, isAuto, flags) { open fun save (root: JSONObject, entity: Entity) { root["ticksLeft"] = (nextExecution - getWorldTicks()).toString() } diff --git a/Server/src/main/core/game/system/timer/RSTimer.kt b/Server/src/main/core/game/system/timer/RSTimer.kt index e1fb6b4c7..a995bac75 100644 --- a/Server/src/main/core/game/system/timer/RSTimer.kt +++ b/Server/src/main/core/game/system/timer/RSTimer.kt @@ -9,7 +9,8 @@ import kotlin.reflect.full.createInstance * default PersistTimer behavior, which automatically starts the timer only if there's saved data for that timer present. In truth, there's very few * timers that should have isAuto true. **/ -abstract class RSTimer (var runInterval: Int, val identifier: String = "generictimer", val isSoft: Boolean = false, val isAuto: Boolean = false) { +abstract class RSTimer (var runInterval: Int, val identifier: String = "generictimer", val isSoft: Boolean = false, val isAuto: Boolean = false, val flags: Array = arrayOf()) { + /** * Executed every time the run interval of the timer elapses. * Execution will be delayed if this timer has `isSoft` set to false (which 99% of timers should) if the entity has a modal open or is otherwise stalled. @@ -28,6 +29,11 @@ abstract class RSTimer (var runInterval: Int, val identifier: String = "generict **/ open fun onRegister (entity: Entity) {} + /** + * Called by core code when the timer is being removed. + */ + open fun onRemoval (entity: Entity) {} + var lastExecution: Int = 0 var nextExecution: Int = 0 diff --git a/Server/src/main/core/game/system/timer/TimerFlag.kt b/Server/src/main/core/game/system/timer/TimerFlag.kt new file mode 100644 index 000000000..dd56d225f --- /dev/null +++ b/Server/src/main/core/game/system/timer/TimerFlag.kt @@ -0,0 +1,5 @@ +package core.game.system.timer + +enum class TimerFlag { + ClearOnDeath +} \ No newline at end of file diff --git a/Server/src/main/core/game/system/timer/TimerManager.kt b/Server/src/main/core/game/system/timer/TimerManager.kt index 81b981345..fd63b7ec5 100644 --- a/Server/src/main/core/game/system/timer/TimerManager.kt +++ b/Server/src/main/core/game/system/timer/TimerManager.kt @@ -27,11 +27,14 @@ class TimerManager (val entity: Entity) { if (timer.nextExecution > getWorldTicks()) continue if (!canRunNormalTimers && !timer.isSoft) continue - if (timer.run(entity)) { - timer.nextExecution = getWorldTicks() + timer.runInterval - } else { - timer.nextExecution = Int.MAX_VALUE - toRemoveTimers.add(timer) + try { + if (timer.run(entity)) { + timer.nextExecution = getWorldTicks() + timer.runInterval + } else removeTimer(timer) + } catch (e: Exception) { + log (this::class.java, Log.ERR, "Prematurely removing timer ${timer::class.java.simpleName} from ${entity.name} because it threw an exception when ran. Exception follows:") + e.printStackTrace() + removeTimer(timer) } } @@ -49,6 +52,13 @@ class TimerManager (val entity: Entity) { toRemoveTimers.clear() } + fun onEntityDeath() { + for (timer in activeTimers) { + if (timer.flags.contains(TimerFlag.ClearOnDeath)) + removeTimer(timer) + } + } + fun saveTimers (root: JSONObject) { for (timer in activeTimers) { if (timer !is PersistTimer) continue @@ -82,10 +92,10 @@ class TimerManager (val entity: Entity) { inline fun removeTimer () { for (timer in activeTimers) if (timer is T) - toRemoveTimers.add(timer) + removeTimer(timer) for (timer in newTimers) if (timer is T) - toRemoveTimers.add(timer) + removeTimer(timer) } inline fun getTimer () : T? { @@ -118,13 +128,15 @@ class TimerManager (val entity: Entity) { fun removeTimer (identifier: String) { for (timer in activeTimers) if (timer.identifier == identifier) - toRemoveTimers.add(timer) + removeTimer(timer) for (timer in newTimers) if (timer.identifier == identifier) - toRemoveTimers.add(timer) + removeTimer(timer) } fun removeTimer (timer: RSTimer) { + timer.nextExecution = Int.MAX_VALUE toRemoveTimers.add(timer) + try { timer.onRemoval(entity) } catch (e: Exception) { e.printStackTrace() } } } diff --git a/Server/src/main/core/game/system/timer/impl/Disease.kt b/Server/src/main/core/game/system/timer/impl/Disease.kt index 8139ee36e..7d270e0a9 100644 --- a/Server/src/main/core/game/system/timer/impl/Disease.kt +++ b/Server/src/main/core/game/system/timer/impl/Disease.kt @@ -9,7 +9,7 @@ import core.game.node.entity.combat.ImpactHandler import core.tools.RandomFunction import org.json.simple.* -class Disease : PersistTimer (30, "disease") { +class Disease : PersistTimer (30, "disease", flags = arrayOf(TimerFlag.ClearOnDeath)) { var hitsLeft = 25 override fun save (root: JSONObject, entity: Entity) { diff --git a/Server/src/main/core/game/system/timer/impl/Frozen.kt b/Server/src/main/core/game/system/timer/impl/Frozen.kt index ed99a9253..94c5456a4 100644 --- a/Server/src/main/core/game/system/timer/impl/Frozen.kt +++ b/Server/src/main/core/game/system/timer/impl/Frozen.kt @@ -7,7 +7,7 @@ import core.game.node.entity.player.Player import core.game.world.repository.Repository import org.json.simple.* -class Frozen : PersistTimer (1, "frozen") { +class Frozen : PersistTimer (1, "frozen", flags = arrayOf(TimerFlag.ClearOnDeath)) { var shouldApplyImmunity = false override fun save (root: JSONObject, entity: Entity) { diff --git a/Server/src/main/core/game/system/timer/impl/FrozenImmunity.kt b/Server/src/main/core/game/system/timer/impl/FrozenImmunity.kt index f8ca86328..250dbfe28 100644 --- a/Server/src/main/core/game/system/timer/impl/FrozenImmunity.kt +++ b/Server/src/main/core/game/system/timer/impl/FrozenImmunity.kt @@ -7,7 +7,7 @@ import core.game.node.entity.player.Player import core.game.world.repository.Repository import org.json.simple.* -class FrozenImmunity : PersistTimer (1, "frozen:immunity") { +class FrozenImmunity : PersistTimer (1, "frozen:immunity", flags = arrayOf(TimerFlag.ClearOnDeath)) { var ticksRemaining = 0 override fun save (root: JSONObject, entity: Entity) { diff --git a/Server/src/main/core/game/system/timer/impl/HealOverTime.kt b/Server/src/main/core/game/system/timer/impl/HealOverTime.kt index 8291857e7..da5016e86 100644 --- a/Server/src/main/core/game/system/timer/impl/HealOverTime.kt +++ b/Server/src/main/core/game/system/timer/impl/HealOverTime.kt @@ -7,7 +7,7 @@ import core.game.node.entity.player.Player import org.json.simple.* import kotlin.math.min -class HealOverTime : PersistTimer (1, "healovertime") { +class HealOverTime : PersistTimer (1, "healovertime", flags = arrayOf(TimerFlag.ClearOnDeath)) { var healRemaining = 0 var healPerTick = 0 diff --git a/Server/src/main/core/game/system/timer/impl/Miasmic.kt b/Server/src/main/core/game/system/timer/impl/Miasmic.kt index e9fff66b3..d9c1eca18 100644 --- a/Server/src/main/core/game/system/timer/impl/Miasmic.kt +++ b/Server/src/main/core/game/system/timer/impl/Miasmic.kt @@ -1,12 +1,15 @@ package core.game.system.timer.impl -import core.game.system.timer.* -import core.api.* +import core.api.hasTimerActive +import core.api.registerTimer +import core.api.removeTimer +import core.api.spawnTimer import core.game.node.entity.Entity -import core.game.node.entity.player.Player -import org.json.simple.* +import core.game.system.timer.PersistTimer +import core.game.system.timer.RSTimer +import core.game.system.timer.TimerFlag -class Miasmic : PersistTimer (1, "miasmic") { +class Miasmic : PersistTimer (1, "miasmic", flags = arrayOf(TimerFlag.ClearOnDeath)) { override fun run (entity: Entity) : Boolean { registerTimer (entity, spawnTimer(entity, 7)) return false diff --git a/Server/src/main/core/game/system/timer/impl/MiasmicImmunity.kt b/Server/src/main/core/game/system/timer/impl/MiasmicImmunity.kt index 52a87f119..2ce41cb34 100644 --- a/Server/src/main/core/game/system/timer/impl/MiasmicImmunity.kt +++ b/Server/src/main/core/game/system/timer/impl/MiasmicImmunity.kt @@ -1,12 +1,13 @@ package core.game.system.timer.impl -import core.game.system.timer.* -import core.api.* +import core.api.hasTimerActive +import core.api.removeTimer import core.game.node.entity.Entity -import core.game.node.entity.player.Player -import org.json.simple.* +import core.game.system.timer.PersistTimer +import core.game.system.timer.RSTimer +import core.game.system.timer.TimerFlag -class MiasmicImmunity : PersistTimer (1, "miasmic:immunity") { +class MiasmicImmunity : PersistTimer (1, "miasmic:immunity", flags = arrayOf(TimerFlag.ClearOnDeath)) { override fun run (entity: Entity) : Boolean { return false } diff --git a/Server/src/main/core/game/system/timer/impl/Poison.kt b/Server/src/main/core/game/system/timer/impl/Poison.kt index 73384a411..5465d55d8 100644 --- a/Server/src/main/core/game/system/timer/impl/Poison.kt +++ b/Server/src/main/core/game/system/timer/impl/Poison.kt @@ -16,7 +16,7 @@ import org.json.simple.* * Every time the damage is applied, the severity decreases by 1. Poison ends when severity reaches 0. * Example: 30 Severity. Deals 6 damage 5 times, then 5 damage 5 times, and so on. **/ -class Poison : PersistTimer (30, "poison") { +class Poison : PersistTimer (30, "poison", flags = arrayOf(TimerFlag.ClearOnDeath)) { lateinit var damageSource: Entity var severity = 0 diff --git a/Server/src/main/core/game/system/timer/impl/PoisonImmunity.kt b/Server/src/main/core/game/system/timer/impl/PoisonImmunity.kt index c3810def2..9827728a4 100644 --- a/Server/src/main/core/game/system/timer/impl/PoisonImmunity.kt +++ b/Server/src/main/core/game/system/timer/impl/PoisonImmunity.kt @@ -15,7 +15,7 @@ import org.json.simple.* * Will notify the player of various levels of remaining poison immunity, and then remove itself once it has run out. * This timer is a "soft" timer, meaning it will tick down even while other timers would normally stall (e.g. during entity delays or when the entity has a modal open.) **/ -class PoisonImmunity : PersistTimer (1, "poison:immunity", isSoft = true) { +class PoisonImmunity : PersistTimer (1, "poison:immunity", isSoft = true, flags = arrayOf(TimerFlag.ClearOnDeath)) { var ticksRemaining = 0 override fun save (root: JSONObject, entity: Entity) { diff --git a/Server/src/main/core/game/system/timer/impl/SkillRestore.kt b/Server/src/main/core/game/system/timer/impl/SkillRestore.kt index 092ae905b..81c7e434c 100644 --- a/Server/src/main/core/game/system/timer/impl/SkillRestore.kt +++ b/Server/src/main/core/game/system/timer/impl/SkillRestore.kt @@ -21,7 +21,7 @@ class SkillRestore : RSTimer (1, "skillrestore", isAuto = true, isSoft = true) { for (i in 0 until 24) { if (i == Skills.PRAYER) continue if (ticksSinceLastRestore[i]++ >= restoreTicks[i]) { - if (i == Skills.HITPOINTS) { + if (i == Skills.HITPOINTS && entity.skills.lifepoints < entity.skills.maximumLifepoints) { skills.heal (getHealAmount(entity)) } else { val max = getStatLevel (entity, i) diff --git a/Server/src/main/core/game/system/timer/impl/Skulled.kt b/Server/src/main/core/game/system/timer/impl/Skulled.kt index 12a51a761..9723a4287 100644 --- a/Server/src/main/core/game/system/timer/impl/Skulled.kt +++ b/Server/src/main/core/game/system/timer/impl/Skulled.kt @@ -6,7 +6,7 @@ import core.game.node.entity.Entity import core.game.node.entity.player.Player import org.json.simple.* -class Skulled : PersistTimer (1, "skulled") { +class Skulled : PersistTimer (1, "skulled", flags = arrayOf(TimerFlag.ClearOnDeath)) { override fun onRegister (entity: Entity) { if (entity !is Player) return entity.skullManager.setSkullIcon(0) diff --git a/Server/src/main/core/game/system/timer/impl/Teleblock.kt b/Server/src/main/core/game/system/timer/impl/Teleblock.kt index 9caa5c4b4..2a472f188 100644 --- a/Server/src/main/core/game/system/timer/impl/Teleblock.kt +++ b/Server/src/main/core/game/system/timer/impl/Teleblock.kt @@ -6,7 +6,7 @@ import core.game.node.entity.Entity import core.game.node.entity.player.Player import org.json.simple.* -class Teleblock : PersistTimer (1, "teleblock") { +class Teleblock : PersistTimer (1, "teleblock", flags = arrayOf(TimerFlag.ClearOnDeath)) { override fun run (entity: Entity) : Boolean { return false } diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index 42e51535b..aca7838ed 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -43,6 +43,7 @@ import java.nio.ByteBuffer object TestUtils { var uidCounter = 0 + const val PLAYER_DEATH_TICKS = 14 fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE, rights: Rights = Rights.ADMINISTRATOR): MockPlayer { val p = MockPlayer(name) diff --git a/Server/src/test/kotlin/core/TimerTests.kt b/Server/src/test/kotlin/core/TimerTests.kt new file mode 100644 index 000000000..348439166 --- /dev/null +++ b/Server/src/test/kotlin/core/TimerTests.kt @@ -0,0 +1,99 @@ +package core + +import TestUtils +import core.api.* +import core.game.node.entity.Entity +import core.game.node.entity.skill.Skills +import core.game.system.timer.RSTimer +import core.game.system.timer.TimerFlag +import core.game.system.timer.impl.SkillRestore +import core.tools.Log +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class TimerTests { + init { TestUtils.preTestSetup() } + + @Test fun timerWithNoFlagsShouldNotBeClearedOnDeath() { + TestUtils.getMockPlayer("noflagnoclear").use { p -> + var incrementer = 0 + val timer = object : RSTimer(1) { + override fun run(entity: Entity): Boolean { + incrementer++ + return true + } + } + + registerTimer(p, timer) + impact(p, p.skills.lifepoints) + TestUtils.advanceTicks(TestUtils.PLAYER_DEATH_TICKS, false) + closeInterface(p) //close the interface that opens after death - as it would pause the timer + + TestUtils.advanceTicks(18, false) + Assertions.assertEquals(20, incrementer) + } + } + + @Test fun timerWithClearOnDeathFlagShouldClearOnDeath() { + TestUtils.getMockPlayer("clearflagtimer").use { p -> + var incrementer = 0 + val timer = object : RSTimer(1, flags = arrayOf(TimerFlag.ClearOnDeath)) { + override fun run(entity: Entity): Boolean { + incrementer++ + return true + } + } + + registerTimer(p, timer) + impact(p, p.skills.lifepoints) + TestUtils.advanceTicks(TestUtils.PLAYER_DEATH_TICKS, false) + closeInterface(p) //close the interface that opens after death - as it would pause the timer + + TestUtils.advanceTicks(18, false) + Assertions.assertEquals(2, incrementer) + } + } + + @Test fun skillRestoreTimerShouldSlowlyRaiseLoweredStats() { + TestUtils.getMockPlayer("statrestore-slowrestore").use { p -> + val timer = SkillRestore() + registerTimer(p, timer) + p.skills.staticLevels[Skills.FARMING] = 20 + setTempLevel(p, Skills.FARMING, 10) + + TestUtils.advanceTicks(timer.restoreTicks[Skills.FARMING] + 3, false) + Assertions.assertEquals(11, getDynLevel(p, Skills.FARMING)) + } + } + @Test fun skillRestoreTimerShouldSlowlyLowerBoostedStats() { + TestUtils.getMockPlayer("statrestore-slowdrain").use { p -> + val timer = SkillRestore() + p.timers.registerTimer(timer) + setTempLevel(p, Skills.FARMING, 6) + + TestUtils.advanceTicks(timer.restoreTicks[Skills.FARMING] + 3, false) + Assertions.assertEquals(5, getDynLevel(p, Skills.FARMING)) + } + } + + @Test fun skillRestoreTimerShouldRaiseLoweredHp() { + TestUtils.getMockPlayer("statrestore-raiseloweredhp").use { p -> + val timer = SkillRestore() + registerTimer(p, timer) + p.skills.lifepoints /= 2 + TestUtils.advanceTicks(600, false) + Assertions.assertEquals(10, p.skills.lifepoints) + } + } + + @Test fun skillRestoreTimerShouldNeverLowerBoostedHp() { + TestUtils.getMockPlayer("statrestore-neverlowerhp").use { p -> + val timer = SkillRestore() + registerTimer(p, timer) + p.skills.lifepoints = 50 + + TestUtils.advanceTicks(500, false) + Assertions.assertEquals(50, p.skills.lifepoints) + } + } +} \ No newline at end of file