Implement Authentic Interaction Subsystem

Implemented authentic script/interaction queues
This does now mean we have a total of 3 interaction systems, but this additional system is necessary to fix certain categories of bug and implement some authentic features
Converted mining to new system
Converted fishing to new system
Converted woodcutting to new system
Provided an example of soft-queued scripts with GrandTreePodListener
Implemented tick-eating (it is now possible to eat a shark, drink a potion, and eat a karambwan all on the same tick)
Can now eat and drop items while stunned
This commit is contained in:
Ceikry
2023-02-28 23:41:14 +00:00
committed by Ryan
parent 5206c99151
commit 124eeab893
61 changed files with 1466 additions and 293 deletions
@@ -0,0 +1,11 @@
package core.game.interaction
object Clocks {
@JvmStatic val MOVEMENT = 0
@JvmStatic val ANIMATION_END = 1
@JvmStatic val NEXT_EAT = 2
@JvmStatic val NEXT_CONSUME = 3
@JvmStatic val NEXT_DRINK = 4
@JvmStatic val NEXT_ATTACK = 5
@JvmStatic val STUN = 6
}
@@ -20,7 +20,7 @@ import core.game.world.GameWorld;
* Handles interaction between nodes.
* @author Emperor
*/
public class Interaction {
public class InteractPlugin {
/**
* The current options.
@@ -41,7 +41,7 @@ public class Interaction {
* Constructs a new {@code Interaction} {@code Object}.
* @param node The node reference.
*/
public Interaction(Node node) {
public InteractPlugin(Node node) {
this.node = node;
}
@@ -23,7 +23,7 @@ interface InteractionListener : ContentInterface{
fun on(ids: IntArray, type: IntType, vararg option: String, handler: (player: Player, node: Node) -> Boolean){
InteractionListeners.add(ids, type.ordinal, option, handler)
}
fun on(option: String, type: IntType, handler: (player: Player, node: Node) -> Boolean){
@Deprecated("Don't use") fun on(option: String, type: IntType, handler: (player: Player, node: Node) -> Boolean){
InteractionListeners.add(option, type.ordinal, handler)
}
fun on(type: IntType, vararg option: String, handler: (player: Player, node: Node) -> Boolean){
@@ -82,5 +82,16 @@ interface InteractionListener : ContentInterface{
InteractionListeners.instantClasses.add(name)
}
fun defineInteraction(type: IntType, ids: IntArray, vararg options: String, persistent: Boolean = false, allowedDistance: Int = 1, handler: (player: Player, node: Node, state: Int) -> Boolean) {
InteractionListeners.addMetadata(ids, type, options, InteractionMetadata(handler, allowedDistance, persistent))
}
fun defineInteraction(type: IntType, vararg options: String, persist: Boolean = false, allowedDistance: Int = 1, handler: (player: Player, node: Node, state: Int) -> Boolean) {
InteractionListeners.addGenericMetadata(options, type, InteractionMetadata(handler, allowedDistance, persist))
}
data class InteractionMetadata(val handler: (player: Player, node: Node, state: Int) -> Boolean, val distance: Int, val persist: Boolean)
data class UseWithMetadata(val handler: (player: Player, used: Node, with: Node, state: Int) -> Boolean, val distance: Int, val persist: Boolean)
fun defineListeners()
}
@@ -1,5 +1,7 @@
package core.game.interaction
import core.api.forceWalk
import core.api.queueScript
import core.game.event.InteractionEvent
import core.game.event.UseWithEvent
import core.game.node.Node
@@ -14,6 +16,8 @@ object InteractionListeners {
private val useWithWildcardListeners = HashMap<Int, ArrayList<Pair<(Int, Int) -> Boolean, (Player, Node, Node) -> Boolean>>>(10)
private val destinationOverrides = HashMap<String,(Entity, Node) -> Location>(100)
private val equipListeners = HashMap<String,(Player,Node) -> Boolean>(10)
private val interactions = HashMap<String, InteractionListener.InteractionMetadata>()
private val useWithInteractions = HashMap<String, InteractionListener.UseWithMetadata>()
val instantClasses = HashSet<String>()
@JvmStatic
@@ -232,10 +236,25 @@ object InteractionListeners {
if(player.locks.isInteractionLocked) return false
val method = get(id,type.ordinal,option) ?: get(option,type.ordinal) ?: return false
val method = get(id,type.ordinal,option) ?: get(option,type.ordinal)
player.setAttribute("interact:option", option.lowercase())
player.dispatch(InteractionEvent(node, option.toLowerCase()))
if (method == null) {
val inter = interactions["${type.ordinal}:$id:${option.lowercase()}"] ?: interactions["${type.ordinal}:${option.lowercase()}"] ?: return false
val script = Interaction(inter.handler, inter.distance, inter.persist)
player.scripts.setInteractionScript(node, script)
player.pulseManager.run(object : MovementPulse(player, node, flag) {
override fun pulse(): Boolean {
return true
}
})
return true
}
val destOverride = getOverride(type.ordinal, id, option) ?: getOverride(type.ordinal,node.id) ?: getOverride(type.ordinal,option.toLowerCase())
player.setAttribute("interact:option", option)
if(option.toLowerCase() == "attack") //Attack needs special handling >.>
{
@@ -250,14 +269,12 @@ object InteractionListeners {
override fun pulse(): Boolean {
if(player.zoneMonitor.interact(node, Option(option, 0))) return true
player.faceLocation(node.location)
player.dispatch(InteractionEvent(node, option.toLowerCase()))
method.invoke(player,node)
return true
}
})
} else {
method.invoke(player,node)
player.dispatch(InteractionEvent(node, option.toLowerCase()))
}
return true
}
@@ -285,4 +302,29 @@ object InteractionListeners {
val className = handler.javaClass.name.substringBefore("$")
return instantClasses.contains(className)
}
fun addMetadata (ids: IntArray, type: IntType, options: Array<out String>, metadata: InteractionListener.InteractionMetadata) {
for (id in ids)
for (opt in options)
interactions["${type.ordinal}:$id:${opt.lowercase()}"] = metadata
}
fun addMetadata (id: Int, type: IntType, options: Array<out String>, metadata: InteractionListener.InteractionMetadata) {
for (opt in options)
interactions["${type.ordinal}:$id:${opt.lowercase()}"] = metadata
}
fun addGenericMetadata (options: Array<out String>, type: IntType, metadata: InteractionListener.InteractionMetadata) {
for (opt in options)
interactions["${type.ordinal}:$opt"] = metadata
}
fun addMetadata (used: Int, with: IntArray, type: IntType, metadata: InteractionListener.UseWithMetadata) {
for (id in with)
useWithInteractions["${type.ordinal}:$used:$with"] = metadata
}
fun addMetadata (used: Int, with: Int, type: IntType, metadata: InteractionListener.UseWithMetadata) {
useWithInteractions["${type.ordinal}:$used:$with"] = metadata
}
}
@@ -0,0 +1,27 @@
package core.game.interaction
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.player.Player
import core.game.world.GameWorld
typealias UseWithExecutor = (Player, Node, Node, Int) -> Boolean
typealias InteractExecutor = (Player, Node, Int) -> Boolean
typealias VoidExecutor = (Int) -> Boolean
enum class QueueStrength {
WEAK,
NORMAL,
STRONG,
SOFT
}
open class Script<T> (val execution: T, val persist: Boolean) {
var state: Int = 0
var nextExecution = 0
}
class Interaction(execution: InteractExecutor, val distance: Int, persist: Boolean) : Script<InteractExecutor>(execution, persist)
class UseWithInteraction(execution: UseWithExecutor, val distance: Int, persist: Boolean, val used: Node, val with: Node) : Script<UseWithExecutor>(execution, persist)
class QueuedScript(executor: VoidExecutor, val strength: QueueStrength, persist: Boolean) : Script<VoidExecutor>(executor, persist)
class QueuedUseWith(executor: UseWithExecutor, val strength: QueueStrength, persist: Boolean, val used: Node, val with: Node) : Script<UseWithExecutor>(executor, persist)
@@ -0,0 +1,295 @@
package core.game.interaction
import core.api.*
import core.game.node.Node
import core.game.node.entity.Entity
import core.game.node.entity.npc.NPC
import core.game.node.entity.player.Player
import core.game.node.item.GroundItem
import core.game.node.scenery.Scenery
import core.game.world.GameWorld
import core.game.world.map.Location
import core.game.world.map.path.Pathfinder
import core.tools.SystemLogger
import java.lang.Integer.max
class ScriptProcessor(val entity: Entity) {
private var apScript: Script<*>? = null
private var opScript: Script<*>? = null
private var interactTarget: Node? = null
private var currentScript: Script<*>? = null
private val queue = ArrayList<Script<*>>()
var delay = 0
var interacted = false
var apRangeCalled = false
var apRange = 10
var persistent = false
var targetDestination: Location? = null
fun preMovement() {
var allSkipped = false
while (!allSkipped) {
allSkipped = processQueue()
}
if (isStunned(entity)) return
if (entity.delayed()) return
var canProcess = !entity.delayed()
if (entity is Player)
canProcess = canProcess && !entity.interfaceManager.isOpened && !entity.interfaceManager.hasChatbox()
if (entity !is Player) return
if (!entity.delayed() && canProcess && interactTarget != null) {
if (opScript != null && inOperableDistance()) {
face(entity, interactTarget?.centerLocation ?: return)
processInteractScript(opScript ?: return)
}
else if (apScript != null && inApproachDistance(apScript ?: return)) {
face(entity, interactTarget?.centerLocation ?: return)
processInteractScript(apScript ?: return)
}
else if (apScript == null && opScript == null && inOperableDistance()) {
sendMessage(entity, "Nothing interesting happens.")
}
}
}
fun postMovement(didMove: Boolean) {
if (didMove)
entity.clocks[Clocks.MOVEMENT] = GameWorld.ticks + 1
var canProcess = !entity.delayed()
if (entity is Player)
canProcess = canProcess && !entity.interfaceManager.isOpened && !entity.interfaceManager.hasChatbox()
if (entity !is Player) return
if (!entity.delayed() && canProcess && interactTarget != null && !interacted) {
if (opScript != null && inOperableDistance()) {
face(entity, interactTarget?.centerLocation ?: return)
processInteractScript(opScript ?: return)
}
else if (apScript != null && inApproachDistance(apScript ?: return)) {
face(entity, interactTarget?.centerLocation ?: return)
processInteractScript(apScript ?: return)
}
else if (apScript == null && opScript == null && inOperableDistance()) {
sendMessage(entity, "Nothing interesting happens.")
}
}
if (canProcess && (apScript != null || opScript != null)) {
if (!interacted && !didMove) {
sendMessage(entity, "I can't reach that!")
reset()
}
}
if (interacted && !apRangeCalled && !persistent) reset()
if (interactTarget != null && interactTarget?.isActive != true) reset()
}
fun processQueue() : Boolean {
var strongInQueue = false
var softInQueue = false
var anyExecuted = false
for (i in 0 until queue.size) {
val script = queue[i]
if (script is QueuedScript && script.strength == QueueStrength.STRONG)
strongInQueue = true
if (script is QueuedUseWith && script.strength == QueueStrength.STRONG)
strongInQueue = true
if (script is QueuedScript && script.strength == QueueStrength.SOFT)
softInQueue = true
if (script is QueuedUseWith && script.strength == QueueStrength.SOFT)
softInQueue = true
}
if (softInQueue) {
removeWeakScripts()
removeNormalScripts()
if (entity is Player) {
entity.interfaceManager.close()
entity.interfaceManager.closeChatbox()
entity.dialogueInterpreter.close()
}
}
if (strongInQueue) {
removeWeakScripts()
if (entity is Player) {
entity.interfaceManager.close()
entity.interfaceManager.closeChatbox()
entity.dialogueInterpreter.close()
}
}
val toRemove = ArrayList<Script<*>>()
for (i in 0 until queue.size) {
when (val script = queue[i]) {
is QueuedScript -> {
if (entity.delayed() && script.strength != QueueStrength.SOFT)
continue
if (script.nextExecution > GameWorld.ticks)
continue
if ((script.strength == QueueStrength.STRONG || script.strength == QueueStrength.SOFT) && entity is Player) {
entity.interfaceManager.close()
entity.interfaceManager.closeChatbox()
entity.dialogueInterpreter.close()
}
script.nextExecution = GameWorld.ticks + 1
val finished = executeScript(script)
script.state++
if (finished && !script.persist)
toRemove.add(script)
else if (finished)
script.state = 0
anyExecuted = true
}
is QueuedUseWith -> {
if (entity.delayed() && script.strength != QueueStrength.SOFT)
continue
if (entity !is Player) {
toRemove.add(script)
SystemLogger.logErr(this::class.java, "Tried to queue an item UseWith interaction for a non-player!")
continue
}
if (script.nextExecution > GameWorld.ticks)
continue
if ((script.strength == QueueStrength.STRONG || script.strength == QueueStrength.SOFT)) {
entity.interfaceManager.close()
entity.interfaceManager.closeChatbox()
entity.dialogueInterpreter.close()
}
script.nextExecution = GameWorld.ticks + 1
val finished = executeScript(script)
script.state++
if (finished && !script.persist)
toRemove.add(script)
else if (finished)
script.state = 0
anyExecuted = true
}
}
}
queue.removeAll(toRemove.toSet())
return !anyExecuted
}
fun isPersist (script: Script<*>) : Boolean {
return script.persist
}
fun processInteractScript(script: Script<*>) {
if (script.nextExecution < GameWorld.ticks) {
val finished = executeScript(script)
script.state++
if (finished && isPersist(script))
script.state = 0
interacted = true
}
}
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)
}
currentScript = null
return true
}
fun removeWeakScripts() {
queue.removeAll(queue.filter { it is QueuedScript && it.strength == QueueStrength.WEAK || it is QueuedUseWith && it.strength == QueueStrength.WEAK }.toSet())
}
fun removeNormalScripts() {
queue.removeAll(queue.filter { it is QueuedScript && it.strength == QueueStrength.NORMAL || it is QueuedUseWith && it.strength == QueueStrength.NORMAL }.toSet())
}
fun inApproachDistance(script: Script<*>) : Boolean {
val distance = when (script) {
is Interaction -> script.distance
is UseWithInteraction -> script.distance
else -> 10
}
targetDestination?.let {
return it.location.getDistance(entity.location) <= distance && hasLineOfSight(entity, it)
}
return false
}
fun inOperableDistance() : Boolean {
targetDestination?.let {
return it.cardinalTiles.any {loc -> loc == entity.location} && hasLineOfSight(entity, it)
}
return false
}
fun reset() {
apScript = null
opScript = null
apRangeCalled = false
interacted = false
apRange = 10
interactTarget = null
persistent = false
targetDestination = null
resetAnimator(entity as? Player ?: return)
}
fun setInteractionScript(target: Node, script: Script<*>?) {
reset()
interactTarget = target
if (script != null) {
apRange = when(script) {
is Interaction -> script.distance
is UseWithInteraction -> script.distance
else -> 10
}
persistent = script.persist
if (apRange == -1)
opScript = script
else
apScript = script
targetDestination = when (interactTarget) {
is NPC -> DestinationFlag.ENTITY.getDestination(entity, interactTarget)
is Scenery -> {
val path = Pathfinder.find(entity, interactTarget).points.lastOrNull()
if (path == null) {
clearScripts(entity)
return
}
Location.create(path.x, path.y, entity.location.z)
}
is GroundItem -> DestinationFlag.ITEM.getDestination(entity, interactTarget)
else -> target.location
}
}
}
fun addToQueue(script: Script<*>, strength: QueueStrength) {
if (script !is QueuedScript && script !is QueuedUseWith) {
SystemLogger.logErr(this::class.java, "Tried to queue ${script::class.java.simpleName} as a queueable script but it's not!")
return
}
if (strength == QueueStrength.STRONG && entity is Player) {
entity.interfaceManager.close()
entity.interfaceManager.closeChatbox()
entity.dialogueInterpreter.close()
}
script.nextExecution = max(GameWorld.ticks + 1, script.nextExecution)
queue.add(script)
}
fun getActiveScript() : Script<*>? {
return currentScript ?: getActiveInteraction()
}
private fun getActiveInteraction() : Script<*>? {
return opScript ?: apScript
}
}