Skip to content

Commit

Permalink
Merge pull request #273 from runejs/feature/action-queue
Browse files Browse the repository at this point in the history
1.0 Action Queue/Strength System + Action Hook Tasks
  • Loading branch information
Tynarus authored Apr 24, 2021
2 parents c1c07af + 0aa4c6b commit e1a01a5
Show file tree
Hide file tree
Showing 35 changed files with 950 additions and 597 deletions.
11 changes: 5 additions & 6 deletions src/game-engine/game-server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { World } from './world';
import { logger } from '@runejs/core';
import { parseServerConfig } from '@runejs/core/net';
import { ServerConfig } from '@engine/config/server-config';
import { Filestore, LandscapeObject } from '@runejs/filestore';

import { ServerConfig } from '@engine/config/server-config';
import { loadPluginFiles } from '@engine/plugins/content-plugin';

import { loadPackets } from '@engine/net/inbound-packets';
import { watchForChanges, watchSource } from '@engine/util/files';
import { openGameServer } from '@engine/net/server/game-server';
Expand All @@ -16,7 +16,6 @@ import { Subject, timer } from 'rxjs';
import { Position } from '@engine/world/position';
import { ActionHook, sortActionHooks } from '@engine/world/action/hooks';
import { ActionType } from '@engine/world/action';
import { Filestore, LandscapeObject } from '@runejs/filestore';


/**
Expand Down Expand Up @@ -219,10 +218,10 @@ export const playerWalkTo = async (player: Player, position: Position, interacti
interactingObject?: LandscapeObject;
}): Promise<void> => {
return new Promise<void>((resolve, reject) => {
player.walkingTo = position;
player.metadata.walkingTo = position;

const inter = setInterval(() => {
if(!player.walkingTo || !player.walkingTo.equals(position)) {
if(!player.metadata.walkingTo || !player.metadata.walkingTo.equals(position)) {
reject();
clearInterval(inter);
return;
Expand All @@ -247,7 +246,7 @@ export const playerWalkTo = async (player: Player, position: Position, interacti
}

clearInterval(inter);
player.walkingTo = null;
player.metadata.walkingTo = null;
}
}, 100);
});
Expand Down
20 changes: 9 additions & 11 deletions src/game-engine/world/action/button.action.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Player } from '@engine/world/actor/player/player';
import { ActionHook, getActionHooks } from '@engine/world/action/hooks';
import { advancedNumberHookFilter, questHookFilter } from '@engine/world/action/hooks/hook-filters';
import { ActionPipe } from '@engine/world/action/index';
import { ActionPipe, RunnableHooks } from '@engine/world/action/index';


/**
* Defines a button action hook.
*/
export interface ButtonActionHook extends ActionHook<buttonActionHandler> {
export interface ButtonActionHook extends ActionHook<ButtonAction, buttonActionHandler> {
// The ID of the UI widget that the button is on.
widgetId?: number;
// The IDs of the UI widgets that the buttons are on.
Expand Down Expand Up @@ -44,7 +44,7 @@ export interface ButtonAction {
* @param widgetId
* @param buttonId
*/
const buttonActionPipe = (player: Player, widgetId: number, buttonId: number) => {
const buttonActionPipe = (player: Player, widgetId: number, buttonId: number): RunnableHooks<ButtonAction> => {
let matchingHooks = getActionHooks<ButtonActionHook>('button')
.filter(plugin =>
questHookFilter(player, plugin) && (
Expand All @@ -69,17 +69,15 @@ const buttonActionPipe = (player: Player, widgetId: number, buttonId: number) =>

if(matchingHooks.length === 0) {
player.outgoingPackets.chatboxMessage(`Unhandled button interaction: ${widgetId}:${buttonId}`);
return;
return null;
}

// Immediately run the hooks
for(const actionHook of matchingHooks) {
if(actionHook.cancelActions) {
player.actionsCancelled.next('button');
return {
hooks: matchingHooks,
action: {
player, widgetId, buttonId
}

actionHook.handler({ player, widgetId, buttonId });
}
};
};


Expand Down
26 changes: 16 additions & 10 deletions src/game-engine/world/action/equipment-change.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { ActionHook, getActionHooks } from '@engine/world/action/hooks';
import { findItem } from '@engine/config';
import { EquipmentSlot, ItemDetails } from '@engine/config/item-config';
import { numberHookFilter, stringHookFilter, questHookFilter } from '@engine/world/action/hooks/hook-filters';
import { ActionPipe } from '@engine/world/action/index';
import { ActionPipe, RunnableHooks } from '@engine/world/action/index';


/**
* Defines an equipment change action hook.
*/
export interface EquipmentChangeActionHook extends ActionHook<equipmentChangeActionHandler> {
export interface EquipmentChangeActionHook extends ActionHook<EquipmentChangeAction, equipmentChangeActionHandler> {
// A single game item ID or a list of item IDs that this action applies to.
itemIds?: number | number[];
// A single option name or a list of option names that this action applies to.
Expand Down Expand Up @@ -53,8 +53,9 @@ export interface EquipmentChangeAction {
* @param eventType
* @param slot
*/
const equipmentChangeActionPipe = (player: Player, itemId: number, eventType: EquipmentChangeType, slot: EquipmentSlot): void => {
let filteredActions = getActionHooks<EquipmentChangeActionHook>('equipment_change', equipActionHook => {
const equipmentChangeActionPipe = (player: Player, itemId: number,
eventType: EquipmentChangeType, slot: EquipmentSlot): RunnableHooks<EquipmentChangeAction> => {
let matchingHooks = getActionHooks<EquipmentChangeActionHook>('equipment_change', equipActionHook => {
if(!questHookFilter(player, equipActionHook)) {
return false;
}
Expand All @@ -74,21 +75,26 @@ const equipmentChangeActionPipe = (player: Player, itemId: number, eventType: Eq
return true;
});

const questActions = filteredActions.filter(plugin => plugin.questRequirement !== undefined);
const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined);

if(questActions.length !== 0) {
filteredActions = questActions;
matchingHooks = questActions;
}

for(const plugin of filteredActions) {
plugin.handler({
if(!matchingHooks || matchingHooks.length === 0) {
return null;
}

return {
hooks: matchingHooks,
action: {
player,
itemId,
itemDetails: findItem(itemId),
eventType,
equipmentSlot: slot
});
}
}
};
};


Expand Down
25 changes: 18 additions & 7 deletions src/game-engine/world/action/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { actionHookMap } from '@engine/game-server';
import { QuestKey } from '@engine/config/quest-config';
import { ActionType } from '@engine/world/action';
import { ActionStrength, ActionType } from '@engine/world/action';
import { HookTask } from '@engine/world/action/hooks/task';


/**
Expand All @@ -16,15 +17,21 @@ export interface QuestRequirement {
/**
* Defines a generic extensible game content action hook.
*/
export interface ActionHook<T = any> {
// The type of action to perform.
export interface ActionHook<A = any, H = any> {
// The type of action to perform
type: ActionType;
// The action's priority over other actions.
// Whether or not this hook will allow other hooks from the same action to queue after it
multi?: boolean;
// The action's priority over other actions
priority?: number;
// [optional] Quest requirements that must be completed in order to run this hook.
// The strength of the action hook
strength?: ActionStrength;
// [optional] Quest requirements that must be completed in order to run this hook
questRequirement?: QuestRequirement;
// The action function to be performed.
handler: T;
// [optional] The action function to be performed
handler?: H;
// [optional] The task to be performed
task?: HookTask<A>;
}


Expand All @@ -51,3 +58,7 @@ export const getActionHooks = <T extends ActionHook>(actionType: ActionType, fil
export function sortActionHooks<T = any>(actionHooks: ActionHook<T>[]): ActionHook<T>[] {
return actionHooks.sort(actionHook => actionHook.questRequirement !== undefined ? -1 : 1);
}


export * from './hook-filters';
export * from './task';
165 changes: 165 additions & 0 deletions src/game-engine/world/action/hooks/task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import uuidv4 from 'uuid/v4';
import { lastValueFrom, Subscription, timer } from 'rxjs';
import { Actor } from '@engine/world/actor/actor';
import { ActionHook } from '@engine/world/action/hooks/index';
import { World } from '@engine/world';
import { logger } from '@runejs/core';
import { Player } from '@engine/world/actor/player/player';
import { Npc } from '@engine/world/actor/npc/npc';
import { ActionStrength } from '@engine/world/action';


export type TaskSessionData = { [key: string]: any };


export interface HookTask<T = any> {
canActivate?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => boolean | Promise<boolean>;
activate: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | undefined | boolean | Promise<void | undefined | boolean>;
onComplete?: <Q = T>(task: TaskExecutor<Q>, iteration?: number) => void | Promise<void>;
delay?: number; // # of ticks before execution
delayMs?: number; // # of milliseconds before execution
interval?: number; // # of ticks between loop intervals (defaults to single run task)
intervalMs?: number; // # of milliseconds between loop intervals (defaults to single run task)
}


// T = current action info (ButtonAction, MoveItemAction, etc)
export class TaskExecutor<T> {

public readonly taskId = uuidv4();
public readonly strength: ActionStrength;
public running: boolean = false;
public session: TaskSessionData = {}; // a session store to use for the lifetime of the task

private iteration: number = 0;
private intervalSubscription: Subscription;

public constructor(public readonly actor: Actor,
public readonly task: HookTask<T>,
public readonly hook: ActionHook,
public readonly actionData: T) {
this.strength = this.hook.strength || 'normal';
}

public async run(): Promise<void> {
this.running = true;

if(!!this.task.delay || !!this.task.delayMs) {
await lastValueFrom(timer(this.task.delayMs !== undefined ? this.task.delayMs :
(this.task.delay * World.TICK_LENGTH)));
}

if(!!this.task.interval || !!this.task.intervalMs) {
// Looping execution task
const intervalMs = this.task.intervalMs !== undefined ? this.task.intervalMs :
(this.task.interval * World.TICK_LENGTH);

await new Promise<void>(resolve => {
this.intervalSubscription = timer(0, intervalMs).subscribe(
async() => {
if(!await this.execute()) {
this.intervalSubscription?.unsubscribe();
resolve();
}
},
error => {
logger.error(error);
resolve();
},
() => resolve());
});
} else {
// Single execution task
await this.execute();
}

if(this.running) {
await this.stop();
}
}

public async execute(): Promise<boolean> {
if(!this.actor) {
// Actor destroyed, cancel the task
return false;
}

if(!await this.canActivate()) {
// Unable to activate the task, cancel
return false;
}

if(this.actor.actionPipeline.paused) {
// Action paused, continue loop if applicable
return true;
}

if(!this.running) {
// Task no longer running, cancel execution
return false;
}

try {
const response = await this.task.activate(this, this.iteration++);
return typeof response === 'boolean' ? response : true;
} catch(error) {
logger.error(`Error executing action task`);
logger.error(error);
return false;
}
}

public async canActivate(): Promise<boolean> {
if(!this.valid) {
return false;
}

if(!this.task.canActivate) {
return true;
}

try {
return this.task.canActivate(this, this.iteration);
} catch(error) {
logger.error(`Error calling action canActivate`, this.task);
logger.error(error);
return false;
}
}

public async stop(): Promise<void> {
this.running = false;
this.intervalSubscription?.unsubscribe();

await this.task?.onComplete(this, this.iteration);

this.session = null;
}

public getDetails(): {
actor: Actor;
player: Player | undefined;
npc: Npc | undefined;
actionData: T;
session: TaskSessionData; } {
const {
type: {
player,
npc
}
} = this.actor;

return {
actor: this.actor,
player,
npc,
actionData: this.actionData,
session: this.session
};
}

public get valid(): boolean {
return !!this.task?.activate && !!this.actionData;
}

}
Loading

0 comments on commit e1a01a5

Please sign in to comment.