From a3e6df109d1cae5a20b940d741959cb92b58c048 Mon Sep 17 00:00:00 2001 From: Ceikry Date: Mon, 14 Aug 2023 12:56:35 +0000 Subject: [PATCH] Random event manager rewrite Time until next random event now persists across logins Events will no longer spawn while the player has an interface like the bank open Replaced the slurry of random event commands with a singular ::revent admin command Random events now support being associated with multiple skills Random event selection is now more robust Players will no longer get skill-based random events if more than 2.5 minutes have passed since training the skill --- .../content/global/ame/RandomEventManager.kt | 94 -------- .../main/content/global/ame/RandomEventNPC.kt | 1 - .../main/content/global/ame/RandomEvents.kt | 23 +- .../ame/events/MysteriousOldManDialogue.kt | 5 +- .../ame/events/certer/CerterDialogue.kt | 7 +- .../ame/events/certer/CerterEventInterface.kt | 4 +- .../drilldemon/SeargentDamienDialogue.kt | 10 +- .../global/ame/events/evilbob/EvilBobNPC.kt | 3 +- .../freakyforester/FreakyForesterNPC.kt | 4 +- .../global/ame/events/genie/GenieDialogue.kt | 3 +- .../sandwichlady/SandwichLadyDialogue.kt | 5 +- .../sandwichlady/SandwichLadyInterface.kt | 4 +- .../global/handlers/npc/NPCTalkListener.kt | 5 +- .../src/main/core/game/activity/Cutscene.kt | 5 +- .../main/core/game/node/entity/Entity.java | 2 +- .../core/game/node/entity/player/Player.java | 4 +- .../player/info/login/PlayerSaveParser.kt | 60 ++--- .../entity/player/info/login/PlayerSaver.kt | 2 +- .../core/game/node/entity/skill/Skills.java | 7 +- .../system/command/sets/MiscCommandSet.kt | 20 -- .../core/game/system/timer/TimerManager.kt | 6 + .../core/game/system/timer/TimerRegistry.kt | 1 - .../core/game/system/timer/impl/AntiMacro.kt | 174 ++++++++++++++ Server/src/test/kotlin/TestUtils.kt | 76 ++++-- .../kotlin/content/RandomEventManagerTests.kt | 81 ------- .../test/kotlin/content/RandomEventTests.kt | 217 ++++++++++++++++++ 26 files changed, 538 insertions(+), 285 deletions(-) delete mode 100644 Server/src/main/content/global/ame/RandomEventManager.kt create mode 100644 Server/src/main/core/game/system/timer/impl/AntiMacro.kt delete mode 100644 Server/src/test/kotlin/content/RandomEventManagerTests.kt create mode 100644 Server/src/test/kotlin/content/RandomEventTests.kt diff --git a/Server/src/main/content/global/ame/RandomEventManager.kt b/Server/src/main/content/global/ame/RandomEventManager.kt deleted file mode 100644 index 5054b110d..000000000 --- a/Server/src/main/content/global/ame/RandomEventManager.kt +++ /dev/null @@ -1,94 +0,0 @@ -package content.global.ame - -import core.api.* -import core.game.event.EventHook -import core.game.event.TickEvent -import core.game.node.entity.Entity -import core.game.node.entity.player.Player -import core.game.world.map.zone.ZoneRestriction -import core.tools.RandomFunction -import core.tools.SystemLogger -import core.game.system.command.Privilege -import core.game.world.GameWorld -import core.game.world.repository.Repository -import core.tools.Log -import kotlin.random.Random -import content.global.ame.RandomEvents - -class RandomEventManager(val player: Player? = null) : LoginListener, EventHook, Commands { - var event: content.global.ame.RandomEventNPC? = null - var enabled: Boolean = false - var nextSpawn = 0 - val skills = arrayOf("WoodcuttingSkillPulse","FishingPulse","MiningSkillPulse","BoneBuryingOptionPlugin") - - override fun login(player: Player) { - if(player.isArtificial) return - val instance = RandomEventManager(player) - player.hook(Event.Tick, instance) - setAttribute(player, "random-manager", instance) - instance.rollNextSpawn() - instance.enabled = true - } - - override fun process(entity: Entity, event: TickEvent) { - if (GameWorld.ticks > nextSpawn && getAttribute(player!!, "tutorial:complete", false)) fireEvent() - } - - fun fireEvent() { - if(!enabled){ - rollNextSpawn() - return - } - if (player!!.zoneMonitor.isRestricted(ZoneRestriction.RANDOM_EVENTS)) { - rollNextSpawn() - return - } - if (getAttribute(player, "re-npc", null) != null) return - val currentAction = player.pulseManager.current?.toString() ?: "None" - - //Give us a decent pool of events to pick from randomly, with the skill-based random event mixed in if applicable. - //This is just to provide a bit more randomization (you don't want to get the skill-based randoms EVERY TIME when training woodcutting, for example.) - val ame = listOf ( - RandomEvents.getSkillBasedRandomEvent (player.skills.lastTrainedSkill), - RandomEvents.getNonSkillRandom(), - RandomEvents.getNonSkillRandom() - ).filter{ it != null }.random() ?: RandomEvents.getNonSkillRandom() //Absolute cosmic bit-flip tier safety fallback - - event = ame.npc.create(player,ame.loot,ame.type) - if (event!!.spawnLocation == null) { - nextSpawn = GameWorld.ticks + 3000 - log(this::class.java, Log.WARN, "Tried to spawn random event for ${player.username} but spawn location was null!") - return - } - event!!.init() - rollNextSpawn() - log(this::class.java, Log.FINE, "Fired ${event!!.name} for ${player.username}") - } - - private fun rollNextSpawn() { - nextSpawn = GameWorld.ticks + RandomFunction.random(MIN_DELAY_TICKS, MAX_DELAY_TICKS) - } - - override fun defineCommands() { - define("targeted-ame", Privilege.ADMIN, "targeted-ame username", "Summons a random for the given user") {player, args -> - val username = args[1] - val target = Repository.getPlayerByName(username) - - if (target == null) - reject(player, "Unable to find player $username!") - - getInstance(target!!)?.fireEvent() - } - } - - companion object { - var AVG_DELAY_TICKS = 6000 // 60 minutes - var MIN_DELAY_TICKS = AVG_DELAY_TICKS / 2 - var MAX_DELAY_TICKS = MIN_DELAY_TICKS + AVG_DELAY_TICKS // window of 60 min centered on 60 min (30 to 90 min) - - @JvmStatic fun getInstance(player: Player): RandomEventManager? - { - return getAttribute(player, "random-manager", null) - } - } -} diff --git a/Server/src/main/content/global/ame/RandomEventNPC.kt b/Server/src/main/content/global/ame/RandomEventNPC.kt index 4503948f5..cbcf794ce 100644 --- a/Server/src/main/content/global/ame/RandomEventNPC.kt +++ b/Server/src/main/content/global/ame/RandomEventNPC.kt @@ -39,7 +39,6 @@ abstract class RandomEventNPC(id: Int) : NPC(id) { open fun terminate() { finalized = true pulseManager.clear(PulseType.STANDARD) - content.global.ame.RandomEventManager.getInstance(player)?.event = null if (initialized) { poofClear(this) } diff --git a/Server/src/main/content/global/ame/RandomEvents.kt b/Server/src/main/content/global/ame/RandomEvents.kt index 32212d157..ec7066858 100644 --- a/Server/src/main/content/global/ame/RandomEvents.kt +++ b/Server/src/main/content/global/ame/RandomEvents.kt @@ -19,7 +19,7 @@ import core.api.utils.WeightBasedTable import core.api.utils.WeightedItem import core.game.node.entity.skill.Skills -enum class RandomEvents(val npc: RandomEventNPC, val loot: WeightBasedTable? = null, val skillId: Int = -1, val type: String = "") { +enum class RandomEvents(val npc: RandomEventNPC, val loot: WeightBasedTable? = null, val skillIds: IntArray = intArrayOf(), val type: String = "") { SANDWICH_LADY(npc = SandwichLadyRENPC()), GENIE(npc = GenieNPC()), CERTER(npc = CerterNPC(), loot = WeightBasedTable.create( @@ -41,14 +41,14 @@ enum class RandomEvents(val npc: RandomEventNPC, val loot: WeightBasedTable? = n )), DRILL_DEMON(npc = SeargentDamienNPC()), EVIL_CHICKEN(npc = EvilChickenNPC()), - EVIL_BOB(npc = EvilBobNPC(), skillId = Skills.FISHING), + EVIL_BOB(npc = EvilBobNPC(), skillIds = intArrayOf(Skills.FISHING, Skills.MAGIC)), SURPRISE_EXAM(npc = MysteriousOldManNPC(), type = "sexam"), - FREAKY_FORESTER(npc = FreakyForesterNPC(), skillId = Skills.WOODCUTTING), - TREE_SPIRIT(npc = TreeSpiritRENPC(), skillId = Skills.WOODCUTTING), - RIVER_TROLL(RiverTrollRENPC(), skillId = Skills.FISHING), - ROCK_GOLEM(RockGolemRENPC(), skillId = Skills.MINING), - SHADE(ShadeRENPC(), skillId = Skills.PRAYER), - ZOMBIE(ZombieRENPC(), skillId = Skills.PRAYER); + FREAKY_FORESTER(npc = FreakyForesterNPC(), skillIds = intArrayOf(Skills.WOODCUTTING)), + TREE_SPIRIT(npc = TreeSpiritRENPC(), skillIds = intArrayOf(Skills.WOODCUTTING)), + RIVER_TROLL(RiverTrollRENPC(), skillIds = intArrayOf(Skills.FISHING)), + ROCK_GOLEM(RockGolemRENPC(), skillIds = intArrayOf(Skills.MINING)), + SHADE(ShadeRENPC(), skillIds = intArrayOf(Skills.PRAYER)), + ZOMBIE(ZombieRENPC(), skillIds = intArrayOf(Skills.PRAYER)); companion object { @JvmField @@ -68,11 +68,12 @@ enum class RandomEvents(val npc: RandomEventNPC, val loot: WeightBasedTable? = n private fun populateMappings() { for (event in values()) { - if (event.skillId != -1) { - val list = skillMap[event.skillId] ?: ArrayList().also { skillMap[event.skillId] = it } + for (id in event.skillIds) { + val list = skillMap[id] ?: ArrayList().also { skillMap[id] = it } list.add (event) } - else nonSkillList.add (event) + if (event.skillIds.isEmpty()) + nonSkillList.add (event) } } } diff --git a/Server/src/main/content/global/ame/events/MysteriousOldManDialogue.kt b/Server/src/main/content/global/ame/events/MysteriousOldManDialogue.kt index 5366ee17b..628a3c8f0 100644 --- a/Server/src/main/content/global/ame/events/MysteriousOldManDialogue.kt +++ b/Server/src/main/content/global/ame/events/MysteriousOldManDialogue.kt @@ -3,6 +3,7 @@ package content.global.ame.events import core.game.node.entity.player.Player import content.global.ame.events.supriseexam.SurpriseExamUtils import core.game.dialogue.DialogueFile +import core.game.system.timer.impl.AntiMacro class MysteriousOldManDialogue(val type: String) : DialogueFile() { @@ -22,11 +23,11 @@ class MysteriousOldManDialogue(val type: String) : DialogueFile() { 1 -> { end() teleport(player!!,type) - content.global.ame.RandomEventManager.getInstance(player!!)?.event?.terminate() + AntiMacro.terminateEventNpc(player!!) } 2 -> { end() - content.global.ame.RandomEventManager.getInstance(player!!)?.event?.terminate() + AntiMacro.terminateEventNpc(player!!) } } } diff --git a/Server/src/main/content/global/ame/events/certer/CerterDialogue.kt b/Server/src/main/content/global/ame/events/certer/CerterDialogue.kt index 25ac73fdf..27f72a317 100644 --- a/Server/src/main/content/global/ame/events/certer/CerterDialogue.kt +++ b/Server/src/main/content/global/ame/events/certer/CerterDialogue.kt @@ -5,6 +5,7 @@ import core.game.component.Component import core.game.node.entity.impl.PulseType import core.game.node.entity.player.link.emote.Emotes import core.game.dialogue.DialogueFile +import core.game.system.timer.impl.AntiMacro import core.tools.END_DIALOGUE class CerterDialogue(val initial: Boolean) : DialogueFile() { @@ -30,13 +31,13 @@ class CerterDialogue(val initial: Boolean) : DialogueFile() { npc("Sorry, I don't think so.").also { player!!.setAttribute("certer:reward", true) stage = END_DIALOGUE - content.global.ame.RandomEventManager.getInstance(player!!)?.event?.terminate() + AntiMacro.terminateEventNpc(player!!) } } else { npc("Thank you, I hope you like your present. I must be", "leaving now though.").also { player!!.setAttribute("certer:reward", true) stage = END_DIALOGUE - content.global.ame.RandomEventManager.getInstance(player!!)!!.event!!.loot!!.roll().forEach { addItemOrDrop(player!!, it.id, it.amount) } + AntiMacro.rollEventLoot(player!!).forEach { addItemOrDrop(player!!, it.id, it.amount) } } } } @@ -51,7 +52,7 @@ class CerterDialogue(val initial: Boolean) : DialogueFile() { // Wave goodbye npc!!.animate(Emotes.WAVE.animation) // Terminate the event - content.global.ame.RandomEventManager.getInstance(player!!)?.event?.terminate() + AntiMacro.terminateEventNpc(player!!) } else { player!!.setAttribute("random:pause", false) } diff --git a/Server/src/main/content/global/ame/events/certer/CerterEventInterface.kt b/Server/src/main/content/global/ame/events/certer/CerterEventInterface.kt index e64cf3403..96d8f313e 100644 --- a/Server/src/main/content/global/ame/events/certer/CerterEventInterface.kt +++ b/Server/src/main/content/global/ame/events/certer/CerterEventInterface.kt @@ -1,10 +1,10 @@ package content.global.ame.events.certer -import content.global.ame.RandomEventManager import core.game.node.entity.player.Player import core.game.node.item.Item import org.rs09.consts.Items import core.game.interaction.InterfaceListener +import core.game.system.timer.impl.AntiMacro class CerterEventInterface : InterfaceListener { val CERTER_INTERFACE = 184 @@ -37,7 +37,7 @@ class CerterEventInterface : InterfaceListener { val correctAnswer = player.getAttribute("certer:correctIndex", 0) player.setAttribute("certer:correct", correctAnswer == answer) player.interfaceManager.close() - player.dialogueInterpreter.open(CerterDialogue(false), RandomEventManager.getInstance(player)!!.event?.asNpc()) + player.dialogueInterpreter.open(CerterDialogue(false), AntiMacro.getEventNpc(player)) return@on true } diff --git a/Server/src/main/content/global/ame/events/drilldemon/SeargentDamienDialogue.kt b/Server/src/main/content/global/ame/events/drilldemon/SeargentDamienDialogue.kt index 6ab770f21..93e529fbf 100644 --- a/Server/src/main/content/global/ame/events/drilldemon/SeargentDamienDialogue.kt +++ b/Server/src/main/content/global/ame/events/drilldemon/SeargentDamienDialogue.kt @@ -1,15 +1,15 @@ package content.global.ame.events.drilldemon -import core.game.dialogue.FacialExpression import core.game.system.task.Pulse import core.game.dialogue.DialogueFile +import core.game.system.timer.impl.AntiMacro import core.tools.END_DIALOGUE import core.tools.START_DIALOGUE class SeargentDamienDialogue(val isCorrect: Boolean = false) : DialogueFile() { override fun handle(componentID: Int, buttonID: Int) { var correctAmt = player!!.getAttribute(DrillDemonUtils.DD_CORRECT_COUNTER,0) - if(correctAmt == 4 && content.global.ame.RandomEventManager.getInstance(player!!)!!.event == null) { + if(correctAmt == 4 && AntiMacro.getEventNpc(player!!) == null) { when(stage){ 0 -> npc(core.game.dialogue.FacialExpression.OLD_NORMAL,"My god you actually did it, you limp","wristed worm-bodied MAGGOT! Take this","and get out of my sight.").also { stage++ } 1 -> { @@ -24,7 +24,7 @@ class SeargentDamienDialogue(val isCorrect: Boolean = false) : DialogueFile() { }) } } - } else if(content.global.ame.RandomEventManager.getInstance(player!!)!!.event == null){ + } else if(AntiMacro.getEventNpc(player!!) == null){ when(stage){ START_DIALOGUE -> if(isCorrect) npc(core.game.dialogue.FacialExpression.OLD_NORMAL,"Good! Now...").also { stage++ } else npc(core.game.dialogue.FacialExpression.OLD_ANGRY1,"WRONG, MAGGOT!").also { stage++ } 1 -> { @@ -40,12 +40,12 @@ class SeargentDamienDialogue(val isCorrect: Boolean = false) : DialogueFile() { 1 -> { end() DrillDemonUtils.teleport(player!!) - content.global.ame.RandomEventManager.getInstance(player!!)!!.event?.terminate() + AntiMacro.terminateEventNpc(player!!) stage = END_DIALOGUE } 2 -> { end() - content.global.ame.RandomEventManager.getInstance(player!!)!!.event?.terminate() + AntiMacro.terminateEventNpc(player!!) stage = END_DIALOGUE } } diff --git a/Server/src/main/content/global/ame/events/evilbob/EvilBobNPC.kt b/Server/src/main/content/global/ame/events/evilbob/EvilBobNPC.kt index 3f6d8cca3..f8e778f96 100644 --- a/Server/src/main/content/global/ame/events/evilbob/EvilBobNPC.kt +++ b/Server/src/main/content/global/ame/events/evilbob/EvilBobNPC.kt @@ -4,6 +4,7 @@ import content.global.ame.RandomEventNPC import core.api.* import core.api.utils.WeightBasedTable import core.game.node.entity.npc.NPC +import core.game.system.timer.impl.AntiMacro import org.rs09.consts.NPCs import org.rs09.consts.Sounds @@ -24,7 +25,7 @@ class EvilBobNPC(override var loot: WeightBasedTable? = null) : RandomEventNPC(N EvilBobUtils.teleport(player) resetAnimator(player) openDialogue(player, EvilBobDialogue(), NPCs.EVIL_BOB_2479) - content.global.ame.RandomEventManager.getInstance(player)?.event?.terminate() + AntiMacro.terminateEventNpc(player) } } } diff --git a/Server/src/main/content/global/ame/events/freakyforester/FreakyForesterNPC.kt b/Server/src/main/content/global/ame/events/freakyforester/FreakyForesterNPC.kt index 4932298f7..5661c0d7c 100644 --- a/Server/src/main/content/global/ame/events/freakyforester/FreakyForesterNPC.kt +++ b/Server/src/main/content/global/ame/events/freakyforester/FreakyForesterNPC.kt @@ -1,12 +1,12 @@ package content.global.ame.events.freakyforester -import content.global.ame.RandomEventManager import content.global.ame.RandomEventNPC import core.api.* import org.rs09.consts.NPCs import core.api.utils.WeightBasedTable import core.game.node.entity.npc.NPC import core.game.system.task.Pulse +import core.game.system.timer.impl.AntiMacro import core.game.world.update.flag.context.Graphics import org.rs09.consts.Sounds @@ -28,7 +28,7 @@ class FreakyForesterNPC(override var loot: WeightBasedTable? = null) : RandomEve 7 -> { FreakUtils.teleport(player) FreakUtils.giveFreakTask(player) - RandomEventManager.getInstance(player)!!.event?.terminate() + AntiMacro.terminateEventNpc(player) openDialogue(player, FreakyForesterDialogue(), FreakUtils.freakNpc) resetAnimator(player) } diff --git a/Server/src/main/content/global/ame/events/genie/GenieDialogue.kt b/Server/src/main/content/global/ame/events/genie/GenieDialogue.kt index 829e0ecc4..b650e8e22 100644 --- a/Server/src/main/content/global/ame/events/genie/GenieDialogue.kt +++ b/Server/src/main/content/global/ame/events/genie/GenieDialogue.kt @@ -3,6 +3,7 @@ package content.global.ame.events.genie import core.api.* import core.game.dialogue.FacialExpression import core.game.dialogue.DialogueFile +import core.game.system.timer.impl.AntiMacro import core.tools.END_DIALOGUE class GenieDialogue : DialogueFile() { @@ -10,7 +11,7 @@ class GenieDialogue : DialogueFile() { val assigned = player!!.getAttribute("genie:item",0) npcl(core.game.dialogue.FacialExpression.NEUTRAL, "Ah, so you are there, ${player!!.name.capitalize()}. I'm so glad you summoned me. Please take this lamp and make your wish.") addItemOrDrop(player!!, assigned) - content.global.ame.RandomEventManager.getInstance(player!!)!!.event?.terminate() + AntiMacro.terminateEventNpc(player!!) stage = END_DIALOGUE } } \ No newline at end of file diff --git a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyDialogue.kt b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyDialogue.kt index 7cfc456ea..8374daf41 100644 --- a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyDialogue.kt +++ b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyDialogue.kt @@ -6,6 +6,7 @@ import core.game.node.entity.combat.ImpactHandler import core.game.node.item.GroundItemManager import core.game.node.item.Item import core.game.dialogue.DialogueFile +import core.game.system.timer.impl.AntiMacro import core.tools.END_DIALOGUE class SandwichLadyDialogue(val isChoice: Boolean) : DialogueFile() { @@ -26,13 +27,13 @@ class SandwichLadyDialogue(val isChoice: Boolean) : DialogueFile() { 0 -> if(choice != assigned){ npc!!.sendChat("That's not what I said you could have!") player!!.impactHandler.manualHit(npc,3,ImpactHandler.HitsplatType.NORMAL) - content.global.ame.RandomEventManager.getInstance(player!!)!!.event?.terminate() + AntiMacro.terminateEventNpc(player!!) } else { npc("Here you are, dear. I hope you enjoy it!") if(!player!!.inventory.add(Item(assigned))){ GroundItemManager.create(Item(assigned),player) } - content.global.ame.RandomEventManager.getInstance(player!!)!!.event?.terminate() + AntiMacro.terminateEventNpc(player!!) stage = END_DIALOGUE } } diff --git a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt index 306dc6a84..c0d4773bd 100644 --- a/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt +++ b/Server/src/main/content/global/ame/events/sandwichlady/SandwichLadyInterface.kt @@ -3,7 +3,7 @@ package content.global.ame.events.sandwichlady import core.game.node.item.Item import org.rs09.consts.Items import core.game.interaction.InterfaceListener -import content.global.ame.RandomEventManager +import core.game.system.timer.impl.AntiMacro class SandwichLadyInterface : InterfaceListener { @@ -18,7 +18,7 @@ class SandwichLadyInterface : InterfaceListener { override fun defineInterfaceListeners() { on(SANDWICH_INTERFACE){player, _, _, buttonID, _, _ -> - val event = RandomEventManager.getInstance(player)!!.event + val event = AntiMacro.getEventNpc(player) if (event == null) { player.interfaceManager.close() return@on true diff --git a/Server/src/main/content/global/handlers/npc/NPCTalkListener.kt b/Server/src/main/content/global/handlers/npc/NPCTalkListener.kt index b5e3b3090..1bc33d344 100644 --- a/Server/src/main/content/global/handlers/npc/NPCTalkListener.kt +++ b/Server/src/main/content/global/handlers/npc/NPCTalkListener.kt @@ -7,6 +7,7 @@ import content.global.ame.RandomEvents import content.minigame.gnomecooking.* import core.game.interaction.InteractionListener import core.game.interaction.IntType +import core.game.system.timer.impl.AntiMacro /** * Handles the NPC talk-to option. @@ -31,10 +32,10 @@ class NPCTalkListener : InteractionListener { on(IntType.NPC,"talk-to","talk","talk to"){ player, node -> val npc = node.asNpc() if(RandomEvents.randomIDs.contains(node.id)){ - if(content.global.ame.RandomEventManager.getInstance(player)!!.event == null || content.global.ame.RandomEventManager.getInstance(player)!!.event!! != node.asNpc()){ + if(AntiMacro.getEventNpc(player) == null || AntiMacro.getEventNpc(player) != node.asNpc()){ player.sendMessage("They aren't interested in talking to you.") } else { - content.global.ame.RandomEventManager.getInstance(player)!!.event!!.talkTo(node.asNpc()) + AntiMacro.getEventNpc(player)?.talkTo(node.asNpc()) } return@on true } diff --git a/Server/src/main/core/game/activity/Cutscene.kt b/Server/src/main/core/game/activity/Cutscene.kt index 9a3bf85b6..992090c4d 100644 --- a/Server/src/main/core/game/activity/Cutscene.kt +++ b/Server/src/main/core/game/activity/Cutscene.kt @@ -23,6 +23,7 @@ import org.rs09.consts.Components import core.ServerConstants import core.api.Event import core.api.utils.PlayerCamera +import core.game.system.timer.impl.AntiMacro import core.tools.SystemLogger import core.game.world.GameWorld import core.tools.Log @@ -238,7 +239,7 @@ abstract class Cutscene(val player: Player) { player.lock() player.hook(Event.SelfDeath, CUTSCENE_DEATH_HOOK) player.logoutListeners["cutscene"] = {player -> player.location = exitLocation; player.getCutscene()?.end() } - content.global.ame.RandomEventManager.getInstance(player)!!.enabled = false + AntiMacro.pause(player) } /** @@ -275,7 +276,7 @@ abstract class Cutscene(val player: Player) { clearNPCs() player.unhook(CUTSCENE_DEATH_HOOK) player.logoutListeners.remove("cutscene") - content.global.ame.RandomEventManager.getInstance(player)?.enabled = true + AntiMacro.unpause(player) PacketRepository.send(MinimapState::class.java, MinimapStateContext(player, 0)) try { endActions?.invoke() diff --git a/Server/src/main/core/game/node/entity/Entity.java b/Server/src/main/core/game/node/entity/Entity.java index c13e4bb2d..cd4ad0a0a 100644 --- a/Server/src/main/core/game/node/entity/Entity.java +++ b/Server/src/main/core/game/node/entity/Entity.java @@ -105,7 +105,7 @@ public abstract class Entity extends Node { * The reward locks. */ private final ActionLocks locks = new ActionLocks(); - public final ScriptProcessor scripts = new ScriptProcessor(this); + public ScriptProcessor scripts = new ScriptProcessor(this); public final int[] clocks = new int[10]; 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 909123d6d..c0d498e8f 100644 --- a/Server/src/main/core/game/node/entity/player/Player.java +++ b/Server/src/main/core/game/node/entity/player/Player.java @@ -219,7 +219,7 @@ public class Player extends Entity { /** * The quest repository. */ - private QuestRepository questRepository = new QuestRepository(this); + public QuestRepository questRepository = new QuestRepository(this); /** * The prayer manager. @@ -239,7 +239,7 @@ public class Player extends Entity { /** * The saved data. */ - private SavedData savedData = new SavedData(this); + public SavedData savedData = new SavedData(this); /** * The request manager. diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt index b28353be4..e5984d7e2 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaveParser.kt @@ -39,7 +39,7 @@ class PlayerSaveParser(val player: Player) { var saveFile: JSONObject? = null var read = true - init { + fun parse() { val JSON = File(ServerConstants.PLAYER_SAVE_PATH + player.name + ".json") if(JSON.exists()) { @@ -49,36 +49,38 @@ class PlayerSaveParser(val player: Player) { if (read) { saveFile = parser.parse(reader) as JSONObject } + + if (read) { + parseData() + } } - fun parse() { - if (read) { - parseCore() - parseAttributes() - parseSkills() - parseSettings() - parseQuests() - parseAppearance() - parseGrave() - parseVarps() - parseStates() - parseSpellbook() - parseSavedData() - parseAutocastSpell() - parseFarming() - parseConfigs() - parseMonitor() - parseMusic() - parseFamiliars() - parseBankPin() - parseHouse() - parseIronman() - parseEmoteManager() - parseStatistics() - parseAchievements() - parsePouches() - parsePouches() - } + fun parseData() { + parseCore() + parseAttributes() + parseSkills() + parseSettings() + parseQuests() + parseAppearance() + parseGrave() + parseVarps() + parseStates() + parseSpellbook() + parseSavedData() + parseAutocastSpell() + parseFarming() + parseConfigs() + parseMonitor() + parseMusic() + parseFamiliars() + parseBankPin() + parseHouse() + parseIronman() + parseEmoteManager() + parseStatistics() + parseAchievements() + parsePouches() + parsePouches() } fun runContentHooks() diff --git a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt index b85e58a2e..29b97f4cd 100644 --- a/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt +++ b/Server/src/main/core/game/node/entity/player/info/login/PlayerSaver.kt @@ -34,7 +34,7 @@ class PlayerSaver (val player: Player){ companion object { val contentHooks = ArrayList() } - private fun populate(): JSONObject { + fun populate(): JSONObject { val saveFile = JSONObject() saveCoreData(saveFile) saveSkills(saveFile) diff --git a/Server/src/main/core/game/node/entity/skill/Skills.java b/Server/src/main/core/game/node/entity/skill/Skills.java index eafb208d9..bb0cc7d27 100644 --- a/Server/src/main/core/game/node/entity/skill/Skills.java +++ b/Server/src/main/core/game/node/entity/skill/Skills.java @@ -26,6 +26,7 @@ import org.rs09.consts.Items; import java.nio.ByteBuffer; import java.util.ArrayList; +import static core.api.ContentAPIKt.getWorldTicks; import static java.lang.Math.floor; import static java.lang.Math.max; @@ -119,7 +120,8 @@ public final class Skills { */ private int skillMilestone; - public int lastTrainedSkill = -1; + public int lastTrainedSkill = -1; + public int lastXpGain = 0; /** * Constructs a new {@code Skills} {@code Object}. @@ -271,7 +273,8 @@ public final class Skills { lastUpdateXp = this.experience.clone(); lastUpdate = GameWorld.getTicks(); } - lastTrainedSkill = slot; + lastTrainedSkill = slot; + lastXpGain = getWorldTicks(); } /** diff --git a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt index 14742786b..6c48e0362 100644 --- a/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt +++ b/Server/src/main/core/game/system/command/sets/MiscCommandSet.kt @@ -546,26 +546,6 @@ class MiscCommandSet : CommandSet(Privilege.ADMIN){ define("finishbins", Privilege.ADMIN, "", "Finishes any in-progress compost bins."){ player, _ -> } - define("testlady", Privilege.ADMIN){ player, _ -> - content.global.ame.RandomEventManager.getInstance(player)!!.event = RandomEvents.RIVER_TROLL.npc.create(player) - content.global.ame.RandomEventManager.getInstance(player)!!.event!!.init() - } - - define("freak", Privilege.ADMIN){ player, _ -> - content.global.ame.RandomEventManager.getInstance(player)!!.event = RandomEvents.FREAKY_FORESTER.npc.create(player) - content.global.ame.RandomEventManager.getInstance(player)!!.event!!.init() - } - - define("bob", Privilege.ADMIN){ player, _ -> - content.global.ame.RandomEventManager.getInstance(player)!!.event = RandomEvents.EVIL_BOB.npc.create(player) - content.global.ame.RandomEventManager.getInstance(player)!!.event!!.init() - } - - define("revent", Privilege.ADMIN){ player, _ -> - println(player.pulseManager.current) - content.global.ame.RandomEventManager.getInstance(player)!!.fireEvent() - } - define("addcredits", Privilege.ADMIN){ player, _ -> player.details.credits += 100 } diff --git a/Server/src/main/core/game/system/timer/TimerManager.kt b/Server/src/main/core/game/system/timer/TimerManager.kt index e3196a431..81b981345 100644 --- a/Server/src/main/core/game/system/timer/TimerManager.kt +++ b/Server/src/main/core/game/system/timer/TimerManager.kt @@ -56,6 +56,12 @@ class TimerManager (val entity: Entity) { timer.save(obj, entity) root [timer.identifier] = obj } + for (timer in newTimers) { + if (timer !is PersistTimer) continue + val obj = JSONObject() + timer.save(obj, entity) + root [timer.identifier] = obj + } } fun parseTimers (root: JSONObject) { diff --git a/Server/src/main/core/game/system/timer/TimerRegistry.kt b/Server/src/main/core/game/system/timer/TimerRegistry.kt index d687474e4..3aaa46878 100644 --- a/Server/src/main/core/game/system/timer/TimerRegistry.kt +++ b/Server/src/main/core/game/system/timer/TimerRegistry.kt @@ -30,7 +30,6 @@ object TimerRegistry { @JvmStatic fun addAutoTimers (entity: Entity) { - (entity as? Player)?.debug ("Adding auto timers...") for (timer in autoTimers) { if (!hasTimerActive (entity, timer.identifier)) registerTimer (entity, timer.retrieveInstance()) diff --git a/Server/src/main/core/game/system/timer/impl/AntiMacro.kt b/Server/src/main/core/game/system/timer/impl/AntiMacro.kt new file mode 100644 index 000000000..482c5dc54 --- /dev/null +++ b/Server/src/main/core/game/system/timer/impl/AntiMacro.kt @@ -0,0 +1,174 @@ +package core.game.system.timer.impl + +import content.global.ame.RandomEventNPC +import content.global.ame.RandomEvents +import core.api.* +import core.game.node.entity.Entity +import core.game.node.entity.player.Player +import core.game.node.item.Item +import core.game.system.command.Privilege +import core.game.system.timer.PersistTimer +import core.game.world.map.zone.ZoneRestriction +import core.game.world.repository.Repository +import core.tools.RandomFunction +import org.json.simple.JSONObject + +class AntiMacro : PersistTimer(0, "antimacro", isAuto = true), Commands { + var paused = false + var nextRandom: RandomEvents? = null + + override fun run(entity: Entity): Boolean { + if (entity !is Player) return false + + setNextExecution() + + if (!canSpawn(entity)) return true + val eventSelection = rollEventPool(entity) + val eventNpc = eventSelection.npc.create(entity, eventSelection.loot, eventSelection.type) + if (eventNpc.spawnLocation == null) { + entity.debug("[AntiMacro] Attempted to spawn random, but spawnLoc was null.") + return true + } + + eventNpc.init() + setAttribute(entity, EVENT_NPC, eventNpc) + entity.debug("[AntiMacro] Fired ${eventSelection.name}.") + return true + } + + override fun onRegister(entity: Entity) { + if (entity !is Player) + entity.timers.removeTimer(this) + + if (runInterval == 0) + setNextExecution() + } + + override fun save(root: JSONObject, entity: Entity) { + root["ticksRemaining"] = (nextExecution - getWorldTicks()).toString() + } + + override fun parse(root: JSONObject, entity: Entity) { + runInterval = (root["ticksRemaining"]?.toString()?.toIntOrNull() ?: 0) + nextExecution = getWorldTicks() + runInterval + } + + private fun canSpawn(entity: Entity) : Boolean { + if (entity.zoneMonitor.isRestricted(ZoneRestriction.RANDOM_EVENTS)) + return false + + val current = getAttribute(entity, EVENT_NPC, null) + if (current != null) + return false + + if (paused) + return false + + return true + } + + private fun setNextExecution() { + runInterval = RandomFunction.random(MIN_DELAY_TICKS, MAX_DELAY_TICKS + 1) + nextExecution = getWorldTicks() + runInterval + } + + private fun rollEventPool(entity: Entity) : RandomEvents { + if (nextRandom != null) { + val result = nextRandom!! + nextRandom = null + return result + } + + val skillBasedRandom = RandomEvents.getSkillBasedRandomEvent(entity.skills.lastTrainedSkill) + val normalRandom = RandomEvents.getNonSkillRandom() + val roll = RandomFunction.random(100) + + if (roll >= 65 && skillBasedRandom != null && getWorldTicks() - entity.skills.lastXpGain < 250) + return skillBasedRandom + return normalRandom + } + + override fun defineCommands() { + define("revent", Privilege.ADMIN, "::revent [-p] player name [-e event name]", "Spawns a random event for the target player.
Optional -e parameter to pass a specific event.") {player, args -> + if (args.size == 1) { + val possible = RandomEvents.values() + for (event in possible) { + notify(player, event.name.lowercase()) + } + return@define + } + + val arg = parseCommandArgs(args.joinToString(" ")) + val target = Repository.getPlayerByName(arg.targetPlayer) + if (target == null) + reject(player, "Unable to find user ${arg.targetPlayer}.") + + forceEvent(target!!, arg.targetEvent) + } + } + + data class CommandArgs (val targetPlayer: String, val targetEvent: RandomEvents?) + + companion object { + const val EVENT_NPC = "re-npc" + const val MIN_DELAY_TICKS = 3000 + const val MAX_DELAY_TICKS = 9000 + + fun terminateEventNpc (player: Player) { + getEventNpc(player)?.terminate() + } + + fun rollEventLoot (player: Player) : ArrayList { + return getEventNpc(player)?.loot?.roll(player) ?: ArrayList() + } + + fun getEventNpc (player: Player) : RandomEventNPC? { + return getAttribute(player, EVENT_NPC, null) + } + + fun pause (player: Player) { + val timer = getTimer(player) ?: return + timer.paused = true + } + + fun unpause (player: Player) { + val timer = getTimer(player) ?: return + timer.paused = false + } + + fun forceEvent (player: Player, event: RandomEvents? = null) { + val timer = getTimer(player) ?: return + timer.nextExecution = getWorldTicks() + timer.nextRandom = event + } + + fun parseCommandArgs (args: String, commandName: String = "revent") : CommandArgs { + val tokens = args.split(" ") + val modeTokens = arrayOf("-p", "-e") + + var userString = "" + var eventString = "" + var lastMode = "-p" + + for (token in tokens) { + when (token) { + commandName -> continue + in modeTokens -> lastMode = token + else -> when (lastMode) { + "-p" -> userString += "$token " + "-e" -> eventString += "$token " + } + } + } + + val username = userString.trim().lowercase().replace(" ", "_") + val eventName = eventString.trim().uppercase().replace(" ", "_") + + var event: RandomEvents? = null + + try { event = RandomEvents.valueOf(eventName) } catch (_: Exception) {} + + return CommandArgs(username, event) + } + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index 20d7d28f0..42e51535b 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -1,29 +1,31 @@ -import content.global.ame.RandomEventManager import content.global.skill.farming.timers.CropGrowth +import core.ServerConstants +import core.api.log import core.cache.Cache import core.cache.crypto.ISAACCipher import core.cache.crypto.ISAACPair -import core.game.node.entity.player.Player -import core.game.node.entity.player.info.PlayerDetails -import core.game.node.entity.player.info.Rights -import core.game.node.entity.player.link.IronmanMode -import core.game.node.item.Item -import core.net.IoSession -import core.net.packet.IoBuffer -import org.rs09.consts.Items -import core.ServerConstants -import core.api.log +import core.game.interaction.ScriptProcessor import core.game.node.Node import core.game.node.entity.combat.equipment.WeaponInterface import core.game.node.entity.npc.NPC -import core.game.node.entity.skill.SkillBonus +import core.game.node.entity.player.Player +import core.game.node.entity.player.VarpManager +import core.game.node.entity.player.info.PlayerDetails +import core.game.node.entity.player.info.Rights +import core.game.node.entity.player.info.login.PlayerSaveParser +import core.game.node.entity.player.info.login.PlayerSaver +import core.game.node.entity.player.link.IronmanMode +import core.game.node.entity.player.link.SavedData +import core.game.node.entity.player.link.quest.QuestRepository +import core.game.node.entity.skill.Skills import core.game.node.item.GroundItem +import core.game.node.item.Item import core.game.shops.Shop import core.game.shops.ShopItem -import core.tools.SystemLogger import core.game.system.config.ConfigParser import core.game.system.config.ServerConfigParser import core.game.system.timer.TimerRegistry +import core.game.system.timer.impl.AntiMacro import core.game.system.timer.impl.Disease import core.game.system.timer.impl.Poison import core.game.world.GameWorld @@ -31,10 +33,11 @@ import core.game.world.map.Location import core.game.world.map.RegionManager import core.game.world.repository.Repository import core.game.world.update.UpdateSequence +import core.net.IoSession +import core.net.packet.IoBuffer import core.net.packet.PacketProcessor import core.tools.Log -import org.rs09.consts.Scenery -import java.io.Closeable +import org.rs09.consts.Items import java.net.URI import java.nio.ByteBuffer @@ -85,6 +88,7 @@ object TestUtils { TimerRegistry.registerTimer(Poison()) TimerRegistry.registerTimer(Disease()) TimerRegistry.registerTimer(CropGrowth()) + TimerRegistry.registerTimer(AntiMacro()) } fun loadFile(path: String) : URI? { @@ -111,12 +115,17 @@ object TestUtils { class MockPlayer(name: String) : Player(PlayerDetails(name)), AutoCloseable { var hasInit = false - init { + init { configureBasicProperties(); flagTutComplete(false); init(); flagTutComplete(true) } + + fun configureBasicProperties() { this.details.session = MockSession() this.location = ServerConstants.HOME_LOCATION this.properties.attackStyle = WeaponInterface.AttackStyle(0, WeaponInterface.BONUS_CRUSH) - init() - this.setAttribute("tutorial:complete", true) + } + + fun flagTutComplete(complete: Boolean) { + this.setAttribute("/save:tutorial:complete", complete) + this.setAttribute("/save:rules:confirmed", complete) } override fun update() { @@ -140,6 +149,37 @@ class MockPlayer(name: String) : Player(PlayerDetails(name)), AutoCloseable { RegionManager.move(this) this.playerFlags.lastSceneGraph = location } + + fun relog(ticksToWait: Int = -1) { + val json = PlayerSaver(this).populate() + val parse = PlayerSaveParser(this) + parse.saveFile = json + + close() + timers.clearTimers() + inventory.clear() + bank.clear() + equipment.clear() + skills = Skills(this) + savedData = SavedData(this) + questRepository = QuestRepository(this) + varpManager = VarpManager(this) + varpMap.clear() + saveVarp.clear() + scripts = ScriptProcessor(this) + clearAttributes() + hasInit = false + + if (ticksToWait > 0) TestUtils.advanceTicks(ticksToWait, false) + + configureBasicProperties() + parse.parseData() + init() + } + + override fun debug(string: String?) { + log (this::class.java, Log.DEBUG, "[$name] -> Received Debug: $string") + } } class MockSession : IoSession(null, null) { diff --git a/Server/src/test/kotlin/content/RandomEventManagerTests.kt b/Server/src/test/kotlin/content/RandomEventManagerTests.kt deleted file mode 100644 index 81e700696..000000000 --- a/Server/src/test/kotlin/content/RandomEventManagerTests.kt +++ /dev/null @@ -1,81 +0,0 @@ -package content - -import TestUtils -import content.global.ame.RandomEventManager -import core.game.node.item.Item -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.rs09.consts.Items -import content.global.ame.RandomEventNPC -import core.game.world.GameWorld - -class RandomEventManagerTests { - init { - TestUtils.preTestSetup() - } - - @Test fun loginShouldEnableManager() { - val p = TestUtils.getMockPlayer("Bill") - RandomEventManager().login(p) - val manager = content.global.ame.RandomEventManager.getInstance(p) - Assertions.assertNotNull(manager) - Assertions.assertEquals(true, manager!!.enabled) - } - - @Test fun loginShouldSetNextSpawn() { - val p = TestUtils.getMockPlayer("Bill") - RandomEventManager().login(p) - val manager = content.global.ame.RandomEventManager.getInstance(p) - Assertions.assertNotNull(manager) - Assertions.assertEquals(true, manager!!.nextSpawn > GameWorld.ticks) - } - - @Test fun shouldSpawnRandomEventWithinMAXTICKSGivenNoRestrictions() { - val p = TestUtils.getMockPlayer("Bill") - p.setAttribute("tutorial:complete", true) //tutorial has to be complete to spawn randoms - RandomEventManager.MIN_DELAY_TICKS = 10 - RandomEventManager.MAX_DELAY_TICKS = 20 - RandomEventManager().login(p) - TestUtils.advanceTicks(RandomEventManager.MAX_DELAY_TICKS + 5) - RandomEventManager.MIN_DELAY_TICKS = 3000 - RandomEventManager.MAX_DELAY_TICKS = 9000 - Assertions.assertNotNull(p.getAttribute("re-npc", null)) - } - - @Test fun teleportAndNotePunishmentShouldNotAffectAlreadyNotedItems() { - val p = TestUtils.getMockPlayer("Shitforbrains") - p.setAttribute("tutorial:complete", true) - RandomEventManager().login(p) - - p.inventory.add(Item(Items.RAW_SHARK_384, 1000)) - content.global.ame.RandomEventManager.getInstance(p)?.fireEvent() - p.getAttribute("re-npc")!!.noteAndTeleport() - - Assertions.assertEquals(1000, p.inventory.getAmount(Items.RAW_SHARK_384)) - } - - @Test fun teleportAndNotePunishmentShouldNoteNotableUnnotedItems() { - val p = TestUtils.getMockPlayer("shitforbrains2") - p.setAttribute("tutorial:complete", true) - RandomEventManager().login(p) - - p.inventory.add(Item(4151, 5)) - content.global.ame.RandomEventManager.getInstance(p)?.fireEvent() - p.getAttribute("re-npc")!!.noteAndTeleport() - - Assertions.assertEquals(5, p.inventory.getAmount(4152)) - Assertions.assertEquals(0, p.inventory.getAmount(4151)) - } - - @Test fun teleportAndNotePunishmentShouldNotAffectUnnotableItems() { - val p = TestUtils.getMockPlayer("shitforbrains3") - p.setAttribute("tutorial:complete", true) - RandomEventManager().login(p) - - p.inventory.add(Item(Items.AIR_RUNE_556, 30)) - content.global.ame.RandomEventManager.getInstance(p)?.fireEvent() - p.getAttribute("re-npc")!!.noteAndTeleport() - - Assertions.assertEquals(30, p.inventory.getAmount(Items.AIR_RUNE_556)) - } -} \ No newline at end of file diff --git a/Server/src/test/kotlin/content/RandomEventTests.kt b/Server/src/test/kotlin/content/RandomEventTests.kt new file mode 100644 index 000000000..efd98a335 --- /dev/null +++ b/Server/src/test/kotlin/content/RandomEventTests.kt @@ -0,0 +1,217 @@ +package content + +import TestUtils +import content.global.ame.RandomEventNPC +import content.global.ame.RandomEvents +import core.api.* +import core.game.system.timer.impl.AntiMacro +import core.game.world.map.Location +import core.game.world.map.zone.impl.WildernessZone +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.rs09.consts.Items +import org.rs09.consts.NPCs + +class RandomEventTests { + init { + TestUtils.preTestSetup() + } + + @Test fun loginShouldRegisterRandomEventTimer() { + TestUtils.getMockPlayer("antimacroAutoRegister").use { p -> + Assertions.assertNotNull(getTimer(p)) + } + } + + @Test fun loginShouldSetNextSpawn() { + TestUtils.getMockPlayer("antimacroautosetsnextspawnifunset").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null.") + Assertions.assertNotEquals(getWorldTicks(), timer.nextExecution) + Assertions.assertEquals(true, timer.nextExecution - getWorldTicks() >= AntiMacro.MIN_DELAY_TICKS) + Assertions.assertEquals(true, timer.nextExecution - getWorldTicks() <= AntiMacro.MAX_DELAY_TICKS) + } + } + + @Test fun remainingDelayShouldPersistRelog() { + TestUtils.getMockPlayer("antimacrotimeremainingpersists").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null.") + timer.nextExecution = getWorldTicks() + 666 + p.relog(ticksToWait = 100) + Assertions.assertEquals(getWorldTicks() + 666, getTimer(p)!!.nextExecution) + } + } + + @Test fun delayShouldBeRestartedOnceDepleted() { + TestUtils.getMockPlayer("delayrestartoncedepleted").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null.") + TestUtils.advanceTicks(2, false) + timer.nextExecution = 5 //run in 5 ticks + TestUtils.advanceTicks(5, false) + Assertions.assertEquals(true, timer.nextExecution - getWorldTicks() >= AntiMacro.MIN_DELAY_TICKS) + Assertions.assertEquals(true, timer.nextExecution - getWorldTicks() <= AntiMacro.MAX_DELAY_TICKS) + } + } + + @Test fun shouldSpawnRandomEventGivenNoRestrictions() { + TestUtils.getMockPlayer("antimacroshouldspawnrandom").use {p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(p.getAttribute(AntiMacro.EVENT_NPC, null)) + Assertions.assertEquals(true, p.getAttribute(AntiMacro.EVENT_NPC).isActive) + } + } + + @Test fun teleportAndNotePunishmentShouldNotAffectAlreadyNotedItems() { + TestUtils.getMockPlayer("teleportpunishment1").use {p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + + addItem(p, Items.RAW_SHARK_384, 1000) + getAttribute(p, AntiMacro.EVENT_NPC, null)!!.noteAndTeleport() + + Assertions.assertEquals(1000, amountInInventory(p, Items.RAW_SHARK_384)) + } + } + + @Test fun teleportAndNotePunishmentShouldNoteNotableUnnotedItems() { + TestUtils.getMockPlayer("teleportpunishment2").use {p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + + addItem(p, Items.ABYSSAL_WHIP_4151, 5) + getAttribute(p, AntiMacro.EVENT_NPC, null)!!.noteAndTeleport() + + Assertions.assertEquals(5, amountInInventory(p, Items.ABYSSAL_WHIP_4152)) + Assertions.assertEquals(0, amountInInventory(p, Items.ABYSSAL_WHIP_4151)) + } + } + + @Test fun teleportAndNotePunishmentShouldNotAffectUnnotableItems() { + TestUtils.getMockPlayer("teleportpunishment3").use {p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer is null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + + addItem(p, Items.AIR_RUNE_556, 30) + getAttribute(p, AntiMacro.EVENT_NPC, null)!!.noteAndTeleport() + + Assertions.assertEquals(30, amountInInventory(p, Items.AIR_RUNE_556)) + } + } + + @Test fun randomEventShouldNotSpawnInEventRestrictedArea() { + + TestUtils.getMockPlayer("antimacronospawninrestrictedzone").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer was null!") + TestUtils.advanceTicks(5, false) + + //Wilderness is Random Event restricted + WildernessZone.getInstance().configure() + val loc = Location.create(3131, 3595) + p.location = loc + + Assertions.assertEquals(loc, p.location) + + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNull(AntiMacro.getEventNpc(p)) + } + } + + @Test fun randomEventShouldNotSpawnIfOneAlreadyActive() { + TestUtils.getMockPlayer("antimacronospawnifalreadyhas").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer was null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + + val previousEvent = AntiMacro.getEventNpc(p) + + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + Assertions.assertEquals(previousEvent, AntiMacro.getEventNpc(p)) + } + } + + @Test fun randomEventShouldSpawnIfCurrentOneIsInactive() { + TestUtils.getMockPlayer("antimacrospawnifcurrentinactive").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer was null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + + val previousEvent = AntiMacro.getEventNpc(p) + AntiMacro.terminateEventNpc(p) + + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + Assertions.assertNotEquals(previousEvent, AntiMacro.getEventNpc(p)) + } + } + + @Test fun randomEventSystemShouldSupportPauseAndUnpause() { + TestUtils.getMockPlayer("antimacropause").use { p -> + val timer = getTimer(p) ?: Assertions.fail("AntiMacro timer was null!") + TestUtils.advanceTicks(5, false) + timer.nextExecution = getWorldTicks() + 5 + + AntiMacro.pause(p) + TestUtils.advanceTicks(10, false) + Assertions.assertNull(AntiMacro.getEventNpc(p)) + + AntiMacro.unpause(p) + timer.nextExecution = getWorldTicks() + 5 + TestUtils.advanceTicks(10, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + } + } + + @Test fun shouldBeAbleToForceRandomEvent() { + TestUtils.getMockPlayer("antimacroforcerand").use { p -> + TestUtils.advanceTicks(5, false) + AntiMacro.forceEvent(p) + TestUtils.advanceTicks(1, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + } + } + + @Test fun shouldBeAbleToForceSpecificRandomEvent() { + TestUtils.getMockPlayer("antimacroforcespecific").use { p -> + TestUtils.advanceTicks(5, false) + AntiMacro.forceEvent(p, RandomEvents.TREE_SPIRIT) + TestUtils.advanceTicks(1, false) + Assertions.assertNotNull(AntiMacro.getEventNpc(p)) + Assertions.assertEquals(NPCs.TREE_SPIRIT_438, AntiMacro.getEventNpc(p)?.originalId) + } + } + + @Test fun parseAntiMacroCommandArgsShouldReturnExpectedValues() { + val testData = arrayOf( + Triple("revent -p test", "test", null), + Triple("revent -p test ", "test", null), + Triple("revent -p test -e certer ", "test", RandomEvents.CERTER), + Triple("revent -p test_user -e Sandwich lady", "test_user", RandomEvents.SANDWICH_LADY), + Triple("revent -p test user -e sandwich Lady ", "test_user", RandomEvents.SANDWICH_LADY), + Triple("revent -e sandwich Lady -p test user ", "test_user", RandomEvents.SANDWICH_LADY), + Triple("revent test", "test", null), + Triple("revent test -e sAndwich Lady", "test", RandomEvents.SANDWICH_LADY) + ) + + for ((commandStr, expectedUser, expectedEvent) in testData) { + val (resultUser, resultEvent) = AntiMacro.parseCommandArgs(commandStr, "revent") + Assertions.assertEquals(expectedUser, resultUser) + Assertions.assertEquals(expectedEvent, resultEvent) + } + } +} \ No newline at end of file