Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.0 Action Queue/Strength System + Action Hook Tasks #273

Merged
merged 21 commits into from
Apr 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd9c643
Preliminary work on a Task Executor and action priority queue system
Tynarus Mar 13, 2021
5b7fa9d
Working on implementing the new RunnableHook system across actions
Tynarus Mar 15, 2021
aac4db6
Fixing dialogue system issues
Tynarus Mar 15, 2021
57f7d5d
Fixing an error message error
Tynarus Mar 15, 2021
88284b1
Converting the Woodcutting plugin over to the new action task system
Tynarus Mar 20, 2021
1e91516
Adding a new file for action tasks and exporting all from the action api
Tynarus Mar 20, 2021
cc65864
Fixing world/index.ts indentation
Tynarus Mar 20, 2021
d343551
Merging in the latest from develop
Tynarus Mar 20, 2021
195c101
Fixing all lint issues
Tynarus Mar 20, 2021
bbbc094
Fixing the new walkTo api function
Tynarus Mar 20, 2021
fc8dd78
Making actions in the pipeline cancel when the actor initiates movement
Tynarus Mar 20, 2021
bcc6447
Merge remote-tracking branch 'origin/develop' into feature/action-queue
Tynarus Mar 20, 2021
30610c0
Fixing a lint issue and adding a pipeline cancellation flag
Tynarus Mar 20, 2021
8824886
Removing one check
Tynarus Mar 20, 2021
cb90a5a
Oopsie v2
Tynarus Mar 20, 2021
e32017e
Running actor actions get cancelled when a new action is performed
Tynarus Mar 20, 2021
71a7dec
Merge branch 'develop' into feature/action-queue
Tynarus Mar 24, 2021
8f5847a
Merge remote-tracking branch 'origin/develop' into feature/action-queue
Tynarus Apr 20, 2021
7ff0593
Nothing to see here
Tynarus Apr 20, 2021
dd6e290
Merge branch 'develop' into feature/action-queue
Tynarus Apr 20, 2021
0aa4c6b
why did I write that like that
Tynarus Apr 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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