317 Is this how I should build an Action System?

Elysian

Programmer
Dec 6, 2018
172
43
0
So for the past couple of days I've been trying to look into how RuneScape handles various "Actions" or "Tasks" not sure which term to use but an example of these things would be
- Equipping armour (unequipping as well(?))
- Opening a door
- Burying bones
- Eating food

And I'm not sure I got the right idea.

My observations from the actual game is that..
- I can equip multiple items on tick 1 and they all get equipped on tick 2. (certain "actions" of the same "type" can be batch processed?)

- I can perform certain "actions" in 1 tick and they get processed the next tick
e.g: (Equip item -> click on door) and they both process the next tick

- I can't perform certain "actions" in 1 tick and they get processed the next tick
e.g: (Click on door -> equip item) but only the equip will process (guessing equip item has some sort of higher priority?)


Based on my understanding here's what seems to be going on.
RuneScape dictates certain rules for certain actions.
For instance, you're not allowed to bury more than 1 bone per I think 2 ticks, not sure if there is an override or just upright ignore the second action, either way it would have the same effect.
You can queue multiple actions on tick 1 and they will execute together, unless one of the actions has a super high priority (which clears the rest?)
e.g: these were all queued the same tick
-> BURY BONE (LOW)
-> EAT FOOD (LOW)
-> EQUIP ITEM (HIGH)
-> OPEN DOOR (LOW)

The next tick it would only process everything up until (and including) the HIGH one and discard the rest?

Now this is all based off of my observations in-game as well as a brief discussion on Discord.
But am I on the right track here or am I completely off?

This is the structure of how everything should flow in my head at least.
C#:
public static void Process()
{
    /* 1. Fetch Data */
    CollectPlayerPackets(); /* <- Reads data from the client */
    ProcessPackets(); /* <- Creates "Actions" based off of the data */
    /* 2. Process Actions / Interactions */
    ProcessActions();
 
    /* 3. Process World Updates (Spawn Ground Items etc.) */
    /* 4. Process NPC Movement */
    /* 5. Process Player Movement */
    /* 6. Combat */
 
    /* 7. Client Visual Updates */
    PlayerUpdateManager.Update();
 
    /* 8. Flush and Reset */
    FlushAllPlayers();
    Reset();
}


Here's a basic implementation I've done to handle the limitation of actions that the player can perform.
I've not implemeted any sort of ActionPriority yet, because I wanted to make sure that what I had for now was at least on the right track.

https://github.com/PtrStruct/Genesis

It works with James 317 refactored client if you want to boot it up, just remember to grab the 317 cache from OpenRS Archive.
https://github.com/Jameskmonger/317refactor

And as a sidenode, in my head, the term "Event" is just an action which loops, for instance, fletching a log into a bow, but done over the course of all the logs in your inventory.
Also, no idea how this all ties together with animation stalling etc (if it even does lol)

Feel free to correct me as much as you want, I'm here to learn so the more information the better! :))

Visual representation of the current implementation
 
Last edited:
  • Like
Reactions: Saturated
Bit off with your assumptions there. There's no priority system in place as you seem to think.

What really happens is that some things execute instantly (e.g. eating food, equipping/unequipping armour, burying a bone) during that packet processing, while other things, like opening a door, set up an interaction that is intended to run later on, once player has walked within range of the object and so on. The instant actions often (but not always) clear your interactions when they execute, so that's why you can't click a door and then eat a piece of food. Any click inside your inventory actually just clears interactions from the engine's perspective, it's a general thing that they can't really control in a per-script basis. However, some other interfaces, like your equipment tab, do not have this implicit clearing, so that's why you can unequip items after clicking on a door, without it clearing the interaction.

Some general rules for these behaviours. It might not always hold up as exceptions do happen in some cases, but it should get you on the right path:
1. Any click on another player, an NPC, an item on the ground, or a map object will result in an interaction being set up. This means a path will be calculated to that target, and the script will invoke when you finally get in range. It will never instantly trigger from the packet handler. It's fairly late in a player's processing block.
2. Any click on an item in your inventory closes interfaces, clears any interactions, path etc. These are processed instantly as the packet gets iterated over. If you have an inventory click, followed by you opening a door, the game will not even be aware of the "open the door" bit that is coming afterwards.
3. Any interface that closes automatically as you attempt to walk is considered a modal interface. These have what's called "protected access" and can modify anything about your character. Any interface that does not close as you attempt to walk, is an overlay, and will often require gaining protected access in order to do anything. This is why you so often see messages like "Please finish what you're doing first." or anything of that nature. That's just a common fallback for when an overlay interface fails to gain protected access. Protected access has some implications, such as the player cannot be delayed during it, and can't have another modal interface open during it (outside of the one calling it).

Now, as for bone burying, this actually uses a queue in a non-traditional sense to throttle you burying bones. When you bury a bone, a strong queue is placed to execute one tick later, which sends the second half of the bone burying message. If you're not familiar with what I'm saying, go on OSRS and bury a bone. One message will send instantly the moment the packet is processed, the second message comes one tick later. They use the presence of the second queue to determine whether you can bury another bone. If the queue exists, the script just does nothing (think of it as an early return). This is why you can only bury one bone per two ticks.

What happens is:
Tick 0: First attempt at burying the bone, item is removed from your inventory, first message is sent, second message is queued. Any further attempts to click on bones fail because you have that message queued up.
Tick 1: Because packet processing is still so early on in the tick, any attempts to bury the bone still fail here. It's because the queue will run later on during player processing, not at this stage, so as far as the script is concerned, the queue still exists. It gets processed later this same cycle.
Tick 2: No more queue, you can bury a bone again. Rinse and repeat.

Furthermore, burying the bone clears any steps/path you previously had, so it might look like it's another one of the delays you might see across all the rest of the game, but this is not the case. One can bury the bone and then click to walk, which will end up having you bury the bone while also walking in the same cycle. You aren't actually delayed (rsps term for this is locked) from doing other stuff during it.
 
Bit off with your assumptions there. There's no priority system in place as you seem to think.

What really happens is that some things execute instantly (e.g. eating food, equipping/unequipping armour, burying a bone) during that packet processing, while other things, like opening a door, set up an interaction that is intended to run later on, once player has walked within range of the object and so on. The instant actions often (but not always) clear your interactions when they execute, so that's why you can't click a door and then eat a piece of food. Any click inside your inventory actually just clears interactions from the engine's perspective, it's a general thing that they can't really control in a per-script basis. However, some other interfaces, like your equipment tab, do not have this implicit clearing, so that's why you can unequip items after clicking on a door, without it clearing the interaction.

Some general rules for these behaviours. It might not always hold up as exceptions do happen in some cases, but it should get you on the right path:
1. Any click on another player, an NPC, an item on the ground, or a map object will result in an interaction being set up. This means a path will be calculated to that target, and the script will invoke when you finally get in range. It will never instantly trigger from the packet handler. It's fairly late in a player's processing block.
2. Any click on an item in your inventory closes interfaces, clears any interactions, path etc. These are processed instantly as the packet gets iterated over. If you have an inventory click, followed by you opening a door, the game will not even be aware of the "open the door" bit that is coming afterwards.
3. Any interface that closes automatically as you attempt to walk is considered a modal interface. These have what's called "protected access" and can modify anything about your character. Any interface that does not close as you attempt to walk, is an overlay, and will often require gaining protected access in order to do anything. This is why you so often see messages like "Please finish what you're doing first." or anything of that nature. That's just a common fallback for when an overlay interface fails to gain protected access. Protected access has some implications, such as the player cannot be delayed during it, and can't have another modal interface open during it (outside of the one calling it).

Now, as for bone burying, this actually uses a queue in a non-traditional sense to throttle you burying bones. When you bury a bone, a strong queue is placed to execute one tick later, which sends the second half of the bone burying message. If you're not familiar with what I'm saying, go on OSRS and bury a bone. One message will send instantly the moment the packet is processed, the second message comes one tick later. They use the presence of the second queue to determine whether you can bury another bone. If the queue exists, the script just does nothing (think of it as an early return). This is why you can only bury one bone per two ticks.

What happens is:
Tick 0: First attempt at burying the bone, item is removed from your inventory, first message is sent, second message is queued. Any further attempts to click on bones fail because you have that message queued up.
Tick 1: Because packet processing is still so early on in the tick, any attempts to bury the bone still fail here. It's because the queue will run later on during player processing, not at this stage, so as far as the script is concerned, the queue still exists. It gets processed later this same cycle.
Tick 2: No more queue, you can bury a bone again. Rinse and repeat.

Furthermore, burying the bone clears any steps/path you previously had, so it might look like it's another one of the delays you might see across all the rest of the game, but this is not the case. One can bury the bone and then click to walk, which will end up having you bury the bone while also walking in the same cycle. You aren't actually delayed (rsps term for this is locked) from doing other stuff during it.
Wow, I couldn’t have asked for a better answer! This actually validates some of the theories I was considering today.
I noticed that the behavior I was experiencing seemed to occur when I tried actions like eating or burying bones while interacting with world objects, but I wasn’t entirely sure—it was all just speculation. Thank you so much for confirming it!

My current implementation seems like a step in the right direction then, it just needs a few more adjustments!
One thing that had me curious was
"Now, as for bone burying, this actually uses a queue in a non-traditional sense to throttle you burying bones."
The way I'm doing it is just stages, where each stage is 1 tick, and this can be controlled by just incrementing the ticks on invocation if you need to wait for longer periods of time, but yeah, could definitely use some changes so it's easier to maintain in the long run.

Example
https://github.com/PtrStruct/Genesis/blob/master/Genesis/Actions/UserActions/BuryAction.cs

Again, thank you for all the information!
 
that won’t work, you have to implement the queue system with the different queue types and their effects, using their LinkList implementation due to a speed up bug in their code. when a server does shutdown, the world tick is changed from 600ms to 1ms and constantly tries to finish player queues before logging them out. you are able to have multiple queues stacked up on your player that’s how Rendi does some of his quest xp tricks. Also food eating is done through a varp, which is not a queue.
 
Wow, I couldn’t have asked for a better answer! This actually validates some of the theories I was considering today.
I noticed that the behavior I was experiencing seemed to occur when I tried actions like eating or burying bones while interacting with world objects, but I wasn’t entirely sure—it was all just speculation. Thank you so much for confirming it!

My current implementation seems like a step in the right direction then, it just needs a few more adjustments!
One thing that had me curious was

The way I'm doing it is just stages, where each stage is 1 tick, and this can be controlled by just incrementing the ticks on invocation if you need to wait for longer periods of time, but yeah, could definitely use some changes so it's easier to maintain in the long run.

Example
https://github.com/PtrStruct/Genesis/blob/master/Genesis/Actions/UserActions/BuryAction.cs

Again, thank you for all the information!
There are many ways you could implement that specific aspect of the game, but my recommendation will always be to try to emulate the actual core mechanics - in this case, the queue system as a whole. You'll always be fighting an uphill battle if you don't do it, having to come up with work-arounds every time your own implementation fails to accomplish the same results. It's kinda what has plagued all RSPS for the past two decades.

As such, my recommendation is just start off with a simple queue system. You can think of it as just:
Code:
data class QueuedScript(val name: String, val executeCycle: Int, val block: Runnable)
Where the executeCycle is the game cycle value on which the runnable is meant to be invoked. E.g. if our current cycle is 50 and we queue something to execute 1 tick in the future, executeCycle would be set to 51. On cycle 51, it would invoke the runnable.

People often fear the word queues when it comes to RS, but this simple system will already cover a solid 95%+ of use cases that RS has. It's a really simple system, essentially just a way to queue up a block of code to run in the future. A lot of servers already support something akin to this too, but most of them do it on a global scope, e.g. Matrix's WorldTasksManager - many other servers have such TaskManager classes as well. They accomplish the exact same thing, but they aren't coupled to specific players, which means they won't be impacted by PID (important for combat), and it becomes uncoupled in a bad way. One of the things RS does not allow you to do is log out while you have something queued on you (outside of "weak queues", which just get deleted at specific times). This becomes hard to accomplish when it's a global system, decoupled from the player.

In the future if you wish to improve the system, you can. OSRS has reworked the processing logic behind queues numerous times in the past few years. The fundamentals remain the same, just the way it gets iterated and processed, and the conditions behind whether to invoke a script differ. Starting with a simple system as I described is still accurate enough and doesn't require you to go and rewrite every use case of queues down the line.

If you need inspiration for what to aim for, I'd recommend some simple syntax like:
Code:
// Define the queue itself, store that in a hashmap on server boot or something like it.
strongqueue(Queues.BURYING_BONES) {
    // player is implicit receiver here, basically just equal to player.message("")
    message("... bone burying message")
}

// Queues.BURYING_BONES could just be a constant string. It helps not to use magic strings throughout the codebase, especially if you end up referring to it multiple times.

// Queueing it on the player (within bone burying script)
player.strongQueue(1, Queues.BURYING_BONES)

// And checking if the queue exists
player.hasQueue(Queues.BURYING_BONES)

Obviously this is in Kotlin, I've no clue what possibilities exist in a language of your choice, but the general principals are the same.

In general, I would recommend aiming for a more concise syntax over what you've currently proposed. Burying bones is really simple, it shouldn't 50 lines long, granted some of it is inevitable boilerplate, but:
Code:
// Once again, defines a script and stores it in a hashmap.
onItemOp(ItemId.BONES, "Bury") {
    if (hasQueue(Queues.BURYING_BONES) return
    anim(Animation.BURY_BONE)
    message("You dig a hole in the ground...")
    strongQueue(1, Queues.BURYING_BONES)
}

strongqueue(Queues.BURYING_BONES) {
    message("You bury the bones.")
}

^Just mimicing what you've already proposed. That's how little code it would be if we were to do it in kotlin via scripting. Other languages will likely require wrapping it in a class so that adds a bit, but you get the gist. Note that this is just an example, I'm not forcing anyone to do it like it, it's not quite like that in my own server either. It's just a starting point which I feel like will end up a lot nicer over time than the verbose implementation you've proposed.

I've also written more about queues here, however I would not recommend you try to implement it according to the documents there. I think it'll just scare you off. Best off start with a simple system you'll understand. The little nuances that come from the document can be done later on when you get comfortable with the system proposed here; first step is understanding what queues really are - it tends to trip people up quite a lot.
Long ramble, intended to post multiple times as I wrote this but I didn't think it was clear enough so I kept on adding examples :vet:
 
There are many ways you could implement that specific aspect of the game, but my recommendation will always be to try to emulate the actual core mechanics - in this case, the queue system as a whole. You'll always be fighting an uphill battle if you don't do it, having to come up with work-arounds every time your own implementation fails to accomplish the same results. It's kinda what has plagued all RSPS for the past two decades.

As such, my recommendation is just start off with a simple queue system. You can think of it as just:
Code:
data class QueuedScript(val name: String, val executeCycle: Int, val block: Runnable)
Where the executeCycle is the game cycle value on which the runnable is meant to be invoked. E.g. if our current cycle is 50 and we queue something to execute 1 tick in the future, executeCycle would be set to 51. On cycle 51, it would invoke the runnable.

People often fear the word queues when it comes to RS, but this simple system will already cover a solid 95%+ of use cases that RS has. It's a really simple system, essentially just a way to queue up a block of code to run in the future. A lot of servers already support something akin to this too, but most of them do it on a global scope, e.g. Matrix's WorldTasksManager - many other servers have such TaskManager classes as well. They accomplish the exact same thing, but they aren't coupled to specific players, which means they won't be impacted by PID (important for combat), and it becomes uncoupled in a bad way. One of the things RS does not allow you to do is log out while you have something queued on you (outside of "weak queues", which just get deleted at specific times). This becomes hard to accomplish when it's a global system, decoupled from the player.

In the future if you wish to improve the system, you can. OSRS has reworked the processing logic behind queues numerous times in the past few years. The fundamentals remain the same, just the way it gets iterated and processed, and the conditions behind whether to invoke a script differ. Starting with a simple system as I described is still accurate enough and doesn't require you to go and rewrite every use case of queues down the line.

If you need inspiration for what to aim for, I'd recommend some simple syntax like:
Code:
// Define the queue itself, store that in a hashmap on server boot or something like it.
strongqueue(Queues.BURYING_BONES) {
    // player is implicit receiver here, basically just equal to player.message("")
    message("... bone burying message")
}

// Queues.BURYING_BONES could just be a constant string. It helps not to use magic strings throughout the codebase, especially if you end up referring to it multiple times.

// Queueing it on the player (within bone burying script)
player.strongQueue(1, Queues.BURYING_BONES)

// And checking if the queue exists
player.hasQueue(Queues.BURYING_BONES)

Obviously this is in Kotlin, I've no clue what possibilities exist in a language of your choice, but the general principals are the same.

In general, I would recommend aiming for a more concise syntax over what you've currently proposed. Burying bones is really simple, it shouldn't 50 lines long, granted some of it is inevitable boilerplate, but:
Code:
// Once again, defines a script and stores it in a hashmap.
onItemOp(ItemId.BONES, "Bury") {
    if (hasQueue(Queues.BURYING_BONES) return
    anim(Animation.BURY_BONE)
    message("You dig a hole in the ground...")
    strongQueue(1, Queues.BURYING_BONES)
}

strongqueue(Queues.BURYING_BONES) {
    message("You bury the bones.")
}

^Just mimicing what you've already proposed. That's how little code it would be if we were to do it in kotlin via scripting. Other languages will likely require wrapping it in a class so that adds a bit, but you get the gist. Note that this is just an example, I'm not forcing anyone to do it like it, it's not quite like that in my own server either. It's just a starting point which I feel like will end up a lot nicer over time than the verbose implementation you've proposed.

I've also written more about queues here, however I would not recommend you try to implement it according to the documents there. I think it'll just scare you off. Best off start with a simple system you'll understand. The little nuances that come from the document can be done later on when you get comfortable with the system proposed here; first step is understanding what queues really are - it tends to trip people up quite a lot.
Long ramble, intended to post multiple times as I wrote this but I didn't think it was clear enough so I kept on adding examples :vet:
Wow, yet another batch of amazing information! Thank you!
I think I got the gist of things, I just need to play around with it and get more comfortable, it's not like I've re-written my server about 10 times or so :lolbert:
The goal for now will be to setup an action system, and also a interaction system (might be able to combine the two somehow we'll see).
Implement a few actions (bury, eat, equip) as well as one or two interactions (climb a ladder and chop a tree). If it works out well I'll try extending it before tackling combat, I'm sitting here speculating over whether or not an entity attack is considered an action or if the combat system will be it's own system. I guess I'll tackle that hurdle when I get there.

The idea of strong queues, weak queues etc. Is a bit daunting, but I'll just start off with one queue of actions I suppose and have the option to set a flag on the Action in the form of "ClearsInteraction" which when an action of that kind is hit, it'll just clear the Interaction (which I believe you can only posses one of at a time if I remember correctly).

Long ramble, intended to post multiple times as I wrote this but I didn't think it was clear enough so I kept on adding examples
No worries, I'm actually super happy you went so in-depth it really answers pretty much all my questions and now I'll be able to go back to this thread whenever I need to double check something related to actions!

And again, thank you for such a detailed answer, I'm sure it'll help a lot more people who stumble onto this thread!
 
there’s no such thing as a combat system, it’s all done the same way as clicking a door or clicking an item on the ground, except it’s a player or npc. and it’s all done through varps.
 
  • Like
Reactions: Elysian
there’s no such thing as a combat system, it’s all done the same way as clicking a door or clicking an item on the ground, except it’s a player or npc. and it’s all done through varps.
Isn't Var stuff related to OSRS and not 317?
 
Isn't Var stuff related to OSRS and not 317?
they have existed since the beginning. we have a page dedicated to trying to map out varps, transmitted and not transmitted to the client here:

many of them have been leaked from jmods and what-have-you in the past, and we've just used that information to create our code.

there are also varns which of course some are used for combat on the npc side:
 

Users who are viewing this thread (total: 1, members: 0, guests: 1)