-
Notifications
You must be signed in to change notification settings - Fork 88
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #382 from Jameskmonger/rs2-tasks
feat: create tick-based task system
- Loading branch information
Showing
17 changed files
with
1,313 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
# Task system | ||
|
||
The task system allows you to write content which will be executed on the tick cycles of the server. | ||
|
||
You can configure a task to execute every `n` ticks (minimum of `1` for every tick), and you can also choose a number of other behaviours, such as whether the task should execute immediately or after a delay, as well as set to repeat indefinitely. | ||
|
||
## Scheduling a task | ||
|
||
You can schedule a task by registering it with a `TaskScheduler`. | ||
|
||
The task in the example of below runs with an interval of `2`, i.e. it will be executed every 2 ticks. | ||
|
||
```ts | ||
class MyTask extends Task { | ||
public constructor() { | ||
super({ interval: 2 }); | ||
} | ||
|
||
public execute(): void { | ||
console.log('2 ticks'); | ||
} | ||
} | ||
|
||
const scheduler = new TaskScheduler(); | ||
|
||
scheduler.addTask(new MyTask()); | ||
|
||
scheduler.tick(); | ||
scheduler.tick(); // '2 ticks' | ||
``` | ||
|
||
Every two times that `scheduler.tick()` is called, it will run the `execute` function of your task. | ||
|
||
## Task configuration | ||
|
||
You can pass a `TaskConfig` object to the `Task` constructor in order to configure various aspects of your task. | ||
|
||
### Timing | ||
|
||
The most simple configuration option for a `Task` is the `interval` option. Your task will be executed every `interval` amount of ticks. | ||
|
||
```ts | ||
/** | ||
* The number of ticks between each execution of the task. | ||
*/ | ||
interval: number; | ||
``` | ||
|
||
For example, with an interval of `1`, your task will run every tick. The default value is `1`. | ||
|
||
### Immediate execution | ||
|
||
You can configure your task to execute immediately with the `immediate` option. | ||
|
||
```ts | ||
/** | ||
* Should the task be executed on the first tick after it is added? | ||
*/ | ||
immediate: boolean; | ||
``` | ||
|
||
For example, if `immediate` is `true` and `interval` is `5`, your task will run on the 1st and 6th ticks (and so on). | ||
|
||
### Repeating | ||
|
||
You can use the `repeat` option to tell your task to run forever. | ||
|
||
```ts | ||
/** | ||
* Should the task be repeated indefinitely? | ||
*/ | ||
repeat: boolean; | ||
``` | ||
|
||
You can use `this.stop()` inside the task to stop it from repeating further. | ||
|
||
### Stacking | ||
|
||
The `stackType` and `stackGroup` properties allow you to control how your task interacts with other, similar tasks. | ||
|
||
```ts | ||
/** | ||
* How the task should be stacked with other tasks of the same stack group. | ||
*/ | ||
stackType: TaskStackType; | ||
|
||
/** | ||
* The stack group for this task. | ||
*/ | ||
stackGroup: string; | ||
``` | ||
|
||
When `stackType` is set to `TaskStackType.NEVER`, other tasks with the same `stackGroup` will be stopped when your task is enqueued. A `stackType` of `TaskStackType.STACK` will allow your task to run with others of the same group. | ||
|
||
The default type is `TaskStackType.STACK` and the group is `TaskStackGroup.ACTION` (`'action'`) | ||
|
||
## Task Subtypes | ||
|
||
Rather than extending `Task`, there are a number of subclasses you can extend which will give you some syntactic sugar around common functionality. | ||
|
||
- `ActorTask` | ||
|
||
This is the base task to be performed by an `Actor`. It will automatically listen to the actor's walking queue, and stop the task if it has a `breakType` of `ON_MOVE`. | ||
|
||
- `ActorWalkToTask` | ||
|
||
This task will make an actor walk to a `Position` or `LandscapeObject` and will expose the `atDestination` property for your extended task to query. You can then begin executing your task logic. | ||
|
||
- `ActorLandscapeObjectInteractionTask` | ||
|
||
This task extends `ActorWalkToTask` and will make an actor walk to a given `LandscapeObject`, before exposing the `landscapeObject` property for your task to use. | ||
|
||
- `ActorWorldItemInteractionTask` | ||
|
||
This task extends `ActorWalkToTask` and will make an actor walk to a given `WorldItem`, before exposing the `worldItem` property for your task to use. | ||
|
||
# Future improvements | ||
|
||
- Stalling executions for certain tasks when interface is open | ||
- should we create a `PlayerTask` to contain this behaviour? The `breakType` behaviour could be moved to this base, rather than `ActorTask` | ||
|
||
- Consider refactoring this system to use functional programming patterns. Composition should be favoured over inheritance generally, and there are some examples of future tasks which may be easier if we could compose tasks from building blocks. Consider the implementation of some task which requires both a `LandscapeObject` and a `WorldItem` - we currently would need to create some custom task which borrowed behaviour from the `ActorLandscapeObjectInteractionTask` and `ActorWorldItemInteractionTask`. TypeScript mixins could be useful here. | ||
|
||
# Content requiring conversion to task system | ||
|
||
Highest priority is to convert pieces of content which make use of the old `task` system. These are: | ||
|
||
- Magic attack | ||
- Magic teleports | ||
- Prayer | ||
- Combat | ||
- Forging (smithing) | ||
- Woodcutting | ||
|
||
The following areas will make interesting use of the task system and would serve as a good demonstration: | ||
|
||
- Health regen | ||
- NPC movement | ||
- Firemaking |
106 changes: 106 additions & 0 deletions
106
src/engine/task/impl/actor-landscape-object-interaction-task.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { LandscapeObject } from '@runejs/filestore'; | ||
import { activeWorld, Position } from '@engine/world'; | ||
import { Actor } from '@engine/world/actor'; | ||
import { ActorWalkToTask } from './actor-walk-to-task'; | ||
|
||
/** | ||
* A task for an actor to interact with a {@link LandscapeObject}. | ||
* | ||
* This task extends {@link ActorWalkToTask} and will walk the actor to the object. | ||
* Once the actor is within range of the object, the task will expose the {@link landscapeObject} property | ||
* | ||
* @author jameskmonger | ||
*/ | ||
export abstract class ActorLandscapeObjectInteractionTask<TActor extends Actor = Actor> extends ActorWalkToTask<TActor, LandscapeObject> { | ||
private _landscapeObject: LandscapeObject; | ||
private _objectPosition: Position; | ||
|
||
/** | ||
* Gets the {@link LandscapeObject} that this task is interacting with. | ||
* | ||
* @returns If the object is still present, and the actor is at the destination, the object. | ||
* Otherwise, `null`. | ||
* | ||
* TODO (jameskmonger) unit test this | ||
*/ | ||
protected get landscapeObject(): LandscapeObject | null { | ||
// TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task | ||
// as currently the subclass has to store it in a subclass property if it wants to use it | ||
// without these checks | ||
if (!this.atDestination) { | ||
return null; | ||
} | ||
|
||
if (!this._landscapeObject) { | ||
return null; | ||
} | ||
|
||
return this._landscapeObject; | ||
} | ||
|
||
/** | ||
* Get the position of this task's landscape object | ||
* | ||
* @returns The position of this task's landscape object, or null if the landscape object is not present | ||
*/ | ||
protected get landscapeObjectPosition(): Position { | ||
if (!this._landscapeObject) { | ||
return null; | ||
} | ||
|
||
return this._objectPosition; | ||
} | ||
|
||
/** | ||
* @param actor The actor executing this task. | ||
* @param landscapeObject The landscape object to interact with. | ||
* @param sizeX The size of the LandscapeObject in the X direction. | ||
* @param sizeY The size of the LandscapeObject in the Y direction. | ||
*/ | ||
constructor ( | ||
actor: TActor, | ||
landscapeObject: LandscapeObject, | ||
sizeX: number = 1, | ||
sizeY: number = 1 | ||
) { | ||
super( | ||
actor, | ||
landscapeObject, | ||
Math.max(sizeX, sizeY) | ||
); | ||
|
||
if (!landscapeObject) { | ||
this.stop(); | ||
return; | ||
} | ||
|
||
// create the Position here to prevent instantiating a new Position every tick | ||
this._objectPosition = new Position(landscapeObject.x, landscapeObject.y, landscapeObject.level); | ||
this._landscapeObject = landscapeObject; | ||
} | ||
|
||
/** | ||
* Checks for the continued presence of the {@link LandscapeObject} and stops the task if it is no longer present. | ||
* | ||
* TODO (jameskmonger) unit test this | ||
*/ | ||
public execute() { | ||
super.execute(); | ||
|
||
if (!this.isActive || !this.atDestination) { | ||
return; | ||
} | ||
|
||
if (!this._landscapeObject) { | ||
this.stop(); | ||
return; | ||
} | ||
|
||
const { object: worldObject } = activeWorld.findObjectAtLocation(this.actor, this._landscapeObject.objectId, this._objectPosition); | ||
|
||
if (!worldObject) { | ||
this.stop(); | ||
return; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Subscription } from 'rxjs'; | ||
import { Actor } from '@engine/world/actor'; | ||
import { TaskBreakType, TaskConfig } from '../types'; | ||
import { Task } from '../task'; | ||
|
||
/** | ||
* A task that is executed by an actor. | ||
* | ||
* If the task has a break type of ON_MOVE, the ActorTask will subscribe to the actor's | ||
* movement events and will stop executing when the actor moves. | ||
* | ||
* @author jameskmonger | ||
*/ | ||
export abstract class ActorTask<TActor extends Actor = Actor> extends Task { | ||
/** | ||
* A function that is called when a movement event is queued on the actor. | ||
* | ||
* This will be `null` if the task does not break on movement. | ||
*/ | ||
private walkingQueueSubscription: Subscription | null = null; | ||
|
||
/** | ||
* @param actor The actor executing this task. | ||
* @param config The task configuration. | ||
*/ | ||
constructor( | ||
protected readonly actor: TActor, | ||
config?: TaskConfig | ||
) { | ||
super(config); | ||
|
||
this.listenForMovement(); | ||
} | ||
|
||
/** | ||
* Called when the task is stopped and unsubscribes from the actor's walking queue if necessary. | ||
* | ||
* TODO (jameskmonger) unit test this | ||
*/ | ||
public onStop(): void { | ||
if (this.walkingQueueSubscription) { | ||
this.walkingQueueSubscription.unsubscribe(); | ||
} | ||
} | ||
|
||
/** | ||
* If required, listen to the actor's walking queue to stop the task | ||
* | ||
* This function uses `setImmediate` to ensure that the subscription to the | ||
* walking queue is not created | ||
* | ||
* TODO (jameskmonger) unit test this | ||
*/ | ||
private listenForMovement(): void { | ||
if (!this.breaksOn(TaskBreakType.ON_MOVE)) { | ||
return; | ||
} | ||
|
||
setImmediate(() => { | ||
this.walkingQueueSubscription = this.actor.walkingQueue.movementQueued$.subscribe(() => { | ||
this.stop(); | ||
}); | ||
}); | ||
} | ||
} |
Oops, something went wrong.