Skip to content

Commit e1a01a5

Browse files
authored
Merge pull request #273 from runejs/feature/action-queue
1.0 Action Queue/Strength System + Action Hook Tasks
2 parents c1c07af + 0aa4c6b commit e1a01a5

35 files changed

+950
-597
lines changed

src/game-engine/game-server.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { World } from './world';
22
import { logger } from '@runejs/core';
33
import { parseServerConfig } from '@runejs/core/net';
4-
import { ServerConfig } from '@engine/config/server-config';
4+
import { Filestore, LandscapeObject } from '@runejs/filestore';
55

6+
import { ServerConfig } from '@engine/config/server-config';
67
import { loadPluginFiles } from '@engine/plugins/content-plugin';
7-
88
import { loadPackets } from '@engine/net/inbound-packets';
99
import { watchForChanges, watchSource } from '@engine/util/files';
1010
import { openGameServer } from '@engine/net/server/game-server';
@@ -16,7 +16,6 @@ import { Subject, timer } from 'rxjs';
1616
import { Position } from '@engine/world/position';
1717
import { ActionHook, sortActionHooks } from '@engine/world/action/hooks';
1818
import { ActionType } from '@engine/world/action';
19-
import { Filestore, LandscapeObject } from '@runejs/filestore';
2019

2120

2221
/**
@@ -219,10 +218,10 @@ export const playerWalkTo = async (player: Player, position: Position, interacti
219218
interactingObject?: LandscapeObject;
220219
}): Promise<void> => {
221220
return new Promise<void>((resolve, reject) => {
222-
player.walkingTo = position;
221+
player.metadata.walkingTo = position;
223222

224223
const inter = setInterval(() => {
225-
if(!player.walkingTo || !player.walkingTo.equals(position)) {
224+
if(!player.metadata.walkingTo || !player.metadata.walkingTo.equals(position)) {
226225
reject();
227226
clearInterval(inter);
228227
return;
@@ -247,7 +246,7 @@ export const playerWalkTo = async (player: Player, position: Position, interacti
247246
}
248247

249248
clearInterval(inter);
250-
player.walkingTo = null;
249+
player.metadata.walkingTo = null;
251250
}
252251
}, 100);
253252
});

src/game-engine/world/action/button.action.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Player } from '@engine/world/actor/player/player';
22
import { ActionHook, getActionHooks } from '@engine/world/action/hooks';
33
import { advancedNumberHookFilter, questHookFilter } from '@engine/world/action/hooks/hook-filters';
4-
import { ActionPipe } from '@engine/world/action/index';
4+
import { ActionPipe, RunnableHooks } from '@engine/world/action/index';
55

66

77
/**
88
* Defines a button action hook.
99
*/
10-
export interface ButtonActionHook extends ActionHook<buttonActionHandler> {
10+
export interface ButtonActionHook extends ActionHook<ButtonAction, buttonActionHandler> {
1111
// The ID of the UI widget that the button is on.
1212
widgetId?: number;
1313
// The IDs of the UI widgets that the buttons are on.
@@ -44,7 +44,7 @@ export interface ButtonAction {
4444
* @param widgetId
4545
* @param buttonId
4646
*/
47-
const buttonActionPipe = (player: Player, widgetId: number, buttonId: number) => {
47+
const buttonActionPipe = (player: Player, widgetId: number, buttonId: number): RunnableHooks<ButtonAction> => {
4848
let matchingHooks = getActionHooks<ButtonActionHook>('button')
4949
.filter(plugin =>
5050
questHookFilter(player, plugin) && (
@@ -69,17 +69,15 @@ const buttonActionPipe = (player: Player, widgetId: number, buttonId: number) =>
6969

7070
if(matchingHooks.length === 0) {
7171
player.outgoingPackets.chatboxMessage(`Unhandled button interaction: ${widgetId}:${buttonId}`);
72-
return;
72+
return null;
7373
}
7474

75-
// Immediately run the hooks
76-
for(const actionHook of matchingHooks) {
77-
if(actionHook.cancelActions) {
78-
player.actionsCancelled.next('button');
75+
return {
76+
hooks: matchingHooks,
77+
action: {
78+
player, widgetId, buttonId
7979
}
80-
81-
actionHook.handler({ player, widgetId, buttonId });
82-
}
80+
};
8381
};
8482

8583

src/game-engine/world/action/equipment-change.action.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { ActionHook, getActionHooks } from '@engine/world/action/hooks';
33
import { findItem } from '@engine/config';
44
import { EquipmentSlot, ItemDetails } from '@engine/config/item-config';
55
import { numberHookFilter, stringHookFilter, questHookFilter } from '@engine/world/action/hooks/hook-filters';
6-
import { ActionPipe } from '@engine/world/action/index';
6+
import { ActionPipe, RunnableHooks } from '@engine/world/action/index';
77

88

99
/**
1010
* Defines an equipment change action hook.
1111
*/
12-
export interface EquipmentChangeActionHook extends ActionHook<equipmentChangeActionHandler> {
12+
export interface EquipmentChangeActionHook extends ActionHook<EquipmentChangeAction, equipmentChangeActionHandler> {
1313
// A single game item ID or a list of item IDs that this action applies to.
1414
itemIds?: number | number[];
1515
// A single option name or a list of option names that this action applies to.
@@ -53,8 +53,9 @@ export interface EquipmentChangeAction {
5353
* @param eventType
5454
* @param slot
5555
*/
56-
const equipmentChangeActionPipe = (player: Player, itemId: number, eventType: EquipmentChangeType, slot: EquipmentSlot): void => {
57-
let filteredActions = getActionHooks<EquipmentChangeActionHook>('equipment_change', equipActionHook => {
56+
const equipmentChangeActionPipe = (player: Player, itemId: number,
57+
eventType: EquipmentChangeType, slot: EquipmentSlot): RunnableHooks<EquipmentChangeAction> => {
58+
let matchingHooks = getActionHooks<EquipmentChangeActionHook>('equipment_change', equipActionHook => {
5859
if(!questHookFilter(player, equipActionHook)) {
5960
return false;
6061
}
@@ -74,21 +75,26 @@ const equipmentChangeActionPipe = (player: Player, itemId: number, eventType: Eq
7475
return true;
7576
});
7677

77-
const questActions = filteredActions.filter(plugin => plugin.questRequirement !== undefined);
78+
const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);
7879

7980
if(questActions.length !== 0) {
80-
filteredActions = questActions;
81+
matchingHooks = questActions;
8182
}
8283

83-
for(const plugin of filteredActions) {
84-
plugin.handler({
84+
if(!matchingHooks || matchingHooks.length === 0) {
85+
return null;
86+
}
87+
88+
return {
89+
hooks: matchingHooks,
90+
action: {
8591
player,
8692
itemId,
8793
itemDetails: findItem(itemId),
8894
eventType,
8995
equipmentSlot: slot
90-
});
91-
}
96+
}
97+
};
9298
};
9399

94100

src/game-engine/world/action/hooks/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { actionHookMap } from '@engine/game-server';
22
import { QuestKey } from '@engine/config/quest-config';
3-
import { ActionType } from '@engine/world/action';
3+
import { ActionStrength, ActionType } from '@engine/world/action';
4+
import { HookTask } from '@engine/world/action/hooks/task';
45

56

67
/**
@@ -16,15 +17,21 @@ export interface QuestRequirement {
1617
/**
1718
* Defines a generic extensible game content action hook.
1819
*/
19-
export interface ActionHook<T = any> {
20-
// The type of action to perform.
20+
export interface ActionHook<A = any, H = any> {
21+
// The type of action to perform
2122
type: ActionType;
22-
// The action's priority over other actions.
23+
// Whether or not this hook will allow other hooks from the same action to queue after it
24+
multi?: boolean;
25+
// The action's priority over other actions
2326
priority?: number;
24-
// [optional] Quest requirements that must be completed in order to run this hook.
27+
// The strength of the action hook
28+
strength?: ActionStrength;
29+
// [optional] Quest requirements that must be completed in order to run this hook
2530
questRequirement?: QuestRequirement;
26-
// The action function to be performed.
27-
handler: T;
31+
// [optional] The action function to be performed
32+
handler?: H;
33+
// [optional] The task to be performed
34+
task?: HookTask<A>;
2835
}
2936

3037

@@ -51,3 +58,7 @@ export const getActionHooks = <T extends ActionHook>(actionType: ActionType, fil
5158
export function sortActionHooks<T = any>(actionHooks: ActionHook<T>[]): ActionHook<T>[] {
5259
return actionHooks.sort(actionHook => actionHook.questRequirement !== undefined ? -1 : 1);
5360
}
61+
62+
63+
export * from './hook-filters';
64+
export * from './task';
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import uuidv4 from 'uuid/v4';
2+
import { lastValueFrom, Subscription, timer } from 'rxjs';
3+
import { Actor } from '@engine/world/actor/actor';
4+
import { ActionHook } from '@engine/world/action/hooks/index';
5+
import { World } from '@engine/world';
6+
import { logger } from '@runejs/core';
7+
import { Player } from '@engine/world/actor/player/player';
8+
import { Npc } from '@engine/world/actor/npc/npc';
9+
import { ActionStrength } from '@engine/world/action';
10+
11+
12+
export type TaskSessionData = { [key: string]: any };
13+
14+
15+
export interface HookTask<T = any> {
16+
canActivate?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => boolean | Promise<boolean>;
17+
activate: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | undefined | boolean | Promise<void | undefined | boolean>;
18+
onComplete?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | Promise<void>;
19+
delay?: number; // # of ticks before execution
20+
delayMs?: number; // # of milliseconds before execution
21+
interval?: number; // # of ticks between loop intervals (defaults to single run task)
22+
intervalMs?: number; // # of milliseconds between loop intervals (defaults to single run task)
23+
}
24+
25+
26+
// T = current action info (ButtonAction, MoveItemAction, etc)
27+
export class TaskExecutor<T> {
28+
29+
public readonly taskId = uuidv4();
30+
public readonly strength: ActionStrength;
31+
public running: boolean = false;
32+
public session: TaskSessionData = {}; // a session store to use for the lifetime of the task
33+
34+
private iteration: number = 0;
35+
private intervalSubscription: Subscription;
36+
37+
public constructor(public readonly actor: Actor,
38+
public readonly task: HookTask<T>,
39+
public readonly hook: ActionHook,
40+
public readonly actionData: T) {
41+
this.strength = this.hook.strength || 'normal';
42+
}
43+
44+
public async run(): Promise<void> {
45+
this.running = true;
46+
47+
if(!!this.task.delay || !!this.task.delayMs) {
48+
await lastValueFrom(timer(this.task.delayMs !== undefined ? this.task.delayMs :
49+
(this.task.delay * World.TICK_LENGTH)));
50+
}
51+
52+
if(!!this.task.interval || !!this.task.intervalMs) {
53+
// Looping execution task
54+
const intervalMs = this.task.intervalMs !== undefined ? this.task.intervalMs :
55+
(this.task.interval * World.TICK_LENGTH);
56+
57+
await new Promise<void>(resolve => {
58+
this.intervalSubscription = timer(0, intervalMs).subscribe(
59+
async() => {
60+
if(!await this.execute()) {
61+
this.intervalSubscription?.unsubscribe();
62+
resolve();
63+
}
64+
},
65+
error => {
66+
logger.error(error);
67+
resolve();
68+
},
69+
() => resolve());
70+
});
71+
} else {
72+
// Single execution task
73+
await this.execute();
74+
}
75+
76+
if(this.running) {
77+
await this.stop();
78+
}
79+
}
80+
81+
public async execute(): Promise<boolean> {
82+
if(!this.actor) {
83+
// Actor destroyed, cancel the task
84+
return false;
85+
}
86+
87+
if(!await this.canActivate()) {
88+
// Unable to activate the task, cancel
89+
return false;
90+
}
91+
92+
if(this.actor.actionPipeline.paused) {
93+
// Action paused, continue loop if applicable
94+
return true;
95+
}
96+
97+
if(!this.running) {
98+
// Task no longer running, cancel execution
99+
return false;
100+
}
101+
102+
try {
103+
const response = await this.task.activate(this, this.iteration++);
104+
return typeof response === 'boolean' ? response : true;
105+
} catch(error) {
106+
logger.error(`Error executing action task`);
107+
logger.error(error);
108+
return false;
109+
}
110+
}
111+
112+
public async canActivate(): Promise<boolean> {
113+
if(!this.valid) {
114+
return false;
115+
}
116+
117+
if(!this.task.canActivate) {
118+
return true;
119+
}
120+
121+
try {
122+
return this.task.canActivate(this, this.iteration);
123+
} catch(error) {
124+
logger.error(`Error calling action canActivate`, this.task);
125+
logger.error(error);
126+
return false;
127+
}
128+
}
129+
130+
public async stop(): Promise<void> {
131+
this.running = false;
132+
this.intervalSubscription?.unsubscribe();
133+
134+
await this.task?.onComplete(this, this.iteration);
135+
136+
this.session = null;
137+
}
138+
139+
public getDetails(): {
140+
actor: Actor;
141+
player: Player | undefined;
142+
npc: Npc | undefined;
143+
actionData: T;
144+
session: TaskSessionData; } {
145+
const {
146+
type: {
147+
player,
148+
npc
149+
}
150+
} = this.actor;
151+
152+
return {
153+
actor: this.actor,
154+
player,
155+
npc,
156+
actionData: this.actionData,
157+
session: this.session
158+
};
159+
}
160+
161+
public get valid(): boolean {
162+
return !!this.task?.activate && !!this.actionData;
163+
}
164+
165+
}

0 commit comments

Comments
 (0)