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
This commit is contained in:
Ceikry
2023-08-14 12:56:35 +00:00
committed by Ryan
parent bb21ad533b
commit a3e6df109d
26 changed files with 538 additions and 285 deletions
@@ -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<TickEvent>, 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<content.global.ame.RandomEventNPC?>(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)
}
}
}
@@ -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)
}
@@ -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<RandomEvents>().also { skillMap[event.skillId] = it }
for (id in event.skillIds) {
val list = skillMap[id] ?: ArrayList<RandomEvents>().also { skillMap[id] = it }
list.add (event)
}
else nonSkillList.add (event)
if (event.skillIds.isEmpty())
nonSkillList.add (event)
}
}
}
@@ -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!!)
}
}
}
@@ -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)
}
@@ -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
}
@@ -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
}
}
@@ -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)
}
}
}
@@ -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)
}
@@ -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
}
}
@@ -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
}
}
@@ -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
@@ -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
}
@@ -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()
@@ -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];
@@ -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.
@@ -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()
@@ -34,7 +34,7 @@ class PlayerSaver (val player: Player){
companion object {
val contentHooks = ArrayList<PersistPlayer>()
}
private fun populate(): JSONObject {
fun populate(): JSONObject {
val saveFile = JSONObject()
saveCoreData(saveFile)
saveSkills(saveFile)
@@ -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();
}
/**
@@ -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
}
@@ -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) {
@@ -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())
@@ -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<RandomEventNPC?>(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] <lt>player name<gt> [-e <lt>event name<gt>]", "Spawns a random event for the target player.<br>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<Item> {
return getEventNpc(player)?.loot?.roll(player) ?: ArrayList()
}
fun getEventNpc (player: Player) : RandomEventNPC? {
return getAttribute<RandomEventNPC?>(player, EVENT_NPC, null)
}
fun pause (player: Player) {
val timer = getTimer<AntiMacro>(player) ?: return
timer.paused = true
}
fun unpause (player: Player) {
val timer = getTimer<AntiMacro>(player) ?: return
timer.paused = false
}
fun forceEvent (player: Player, event: RandomEvents? = null) {
val timer = getTimer<AntiMacro>(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)
}
}
}
+58 -18
View File
@@ -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) {
@@ -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<RandomEventNPC>("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<RandomEventNPC>("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<RandomEventNPC>("re-npc")!!.noteAndTeleport()
Assertions.assertEquals(30, p.inventory.getAmount(Items.AIR_RUNE_556))
}
}
@@ -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<AntiMacro>(p))
}
}
@Test fun loginShouldSetNextSpawn() {
TestUtils.getMockPlayer("antimacroautosetsnextspawnifunset").use { p ->
val timer = getTimer<AntiMacro>(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<AntiMacro>(p) ?: Assertions.fail("AntiMacro timer is null.")
timer.nextExecution = getWorldTicks() + 666
p.relog(ticksToWait = 100)
Assertions.assertEquals(getWorldTicks() + 666, getTimer<AntiMacro>(p)!!.nextExecution)
}
}
@Test fun delayShouldBeRestartedOnceDepleted() {
TestUtils.getMockPlayer("delayrestartoncedepleted").use { p ->
val timer = getTimer<AntiMacro>(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<AntiMacro>(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<RandomEventNPC>(AntiMacro.EVENT_NPC).isActive)
}
}
@Test fun teleportAndNotePunishmentShouldNotAffectAlreadyNotedItems() {
TestUtils.getMockPlayer("teleportpunishment1").use {p ->
val timer = getTimer<AntiMacro>(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<RandomEventNPC?>(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<AntiMacro>(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<RandomEventNPC?>(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<AntiMacro>(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<RandomEventNPC?>(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<AntiMacro>(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<AntiMacro>(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<AntiMacro>(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<AntiMacro>(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)
}
}
}