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