Fixed some error spam about a nullref in animateWoodcutting

Fixed a general script processing oversight that could cause error-throwing scripts to continue executing
Scripts being processed now automatically cancel if the interaction-target-node is no longer active (fixes object could not be replaced spam)
Fixed the stopExecuting() spam
Fixed a CME in the disconnection queue (only affected server shutdown)
Future-proofed the ground spawn parser to work with the format Zaros now outputs
Added in proper SKILLING global clock for use with the script system - addresses some quirks and brings us closer to authenticity
This commit is contained in:
Ceikry
2023-03-21 03:50:27 +00:00
committed by Ryan
parent 48805eea66
commit 7ec901297b
8 changed files with 159 additions and 107 deletions
@@ -19,33 +19,33 @@ class ConsumableListener : InteractionListener {
}
private fun handleConsumable(player: Player, node: Node) : Boolean {
val consumable = Consumables.getConsumableById(node.id) ?: return stopExecuting(player)
val consumable = Consumables.getConsumableById(node.id) ?: return true
val food = getUsedOption(player) == "eat"
val isIgnoreMainClock = consumable.isIgnoreMainClock
if (food) {
if (isIgnoreMainClock && player.clocks[Clocks.NEXT_CONSUME] < GameWorld.ticks) {
consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player)
consumable.consumable.consume(node as? Item ?: return true, player)
player.clocks[Clocks.NEXT_CONSUME] = getWorldTicks() + 2
player.clocks[Clocks.NEXT_EAT] = getWorldTicks() + 2
delayAttack(player, 3)
} else if (player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks() && player.clocks[Clocks.NEXT_EAT] < getWorldTicks()) {
consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player)
consumable.consumable.consume(node as? Item ?: return true, player)
player.clocks[Clocks.NEXT_EAT] = getWorldTicks() + 2
delayAttack(player, 3)
}
} else {
if (isIgnoreMainClock && player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks()) {
consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player)
consumable.consumable.consume(node as? Item ?: return true, player)
player.clocks[Clocks.NEXT_CONSUME] = getWorldTicks() + 3
player.clocks[Clocks.NEXT_DRINK] = getWorldTicks() + 3
} else if (player.clocks[Clocks.NEXT_CONSUME] < getWorldTicks() && player.clocks[Clocks.NEXT_DRINK] < getWorldTicks()) {
consumable.consumable.consume(node as? Item ?: return stopExecuting(player), player)
consumable.consumable.consume(node as? Item ?: return true, player)
player.clocks[Clocks.NEXT_DRINK] = getWorldTicks() + 3
}
}
return stopExecuting(player)
return true
}
}
@@ -10,6 +10,7 @@ import core.api.*
import core.game.event.ResourceProducedEvent
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.interaction.Clocks
import core.game.node.Node
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
@@ -47,36 +48,40 @@ class FishingListener : InteractionListener{
return restartScript(player)
if (state == 0) {
sendMessage(player, "You attempt to catch some fish...")
}
if (state == 1) {
if (!checkRequirements(player, op, node))
return clearScripts(player)
forager?.let {
val dest = player.location.transform(player.direction)
Pathfinder.find(it, dest).walk(it)
}
sendMessage(player, "You attempt to catch some fish...")
}
anim(player, op)
forager?.handlePassiveAction()
if (clockReady(player, Clocks.SKILLING)) {
anim(player, op)
forager?.handlePassiveAction()
val fish = op.rollFish(player) ?: return delayScript(player, 5)
if (!hasSpaceFor(player, fish.item)) return restartScript(player)
if (!op.removeBait(player.inventory)) return restartScript(player)
player.dispatch(ResourceProducedEvent(fish.item.id, fish.item.amount, node))
val fish = op.rollFish(player) ?: return delayClock(player, Clocks.SKILLING, 5)
val item = fish.item
if (isActive(SkillcapePerks.GREAT_AIM, player) && RandomFunction.roll(20)) {
addItem(player, item.id, item.amount)
sendMessage(player, colorize("%RYour expert aim catches you a second fish."))
if (!hasSpaceFor(player, fish.item) || !op.removeBait(player.inventory)) {
return restartScript(player)
}
player.dispatch(ResourceProducedEvent(fish.item.id, fish.item.amount, node))
val item = fish.item
if (isActive(SkillcapePerks.GREAT_AIM, player) && RandomFunction.roll(20)) {
addItemOrDrop(player, item.id, item.amount)
sendMessage(player, colorize("%RYour expert aim catches you a second fish."))
}
addItemOrDrop(player, item.id, item.amount)
player.incrementAttribute("$STATS_BASE:$STATS_FISH")
rewardXP(player, Skills.FISHING, fish.experience)
delayClock(player, Clocks.SKILLING, 5)
if (!checkRequirements(player, op, node))
return clearScripts(player)
}
addItemOrDrop(player, item.id, item.amount)
player.incrementAttribute("$STATS_BASE:$STATS_FISH")
rewardXP(player, Skills.FISHING, fish.experience)
setCurrentScriptState(player, 1)
return keepRunning(player)
}
@@ -12,6 +12,7 @@ import core.game.container.impl.EquipmentContainer
import core.game.event.ResourceProducedEvent
import core.game.interaction.IntType
import core.game.interaction.InteractionListener
import core.game.interaction.Clocks
import core.game.node.Node
import core.game.node.entity.impl.Projectile
import core.game.node.entity.player.Player
@@ -56,79 +57,79 @@ class WoodcuttingListener : InteractionListener {
val tool = SkillingTool.getHatchet(player)
if (!finishedMoving(player))
return true
return restartScript(player)
if (state == 0) {
if (!checkWoodcuttingRequirements(player, resource, node)) {
return clearScripts(player)
}
sendMessage(player, "You swing your axe at the tree...")
}
if (state == 1) {
if (!checkWoodcuttingRequirements(player, resource, node)) {
player.scripts.reset()
return true
}
}
if (clockReady(player, Clocks.SKILLING)) {
animateWoodcutting(player)
if (!checkReward(player, resource, tool))
return delayClock(player, Clocks.SKILLING, 3)
animateWoodcutting(player)
if (!checkReward(player, resource, tool))
return delayScript(player, 3)
if (tool.id == Items.INFERNO_ADZE_13661 && RandomFunction.roll(4)) {
sendMessage(player, "You chop some logs. The heat of the inferno adze incinerates them.")
Projectile.create(
player, null,
1776,
35, 30,
20, 25
).transform(
player,
player.location.transform(2, 0, 0),
true,
25, 25
).send()
delayScript(player, 3)
return rollDepletion(player, node.asScenery(), resource)
}
val reward = resource.getReward()
val rewardAmount: Int
if (reward > 0) {
rewardAmount = calculateRewardAmount(player, reward) // calculate amount
SkillingPets.checkPetDrop(player, SkillingPets.BEAVER) // roll for pet
//add experience
val experience: Double = calculateExperience(player, resource, rewardAmount)
player.getSkills().addExperience(Skills.WOODCUTTING, experience, true)
//send the message for the resource reward
if (resource == WoodcuttingNode.DRAMEN_TREE) {
player.packetDispatch.sendMessage("You cut a branch from the Dramen tree.")
} else {
player.packetDispatch.sendMessage("You get some " + ItemDefinition.forId(reward).name.lowercase(Locale.getDefault()) + ".")
if (tool.id == Items.INFERNO_ADZE_13661 && RandomFunction.roll(4)) {
sendMessage(player, "You chop some logs. The heat of the inferno adze incinerates them.")
Projectile.create(
player, null,
1776,
35, 30,
20, 25
).transform(
player,
player.location.transform(2, 0, 0),
true,
25, 25
).send()
delayClock(player, Clocks.SKILLING, 3)
return rollDepletion(player, node.asScenery(), resource)
}
//give the reward
player.inventory.add(Item(reward, rewardAmount))
player.dispatch(ResourceProducedEvent(reward, rewardAmount, node, -1))
var cutLogs = player.getAttribute("$STATS_BASE:$STATS_LOGS", 0)
player.setAttribute("/save:$STATS_BASE:$STATS_LOGS", ++cutLogs)
val reward = resource.getReward()
val rewardAmount: Int
if (reward > 0) {
rewardAmount = calculateRewardAmount(player, reward) // calculate amount
SkillingPets.checkPetDrop(player, SkillingPets.BEAVER) // roll for pet
//calculate bonus bird nest for mining
val chance = 282
if (RandomFunction.random(chance) == chance / 2) {
if (isActive(SkillcapePerks.NEST_HUNTER, player)) {
if (!player.inventory.add(BirdNest.getRandomNest(false).nest)) {
//add experience
val experience: Double = calculateExperience(player, resource, rewardAmount)
player.getSkills().addExperience(Skills.WOODCUTTING, experience, true)
//send the message for the resource reward
if (resource == WoodcuttingNode.DRAMEN_TREE) {
player.packetDispatch.sendMessage("You cut a branch from the Dramen tree.")
} else {
player.packetDispatch.sendMessage("You get some " + ItemDefinition.forId(reward).name.lowercase(Locale.getDefault()) + ".")
}
//give the reward
player.inventory.add(Item(reward, rewardAmount))
player.dispatch(ResourceProducedEvent(reward, rewardAmount, node, -1))
var cutLogs = player.getAttribute("$STATS_BASE:$STATS_LOGS", 0)
player.setAttribute("/save:$STATS_BASE:$STATS_LOGS", ++cutLogs)
//calculate bonus bird nest for mining
val chance = 282
if (RandomFunction.random(chance) == chance / 2) {
if (isActive(SkillcapePerks.NEST_HUNTER, player)) {
if (!player.inventory.add(BirdNest.getRandomNest(false).nest)) {
BirdNest.drop(player)
}
} else {
BirdNest.drop(player)
}
} else {
BirdNest.drop(player)
}
}
}
delayScript(player, 3)
rollDepletion(player, node.asScenery(), resource)
setCurrentScriptState(player, 1)
delayClock(player, Clocks.SKILLING, 3)
rollDepletion(player, node.asScenery(), resource)
if (!checkWoodcuttingRequirements(player, resource, node)) {
return clearScripts(player)
}
}
return keepRunning(player)
}
+22
View File
@@ -2346,6 +2346,28 @@ fun queueScript(entity: Entity, delay: Int = 1, strength: QueueStrength = QueueS
entity.scripts.addToQueue(s, strength)
}
/**
* Sets the clock to the value of WORLD_TICSK + ticks.
* @param entity the entity whose clock we are updating
* @param clock the clock we are updating. Please use [core.game.interaction.Clocks] for this argument.
* @param ticks the number of ticks to delay by
* @return always returns false so this can be used as a script return value.
**/
fun delayClock(entity: Entity, clock: Int, ticks: Int) : Boolean {
entity.clocks[clock] = getWorldTicks() + ticks
return false
}
/**
* Checks if a clock is ready (have we elapsed any delay put into it)
* @param entity the entity whose clock we are checking
* @param clock the clock we are checking. Please use [core.game.interaction.Clocks] for this argument.
* @return true if we have elapsed the clock's wait
**/
fun clockReady(entity: Entity, clock: Int) : Boolean {
return entity.clocks[clock] <= getWorldTicks()
}
fun delayAttack(entity: Entity, ticks: Int) {
entity.properties.combatPulse.delayNextAttack(3)
entity.clocks[Clocks.NEXT_ATTACK] = getWorldTicks() + ticks
@@ -8,4 +8,5 @@ object Clocks {
@JvmStatic val NEXT_DRINK = 4
@JvmStatic val NEXT_ATTACK = 5
@JvmStatic val STUN = 6
@JvmStatic val SKILLING = 7
}
@@ -13,6 +13,7 @@ import core.game.world.map.path.Pathfinder
import core.tools.Log
import core.tools.SystemLogger
import java.lang.Integer.max
import java.io.*
class ScriptProcessor(val entity: Entity) {
private var apScript: Script<*>? = null
@@ -182,6 +183,10 @@ class ScriptProcessor(val entity: Entity) {
}
fun processInteractScript(script: Script<*>) {
if (interactTarget == null || !interactTarget!!.isActive) {
log(this::class.java, Log.FINE, "Interact target $interactTarget no longer active, cancelling interaction.")
reset()
}
if (script.nextExecution < GameWorld.ticks) {
val finished = executeScript(script)
script.state++
@@ -193,11 +198,19 @@ class ScriptProcessor(val entity: Entity) {
fun executeScript(script: Script<*>) : Boolean {
currentScript = script
when (script) {
is Interaction -> return script.execution.invoke(entity as? Player ?: return true, interactTarget ?: return true, script.state)
is UseWithInteraction -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state)
is QueuedScript -> return script.execution.invoke(script.state)
is QueuedUseWith -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state)
try {
when (script) {
is Interaction -> return script.execution.invoke(entity as? Player ?: return true, interactTarget ?: return true, script.state)
is UseWithInteraction -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state)
is QueuedScript -> return script.execution.invoke(script.state)
is QueuedUseWith -> return script.execution.invoke(entity as? Player ?: return true, script.used, script.with, script.state)
}
} catch (e: Exception) {
val sw = StringWriter()
val pw = PrintWriter(sw)
e.printStackTrace(pw)
log(this::class.java, Log.ERR, "Error processing ${script::class.java.simpleName} - stopping the script. Exception follows: $sw")
reset()
}
currentScript = null
return true
@@ -243,6 +256,8 @@ class ScriptProcessor(val entity: Entity) {
}
fun setInteractionScript(target: Node, script: Script<*>?) {
if (apScript != null && script != null && script.execution == apScript!!.execution) return
if (opScript != null && script != null && script.execution == opScript!!.execution) return
reset()
interactTarget = target
if (script != null) {
@@ -14,7 +14,7 @@ import core.tools.SystemLogger
import core.game.world.GameWorld
import core.game.world.repository.Repository
import core.tools.Log
import java.io.FileReader
import java.io.*
import java.nio.ByteBuffer
class GroundSpawnLoader {
@@ -26,14 +26,22 @@ class GroundSpawnLoader {
reader = FileReader(ServerConstants.CONFIG_PATH + "ground_spawns.json")
var configs = parser.parse(reader) as JSONArray
for(config in configs){
val e = config as JSONObject
val datas = e["loc_data"].toString().split("-")
val id = e["item_id"].toString().toInt()
for(d in datas){
val tokens = d.replace("{", "").replace("}", "").split(",".toRegex()).toTypedArray()
val spawn = GroundSpawn(tokens[4].toInt(), Item(id, tokens[0].toInt()), Location(Integer.valueOf(tokens[1]), Integer.valueOf(tokens[2]), Integer.valueOf(tokens[3])))
spawn.init()
count++
try {
val e = config as JSONObject
val datas = e["loc_data"].toString().split("-")
val id = e["item_id"].toString().toInt()
for(d in datas){
if (d.isNullOrEmpty()) continue
val tokens = d.replace("{", "").replace("}", "").split(",".toRegex()).toTypedArray()
val spawn = GroundSpawn(tokens[4].toInt(), Item(id, tokens[0].toInt()), Location(Integer.valueOf(tokens[1]), Integer.valueOf(tokens[2]), Integer.valueOf(tokens[3])))
spawn.init()
count++
}
} catch (e: Exception) {
val sw = StringWriter()
val pw = PrintWriter(sw)
e.printStackTrace(pw)
log(this::class.java, Log.ERR, "Error parsing config entry ${config.toString()}: $sw")
}
}
log(this::class.java, Log.FINE, "Initialized $count ground items.")
@@ -102,7 +102,7 @@ class DisconnectionQueue {
* Clears the queue.
*/
fun clear() {
for (entry in queue.values) {
for (entry in queue.values.toTypedArray()) {
finish(entry, true)
}
queue.clear()