diff --git a/src/game-engine/game-server.ts b/src/game-engine/game-server.ts index 1e8404678..4f2b00b77 100644 --- a/src/game-engine/game-server.ts +++ b/src/game-engine/game-server.ts @@ -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'; @@ -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'; /** @@ -219,10 +218,10 @@ export const playerWalkTo = async (player: Player, position: Position, interacti interactingObject?: LandscapeObject; }): Promise => { return new Promise((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; @@ -247,7 +246,7 @@ export const playerWalkTo = async (player: Player, position: Position, interacti } clearInterval(inter); - player.walkingTo = null; + player.metadata.walkingTo = null; } }, 100); }); diff --git a/src/game-engine/world/action/button.action.ts b/src/game-engine/world/action/button.action.ts index d074ed0bf..82e0235e9 100644 --- a/src/game-engine/world/action/button.action.ts +++ b/src/game-engine/world/action/button.action.ts @@ -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 { +export interface ButtonActionHook extends ActionHook { // The ID of the UI widget that the button is on. widgetId?: number; // The IDs of the UI widgets that the buttons are on. @@ -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 => { let matchingHooks = getActionHooks('button') .filter(plugin => questHookFilter(player, plugin) && ( @@ -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 }); - } + }; }; diff --git a/src/game-engine/world/action/equipment-change.action.ts b/src/game-engine/world/action/equipment-change.action.ts index 0710455fe..e3123fb86 100644 --- a/src/game-engine/world/action/equipment-change.action.ts +++ b/src/game-engine/world/action/equipment-change.action.ts @@ -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 { +export interface EquipmentChangeActionHook extends ActionHook { // 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. @@ -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('equipment_change', equipActionHook => { +const equipmentChangeActionPipe = (player: Player, itemId: number, + eventType: EquipmentChangeType, slot: EquipmentSlot): RunnableHooks => { + let matchingHooks = getActionHooks('equipment_change', equipActionHook => { if(!questHookFilter(player, equipActionHook)) { return false; } @@ -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 - }); - } + } + }; }; diff --git a/src/game-engine/world/action/hooks/index.ts b/src/game-engine/world/action/hooks/index.ts index ff3c1721b..4154bed53 100644 --- a/src/game-engine/world/action/hooks/index.ts +++ b/src/game-engine/world/action/hooks/index.ts @@ -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'; /** @@ -16,15 +17,21 @@ export interface QuestRequirement { /** * Defines a generic extensible game content action hook. */ -export interface ActionHook { - // The type of action to perform. +export interface ActionHook { + // 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; } @@ -51,3 +58,7 @@ export const getActionHooks = (actionType: ActionType, fil export function sortActionHooks(actionHooks: ActionHook[]): ActionHook[] { return actionHooks.sort(actionHook => actionHook.questRequirement !== undefined ? -1 : 1); } + + +export * from './hook-filters'; +export * from './task'; diff --git a/src/game-engine/world/action/hooks/task.ts b/src/game-engine/world/action/hooks/task.ts new file mode 100644 index 000000000..d257f3ac2 --- /dev/null +++ b/src/game-engine/world/action/hooks/task.ts @@ -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 { + canActivate?: (task: TaskExecutor, iteration?: number) => boolean | Promise; + activate: (task: TaskExecutor, iteration?: number) => void | undefined | boolean | Promise; + onComplete?: (task: TaskExecutor, iteration?: number) => void | Promise; + 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 { + + 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, + public readonly hook: ActionHook, + public readonly actionData: T) { + this.strength = this.hook.strength || 'normal'; + } + + public async run(): Promise { + 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(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 { + 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 { + 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 { + 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; + } + +} diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index d8e1af6ab..cdbeb186c 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -2,12 +2,18 @@ import { gameEngineDist } from '@engine/util/directories'; import { getFiles } from '@engine/util/files'; import { logger } from '@runejs/core'; import { Actor } from '@engine/world/actor/actor'; +import { ActionHook } from '@engine/world/action/hooks'; +import { Position } from '@engine/world/position'; +import { Player } from '@engine/world/actor/player/player'; +import { TaskExecutor } from '@engine/world/action/hooks/task'; +import { Subscription } from 'rxjs'; +import { LandscapeObject } from '@runejs/filestore'; /** - * The priority of an action within the pipeline. + * The priority of an queueable action within the pipeline. */ -export type ActionPriority = 'weak' | 'normal' | 'strong'; +export type ActionStrength = 'weak' | 'normal' | 'strong'; /** @@ -38,6 +44,12 @@ export type ActionType = | 'equipment_change'; +export const gentleActions: ActionType[] = [ + 'button', 'widget_interaction', 'player_init', 'npc_init', + 'move_item', 'item_swap', 'player_command', 'region_change' +]; + + /** * Methods in which action hooks in progress may be cancelled. */ @@ -56,6 +68,19 @@ export type ActionCancelType = export type ActionPipe = [ ActionType, (...args: any[]) => void ]; +/** + * A list of filtered hooks for an actor to run. + */ +export interface RunnableHooks { + // The action in progress + action: T; + // Matching action hooks + hooks?: ActionHook[]; + // If a location is provided, then the actor must first move to that location to run the action + actionPosition?: Position; +} + + /** * A specific actor's action pipeline handler. * Records action pipes and distributes content actions from the game engine down to execute plugin hooks. @@ -64,7 +89,13 @@ export class ActionPipeline { private static pipes = new Map(); + private runningTasks: TaskExecutor[] = []; + private canceling: boolean = false; + private movementSubscription: Subscription; + public constructor(public readonly actor: Actor) { + this.movementSubscription = this.actor.walkingQueue.movementQueued$ + .subscribe(async () => this.cancelRunningTasks()); } public static getPipe(action: ActionType): Map { @@ -75,21 +106,114 @@ export class ActionPipeline { ActionPipeline.pipes.set(action.toString(), actionPipe); } + public shutdown(): void { + this.movementSubscription.unsubscribe(); + } + public async call(action: ActionType, ...args: any[]): Promise { const actionHandler = ActionPipeline.pipes.get(action.toString()); if(actionHandler) { try { - await new Promise(resolve => { - actionHandler(...args); - resolve(); - }); + await this.runActionHandler(actionHandler, ...args); } catch(error) { - logger.error(`Error handling action ${action.toString()}`); - logger.error(error); + if(error) { + logger.error(`Error handling action ${action.toString()}`); + logger.error(error); + } + } + } + } + + public async cancelRunningTasks(): Promise { + if(this.canceling || !this.runningTasks || this.runningTasks.length === 0) { + return; + } + + this.canceling = true; + + for(const runningTask of this.runningTasks) { + if(runningTask.running) { + await runningTask.stop(); + } + } + + // Remove all tasks + this.runningTasks = []; + this.canceling = false; + } + + private async runActionHandler(actionHandler: any, ...args: any[]): Promise { + const runnableHooks: RunnableHooks | null | undefined = actionHandler(...args); + + if(!runnableHooks?.hooks || runnableHooks.hooks.length === 0) { + return; + } + + for(let i = 0; i < runnableHooks.hooks.length; i++) { + const hook = runnableHooks.hooks[i]; + + if(!hook) { + continue; + } + + // Some actions are non-cancelling + if(gentleActions.indexOf(hook.type) === -1) { + await this.cancelRunningTasks(); + } + + if(runnableHooks.actionPosition) { + try { + const gameObject = runnableHooks.action['object'] || null; + await this.actor.waitForPathing( + !gameObject ? runnableHooks.actionPosition : (gameObject as LandscapeObject)); + } catch(error) { + logger.error(`Error pathing to hook target`, error); + return; + } + } + + await this.runHook(hook, runnableHooks.action); + if(!hook.multi) { + // If the highest priority hook does not allow other hooks + // to run during this same action, then return here to break + // out of the loop and complete execution. + return; } } } + private async runHook(actionHook: ActionHook, action: any): Promise { + const { handler, task } = actionHook; + + if(task) { + // Schedule task-based hook + const taskExecutor = new TaskExecutor(this.actor, task, actionHook, action); + this.runningTasks.push(taskExecutor); + + // Run the task until complete + await taskExecutor.run(); + + // Cleanup and remove the task once completed + const taskIdx = this.runningTasks.findIndex(task => task.taskId === taskExecutor.taskId); + if(taskIdx !== -1) { + this.runningTasks.splice(taskIdx, 1); + } + } else if(handler) { + // Run basic hook + await handler(action); + } + } + + public get paused(): boolean { + if(this.actor instanceof Player) { + if(this.actor.interfaceState.widgetOpen()) { + return true; + } + } + + return false; + } + } @@ -122,3 +246,6 @@ export async function loadActionFiles(): Promise { return Promise.resolve(); } + + +export * from './hooks/index'; diff --git a/src/game-engine/world/action/item-interaction.action.ts b/src/game-engine/world/action/item-interaction.action.ts index e1eb8a89c..964e5ea52 100644 --- a/src/game-engine/world/action/item-interaction.action.ts +++ b/src/game-engine/world/action/item-interaction.action.ts @@ -3,13 +3,13 @@ import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; import { ItemDetails } from '@engine/config/item-config'; import { findItem } from '@engine/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 item action hook. */ -export interface ItemInteractionActionHook extends ActionHook { +export interface ItemInteractionActionHook extends ActionHook { // A single game item ID or a list of item IDs that this action applies to. itemIds?: number | number[]; // A single UI widget ID or a list of widget IDs that this action applies to. @@ -57,12 +57,8 @@ export interface ItemInteractionAction { * @param containerId * @param option */ -const itemInteractionActionPipe = (player: Player, itemId: number, slot: number, widgetId: number, containerId: number, option: string): void => { - if(player.busy) { - return; - } - - let cancelActions = false; +const itemInteractionActionPipe = (player: Player, itemId: number, slot: number, widgetId: number, + containerId: number, option: string): RunnableHooks => { const playerWidget = Object.values(player.interfaceState.widgetSlots).find((widget) => widget && widget.widgetId === widgetId); if(playerWidget && playerWidget.fakeWidget != undefined) { @@ -70,7 +66,7 @@ const itemInteractionActionPipe = (player: Player, itemId: number, slot: number, } // Find all object action plugins that reference this location object - let interactionActions = getActionHooks('item_interaction', plugin => { + let matchingHooks = getActionHooks('item_interaction', plugin => { if(!questHookFilter(player, plugin)) { return false; } @@ -106,31 +102,24 @@ const itemInteractionActionPipe = (player: Player, itemId: number, slot: number, return false; } } - - if(plugin.cancelOtherActions) { - cancelActions = true; - } return true; }); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item option: ${option} ${itemId} in slot ${slot} within widget ${widgetId}:${containerId}`); - return; + return null; } - if(cancelActions) { - player.actionsCancelled.next(null); - } - - for(const plugin of interactionActions) { - plugin.handler({ + return { + hooks: matchingHooks, + action: { player, itemId, itemSlot: slot, @@ -138,7 +127,7 @@ const itemInteractionActionPipe = (player: Player, itemId: number, slot: number, containerId, itemDetails: findItem(itemId), option - }); + } } }; diff --git a/src/game-engine/world/action/item-on-item.action.ts b/src/game-engine/world/action/item-on-item.action.ts index 3f14c605a..f91515bce 100644 --- a/src/game-engine/world/action/item-on-item.action.ts +++ b/src/game-engine/world/action/item-on-item.action.ts @@ -2,13 +2,13 @@ import { Player } from '@engine/world/actor/player/player'; import { Item } from '@engine/world/items/item'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; import { 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 item-on-item action hook. */ -export interface ItemOnItemActionHook extends ActionHook { +export interface ItemOnItemActionHook extends ActionHook { // The item pairs being used. Each item can be used on the other, so item order does not matter. items: { item1: number, item2?: number }[]; } @@ -52,36 +52,36 @@ export interface ItemOnItemAction { * @param usedWithWidgetId */ const itemOnItemActionPipe = (player: Player, usedItem: Item, usedSlot: number, usedWidgetId: number, - usedWithItem: Item, usedWithSlot: number, usedWithWidgetId: number): void => { + usedWithItem: Item, usedWithSlot: number, usedWithWidgetId: number): RunnableHooks => { if(player.busy) { return; } // Find all item on item action plugins that match this action - let interactionActions = getActionHooks('item_on_item').filter(plugin => + let matchingHooks = getActionHooks('item_on_item').filter(plugin => questHookFilter(player, plugin) && (plugin.items.findIndex(i => i.item1 === usedItem.itemId && i.item2 === usedWithItem.itemId) !== -1 || plugin.items.findIndex(i => i.item2 === usedItem.itemId && i.item1 === usedWithItem.itemId) !== -1 || plugin.items.findIndex(i => i.item1 === usedItem.itemId && !i.item2 || i.item1 === usedWithItem.itemId && !i.item2 ) !== -1)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item on item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`); - return; + return null; } - player.actionsCancelled.next(null); - - // Immediately run the plugins - for(const plugin of interactionActions) { - plugin.handler({ player, usedItem, usedWithItem, usedSlot, usedWithSlot, - usedWidgetId: usedWidgetId, usedWithWidgetId: usedWithWidgetId }); + return { + hooks: matchingHooks, + action: { + player, usedItem, usedWithItem, usedSlot, usedWithSlot, + usedWidgetId: usedWidgetId, usedWithWidgetId: usedWithWidgetId + } } }; diff --git a/src/game-engine/world/action/item-on-npc.action.ts b/src/game-engine/world/action/item-on-npc.action.ts index aa7b593bb..9430d6b97 100644 --- a/src/game-engine/world/action/item-on-npc.action.ts +++ b/src/game-engine/world/action/item-on-npc.action.ts @@ -1,18 +1,17 @@ import { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { logger } from '@runejs/core'; import { Item } from '@engine/world/items/item'; import { Npc } from '@engine/world/actor/npc/npc'; import { playerWalkTo } from '@engine/game-server'; import { advancedNumberHookFilter, questHookFilter, stringHookFilter } from '@engine/world/action/hooks/hook-filters'; -import { ActionPipe } from '@engine/world/action/index'; +import { ActionPipe, RunnableHooks } from '@engine/world/action/index'; /** * Defines an item-on-npc action hook. */ -export interface ItemOnNpcActionHook extends ActionHook { +export interface ItemOnNpcActionHook extends ActionHook { // A single npc key or a list of npc keys that this action applies to. npcs: string | string[]; // A single game item ID or a list of item IDs that this action applies to. @@ -57,67 +56,39 @@ export interface ItemOnNpcAction { * @param itemContainerId */ const itemOnNpcActionPipe = (player: Player, npc: Npc, position: Position, item: Item, - itemWidgetId: number, itemContainerId: number): void => { - if(player.busy) { - return; - } - + itemWidgetId: number, itemContainerId: number): RunnableHooks => { const morphedNpc = player.getMorphedNpcDetails(npc); // Find all item on npc action plugins that reference this npc and item - let interactionActions = getActionHooks('item_on_npc').filter(plugin => + let matchingHooks = getActionHooks('item_on_npc').filter(plugin => questHookFilter(player, plugin) && stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key) && advancedNumberHookFilter(plugin.itemIds, item.itemId)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled item on npc interaction: ${ item.itemId } on ${ morphedNpc?.name || npc.name } ` + `(id-${ morphedNpc?.gameId || npc.id }) @ ${ position.x },${ position.y },${ position.level }`); if (morphedNpc) { player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } - return; - } - - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediateHooks = interactionActions.filter(plugin => !plugin.walkTo); - - // Make sure we walk to the npc before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, position) - .then(() => { - player.face(position); - - walkToPlugins.forEach(plugin => - plugin.handler({ - player, - npc, - position, - item, - itemWidgetId, - itemContainerId - })); - }) - .catch(() => logger.warn(`Unable to complete walk-to action.`)); + return null; } - // Immediately run any non-walk-to plugins - for(const actionHook of immediateHooks) { - actionHook.handler({ + return { + hooks: matchingHooks, + actionPosition: position, + action: { player, npc, position, item, itemWidgetId, itemContainerId - }); + } } }; diff --git a/src/game-engine/world/action/item-on-object.action.ts b/src/game-engine/world/action/item-on-object.action.ts index e73a41e25..9b5c2cf71 100644 --- a/src/game-engine/world/action/item-on-object.action.ts +++ b/src/game-engine/world/action/item-on-object.action.ts @@ -1,10 +1,9 @@ import { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { logger } from '@runejs/core'; import { Item } from '@engine/world/items/item'; 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'; import { LandscapeObject, ObjectConfig } from '@runejs/filestore'; import { playerWalkTo } from '@engine/game-server'; @@ -12,7 +11,7 @@ import { playerWalkTo } from '@engine/game-server'; /** * Defines an item-on-object action hook. */ -export interface ItemOnObjectActionHook extends ActionHook { +export interface ItemOnObjectActionHook extends ActionHook { // A single game object ID or a list of object IDs that this action applies to. objectIds: number | number[]; // A single game item ID or a list of item IDs that this action applies to. @@ -65,71 +64,41 @@ export interface ItemOnObjectAction { const itemOnObjectActionPipe = (player: Player, landscapeObject: LandscapeObject, objectConfig: ObjectConfig, position: Position, item: Item, itemWidgetId: number, itemContainerId: number, - cacheOriginal: boolean): void => { - if(player.busy) { - return; - } - + cacheOriginal: boolean): RunnableHooks => { // Find all item on object action plugins that reference this location object - let interactionActions = getActionHooks('item_on_object') - .filter(plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + let matchingHooks = getActionHooks('item_on_object') + .filter(plugin => questHookFilter(player, plugin) && + advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId)); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } // Find all item on object action plugins that reference this item - if(interactionActions.length !== 0) { - interactionActions = interactionActions.filter(plugin => advancedNumberHookFilter(plugin.itemIds, item.itemId)); + if(matchingHooks.length !== 0) { + matchingHooks = matchingHooks.filter(plugin => advancedNumberHookFilter(plugin.itemIds, item.itemId)); } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled item on object interaction: ${ item.itemId } on ${ objectConfig.name } ` + `(id-${ landscapeObject.objectId }) @ ${ position.x },${ position.y },${ position.level }`); - return; - } - - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediatePlugins = interactionActions.filter(plugin => !plugin.walkTo); - - // Make sure we walk to the object before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, position, { interactingObject: landscapeObject }) - .then(() => { - player.face(position); - - walkToPlugins.forEach(plugin => - plugin.handler({ - player, - object: landscapeObject, - objectConfig, - position, - item, - itemWidgetId, - itemContainerId, - cacheOriginal - })); - }) - .catch(() => logger.warn(`Unable to complete walk-to action.`)); + return null; } - // Immediately run any non-walk-to plugins - if(immediatePlugins.length !== 0) { - immediatePlugins.forEach(plugin => - plugin.handler({ - player, - object: landscapeObject, - objectConfig, - position, - item, - itemWidgetId, - itemContainerId, - cacheOriginal - })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, + object: landscapeObject, + objectConfig, + position, + item, + itemWidgetId, + itemContainerId, + cacheOriginal + } } }; diff --git a/src/game-engine/world/action/item-on-player.action.ts b/src/game-engine/world/action/item-on-player.action.ts index b5e7422b6..551274826 100644 --- a/src/game-engine/world/action/item-on-player.action.ts +++ b/src/game-engine/world/action/item-on-player.action.ts @@ -44,6 +44,7 @@ export interface ItemOnPlayerAction { } +// @TODO update /** * The pipe that the game engine hands item-on-player actions off to. * @param player @@ -74,8 +75,6 @@ const itemOnPlayerActionPipe = (player: Player, otherPlayer: Player, position: P return; } - player.actionsCancelled.next(null); - // Separate out walk-to actions from immediate actions const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); const immediateHooks = interactionActions.filter(plugin => !plugin.walkTo); diff --git a/src/game-engine/world/action/item-swap.action.ts b/src/game-engine/world/action/item-swap.action.ts index 1d85e5ec7..0f354e11d 100644 --- a/src/game-engine/world/action/item-swap.action.ts +++ b/src/game-engine/world/action/item-swap.action.ts @@ -1,14 +1,13 @@ import { Player } from '../actor/player/player'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { logger } from '@runejs/core'; import { numberHookFilter } from '@engine/world/action/hooks/hook-filters'; -import { ActionPipe } from '@engine/world/action/index'; +import { ActionPipe, RunnableHooks } from '@engine/world/action/index'; /** * Defines a swap items action hook. */ -export interface ItemSwapActionHook extends ActionHook { +export interface ItemSwapActionHook extends ActionHook { widgetId?: number; widgetIds?: number[]; } @@ -44,27 +43,26 @@ export interface ItemSwapAction { * @param toSlot * @param widget */ -const itemSwapActionPipe = async (player: Player, fromSlot: number, toSlot: number, widget: { widgetId: number, containerId: number }): Promise => { - const swapItemsActions = getActionHooks('item_swap') +const itemSwapActionPipe = async (player: Player, fromSlot: number, toSlot: number, widget: { + widgetId: number; containerId: number; }): Promise> => { + const matchingHooks = getActionHooks('item_swap') .filter(plugin => numberHookFilter(plugin.widgetId || plugin.widgetIds, widget.widgetId)); - if(!swapItemsActions || swapItemsActions.length === 0) { + if(!matchingHooks || matchingHooks.length === 0) { await player.sendMessage(`Unhandled Swap Items action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`); - } else { - try { - swapItemsActions.forEach(plugin => - plugin.handler({ - player, - widgetId: widget.widgetId, - containerId: widget.containerId, - fromSlot, - toSlot - })); - } catch(error) { - logger.error(`Error handling Swap Items action.`); - logger.error(error); - } + return null; } + + return { + hooks: matchingHooks, + action: { + player, + widgetId: widget.widgetId, + containerId: widget.containerId, + fromSlot, + toSlot + } + }; }; diff --git a/src/game-engine/world/action/move-item.action.ts b/src/game-engine/world/action/move-item.action.ts index 1131be50b..75a07535c 100644 --- a/src/game-engine/world/action/move-item.action.ts +++ b/src/game-engine/world/action/move-item.action.ts @@ -2,13 +2,13 @@ import { Player } from '@engine/world/actor/player/player'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; import { logger } from '@runejs/core'; import { numberHookFilter } from '@engine/world/action/hooks/hook-filters'; -import { ActionPipe } from '@engine/world/action/index'; +import { ActionPipe, RunnableHooks } from '@engine/world/action/index'; /** * Defines a move item action hook. */ -export interface MoveItemActionHook extends ActionHook { +export interface MoveItemActionHook extends ActionHook { widgetId?: number; widgetIds?: number[]; } @@ -44,27 +44,26 @@ export interface MoveItemAction { * @param toSlot * @param widget */ -const moveItemActionPipe = async (player: Player, fromSlot: number, toSlot: number, widget: { widgetId: number, containerId: number }): Promise => { - const moveItemActions = getActionHooks('move_item') +const moveItemActionPipe = async (player: Player, fromSlot: number, toSlot: number, + widget: { widgetId: number, containerId: number }): Promise> => { + const matchingHooks = getActionHooks('move_item') .filter(plugin => numberHookFilter(plugin.widgetId || plugin.widgetIds, widget.widgetId)); - if(!moveItemActions || moveItemActions.length === 0) { + if(!matchingHooks || matchingHooks.length === 0) { await player.sendMessage(`Unhandled Move Item action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`); - } else { - try { - moveItemActions.forEach(actionHook => - actionHook.handler({ - player, - widgetId: widget.widgetId, - containerId: widget.containerId, - fromSlot, - toSlot - })); - } catch(error) { - logger.error(`Error handling Move Item action.`); - logger.error(error); - } + return null; } + + return { + hooks: matchingHooks, + action: { + player, + widgetId: widget.widgetId, + containerId: widget.containerId, + fromSlot, + toSlot + } + }; }; diff --git a/src/game-engine/world/action/npc-init.action.ts b/src/game-engine/world/action/npc-init.action.ts index ac7a8cca3..05bb228bb 100644 --- a/src/game-engine/world/action/npc-init.action.ts +++ b/src/game-engine/world/action/npc-init.action.ts @@ -7,7 +7,7 @@ import { ActionPipe } from '@engine/world/action/index'; /** * Defines an npc init action hook. */ -export interface NpcInitActionHook extends ActionHook { +export interface NpcInitActionHook extends ActionHook { // A single NPC key or a list of NPC keys that this action applies to. npcs?: string | string[]; } diff --git a/src/game-engine/world/action/npc-interaction.action.ts b/src/game-engine/world/action/npc-interaction.action.ts index 4f07e615b..865147055 100644 --- a/src/game-engine/world/action/npc-interaction.action.ts +++ b/src/game-engine/world/action/npc-interaction.action.ts @@ -2,16 +2,15 @@ import { Player } from '@engine/world/actor/player/player'; import { Npc } from '@engine/world/actor/npc/npc'; import { Position } from '@engine/world/position'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { logger } from '@runejs/core'; import { playerWalkTo } from '@engine/game-server'; import { 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 npc action hook. */ -export interface NpcInteractionActionHook extends ActionHook { +export interface NpcInteractionActionHook extends ActionHook { // A single NPC key or a list of NPC keys that this action applies to. npcs?: string | string[]; // A single option name or a list of option names that this action applies to. @@ -37,6 +36,8 @@ export interface NpcInteractionAction { npc: Npc; // The position that the NPC was at when the action was initiated. position: Position; + // The option used when interacting with the NPC + option: string; } @@ -47,7 +48,7 @@ export interface NpcInteractionAction { * @param position * @param option */ -const npcInteractionActionPipe = (player: Player, npc: Npc, position: Position, option: string): void => { +const npcInteractionActionPipe = (player: Player, npc: Npc, position: Position, option: string): RunnableHooks => { if(player.busy) { return; } @@ -55,44 +56,30 @@ const npcInteractionActionPipe = (player: Player, npc: Npc, position: Position, const morphedNpc = player.getMorphedNpcDetails(npc); // Find all NPC action plugins that reference this NPC - let interactionActions = getActionHooks('npc_interaction') + let matchingHooks = getActionHooks('npc_interaction') .filter(plugin => questHookFilter(player, plugin) && (!plugin.npcs || stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key)) && (!plugin.options || stringHookFilter(plugin.options, option))); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled NPC interaction: ${option} ${morphedNpc?.key || npc.key} (id-${morphedNpc?.gameId || npc.id}) @ ${position.x},${position.y},${position.level}`); if (morphedNpc) { player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } - return; - } - - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediatePlugins = interactionActions.filter(plugin => !plugin.walkTo); - - // Make sure we walk to the NPC before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, position) - .then(() => { - player.face(npc); - npc.face(player); - walkToPlugins.forEach(plugin => plugin.handler({ player, npc, position })); - }) - .catch(() => logger.warn(`Unable to complete walk-to action.`)); + return null; } - // Immediately run any non-walk-to plugins - if(immediatePlugins.length !== 0) { - immediatePlugins.forEach(plugin => plugin.handler({ player, npc, position })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, npc, position, option + } } }; diff --git a/src/game-engine/world/action/object-interaction.action.ts b/src/game-engine/world/action/object-interaction.action.ts index e10ce8482..ddcfc70af 100644 --- a/src/game-engine/world/action/object-interaction.action.ts +++ b/src/game-engine/world/action/object-interaction.action.ts @@ -1,17 +1,16 @@ import { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { logger } from '@runejs/core'; import { playerWalkTo } from '@engine/game-server'; 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'; import { LandscapeObject, ObjectConfig } from '@runejs/filestore'; /** * Defines an object action hook. */ -export interface ObjectInteractionActionHook extends ActionHook { +export interface ObjectInteractionActionHook extends ActionHook { // A single game object ID or a list of object IDs that this action applies to. objectIds: number | number[]; // A single option name or a list of option names that this action applies to. @@ -56,66 +55,38 @@ export interface ObjectInteractionAction { * @param cacheOriginal */ const objectInteractionActionPipe = (player: Player, landscapeObject: LandscapeObject, objectConfig: ObjectConfig, - position: Position, option: string, cacheOriginal: boolean): void => { - if(player.busy || player.metadata.blockObjectInteractions) { - return; + position: Position, option: string, cacheOriginal: boolean): RunnableHooks => { + if(player.metadata.blockObjectInteractions) { + return null; } // Find all object action plugins that reference this location object - let interactionActions = getActionHooks('object_interaction') - .filter(plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, - landscapeObject.objectId, plugin.options, option)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + let matchingHooks = getActionHooks('object_interaction') + .filter(plugin => questHookFilter(player, plugin) && + advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId, plugin.options, option)); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled object interaction: ${option} ${objectConfig.name} ` + `(id-${landscapeObject.objectId}) @ ${position.x},${position.y},${position.level}`); - return; + return null; } - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediatePlugins = interactionActions.filter(plugin => !plugin.walkTo); - - // Make sure we walk to the object before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, position, { interactingObject: landscapeObject }) - .then(() => { - player.face(position); - - walkToPlugins.forEach(plugin => - plugin.handler({ - player, - object: landscapeObject, - objectConfig, - option, - position, - cacheOriginal - })); - }) - .catch(error => { - logger.warn(`Unable to complete walk-to action.`); - console.error(error); - }); - } - - // Immediately run any non-walk-to plugins - if(immediatePlugins.length !== 0) { - immediatePlugins.forEach(plugin => - plugin.handler({ - player, - object: landscapeObject, - objectConfig, - option, - position, - cacheOriginal - })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, + object: landscapeObject, + objectConfig, + option, + position, + cacheOriginal + } } }; diff --git a/src/game-engine/world/action/player-command.action.ts b/src/game-engine/world/action/player-command.action.ts index 420b2c8fe..ccdf45e9f 100644 --- a/src/game-engine/world/action/player-command.action.ts +++ b/src/game-engine/world/action/player-command.action.ts @@ -1,13 +1,12 @@ import { Player } from '../actor/player/player'; -import { logger } from '@runejs/core'; import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; -import { ActionPipe } from '@engine/world/action/index'; +import { ActionPipe, RunnableHooks } from '@engine/world/action/index'; /** * Defines a player command action hook. */ -export interface PlayerCommandActionHook extends ActionHook { +export interface PlayerCommandActionHook extends ActionHook { // The single command or list of commands that this action applies to. commands: string | string[]; // The potential arguments for this command action. @@ -47,78 +46,86 @@ export interface PlayerCommandAction { * @param isConsole * @param inputArgs */ -const playerCommandActionPipe = (player: Player, command: string, isConsole: boolean, inputArgs: string[]): void => { +const playerCommandActionPipe = (player: Player, command: string, isConsole: boolean, + inputArgs: string[]): RunnableHooks => { command = command.toLowerCase(); - const plugins = getActionHooks('player_command').filter(plugin => { - if(Array.isArray(plugin.commands)) { - return plugin.commands.indexOf(command) !== -1; + const actionArgs = {}; + + const plugins = getActionHooks('player_command').filter(actionHook => { + let valid: boolean; + if(Array.isArray(actionHook.commands)) { + valid = actionHook.commands.indexOf(command) !== -1; } else { - return plugin.commands === command; + valid = actionHook.commands === command; } - }); - if(plugins.length === 0) { - player.sendLogMessage(`Unhandled command: ${ command }`, isConsole); - return; - } + if(!valid) { + return false; + } - plugins.forEach(plugin => { - try { - if(plugin.args) { - const pluginArgs = plugin.args; - let syntaxError = `Syntax error. Try ::${ command }`; + if(actionHook.args) { + const args = actionHook.args; + let syntaxError = `Syntax error. Try ::${command}`; - pluginArgs.forEach(pluginArg => { - syntaxError += ` ${ pluginArg.name }:${ pluginArg.type }${ pluginArg.defaultValue === undefined ? '' : '?' }`; - }); + args.forEach(commandArg => { + syntaxError += ` ${commandArg.name}:${commandArg.type}${commandArg.defaultValue === undefined ? '' : '?'}`; + }); - const requiredArgLength = plugin.args.filter(arg => arg.defaultValue === undefined).length; - if(requiredArgLength > inputArgs.length) { - player.sendLogMessage(syntaxError, isConsole); - return; - } + const requiredArgLength = actionHook.args.filter(arg => arg.defaultValue === undefined).length; + if(requiredArgLength > inputArgs.length) { + player.sendLogMessage(syntaxError, isConsole); + return; + } - const actionArgs = {}; - for(let i = 0; i < plugin.args.length; i++) { - let argValue: string | number = inputArgs[i] || null; - const pluginArg = plugin.args[i]; + for(let i = 0; i < actionHook.args.length; i++) { + let argValue: string | number = inputArgs[i] || null; + const pluginArg = actionHook.args[i]; - if(argValue === null || argValue === undefined) { - if(pluginArg.defaultValue === undefined) { + if(argValue === null || argValue === undefined) { + if(pluginArg.defaultValue === undefined) { + player.sendLogMessage(syntaxError, isConsole); + return; + } else { + argValue = pluginArg.defaultValue; + } + } else { + if(pluginArg.type === 'number') { + argValue = parseInt(argValue); + if(isNaN(argValue)) { player.sendLogMessage(syntaxError, isConsole); return; - } else { - argValue = pluginArg.defaultValue; } - } else { - if(pluginArg.type === 'number') { - argValue = parseInt(argValue); - if(isNaN(argValue)) { - player.sendLogMessage(syntaxError, isConsole); - return; - } - } else if(pluginArg.type === 'string') { - if(!argValue || argValue.trim() === '') { - player.sendLogMessage(syntaxError, isConsole); - return; - } + } else if(pluginArg.type === 'string') { + if(!argValue || argValue.trim() === '') { + player.sendLogMessage(syntaxError, isConsole); + return; } } - - actionArgs[pluginArg.name] = argValue; } - plugin.handler({ player, command, isConsole, args: actionArgs }); - } else { - plugin.handler({ player, command, isConsole, args: {} }); + actionArgs[pluginArg.name] = argValue; } - } catch(commandError) { - player.sendLogMessage(`Command error: ${ commandError }`, isConsole); - logger.error(commandError); } + + return true; }); + + if(plugins.length === 0) { + player.sendLogMessage(`Unhandled command: ${ command }`, isConsole); + return; + } + + return { + hooks: plugins, + action: { + player, + command, + isConsole, + args: actionArgs + } + } }; diff --git a/src/game-engine/world/action/player-init.action.ts b/src/game-engine/world/action/player-init.action.ts index 44e0487ee..7125130c3 100644 --- a/src/game-engine/world/action/player-init.action.ts +++ b/src/game-engine/world/action/player-init.action.ts @@ -6,7 +6,7 @@ import { ActionPipe } from '@engine/world/action/index'; /** * Defines a player init action hook. */ -export type PlayerInitActionHook = ActionHook; +export type PlayerInitActionHook = ActionHook; /** diff --git a/src/game-engine/world/action/player-interaction.action.ts b/src/game-engine/world/action/player-interaction.action.ts index e416cb0b7..e22ddeefa 100644 --- a/src/game-engine/world/action/player-interaction.action.ts +++ b/src/game-engine/world/action/player-interaction.action.ts @@ -4,12 +4,13 @@ import { ActionHook, getActionHooks } from '@engine/world/action/hooks'; import { logger } from '@runejs/core'; import { playerWalkTo } from '@engine/game-server'; import { stringHookFilter, questHookFilter } from '@engine/world/action/hooks/hook-filters'; +import { RunnableHooks } from '@engine/world/action/index'; /** * Defines a player action hook. */ -export interface PlayerInteractionActionHook extends ActionHook { +export interface PlayerInteractionActionHook extends ActionHook { // A single option name or a list of option names that this action applies to. options: string | string[]; // Whether or not the player needs to walk to the other player before performing the action. @@ -43,44 +44,30 @@ export interface PlayerInteractionAction { * @param position * @param option */ -const playerInteractionActionPipe = (player: Player, otherPlayer: Player, position: Position, option: string): void => { - if(player.busy) { - return; - } - +const playerInteractionActionPipe = (player: Player, otherPlayer: Player, position: Position, + option: string): RunnableHooks => { // Find all player action plugins that reference this option - let interactionActions = getActionHooks('player_interaction') + let matchingHooks = getActionHooks('player_interaction') .filter(plugin => questHookFilter(player, plugin) && stringHookFilter(plugin.options, option)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.sendMessage(`Unhandled Player interaction: ${option} @ ${position.x},${position.y},${position.level}`); - return; - } - - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediatePlugins = interactionActions.filter(plugin => !plugin.walkTo); - - // Make sure we walk to the other player before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, position) - .then(() => { - player.face(otherPlayer); - walkToPlugins.forEach(plugin => plugin.handler({ player, otherPlayer, position })); - }) - .catch(() => logger.warn(`Unable to complete walk-to action.`)); + return null; } - // Immediately run any non-walk-to plugins - if(immediatePlugins.length !== 0) { - immediatePlugins.forEach(plugin => plugin.handler({ player, otherPlayer, position })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, + otherPlayer, + position + } } }; diff --git a/src/game-engine/world/action/region-change.action.ts b/src/game-engine/world/action/region-change.action.ts index 80f43cacc..809b83c52 100644 --- a/src/game-engine/world/action/region-change.action.ts +++ b/src/game-engine/world/action/region-change.action.ts @@ -8,7 +8,7 @@ import { ActionPipe } from '@engine/world/action/index'; /** * Defines a player region change action hook. */ -export interface RegionChangeActionHook extends ActionHook { +export interface RegionChangeActionHook extends ActionHook { // Optional single region type for the action hook to apply to. regionType?: RegionType; // Optional multiple region types for the action hook to apply to. diff --git a/src/game-engine/world/action/spawned-item-interaction.action.ts b/src/game-engine/world/action/spawned-item-interaction.action.ts index 014e4f191..d7275cd67 100644 --- a/src/game-engine/world/action/spawned-item-interaction.action.ts +++ b/src/game-engine/world/action/spawned-item-interaction.action.ts @@ -6,13 +6,13 @@ import { ItemDetails } from '@engine/config/item-config'; import { findItem } from '@engine/config'; import { playerWalkTo } from '@engine/game-server'; 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 a world item action hook. */ -export interface SpawnedItemInteractionHook extends ActionHook { +export interface SpawnedItemInteractionHook extends ActionHook { // 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. @@ -47,13 +47,9 @@ export interface SpawnedItemInteractionAction { * @param worldItem * @param option */ -const spawnedItemInteractionPipe = (player: Player, worldItem: WorldItem, option: string): void => { - if(player.busy) { - return; - } - +const spawnedItemInteractionPipe = (player: Player, worldItem: WorldItem, option: string): RunnableHooks => { // Find all world item action plugins that reference this world item - let interactionActions = getActionHooks('spawned_item_interaction').filter(plugin => { + let matchingHooks = getActionHooks('spawned_item_interaction').filter(plugin => { if(!questHookFilter(player, plugin)) { return false; } @@ -70,39 +66,28 @@ const spawnedItemInteractionPipe = (player: Player, worldItem: WorldItem, option return true; }); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled world item interaction: ${option} ${worldItem.itemId}`); - return; + return null; } - player.actionsCancelled.next(null); - - // Separate out walk-to actions from immediate actions - const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); - const immediatePlugins = interactionActions.filter(plugin => !plugin.walkTo); - const itemDetails = findItem(worldItem.itemId); - // Make sure we walk to the NPC before running any of the walk-to plugins - if(walkToPlugins.length !== 0) { - playerWalkTo(player, worldItem.position) - .then(() => walkToPlugins.forEach(plugin => plugin.handler({ - player, worldItem, itemDetails - }))) - .catch(() => logger.warn(`Unable to complete walk-to action.`)); - } - - // Immediately run any non-walk-to plugins - if(immediatePlugins.length !== 0) { - immediatePlugins.forEach(plugin => plugin.handler({ - player, worldItem, itemDetails - })); + return { + hooks: matchingHooks, + actionPosition: worldItem.position, + action: { + player, + worldItem, + itemDetails + } } }; diff --git a/src/game-engine/world/action/widget-interaction.action.ts b/src/game-engine/world/action/widget-interaction.action.ts index 0db938343..8daa5d504 100644 --- a/src/game-engine/world/action/widget-interaction.action.ts +++ b/src/game-engine/world/action/widget-interaction.action.ts @@ -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 widget action hook. */ -export interface WidgetInteractionActionHook extends ActionHook { +export interface WidgetInteractionActionHook extends ActionHook { // A single UI widget ID or a list of widget IDs that this action applies to. widgetIds: number | number[]; // A single UI widget child ID or a list of child IDs that this action applies to. @@ -47,7 +47,7 @@ export interface WidgetInteractionAction { * @param childId The ID of the widget child being interacted with. * @param optionId The widget context option chosen by the player. */ -const widgetActionPipe = (player: Player, widgetId: number, childId: number, optionId: number): void => { +const widgetActionPipe = (player: Player, widgetId: number, childId: number, optionId: number): RunnableHooks => { const playerWidget = Object.values(player.interfaceState.widgetSlots).find((widget) => widget && widget.widgetId === widgetId); if(playerWidget && playerWidget.fakeWidget != undefined) { @@ -55,7 +55,7 @@ const widgetActionPipe = (player: Player, widgetId: number, childId: number, opt } // Find all item on item action plugins that match this action - let interactionActions = getActionHooks('widget_interaction').filter(plugin => { + let matchingHooks = getActionHooks('widget_interaction').filter(plugin => { if(!questHookFilter(player, plugin)) { return false; } @@ -74,25 +74,22 @@ const widgetActionPipe = (player: Player, widgetId: number, childId: number, opt return true; }); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + + const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if(questActions.length !== 0) { - interactionActions = questActions; + matchingHooks = questActions; } - if(interactionActions.length === 0) { + if(matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled widget option: ${widgetId}, ${childId}:${optionId}`); - return; + return null; } - // Immediately run the plugins - interactionActions.forEach(plugin => { - if(plugin.cancelActions) { - player.actionsCancelled.next('widget'); - } - - plugin.handler({ player, widgetId, childId, optionId }); - }); + return { + hooks: matchingHooks, + action: { player, widgetId, childId, optionId } + }; }; diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index 74fe8508f..143755440 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -13,46 +13,49 @@ import { world } from '@engine/game-server'; import { WorldInstance } from '@engine/world/instances'; import { Player } from '@engine/world/actor/player/player'; import { ActionCancelType, ActionPipeline } from '@engine/world/action'; +import { LocationObject } from '@runejs/cache-parser'; /** * Handles an actor within the game world. */ export abstract class Actor { + public readonly updateFlags: UpdateFlags = new UpdateFlags(); + public readonly skills: Skills = new Skills(this); + public readonly walkingQueue: WalkingQueue = new WalkingQueue(this); + public readonly inventory: ItemContainer = new ItemContainer(28); + public readonly bank: ItemContainer = new ItemContainer(376); public readonly actionPipeline = new ActionPipeline(this); - public readonly updateFlags: UpdateFlags; - public readonly skills: Skills; public readonly metadata: { [key: string]: any } = {}; - public readonly actionsCancelled: Subject; - public readonly movementEvent: Subject; - public pathfinding: Pathfinding; + + /** + * @deprecated - use new action system instead + */ + public readonly actionsCancelled: Subject = new Subject(); + + public pathfinding: Pathfinding = new Pathfinding(this); public lastMovementPosition: Position; + protected randomMovementInterval; - private readonly _walkingQueue: WalkingQueue; - private readonly _inventory: ItemContainer; - private readonly _bank: ItemContainer; + private _position: Position; private _lastMapRegionUpdatePosition: Position; private _worldIndex: number; private _walkDirection: number; private _runDirection: number; private _faceDirection: number; - private _busy: boolean; private _instance: WorldInstance = null; + /** + * @deprecated - use new action system instead + */ + private _busy: boolean; + protected constructor() { - this.updateFlags = new UpdateFlags(); - this._walkingQueue = new WalkingQueue(this); this._walkDirection = -1; this._runDirection = -1; this._faceDirection = 6; - this._inventory = new ItemContainer(28); - this._bank = new ItemContainer(376); - this.skills = new Skills(this); this._busy = false; - this.pathfinding = new Pathfinding(this); - this.actionsCancelled = new Subject(); - this.movementEvent = new Subject(); } public damage(amount: number, damageType: DamageType = DamageType.DAMAGE): 'alive' | 'dead' { @@ -69,6 +72,63 @@ export abstract class Actor { return remainingHitpoints === 0 ? 'dead' : 'alive'; } + /** + * Waits for the actor to reach the specified position before resolving it's promise. + * The promise will be rejected if the actor's walking queue changes or their movement is otherwise canceled. + * @param position The position that the actor needs to reach for the promise to resolve. + */ + public async waitForPathing(position: Position): Promise; + + /** + * Waits for the actor to reach the specified game object before resolving it's promise. + * The promise will be rejected if the actor's walking queue changes or their movement is otherwise canceled. + * @param gameObject The game object to wait for the actor to reach. + */ + public async waitForPathing(gameObject: LocationObject): Promise; + + /** + * Waits for the actor to reach the specified game object before resolving it's promise. + * The promise will be rejected if the actor's walking queue changes or their movement is otherwise canceled. + * @param target The position or game object that the actor needs to reach for the promise to resolve. + */ + public async waitForPathing(target: Position | LocationObject): Promise; + public async waitForPathing(target: Position | LocationObject): Promise { + if(this.position.withinInteractionDistance(target)) { + return; + } + + await new Promise((resolve, reject) => { + this.metadata.walkingTo = target instanceof Position ? target : new Position(target.x, target.y, target.level); + + const inter = setInterval(() => { + if(!this.metadata.walkingTo || !this.metadata.walkingTo.equals(target)) { + reject(); + clearInterval(inter); + return; + } + + if(!this.walkingQueue.moving()) { + if(target instanceof Position) { + if(this.position.distanceBetween(target) > 1) { + reject(); + } else { + resolve(); + } + } else { + if(this.position.withinInteractionDistance(target)) { + resolve(); + } else { + reject(); + } + } + + clearInterval(inter); + this.metadata.walkingTo = null; + } + }, 100); + }); + } + public async moveBehind(target: Actor): Promise { const distance = Math.floor(this.position.distanceBetween(target.position)); if(distance > 16) { @@ -111,7 +171,7 @@ export abstract class Actor { this.metadata['following'] = target; this.moveBehind(target); - const subscription = target.movementEvent.subscribe(async () => this.moveBehind(target)); + const subscription = target.walkingQueue.movementEvent.subscribe(async () => this.moveBehind(target)); this.actionsCancelled.pipe( filter(type => type !== 'pathing-movement'), @@ -123,8 +183,12 @@ export abstract class Actor { }); } - public async walkTo(target: Actor): Promise { - const distance = Math.floor(this.position.distanceBetween(target.position)); + public async walkTo(target: Actor): Promise; + public async walkTo(position: Position): Promise; + public async walkTo(target: Actor | Position): Promise { + const desiredPosition = target instanceof Position ? target : target.position; + + const distance = Math.floor(this.position.distanceBetween(desiredPosition)); if(distance <= 1) { return false; @@ -136,8 +200,6 @@ export abstract class Actor { return false; } - const desiredPosition = target.position; - await this.pathfinding.walkTo(desiredPosition, { pathingSearchRadius: distance + 2, ignoreDestination: true @@ -156,7 +218,7 @@ export abstract class Actor { this.metadata['tailing'] = target; this.moveTo(target); - const subscription = target.movementEvent.subscribe(async () => this.moveTo(target)); + const subscription = target.walkingQueue.movementEvent.subscribe(async () => this.moveTo(target)); this.actionsCancelled.pipe( filter(type => type !== 'pathing-movement'), @@ -211,8 +273,7 @@ export abstract class Actor { } public stopAnimation(): void { - const animation = { id: -1, delay: 0 }; - this.updateFlags.animation = animation; + this.updateFlags.animation = { id: -1, delay: 0 }; } public playGraphics(graphics: number | Graphic): void { @@ -224,30 +285,29 @@ export abstract class Actor { } public stopGraphics(): void { - const graphics = { id: -1, delay: 0, height: 120 }; - this.updateFlags.graphics = graphics; + this.updateFlags.graphics = { id: -1, delay: 0, height: 120 }; } public removeItem(slot: number): void { - this._inventory.remove(slot); + this.inventory.remove(slot); } public removeBankItem(slot: number): void { - this._bank.remove(slot); + this.bank.remove(slot); } public giveItem(item: number | Item): boolean { - return this._inventory.add(item) !== null; + return this.inventory.add(item) !== null; } public giveBankItem(item: number | Item): boolean { - return this._bank.add(item) !== null; + return this.bank.add(item) !== null; } public hasItemInInventory(item: number | Item): boolean { - return this._inventory.has(item); + return this.inventory.has(item); } public hasItemInBank(item: number | Item): boolean { - return this._bank.has(item); + return this.bank.has(item); } public hasItemOnPerson(item: number | Item): boolean { @@ -409,10 +469,6 @@ export abstract class Actor { this._worldIndex = value; } - public get walkingQueue(): WalkingQueue { - return this._walkingQueue; - } - public get walkDirection(): number { return this._walkDirection; } @@ -437,13 +493,6 @@ export abstract class Actor { this._faceDirection = value; } - public get inventory(): ItemContainer { - return this._inventory; - } - public get bank(): ItemContainer { - return this._bank; - } - public get busy(): boolean { return this._busy; } @@ -470,4 +519,20 @@ export abstract class Actor { this._instance = value; } + + public get isPlayer(): boolean { + return this instanceof Player; + } + + public get isNpc(): boolean { + return this instanceof Npc; + } + + public get type(): { player?: Player, npc?: Npc } { + return { + player: this.isPlayer ? this as unknown as Player : undefined, + npc: this.isNpc ? this as unknown as Npc : undefined + }; + } + } diff --git a/src/game-engine/world/actor/dialogue.ts b/src/game-engine/world/actor/dialogue.ts index 982ba390d..e686b3d47 100644 --- a/src/game-engine/world/actor/dialogue.ts +++ b/src/game-engine/world/actor/dialogue.ts @@ -359,7 +359,7 @@ function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], di } async function runDialogueAction(player: Player, dialogueAction: string | DialogueFunction | DialogueAction, - tag?: string, additionalOptions?: AdditionalOptions): Promise { + tag?: string | undefined | false, additionalOptions?: AdditionalOptions): Promise { if(dialogueAction instanceof DialogueFunction && !tag) { // Code execution dialogue. dialogueAction.execute(); @@ -531,18 +531,18 @@ async function runDialogueAction(player: Player, dialogueAction: string | Dialog const widgetClosedEvent = await player.interfaceState.widgetClosed('chatbox'); - if(widgetClosedEvent.data === undefined) { - throw new Error('Dialogue Cancelled.'); - } - - if(isOptions && typeof widgetClosedEvent.data === 'number') { - const optionsAction = dialogueAction as OptionsDialogueAction; - const options = Object.keys(optionsAction.options); - const trees = options.map(option => optionsAction.options[option]); - const tree: ParsedDialogueTree = trees[widgetClosedEvent.data - 1]; - if(tree && tree.length !== 0) { - await runParsedDialogue(player, tree, tag, additionalOptions); + if(widgetClosedEvent.data !== undefined) { + if(isOptions && typeof widgetClosedEvent.data === 'number') { + const optionsAction = dialogueAction as OptionsDialogueAction; + const options = Object.keys(optionsAction.options); + const trees = options.map(option => optionsAction.options[option]); + const tree: ParsedDialogueTree = trees[widgetClosedEvent.data - 1]; + if(tree && tree.length !== 0) { + await runParsedDialogue(player, tree, tag, additionalOptions); + } } + } else { + return false; } } } @@ -550,9 +550,13 @@ async function runDialogueAction(player: Player, dialogueAction: string | Dialog return tag; } -async function runParsedDialogue(player: Player, dialogueTree: ParsedDialogueTree, tag?: string, additionalOptions?: AdditionalOptions): Promise { +async function runParsedDialogue(player: Player, dialogueTree: ParsedDialogueTree, tag?: string | undefined | false, + additionalOptions?: AdditionalOptions): Promise { for(let i = 0; i < dialogueTree.length; i++) { tag = await runDialogueAction(player, dialogueTree[i], tag, additionalOptions); + if(tag === false) { + break; + } } return tag === undefined; @@ -575,23 +579,13 @@ export async function dialogue(participants: (Player | NpcParticipant)[], dialog const parsedDialogueTree = parseDialogueTree(player, npcParticipants, dialogueTree); player.metadata.dialogueTree = parsedDialogueTree; - async function run(): Promise { - return await new Promise((resolve, reject) => { - runParsedDialogue(player, parsedDialogueTree, undefined, additionalOptions).then(() => { - resolve(); - }).catch(error => { - reject(error); - }); - }); - } - try { - await run(); + await runParsedDialogue(player, parsedDialogueTree, undefined, additionalOptions); player.interfaceState.closeAllSlots(); return true; } catch(error) { player.interfaceState.closeAllSlots(); - logger.warn(Object.keys(error.message)); + logger.warn(error); return false; } } diff --git a/src/game-engine/world/actor/player/interface-state.ts b/src/game-engine/world/actor/player/interface-state.ts index 71f509df7..7faed6649 100644 --- a/src/game-engine/world/actor/player/interface-state.ts +++ b/src/game-engine/world/actor/player/interface-state.ts @@ -66,6 +66,7 @@ export class Widget { export interface WidgetClosedEvent { widget: Widget; + widgetId?: number; data?: number; } @@ -127,25 +128,18 @@ export class InterfaceState { } public async widgetClosed(slot: GameInterfaceSlot): Promise { - return await lastValueFrom(this.closed.pipe( + return await lastValueFrom(this.closed.asObservable().pipe( filter(event => event.widget.slot === slot)).pipe(take(1))); } - public closeWidget(widgetId: number, data?: number): void; - public closeWidget(slot: GameInterfaceSlot, data?: number): void; - public closeWidget(i: GameInterfaceSlot | number, data?: number): void { - let widget: Widget | null; - if(typeof i === 'number') { - widget = this.findWidget(i); - } else { - widget = this.widgetSlots[i] || null; - } + public closeWidget(slot: GameInterfaceSlot, widgetId?: number, data?: number): void { + const widget: Widget | null = (slot ? this.widgetSlots[slot] : this.findWidget(widgetId)) || null; if(!widget) { return; } - this.closed.next({ widget, data }); + this.closed.next({ widget, widgetId, data }); this.widgetSlots[widget.slot] = null; } @@ -208,7 +202,12 @@ export class InterfaceState { return widget || null; } - public widgetOpen(slot: GameInterfaceSlot, widgetId?: number): boolean { + public widgetOpen(slot?: GameInterfaceSlot, widgetId?: number): boolean { + if(!slot) { + const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[]; + return slots.some(s => this.getWidget(s) !== null); + } + if(widgetId === undefined) { return this.getWidget(slot) !== null; } else { diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index 0a27ffde7..bc6e6a9e4 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -141,7 +141,6 @@ export class Player extends Actor { private _bonuses: { offensive: OffensiveBonuses, defensive: DefensiveBonuses, skill: SkillBonuses }; private _carryWeight: number; private _settings: PlayerSettings; - private _walkingTo: Position; private _nearbyChunks: Chunk[]; private quadtreeKey: QuadtreeKey = null; private privateMessageIndex: number = 1; @@ -282,7 +281,9 @@ export class Player extends Actor { this.save(); this.actionsCancelled.complete(); - this.movementEvent.complete(); + this.walkingQueue.movementEvent.complete(); + this.walkingQueue.movementQueued.complete(); + this.actionPipeline.shutdown(); this.outgoingPackets.logout(); this.instance = null; world.chunkManager.getChunkForWorldPosition(this.position).removePlayer(this); @@ -597,11 +598,13 @@ export class Player extends Actor { let showDialogue = false; let showInConsole = false; - if(options && typeof options === 'boolean') { - showDialogue = true; - } else if(options) { - showDialogue = options.dialogue || false; - showInConsole = options.console || false; + if(options) { + if(typeof options === 'boolean') { + showDialogue = true; + } else { + showDialogue = options.dialogue || false; + showInConsole = options.console || false; + } } if(!showDialogue) { @@ -1269,14 +1272,6 @@ export class Player extends Actor { return this._settings; } - public get walkingTo(): Position { - return this._walkingTo; - } - - public set walkingTo(value: Position) { - this._walkingTo = value; - } - public get nearbyChunks(): Chunk[] { return this._nearbyChunks; } diff --git a/src/game-engine/world/actor/player/sync/actor-sync.ts b/src/game-engine/world/actor/player/sync/actor-sync.ts index cc01dfe8f..7a55eff32 100644 --- a/src/game-engine/world/actor/player/sync/actor-sync.ts +++ b/src/game-engine/world/actor/player/sync/actor-sync.ts @@ -141,3 +141,9 @@ export function appendMovement(actor: Actor, packet: ByteBuffer): void { } } } + +export abstract class SyncTask { + + public abstract async execute(): Promise; + +} diff --git a/src/game-engine/world/actor/player/sync/npc-sync-task.ts b/src/game-engine/world/actor/player/sync/npc-sync-task.ts index 5a639eb33..97596d15a 100644 --- a/src/game-engine/world/actor/player/sync/npc-sync-task.ts +++ b/src/game-engine/world/actor/player/sync/npc-sync-task.ts @@ -1,17 +1,16 @@ import { ByteBuffer } from '@runejs/core/buffer'; -import { Task } from '@engine/world/task'; import { Packet, PacketType } from '@engine/net/packet'; import { Npc } from '@engine/world/actor/npc/npc'; import { world } from '@engine/game-server'; -import { registerNewActors, syncTrackedActors } from './actor-sync'; +import { registerNewActors, SyncTask, syncTrackedActors } from './actor-sync'; import { Player } from '../player'; /** * Handles the chonky npc synchronization packet for a specific player. */ -export class NpcSyncTask extends Task { +export class NpcSyncTask extends SyncTask { private readonly player: Player; diff --git a/src/game-engine/world/actor/player/sync/player-sync-task.ts b/src/game-engine/world/actor/player/sync/player-sync-task.ts index 4bea8da76..3b24a5351 100644 --- a/src/game-engine/world/actor/player/sync/player-sync-task.ts +++ b/src/game-engine/world/actor/player/sync/player-sync-task.ts @@ -1,20 +1,19 @@ import { ByteBuffer } from '@runejs/core/buffer'; -import { Task } from '@engine/world/task'; import { UpdateFlags } from '@engine/world/actor/update-flags'; import { Packet, PacketType } from '@engine/net/packet'; import { world } from '@engine/game-server'; import { stringToLong } from '@engine/util/strings'; import { findItem, findNpc } from '@engine/config'; import { EquipmentSlot, ItemDetails } from '@engine/config/item-config'; -import { appendMovement, registerNewActors, syncTrackedActors } from './actor-sync'; +import { appendMovement, registerNewActors, SyncTask, syncTrackedActors } from './actor-sync'; import { Player } from '../player'; /** * Handles the chonky player synchronization packet. */ -export class PlayerSyncTask extends Task { +export class PlayerSyncTask extends SyncTask { private readonly player: Player; diff --git a/src/game-engine/world/actor/walking-queue.ts b/src/game-engine/world/actor/walking-queue.ts index 8c55fe5df..db7e43c15 100644 --- a/src/game-engine/world/actor/walking-queue.ts +++ b/src/game-engine/world/actor/walking-queue.ts @@ -4,6 +4,7 @@ import { Player } from './player/player'; import { world } from '@engine/game-server'; import { Npc } from './npc/npc'; import { regionChangeActionFactory } from '@engine/world/action/region-change.action'; +import { Subject } from 'rxjs'; /** @@ -11,6 +12,11 @@ import { regionChangeActionFactory } from '@engine/world/action/region-change.ac */ export class WalkingQueue { + public readonly movementQueued = new Subject(); + public readonly movementEvent = new Subject(); + public readonly movementQueued$ = this.movementQueued.asObservable(); + public readonly movementEvent$ = this.movementEvent.asObservable(); + private queue: Position[]; private _valid: boolean; @@ -63,6 +69,7 @@ export class WalkingQueue { lastPosition = newPosition; newPosition.metadata = positionMetadata; this.queue.push(newPosition); + this.movementQueued.next(newPosition); } else { this.valid = false; break; @@ -75,6 +82,7 @@ export class WalkingQueue { if(this.actor.pathfinding.canMoveTo(lastPosition, newPosition)) { newPosition.metadata = positionMetadata; this.queue.push(newPosition); + this.movementQueued.next(newPosition); } else { this.valid = false; } @@ -136,15 +144,6 @@ export class WalkingQueue { const walkPosition = this.queue.shift(); - if(this.actor instanceof Player) { - this.actor.actionsCancelled.next('pathing-movement'); - // if(activeWidget.disablePlayerMovement) { - // this.resetDirections(); - // return; - // } - //this.actor.interfaceState.closeAllSlots(); - } - if(this.actor.metadata['faceActorClearedByWalking'] === undefined || this.actor.metadata['faceActorClearedByWalking']) { this.actor.clearFaceActor(); } @@ -201,7 +200,7 @@ export class WalkingQueue { const newChunk = world.chunkManager.getChunkForWorldPosition(this.actor.position); - this.actor.movementEvent.next(this.actor.position); + this.movementEvent.next(this.actor.position); if(this.actor instanceof Player) { const mapDiffX = this.actor.position.x - (lastMapRegionUpdatePosition.chunkX * 8); diff --git a/src/game-engine/world/index.ts b/src/game-engine/world/index.ts index 1e3f5fb0f..3e4afd4f5 100644 --- a/src/game-engine/world/index.ts +++ b/src/game-engine/world/index.ts @@ -70,24 +70,34 @@ export class World { /** * Searched for an object by ID at the given position in any of the player's active instances. - * @param player The player to find the object for. + * @param actor The actor to find the object for. * @param objectId The game ID of the object. * @param objectPosition The game world position that the object is expected at. */ - public findObjectAtLocation(player: Player, objectId: number, + public findObjectAtLocation(actor: Actor, objectId: number, objectPosition: Position): { object: LandscapeObject, cacheOriginal: boolean } { const x = objectPosition.x; const y = objectPosition.y; const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition); let cacheOriginal = true; - const tileModifications = player.instance.getTileModifications(objectPosition); - const personalTileModifications = player.personalInstance.getTileModifications(objectPosition); + let tileModifications; + let personalTileModifications; + + if(actor.isPlayer) { + tileModifications = (actor as Player).instance.getTileModifications(objectPosition); + personalTileModifications = (actor as Player).personalInstance.getTileModifications(objectPosition); + } else { + tileModifications = this.globalInstance.getTileModifications(objectPosition); + } let landscapeObject = objectChunk.getFilestoreLandscapeObject(objectId, objectPosition); if(!landscapeObject) { - const tileObjects = [ ...tileModifications.mods.spawnedObjects, - ...personalTileModifications.mods.spawnedObjects ]; + const tileObjects = [ ...tileModifications.mods.spawnedObjects ]; + + if(actor.isPlayer) { + tileObjects.push(...personalTileModifications.mods.spawnedObjects); + } landscapeObject = tileObjects.find(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) || null; @@ -99,8 +109,11 @@ export class World { } } - const hiddenTileObjects = [ ...tileModifications.mods.hiddenObjects, - ...personalTileModifications.mods.hiddenObjects ]; + const hiddenTileObjects = [ ...tileModifications.mods.hiddenObjects ]; + + if(actor.isPlayer) { + hiddenTileObjects.push(...personalTileModifications.mods.hiddenObjects); + } if(hiddenTileObjects.findIndex(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) !== -1) { diff --git a/src/game-engine/world/position.ts b/src/game-engine/world/position.ts index 2329d2df2..8b42c4e0a 100644 --- a/src/game-engine/world/position.ts +++ b/src/game-engine/world/position.ts @@ -44,33 +44,40 @@ export class Position { return new Position(this.x, this.y, this.level); } - public withinInteractionDistance(locationObject: LandscapeObject): boolean { - const definition = filestore.configStore.objectStore.getObject(locationObject.objectId); - const occupantX = locationObject.x; - const occupantY = locationObject.y; - let width = definition.rendering?.sizeX || 1; - let height = definition.rendering?.sizeY || 1; - - if(width === undefined || width === null || width < 1) { - width = 1; - } - if(height === undefined || height === null || height < 1) { - height = 1; - } - - if(width === 1 && height === 1) { - return this.distanceBetween(new Position(occupantX, occupantY, locationObject.level)) <= 1; + public withinInteractionDistance(gameObject: LandscapeObject, minimumDistance?: number): boolean; + public withinInteractionDistance(position: Position, minimumDistance?: number): boolean; + public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance?: number): boolean; + public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance: number = 1): boolean { + if(target instanceof Position) { + return this.distanceBetween(target) <= minimumDistance; } else { - if(locationObject.orientation == 1 || locationObject.orientation == 3) { - const off = width; - width = height; - height = off; + const definition = filestore.configStore.objectStore.getObject(target.objectId); + const occupantX = target.x; + const occupantY = target.y; + let width = definition.rendering?.sizeX || 1; + let height = definition.rendering?.sizeY || 1; + + if(width === undefined || width === null || width < 1) { + width = 1; } + if(height === undefined || height === null || height < 1) { + height = 1; + } + + if(width === 1 && height === 1) { + return this.distanceBetween(new Position(occupantX, occupantY, target.level)) <= minimumDistance; + } else { + if(target.orientation === 1 || target.orientation === 3) { + const off = width; + width = height; + height = off; + } - for(let x = occupantX; x < occupantX + width; x++) { - for(let y = occupantY; y < occupantY + height; y++) { - if(this.distanceBetween(new Position(x, y, locationObject.level)) <= 1) { - return true; + for(let x = occupantX; x < occupantX + width; x++) { + for(let y = occupantY; y < occupantY + height; y++) { + if(this.distanceBetween(new Position(x, y, target.level)) <= minimumDistance) { + return true; + } } } } diff --git a/src/game-engine/world/task.ts b/src/game-engine/world/task.ts index 288752034..e525ee510 100644 --- a/src/game-engine/world/task.ts +++ b/src/game-engine/world/task.ts @@ -2,11 +2,6 @@ import { lastValueFrom, timer } from 'rxjs'; import { World } from '@engine/world/index'; import { take } from 'rxjs/operators'; -export abstract class Task { - - public abstract execute(): T | Promise; - -} export const schedule = async (ticks: number): Promise => { return lastValueFrom(timer(ticks * World.TICK_LENGTH).pipe(take(1))); diff --git a/src/plugins/dialogue/dialogue-option.plugin.ts b/src/plugins/dialogue/dialogue-option.plugin.ts index 1db56b250..4fe989d1c 100644 --- a/src/plugins/dialogue/dialogue-option.plugin.ts +++ b/src/plugins/dialogue/dialogue-option.plugin.ts @@ -12,7 +12,7 @@ const dialogueIds = [ */ export const action: widgetInteractionActionHandler = (details) => { const { player, widgetId, childId } = details; - player.interfaceState.closeWidget(widgetId, childId); + player.interfaceState.closeWidget('chatbox', widgetId, childId); }; export default { diff --git a/src/plugins/skills/woodcutting/woodcutting.plugin.ts b/src/plugins/skills/woodcutting/woodcutting.plugin.ts index a872fb029..dcd847563 100644 --- a/src/plugins/skills/woodcutting/woodcutting.plugin.ts +++ b/src/plugins/skills/woodcutting/woodcutting.plugin.ts @@ -1,17 +1,129 @@ -import { objectInteractionActionHandler } from '@engine/world/action/object-interaction.action'; +import { + ObjectInteractionAction, + ObjectInteractionActionHook +} from '@engine/world/action/object-interaction.action'; import { Skill } from '@engine/world/actor/skills'; -import { canInitiateHarvest, handleHarvesting } from '@engine/world/skill-util/harvest-skill'; -import { getTreeFromHealthy, getTreeIds } from '@engine/world/config/harvestable-object'; +import { canInitiateHarvest } from '@engine/world/skill-util/harvest-skill'; +import { getTreeFromHealthy, getTreeIds, IHarvestable } from '@engine/world/config/harvestable-object'; +import { randomBetween } from '@engine/util/num'; +import { colorText } from '@engine/util/strings'; +import { colors } from '@engine/util/colors'; +import { rollBirdsNestType } from '@engine/world/skill-util/harvest-roll'; +import { world } from '@engine/game-server'; +import { soundIds } from '@engine/world/config/sound-ids'; +import { Axe, getAxe, HarvestTool } from '@engine/world/config/harvest-tool'; +import { TaskExecutor } from '@engine/world/action'; +import { findItem } from '@engine/config'; -const action: objectInteractionActionHandler = (details) => { - const tree = getTreeFromHealthy(details.object.objectId); - const tool = canInitiateHarvest(details.player, tree, Skill.WOODCUTTING); - if (!tool) { - return; +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { + const { actor, actionData: { position, object } } = task; + const tree = getTreeFromHealthy(object.objectId); + + if(!tree) { + return false; + } + + const { type: { player }, isPlayer } = actor; + + const tool = isPlayer ? canInitiateHarvest(player, tree, Skill.WOODCUTTING) : getAxe(Axe.STEEL); + + if(!tool) { + return false; + } + + task.session.tool = tool; + task.session.tree = tree; + + if(taskIteration === 0) { + // First run + + player?.sendMessage('You swing your axe at the tree.'); + + actor.face(position); + actor.playAnimation(tool.animation); } - handleHarvesting(details, tool, tree, Skill.WOODCUTTING); + return true; +}; + + +const activate = (task: TaskExecutor, taskIteration: number): boolean => { + const { actor, player, actionData, session } = task.getDetails(); + const { position: objectPosition, object: actionObject } = actionData; + const tree: IHarvestable = session.tree; + const tool: HarvestTool = session.tool; + + // Cancel if the actor no longer has their tool or level requirements. + if(!tool || !tree) { + return false; + } + + // Grab the tree manually every loop so that we can make sure it's still alive. + const { object } = world.findObjectAtLocation(actor, actionObject.objectId, objectPosition); + + if(!object) { + // Tree has been chopped down, cancel. + return false; + } + + // Check if the amount of ticks passed equal the tools pulses. + if(taskIteration % 3 === 0 && taskIteration != 0) { + const successChance = randomBetween(0, 255); + + let toolLevel = tool.level - 1; + if(tool.itemId === 1349 || tool.itemId === 1267) { + toolLevel = 2; + } + + const percentNeeded = tree.baseChance + toolLevel + actor.skills.woodcutting.level; + if(successChance <= percentNeeded) { + const targetName: string = findItem(tree.itemId).name.toLowerCase(); + + if(actor.inventory.hasSpace()) { + const itemToAdd = tree.itemId; + const roll = randomBetween(1, 256); + + if(roll === 1) { // Bird nest chance + player?.sendMessage(colorText(`A bird's nest falls out of the tree.`, colors.red)); + world.globalInstance.spawnWorldItem(rollBirdsNestType(), actor.position, + { owner: player || null, expires: 300 }); + } else { // Standard log chopper + player?.sendMessage(`You manage to chop some ${targetName}.`); + actor.giveItem(itemToAdd); + } + + player?.skills.woodcutting.addExp(tree.experience); + + if(randomBetween(0, 100) <= tree.break) { + player?.playSound(soundIds.oreDepeleted); + actor.instance.replaceGameObject(tree.objects.get(actionObject.objectId), + object, randomBetween(tree.respawnLow, tree.respawnHigh)); + return false; + } + } else { + player?.sendMessage( + `Your inventory is too full to hold any more ${targetName}.`, true); + player?.playSound(soundIds.inventoryFull); + return false; + } + } + } else { + if(taskIteration % 1 === 0 && taskIteration !== 0) { + const randomSoundIdx = Math.floor(Math.random() * soundIds.axeSwing.length); + player?.playSound(soundIds.axeSwing[randomSoundIdx], 7, 0); + } + } + + if(taskIteration % 3 === 0 && taskIteration !== 0) { + actor.playAnimation(tool.animation); + } + + return true; +}; + +const onComplete = (task: TaskExecutor): void => { + task.actor.stopAnimation(); }; @@ -22,8 +134,13 @@ export default { type: 'object_interaction', options: [ 'chop down', 'chop' ], objectIds: getTreeIds(), - walkTo: true, - handler: action - } + strength: 'normal', + task: { + canActivate, + activate, + onComplete, + interval: 1 + } + } as ObjectInteractionActionHook ] };