From fd9c64343badefec07b407d0f50f963690f9bd69 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 13 Mar 2021 17:46:33 -0600 Subject: [PATCH 01/16] Preliminary work on a Task Executor and action priority queue system --- src/game-engine/world/action/hooks/index.ts | 14 +- src/game-engine/world/action/index.ts | 122 +++++++++++++++++- .../world/actor/player/sync/actor-sync.ts | 6 + .../world/actor/player/sync/npc-sync-task.ts | 5 +- .../actor/player/sync/player-sync-task.ts | 5 +- src/game-engine/world/task.ts | 8 +- 6 files changed, 145 insertions(+), 15 deletions(-) diff --git a/src/game-engine/world/action/hooks/index.ts b/src/game-engine/world/action/hooks/index.ts index ff3c1721b..4e7dee938 100644 --- a/src/game-engine/world/action/hooks/index.ts +++ b/src/game-engine/world/action/hooks/index.ts @@ -1,6 +1,18 @@ import { actionHookMap } from '@engine/game-server'; import { QuestKey } from '@engine/config/quest-config'; -import { ActionType } from '@engine/world/action'; +import { ActionPriority, TaskExecutor, ActionType } from '@engine/world/action'; + + +export interface HookTask { + priority?: ActionPriority; + canActivate?: (task: TaskExecutor) => boolean | Promise; + execute: (task: TaskExecutor) => void | undefined | boolean | Promise; + onComplete?: (task: TaskExecutor) => 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) +} /** diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 558095ba1..a70ac14d5 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -2,14 +2,126 @@ 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 { HookTask } from '@engine/world/action/hooks'; +import { lastValueFrom, Subscription, timer } from 'rxjs'; +import { World } from '@engine/world'; /** - * The priority of an action within the pipeline. + * The priority of an queueable action within the pipeline. */ export type ActionPriority = 'weak' | 'normal' | 'strong'; +// T = current action info (ButtonAction, MoveItemAction, etc) +export class TaskExecutor { + + public running: boolean = false; + private intervalSubscription: Subscription; + + public constructor(public readonly actor: Actor, + public readonly task: HookTask, + public readonly actionData: T) { + } + + public async run(): Promise { + this.running = true; + + if(!!this.task.delay || !!this.task.delayMs) { + await lastValueFrom(timer(!!this.task.delayMs ? this.task.delayMs : (this.task.delay * World.TICK_LENGTH))); + } + + if(!this.running) { + return; + } + + if(!!this.task.interval || !!this.task.intervalMs) { + // Looping execution task + const intervalMs = 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(); + } + }, + async error => { + logger.error(error); + resolve(); + }, + async () => 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(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.execute(this); + 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); + } catch(error) { + logger.error(`Error calling action canActivate`, this.task); + logger.error(error); + return false; + } + } + + private async stop(): Promise { + this.intervalSubscription?.unsubscribe(); + + await this.task?.onComplete(this); + + this.running = false; + } + + public get valid(): boolean { + return !!this.task?.execute && !!this.actionData; + } + +} + + /** * Content action type definitions. */ @@ -74,6 +186,10 @@ export class ActionPipeline { ActionPipeline.pipes.set(action.toString(), actionPipe); } + public async queue(action: ActionType, ...args: any[]): Promise { + + } + public async call(action: ActionType, ...args: any[]): Promise { const actionHandler = ActionPipeline.pipes.get(action.toString()); if(actionHandler) { @@ -89,6 +205,10 @@ export class ActionPipeline { } } + public get paused(): boolean { + return false; + } + } 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 27577042a..62da8352f 100644 --- a/src/game-engine/world/actor/player/sync/actor-sync.ts +++ b/src/game-engine/world/actor/player/sync/actor-sync.ts @@ -139,3 +139,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 5dd5f89d3..9b5c5bf03 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,15 +1,14 @@ -import { Task } from '@engine/world/task'; import { Player } from '../player'; 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, syncTrackedActors, SyncTask } from './actor-sync'; import { ByteBuffer } from '@runejs/core'; /** * 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 d04d957b0..fcecdd0b7 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,9 +1,8 @@ import { Player } from '../player'; -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 { appendMovement, registerNewActors, syncTrackedActors } from './actor-sync'; +import { appendMovement, registerNewActors, syncTrackedActors, SyncTask } from './actor-sync'; import { ByteBuffer } from '@runejs/core'; import { stringToLong } from '@engine/util/strings'; import { findItem, findNpc } from '@engine/config'; @@ -12,7 +11,7 @@ import { EquipmentSlot, ItemDetails } from '@engine/config/item-config'; /** * 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/task.ts b/src/game-engine/world/task.ts index cc2252da0..537a73a22 100644 --- a/src/game-engine/world/task.ts +++ b/src/game-engine/world/task.ts @@ -1,13 +1,7 @@ -import { lastValueFrom, Observable, timer } from 'rxjs'; +import { lastValueFrom, timer } from 'rxjs'; import { World } from '@engine/world/index'; import { take } from 'rxjs/operators'; -export abstract class Task { - - public abstract async execute(): Promise; - -} - export const schedule = async (ticks: number): Promise => { return lastValueFrom(timer(ticks * World.TICK_LENGTH).pipe(take(1))); }; From 5b7fa9d5b4b5862b931000618c6505fbd3564818 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Mon, 15 Mar 2021 17:18:06 -0600 Subject: [PATCH 02/16] Working on implementing the new RunnableHook system across actions --- src/game-engine/game-server.ts | 6 +- src/game-engine/world/action/button.action.ts | 20 ++- .../world/action/equipment-change.action.ts | 26 ++-- src/game-engine/world/action/hooks/index.ts | 21 ++-- src/game-engine/world/action/index.ts | 96 +++++++++++++-- .../world/action/item-interaction.action.ts | 38 ++---- .../world/action/item-on-item.action.ts | 28 ++--- .../world/action/item-on-npc.action.ts | 54 ++------ .../world/action/item-on-object.action.ts | 80 ++++-------- .../world/action/item-swap.action.ts | 37 +++--- .../world/action/move-item.action.ts | 37 +++--- .../world/action/npc-init.action.ts | 2 +- .../world/action/npc-interaction.action.ts | 44 +++---- .../world/action/object-interaction.action.ts | 65 +++------- .../world/action/player-command.action.ts | 115 ++++++++++-------- .../world/action/player-init.action.ts | 2 +- .../world/action/player-interaction.action.ts | 47 +++---- .../world/action/region-change.action.ts | 2 +- .../action/spawned-item-interaction.action.ts | 49 +++----- .../world/action/widget-interaction.action.ts | 29 ++--- src/game-engine/world/actor/actor.ts | 44 ++++++- src/game-engine/world/actor/dialogue.ts | 22 ++-- .../world/actor/player/interface-state.ts | 2 +- src/game-engine/world/actor/player/player.ts | 9 -- 24 files changed, 426 insertions(+), 449 deletions(-) diff --git a/src/game-engine/game-server.ts b/src/game-engine/game-server.ts index 4899dc177..edc9ca9e7 100644 --- a/src/game-engine/game-server.ts +++ b/src/game-engine/game-server.ts @@ -216,10 +216,10 @@ export const playerWalkTo = async (player: Player, position: Position, interacti interactingObject?: LocationObject; }): 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; @@ -244,7 +244,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 09bad3a70..f2af0aff3 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 4e7dee938..1d9b645a6 100644 --- a/src/game-engine/world/action/hooks/index.ts +++ b/src/game-engine/world/action/hooks/index.ts @@ -1,10 +1,9 @@ import { actionHookMap } from '@engine/game-server'; import { QuestKey } from '@engine/config/quest-config'; -import { ActionPriority, TaskExecutor, ActionType } from '@engine/world/action'; +import { TaskExecutor, ActionType, ActionStrength } from '@engine/world/action'; export interface HookTask { - priority?: ActionPriority; canActivate?: (task: TaskExecutor) => boolean | Promise; execute: (task: TaskExecutor) => void | undefined | boolean | Promise; onComplete?: (task: TaskExecutor) => void | Promise; @@ -28,15 +27,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; } diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index a70ac14d5..92b2e619d 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -2,29 +2,39 @@ 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 { HookTask } from '@engine/world/action/hooks'; +import { ActionHook, HookTask } from '@engine/world/action/hooks'; import { lastValueFrom, Subscription, timer } from 'rxjs'; import { World } from '@engine/world'; +import { Position } from '@engine/world/position'; +import uuidv4 from 'uuid/v4'; /** * The priority of an queueable action within the pipeline. */ -export type ActionPriority = 'weak' | 'normal' | 'strong'; +export type ActionStrength = 'weak' | 'normal' | 'strong'; // T = current action info (ButtonAction, MoveItemAction, etc) export class TaskExecutor { public running: boolean = false; + public readonly taskId = uuidv4(); private intervalSubscription: Subscription; + private readonly strength: ActionStrength; 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 { + if(!await this.canActivate()) { + return; + } + this.running = true; if(!!this.task.delay || !!this.task.delayMs) { @@ -167,6 +177,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. @@ -175,6 +198,8 @@ export class ActionPipeline { private static pipes = new Map(); + private runningTasks: TaskExecutor[] = []; + public constructor(public readonly actor: Actor) { } @@ -186,18 +211,11 @@ export class ActionPipeline { ActionPipeline.pipes.set(action.toString(), actionPipe); } - public async queue(action: ActionType, ...args: any[]): Promise { - - } - 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); @@ -205,6 +223,64 @@ export class ActionPipeline { } } + private cancelWeakerActions(runningActionPriority: ActionStrength): void { + if(!this.runningTasks || this.runningTasks.length === 0) { + return; + } + + // @TODO + } + + private async runActionHandler(actionHandler: any, ...args: any[]): Promise { + const runnableHooks: RunnableHooks | null | undefined = actionHandler(...args); + + if(!runnableHooks?.hooks || runnableHooks.hooks.length === 0) { + return; + } + + if(runnableHooks.actionPosition) { + await this.actor.waitForPathing(runnableHooks.actionPosition); + } + + for(let i = 0; i < runnableHooks.hooks.length; i++) { + const hook = runnableHooks.hooks[i]; + 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; + + await this.cancelWeakerActions(actionHook.strength || 'normal'); + + // @TODO remove when existing actions are converted away from this + this.actor.actionsCancelled.next(null); + + 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 { return false; } diff --git a/src/game-engine/world/action/item-interaction.action.ts b/src/game-engine/world/action/item-interaction.action.ts index 2e58c4a30..ec3f4d2d9 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,15 +57,10 @@ 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 => { // 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; } @@ -101,31 +96,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; - } - - if(cancelActions) { - player.actionsCancelled.next(null); + return null; } - for(const plugin of interactionActions) { - plugin.handler({ + return { + hooks: matchingHooks, + action: { player, itemId, itemSlot: slot, @@ -133,7 +121,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 d6df9354f..f50a8c623 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,35 +52,35 @@ 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)); - 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 e4233c12f..2c9c5ce48 100644 --- a/src/game-engine/world/action/item-on-npc.action.ts +++ b/src/game-engine/world/action/item-on-npc.action.ts @@ -6,13 +6,13 @@ 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,62 +57,34 @@ 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 => { // 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, 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 ${ npc.name } ` + `(id-${ npc.id }) @ ${ 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 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 15dbe2c4f..a2f3dc764 100644 --- a/src/game-engine/world/action/item-on-object.action.ts +++ b/src/game-engine/world/action/item-on-object.action.ts @@ -6,13 +6,13 @@ import { logger } from '@runejs/core'; import { Item } from '@engine/world/items/item'; 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'; /** * 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 +65,41 @@ export interface ItemOnObjectAction { const itemOnObjectActionPipe = (player: Player, locationObject: LocationObject, locationObjectDefinition: LocationObjectDefinition, 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, locationObject.objectId)); - const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); + let matchingHooks = getActionHooks('item_on_object') + .filter(plugin => questHookFilter(player, plugin) && + advancedNumberHookFilter(plugin.objectIds, locationObject.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 ${ locationObjectDefinition.name } ` + `(id-${ locationObject.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: locationObject }) - .then(() => { - player.face(position); - - walkToPlugins.forEach(plugin => - plugin.handler({ - player, - object: locationObject, - objectDefinition: locationObjectDefinition, - 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: locationObject, - objectDefinition: locationObjectDefinition, - position, - item, - itemWidgetId, - itemContainerId, - cacheOriginal - })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, + object: locationObject, + objectDefinition: locationObjectDefinition, + position, + item, + itemWidgetId, + itemContainerId, + cacheOriginal + } } }; diff --git a/src/game-engine/world/action/item-swap.action.ts b/src/game-engine/world/action/item-swap.action.ts index 1d85e5ec7..2eb8865c6 100644 --- a/src/game-engine/world/action/item-swap.action.ts +++ b/src/game-engine/world/action/item-swap.action.ts @@ -2,13 +2,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 +44,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 d41e3d417..56d0f1b7b 100644 --- a/src/game-engine/world/action/npc-interaction.action.ts +++ b/src/game-engine/world/action/npc-interaction.action.ts @@ -5,13 +5,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 { 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 +37,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,48 +49,34 @@ 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; } // 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, 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) { logger.warn(`Unhandled NPC interaction: ${option} ${npc.key} (id-${npc.id}) @ ${position.x},${position.y},${position.level}`); logger.warn(npc.id, npc.key, npc.name); - 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 9c9afde52..b6225781e 100644 --- a/src/game-engine/world/action/object-interaction.action.ts +++ b/src/game-engine/world/action/object-interaction.action.ts @@ -5,13 +5,13 @@ 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'; /** * 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,62 +56,37 @@ export interface ObjectInteractionAction { * @param cacheOriginal */ const objectInteractionActionPipe = (player: Player, locationObject: LocationObject, locationObjectDefinition: LocationObjectDefinition, - position: Position, option: string, cacheOriginal: boolean): void => { - if(player.busy || player.metadata.blockObjectInteractions) { + position: Position, option: string, cacheOriginal: boolean): RunnableHooks => { + if(player.metadata.blockObjectInteractions) { return; } // Find all object action plugins that reference this location object - let interactionActions = getActionHooks('object_interaction') + let matchingHooks = getActionHooks('object_interaction') .filter(plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, locationObject.objectId, 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 object interaction: ${option} ${locationObjectDefinition.name} ` + `(id-${locationObject.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: locationObject }) - .then(() => { - player.face(position); - - walkToPlugins.forEach(plugin => - plugin.handler({ - player, - object: locationObject, - objectDefinition: locationObjectDefinition, - option, - position, - 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: locationObject, - objectDefinition: locationObjectDefinition, - option, - position, - cacheOriginal - })); + return { + hooks: matchingHooks, + actionPosition: position, + action: { + player, + object: locationObject, + objectDefinition: locationObjectDefinition, + 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 54821c9cd..dd7c34f6e 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 8afa7fa41..46e131381 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 896abc07e..9b258ebae 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,9 +47,9 @@ 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 => { // 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; } @@ -68,25 +68,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..68a4ad80f 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -13,6 +13,7 @@ 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. @@ -69,6 +70,39 @@ export abstract class Actor { return remainingHitpoints === 0 ? 'dead' : 'alive'; } + public async waitForPathing(position: Position | LocationObject): Promise { + await new Promise((resolve, reject) => { + this.metadata.walkingTo = position; + + const inter = setInterval(() => { + if(!this.metadata.walkingTo || !this.metadata.walkingTo.equals(position)) { + reject(); + clearInterval(inter); + return; + } + + if(!this.walkingQueue.moving()) { + if(position instanceof Position) { + if(this.position.distanceBetween(position) > 1) { + reject(); + } else { + resolve(); + } + } else { + if(this.position.withinInteractionDistance(position)) { + 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) { @@ -123,8 +157,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 +174,6 @@ export abstract class Actor { return false; } - const desiredPosition = target.position; - await this.pathfinding.walkTo(desiredPosition, { pathingSearchRadius: distance + 2, ignoreDestination: true diff --git a/src/game-engine/world/actor/dialogue.ts b/src/game-engine/world/actor/dialogue.ts index 8f468a031..d60efccf2 100644 --- a/src/game-engine/world/actor/dialogue.ts +++ b/src/game-engine/world/actor/dialogue.ts @@ -530,17 +530,15 @@ 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); + } } } } @@ -590,7 +588,7 @@ export async function dialogue(participants: (Player | NpcParticipant)[], dialog 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 512c5f42d..bba42e553 100644 --- a/src/game-engine/world/actor/player/interface-state.ts +++ b/src/game-engine/world/actor/player/interface-state.ts @@ -121,7 +121,7 @@ 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))); } diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index 5fbcaed49..47d571f66 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -136,7 +136,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; @@ -1165,14 +1164,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; } From aac4db6911827fc868dc445b57e13eac823a2d7c Mon Sep 17 00:00:00 2001 From: Tynarus Date: Mon, 15 Mar 2021 17:51:06 -0600 Subject: [PATCH 03/16] Fixing dialogue system issues --- src/game-engine/world/action/index.ts | 34 ++++++++++++++++++++----- src/game-engine/world/actor/dialogue.ts | 22 +++++++--------- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 92b2e619d..f267459bf 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -20,8 +20,8 @@ export class TaskExecutor { public running: boolean = false; public readonly taskId = uuidv4(); + public readonly strength: ActionStrength; private intervalSubscription: Subscription; - private readonly strength: ActionStrength; public constructor(public readonly actor: Actor, public readonly task: HookTask, @@ -117,7 +117,7 @@ export class TaskExecutor { } } - private async stop(): Promise { + public async stop(): Promise { this.intervalSubscription?.unsubscribe(); await this.task?.onComplete(this); @@ -223,12 +223,35 @@ export class ActionPipeline { } } - private cancelWeakerActions(runningActionPriority: ActionStrength): void { + private async cancelWeakerActions(newActionStrength: ActionStrength): Promise { if(!this.runningTasks || this.runningTasks.length === 0) { return; } - // @TODO + const pendingRemoval: string[] = []; + + for(const runningTask of this.runningTasks) { + if(!runningTask.running) { + pendingRemoval.push(runningTask.taskId); + continue; + } + + if(runningTask.strength === 'weak' || (runningTask.strength === 'normal' && newActionStrength === 'strong')) { + // Cancel obviously weaker tasks + await runningTask.stop(); + pendingRemoval.push(runningTask.taskId); + continue; + } + + if(runningTask.strength === 'normal') { + // @TODO normal task handling + } else if(runningTask.strength === 'strong') { + // @TODO strong task handling + } + } + + // Remove all non-running and ceased tasks + this.runningTasks = this.runningTasks.filter(task => !pendingRemoval.includes(task.taskId)); } private async runActionHandler(actionHandler: any, ...args: any[]): Promise { @@ -259,9 +282,6 @@ export class ActionPipeline { await this.cancelWeakerActions(actionHook.strength || 'normal'); - // @TODO remove when existing actions are converted away from this - this.actor.actionsCancelled.next(null); - if(task) { // Schedule task-based hook const taskExecutor = new TaskExecutor(this.actor, task, actionHook, action); diff --git a/src/game-engine/world/actor/dialogue.ts b/src/game-engine/world/actor/dialogue.ts index d60efccf2..414e3d84e 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(); @@ -540,6 +540,8 @@ async function runDialogueAction(player: Player, dialogueAction: string | Dialog await runParsedDialogue(player, tree, tag, additionalOptions); } } + } else { + return false; } } } @@ -547,9 +549,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; @@ -572,18 +578,8 @@ 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) { From 57f7d5d0c303f15359eb5419c518ac2f39fbb6ec Mon Sep 17 00:00:00 2001 From: Tynarus Date: Mon, 15 Mar 2021 17:53:24 -0600 Subject: [PATCH 04/16] Fixing an error message error --- src/game-engine/world/action/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index f267459bf..dee8c6274 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -217,8 +217,10 @@ export class ActionPipeline { try { 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); + } } } } From 88284b17109abff0416c768456e950892ab1afe1 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Fri, 19 Mar 2021 19:11:22 -0500 Subject: [PATCH 05/16] Converting the Woodcutting plugin over to the new action task system --- src/game-engine/world/action/hooks/index.ts | 2 +- src/game-engine/world/action/index.ts | 18 ++- src/game-engine/world/actor/actor.ts | 29 ++++ .../world/actor/player/interface-state.ts | 7 +- src/game-engine/world/index.ts | 31 ++-- .../skills/woodcutting/woodcutting.plugin.ts | 151 ++++++++++++++++-- 6 files changed, 213 insertions(+), 25 deletions(-) diff --git a/src/game-engine/world/action/hooks/index.ts b/src/game-engine/world/action/hooks/index.ts index 1d9b645a6..6a1577f64 100644 --- a/src/game-engine/world/action/hooks/index.ts +++ b/src/game-engine/world/action/hooks/index.ts @@ -5,7 +5,7 @@ import { TaskExecutor, ActionType, ActionStrength } from '@engine/world/action'; export interface HookTask { canActivate?: (task: TaskExecutor) => boolean | Promise; - execute: (task: TaskExecutor) => void | undefined | boolean | Promise; + activate: (task: TaskExecutor, index?: number) => void | undefined | boolean | Promise; onComplete?: (task: TaskExecutor) => void | Promise; delay?: number; // # of ticks before execution delayMs?: number; // # of milliseconds before execution diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index dee8c6274..847051729 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -7,6 +7,7 @@ import { lastValueFrom, Subscription, timer } from 'rxjs'; import { World } from '@engine/world'; import { Position } from '@engine/world/position'; import uuidv4 from 'uuid/v4'; +import { Player } from '@engine/world/actor/player/player'; /** @@ -19,6 +20,7 @@ export type ActionStrength = 'weak' | 'normal' | 'strong'; export class TaskExecutor { public running: boolean = false; + public session: { [key: string]: any } = {}; // a session store to use for the lifetime of the task public readonly taskId = uuidv4(); public readonly strength: ActionStrength; private intervalSubscription: Subscription; @@ -50,9 +52,10 @@ export class TaskExecutor { const intervalMs = this.task.intervalMs || (this.task.interval * World.TICK_LENGTH); await new Promise(resolve => { + let index: number = 0; this.intervalSubscription = timer(0, intervalMs).subscribe( async () => { - if(!await this.execute()) { + if(!await this.execute(index++)) { this.intervalSubscription?.unsubscribe(); resolve(); } @@ -73,7 +76,7 @@ export class TaskExecutor { } } - public async execute(): Promise { + public async execute(index: number = 0): Promise { if(!this.actor) { // Actor destroyed, cancel the task return false; @@ -90,7 +93,7 @@ export class TaskExecutor { } try { - const response = await this.task.execute(this); + const response = await this.task.activate(this, index); return typeof response === 'boolean' ? response : true; } catch(error) { logger.error(`Error executing action task`); @@ -123,10 +126,11 @@ export class TaskExecutor { await this.task?.onComplete(this); this.running = false; + this.session = null; } public get valid(): boolean { - return !!this.task?.execute && !!this.actionData; + return !!this.task?.activate && !!this.actionData; } } @@ -304,6 +308,12 @@ export class ActionPipeline { } public get paused(): boolean { + if(this.actor instanceof Player) { + if(this.actor.interfaceState.widgetOpen()) { + return true; + } + } + return false; } diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index 68a4ad80f..a625f64d6 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -70,6 +70,19 @@ 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; public async waitForPathing(position: Position | LocationObject): Promise { await new Promise((resolve, reject) => { this.metadata.walkingTo = position; @@ -506,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/player/interface-state.ts b/src/game-engine/world/actor/player/interface-state.ts index bba42e553..67e2c3737 100644 --- a/src/game-engine/world/actor/player/interface-state.ts +++ b/src/game-engine/world/actor/player/interface-state.ts @@ -202,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/index.ts b/src/game-engine/world/index.ts index 806b7402b..7cd51cc9b 100644 --- a/src/game-engine/world/index.ts +++ b/src/game-engine/world/index.ts @@ -70,23 +70,33 @@ 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, objectPosition: Position): { object: LocationObject, cacheOriginal: boolean } { + public findObjectAtLocation(actor: Actor, objectId: number, objectPosition: Position): { object: LocationObject, 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 locationObject = objectChunk.getCacheObject(objectId, objectPosition); if (!locationObject) { - const tileObjects = [...tileModifications.mods.spawnedObjects, - ...personalTileModifications.mods.spawnedObjects]; + const tileObjects = [...tileModifications.mods.spawnedObjects ]; + + if(actor.isPlayer) { + tileObjects.push(...personalTileModifications.mods.spawnedObjects); + } locationObject = tileObjects.find(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) || null; @@ -98,10 +108,13 @@ 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 => + if(hiddenTileObjects.findIndex(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) !== -1) { return { object: null, cacheOriginal: false }; } diff --git a/src/plugins/skills/woodcutting/woodcutting.plugin.ts b/src/plugins/skills/woodcutting/woodcutting.plugin.ts index a872fb029..37323e5fe 100644 --- a/src/plugins/skills/woodcutting/woodcutting.plugin.ts +++ b/src/plugins/skills/woodcutting/woodcutting.plugin.ts @@ -1,17 +1,142 @@ -import { objectInteractionActionHandler } from '@engine/world/action/object-interaction.action'; +import { + ObjectInteractionAction, + objectInteractionActionHandler, + 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 { getTreeFromHealthy, getTreeIds, IHarvestable, Tree } from '@engine/world/config/harvestable-object'; +import { TaskExecutor } from '@engine/world/action'; +import { Player } from '@engine/world/actor/player/player'; +import { randomBetween } from '@engine/util/num'; +import { checkForGemBoost } from '@engine/world/skill-util/glory-boost'; +import { colorText } from '@engine/util/strings'; +import { colors } from '@engine/util/colors'; +import { rollBirdsNestType } from '@engine/world/skill-util/harvest-roll'; +import { cache, world } from '@engine/game-server'; +import { soundIds } from '@engine/world/config/sound-ids'; +import { Axe, getAxe } from '@engine/world/config/harvest-tool'; -const action: objectInteractionActionHandler = (details) => { - const tree = getTreeFromHealthy(details.object.objectId); - const tool = canInitiateHarvest(details.player, tree, Skill.WOODCUTTING); - if (!tool) { +const canActivate = (task: TaskExecutor): 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; + } + + player?.sendMessage('You swing your axe at the tree.'); + + actor.face(position); + actor.playAnimation(tool.animation); + + return true; +}; + + +const activate = (task: TaskExecutor, elapsedTicks: number): void => { + const { + actor, + actionData: { + position: objectPosition, + object: { + objectId + } + } + } = task; + + const { + type: { + player + }, isPlayer + } = actor; + + const tree = getTreeFromHealthy(objectId); + const tool = isPlayer ? canInitiateHarvest(player, tree, Skill.WOODCUTTING) : getAxe(Axe.STEEL); + + // Cancel if the actor no longer has their tool or level requirements. + if(!tool) { + task.stop(); + return; + } + + // Grab the tree manually every loop so that we can make sure it's still alive. + const { object } = world.findObjectAtLocation(actor, objectId, objectPosition); + + if(!object) { + // Tree has been chopped down, cancel. + task.stop(); return; } - handleHarvesting(details, tool, tree, Skill.WOODCUTTING); + // Check if the amount of ticks passed equal the tools pulses. + if(elapsedTicks % 3 === 0 && elapsedTicks != 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 = cache.itemDefinitions.get(tree.itemId).name.toLowerCase(); + + if(actor.inventory.hasSpace()) { + let randomLoot = false; + let roll = randomBetween(1, 256); + if(roll === 1) { + randomLoot = true; + player?.sendMessage(colorText(`A bird's nest falls out of the tree.`, colors.red)); + world.globalInstance.spawnWorldItem(rollBirdsNestType(), actor.position, + isPlayer ? { owner: player, expires: 300 } : { owner: null, expires: 300 }); + } + + const itemToAdd = tree.itemId; + + if(!randomLoot) { + 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(objectId), + object, randomBetween(tree.respawnLow, tree.respawnHigh)); + task.stop(); + return; + } + } else { + player?.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true); + player?.playSound(soundIds.inventoryFull); + task.stop(); + return; + } + } + } else { + if(elapsedTicks % 1 == 0 && elapsedTicks != 0) { + player?.playSound(soundIds.axeSwing[Math.floor(Math.random() * soundIds.axeSwing.length)], 7, 0); + } + } + + if(elapsedTicks % 3 == 0 && elapsedTicks != 0) { + actor.playAnimation(tool.animation); + } +}; + +const onComplete = (task: TaskExecutor): void => { + task.actor.stopAnimation(); }; @@ -22,8 +147,14 @@ export default { type: 'object_interaction', options: [ 'chop down', 'chop' ], objectIds: getTreeIds(), - walkTo: true, - handler: action - } + // handler: action, + strength: 'normal', + task: { + canActivate, + activate, + onComplete, + interval: 1 + } + } as ObjectInteractionActionHook ] }; From 1e9151689cd117e6949158a3166f6ea41d274ba2 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 10:20:01 -0500 Subject: [PATCH 06/16] Adding a new file for action tasks and exporting all from the action api --- src/game-engine/world/action/hooks/index.ts | 18 +- src/game-engine/world/action/hooks/task.ts | 165 ++++++++++++++++++ src/game-engine/world/action/index.ts | 129 +------------- .../skills/woodcutting/woodcutting.plugin.ts | 79 ++++----- 4 files changed, 209 insertions(+), 182 deletions(-) create mode 100644 src/game-engine/world/action/hooks/task.ts diff --git a/src/game-engine/world/action/hooks/index.ts b/src/game-engine/world/action/hooks/index.ts index 6a1577f64..4154bed53 100644 --- a/src/game-engine/world/action/hooks/index.ts +++ b/src/game-engine/world/action/hooks/index.ts @@ -1,17 +1,7 @@ import { actionHookMap } from '@engine/game-server'; import { QuestKey } from '@engine/config/quest-config'; -import { TaskExecutor, ActionType, ActionStrength } from '@engine/world/action'; - - -export interface HookTask { - canActivate?: (task: TaskExecutor) => boolean | Promise; - activate: (task: TaskExecutor, index?: number) => void | undefined | boolean | Promise; - onComplete?: (task: TaskExecutor) => 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) -} +import { ActionStrength, ActionType } from '@engine/world/action'; +import { HookTask } from '@engine/world/action/hooks/task'; /** @@ -68,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..75f718ab6 --- /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 ? this.task.delayMs : + (this.task.delay * World.TICK_LENGTH))); + } + + if(!!this.task.interval || !!this.task.intervalMs) { + // Looping execution task + const intervalMs = 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(); + } + }, + async error => { + logger.error(error); + resolve(); + }, + async() => 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.intervalSubscription?.unsubscribe(); + + await this.task?.onComplete(this, this.iteration); + + this.running = false; + 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 847051729..103b561cb 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -2,12 +2,10 @@ 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, HookTask } from '@engine/world/action/hooks'; -import { lastValueFrom, Subscription, timer } from 'rxjs'; -import { World } from '@engine/world'; +import { ActionHook } from '@engine/world/action/hooks'; import { Position } from '@engine/world/position'; -import uuidv4 from 'uuid/v4'; import { Player } from '@engine/world/actor/player/player'; +import { TaskExecutor } from '@engine/world/action/hooks/task'; /** @@ -16,126 +14,6 @@ import { Player } from '@engine/world/actor/player/player'; export type ActionStrength = 'weak' | 'normal' | 'strong'; -// T = current action info (ButtonAction, MoveItemAction, etc) -export class TaskExecutor { - - public running: boolean = false; - public session: { [key: string]: any } = {}; // a session store to use for the lifetime of the task - public readonly taskId = uuidv4(); - public readonly strength: ActionStrength; - 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 { - if(!await this.canActivate()) { - return; - } - - this.running = true; - - if(!!this.task.delay || !!this.task.delayMs) { - await lastValueFrom(timer(!!this.task.delayMs ? this.task.delayMs : (this.task.delay * World.TICK_LENGTH))); - } - - if(!this.running) { - return; - } - - if(!!this.task.interval || !!this.task.intervalMs) { - // Looping execution task - const intervalMs = this.task.intervalMs || (this.task.interval * World.TICK_LENGTH); - - await new Promise(resolve => { - let index: number = 0; - this.intervalSubscription = timer(0, intervalMs).subscribe( - async () => { - if(!await this.execute(index++)) { - this.intervalSubscription?.unsubscribe(); - resolve(); - } - }, - async error => { - logger.error(error); - resolve(); - }, - async () => resolve()); - }); - } else { - // Single execution task - await this.execute(); - } - - if(this.running) { - await this.stop(); - } - } - - public async execute(index: number = 0): Promise { - if(!this.actor) { - // Actor destroyed, cancel the task - 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, index); - 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); - } catch(error) { - logger.error(`Error calling action canActivate`, this.task); - logger.error(error); - return false; - } - } - - public async stop(): Promise { - this.intervalSubscription?.unsubscribe(); - - await this.task?.onComplete(this); - - this.running = false; - this.session = null; - } - - public get valid(): boolean { - return !!this.task?.activate && !!this.actionData; - } - -} - - /** * Content action type definitions. */ @@ -349,3 +227,6 @@ export async function loadActionFiles(): Promise { return Promise.resolve(); } + + +export * from './hooks/index'; diff --git a/src/plugins/skills/woodcutting/woodcutting.plugin.ts b/src/plugins/skills/woodcutting/woodcutting.plugin.ts index 37323e5fe..00e4e56ce 100644 --- a/src/plugins/skills/woodcutting/woodcutting.plugin.ts +++ b/src/plugins/skills/woodcutting/woodcutting.plugin.ts @@ -1,24 +1,21 @@ import { ObjectInteractionAction, - objectInteractionActionHandler, 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, IHarvestable, Tree } from '@engine/world/config/harvestable-object'; -import { TaskExecutor } from '@engine/world/action'; -import { Player } from '@engine/world/actor/player/player'; +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 { checkForGemBoost } from '@engine/world/skill-util/glory-boost'; import { colorText } from '@engine/util/strings'; import { colors } from '@engine/util/colors'; import { rollBirdsNestType } from '@engine/world/skill-util/harvest-roll'; import { cache, world } from '@engine/game-server'; import { soundIds } from '@engine/world/config/sound-ids'; -import { Axe, getAxe } from '@engine/world/config/harvest-tool'; +import { Axe, getAxe, HarvestTool } from '@engine/world/config/harvest-tool'; +import { TaskExecutor } from '@engine/world/action'; -const canActivate = (task: TaskExecutor): boolean => { +const canActivate = (task: TaskExecutor, taskIteration: number): boolean => { const { actor, actionData: { position, object } } = task; const tree = getTreeFromHealthy(object.objectId); @@ -34,52 +31,43 @@ const canActivate = (task: TaskExecutor): boolean => { return false; } - player?.sendMessage('You swing your axe at the tree.'); + task.session.tool = tool; + task.session.tree = tree; - actor.face(position); - actor.playAnimation(tool.animation); + if(taskIteration === 0) { + // First run - return true; -}; + player?.sendMessage('You swing your axe at the tree.'); + actor.face(position); + actor.playAnimation(tool.animation); + } -const activate = (task: TaskExecutor, elapsedTicks: number): void => { - const { - actor, - actionData: { - position: objectPosition, - object: { - objectId - } - } - } = task; + return true; +}; - const { - type: { - player - }, isPlayer - } = actor; - const tree = getTreeFromHealthy(objectId); - const tool = isPlayer ? canInitiateHarvest(player, tree, Skill.WOODCUTTING) : getAxe(Axe.STEEL); +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) { - task.stop(); - return; + 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, objectId, objectPosition); + const { object } = world.findObjectAtLocation(actor, actionObject.objectId, objectPosition); if(!object) { // Tree has been chopped down, cancel. - task.stop(); - return; + return false; } // Check if the amount of ticks passed equal the tools pulses. - if(elapsedTicks % 3 === 0 && elapsedTicks != 0) { + if(taskIteration % 3 === 0 && taskIteration != 0) { const successChance = randomBetween(0, 255); let toolLevel = tool.level - 1; @@ -98,7 +86,7 @@ const activate = (task: TaskExecutor, elapsedTicks: num randomLoot = true; player?.sendMessage(colorText(`A bird's nest falls out of the tree.`, colors.red)); world.globalInstance.spawnWorldItem(rollBirdsNestType(), actor.position, - isPlayer ? { owner: player, expires: 300 } : { owner: null, expires: 300 }); + { owner: player || null, expires: 300 }); } const itemToAdd = tree.itemId; @@ -112,27 +100,27 @@ const activate = (task: TaskExecutor, elapsedTicks: num if(randomBetween(0, 100) <= tree.break) { player?.playSound(soundIds.oreDepeleted); - actor.instance.replaceGameObject(tree.objects.get(objectId), + actor.instance.replaceGameObject(tree.objects.get(actionObject.objectId), object, randomBetween(tree.respawnLow, tree.respawnHigh)); - task.stop(); - return; + return false; } } else { player?.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true); player?.playSound(soundIds.inventoryFull); - task.stop(); - return; + return false; } } } else { - if(elapsedTicks % 1 == 0 && elapsedTicks != 0) { + if(taskIteration % 1 === 0 && taskIteration !== 0) { player?.playSound(soundIds.axeSwing[Math.floor(Math.random() * soundIds.axeSwing.length)], 7, 0); } } - if(elapsedTicks % 3 == 0 && elapsedTicks != 0) { + if(taskIteration % 3 === 0 && taskIteration !== 0) { actor.playAnimation(tool.animation); } + + return true; }; const onComplete = (task: TaskExecutor): void => { @@ -147,7 +135,6 @@ export default { type: 'object_interaction', options: [ 'chop down', 'chop' ], objectIds: getTreeIds(), - // handler: action, strength: 'normal', task: { canActivate, From cc6586456b83c0c68b0af067000ab8c315c46142 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 11:39:28 -0500 Subject: [PATCH 07/16] Fixing world/index.ts indentation --- src/game-engine/world/index.ts | 840 ++++++++++++++++----------------- 1 file changed, 420 insertions(+), 420 deletions(-) diff --git a/src/game-engine/world/index.ts b/src/game-engine/world/index.ts index 7cd51cc9b..ac44b5228 100644 --- a/src/game-engine/world/index.ts +++ b/src/game-engine/world/index.ts @@ -20,9 +20,9 @@ import { loadActionFiles } from '@engine/world/action'; export interface QuadtreeKey { - x: number; - y: number; - actor: Actor; + x: number; + y: number; + actor: Actor; } /** @@ -30,422 +30,422 @@ export interface QuadtreeKey { */ export class World { - public static readonly MAX_PLAYERS = 1600; - public static readonly MAX_NPCS = 30000; - public static readonly TICK_LENGTH = 600; - - public readonly playerList: Player[] = new Array(World.MAX_PLAYERS).fill(null); - public readonly npcList: Npc[] = new Array(World.MAX_NPCS).fill(null); - public readonly chunkManager: ChunkManager = new ChunkManager(); - public readonly examine: ExamineCache = new ExamineCache(); - public readonly scenerySpawns: LocationObject[]; - public readonly travelLocations: TravelLocations = new TravelLocations(); - public readonly playerTree: Quadtree; - public readonly npcTree: Quadtree; - public readonly globalInstance = new WorldInstance(); - - private readonly debugCycleDuration: boolean = process.argv.indexOf('-tickTime') !== -1; - - public constructor() { - this.scenerySpawns = parseScenerySpawns(); - this.playerTree = new Quadtree({ - width: 10000, - height: 10000 - }); - this.npcTree = new Quadtree({ - width: 10000, - height: 10000 - }); - - this.setupWorldTick(); - } - - public async init(): Promise { - await loadPlugins(); - await loadActionFiles(); - this.spawnGlobalNpcs(); - this.spawnWorldItems(); - this.spawnScenery(); - } - - /** - * Searched for an object by ID at the given position in any of the player's active instances. - * @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(actor: Actor, objectId: number, objectPosition: Position): { object: LocationObject, cacheOriginal: boolean } { - const x = objectPosition.x; - const y = objectPosition.y; - const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition); - let cacheOriginal = true; - - 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 locationObject = objectChunk.getCacheObject(objectId, objectPosition); - if (!locationObject) { - const tileObjects = [...tileModifications.mods.spawnedObjects ]; - - if(actor.isPlayer) { - tileObjects.push(...personalTileModifications.mods.spawnedObjects); - } - - locationObject = tileObjects.find(spawnedObject => - spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) || null; - - cacheOriginal = false; - - if (!locationObject) { - return { object: null, cacheOriginal: false }; - } - } - - 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) { - return { object: null, cacheOriginal: false }; - } - - return { - object: locationObject, - cacheOriginal - }; - } - - /** - * Saves player data for every active player within the game world. - */ - public saveOnlinePlayers(): void { - if (!this.playerList) { - return; - } - - logger.info(`Saving player data...`); - - this.playerList - .filter(player => player !== null) - .forEach(player => player.save()); - - logger.info(`Player data saved.`); - } - - /** - * Players a sound at a specific position for all players within range of that position. - * @param position The position to play the sound at. - * @param soundId The ID of the sound effect. - * @param volume The volume the sound should play at. - * @param distance The distance which the sound should reach. - */ - public playLocationSound(position: Position, soundId: number, volume: number, distance: number = 10): void { - this.findNearbyPlayers(position, distance).forEach(player => { - player.outgoingPackets.updateReferencePosition(position); - player.outgoingPackets.playSoundAtPosition( - soundId, - position.x, - position.y, - volume - ); - }); - } - - /** - * Finds all NPCs within the given distance from the given position that have the specified Npc ID. - * @param position The center position to search from. - * @param npcId The ID of the NPCs to find. - * @param distance The maximum distance to search for NPCs. - * @param instanceId The NPC's active instance. - */ - public findNearbyNpcsById(position: Position, npcId: number, distance: number, instanceId: string = null): Npc[] { - return this.npcTree.colliding({ - x: position.x - (distance / 2), - y: position.y - (distance / 2), - width: distance, - height: distance - }).map(quadree => quadree.actor as Npc).filter(npc => npc.id === npcId && npc.instanceId === instanceId); - } - - /** - * Finds all NPCs within the game world that have the specified Npc Key. - * @param npcKey The Key of the NPCs to find. - * @param instanceId The NPC's active instance. - */ - public findNpcsByKey(npcKey: string, instanceId: string = null): Npc[] { - return this.npcList.filter(npc => npc && npc.key === npcKey && npc.instanceId === instanceId); - } - - /** - * Finds all NPCs within the game world that have the specified Npc ID. - * @param npcId The ID of the NPCs to find. - * @param instanceId The NPC's active instance. - */ - public findNpcsById(npcId: number, instanceId: string = null): Npc[] { - return this.npcList.filter(npc => npc && npc.id === npcId && npc.instanceId === instanceId); - } - - /** - * Finds all NPCs within the specified instance. - * @param instanceId The NPC's active instance. - */ - public findNpcsByInstance(instanceId: string): Npc[] { - return this.npcList.filter(npc => npc && npc.instanceId === instanceId); - } - - /** - * Finds all NPCs within the given distance from the given position. - * @param position The center position to search from. - * @param distance The maximum distance to search for NPCs. - * @param instanceId The NPC's active instance. - */ - public findNearbyNpcs(position: Position, distance: number, instanceId: string = null): Npc[] { - return this.npcTree.colliding({ - x: position.x - (distance / 2), - y: position.y - (distance / 2), - width: distance, - height: distance - }).map(quadree => quadree.actor as Npc).filter(npc => npc.instanceId === instanceId); - } - - /** - * Finds all Players within the given distance from the given position. - * @param position The center position to search from. - * @param distance The maximum distance to search for Players. - * @param instanceId The player's active instance. - */ - public findNearbyPlayers(position: Position, distance: number, instanceId: string = null): Player[] { - return this.playerTree.colliding({ - x: position.x - (distance / 2), - y: position.y - (distance / 2), - width: distance, - height: distance - }) - .map(quadree => quadree.actor as Player) - .filter(player => player.personalInstance.instanceId === instanceId || - player.instance.instanceId === instanceId); - } - - /** - * Finds a logged in player via their username. - * @param username The player's username. - */ - public findActivePlayerByUsername(username: string): Player { - username = username.toLowerCase(); - return this.playerList.find(p => p && p.username.toLowerCase() === username); - } - - /** - * Spawns the list of pre-configured items into either the global instance or a player's personal instance. - * @param player [optional] The player to load the instanced items for. Uses the global world instance if not provided. - */ - public spawnWorldItems(player?: Player): void { - const instance = player ? player.personalInstance : this.globalInstance; - - itemSpawns.filter(spawn => player ? spawn.instance === 'player' : spawn.instance === 'global') - .forEach(itemSpawn => { - const itemDetails = findItem(itemSpawn.itemKey); - if (itemDetails && itemDetails.gameId !== undefined) { - instance.spawnWorldItem({ itemId: itemDetails.gameId, amount: itemSpawn.amount }, - itemSpawn.spawnPosition, { respawns: itemSpawn.respawn, owner: player || undefined }); - } else { - logger.error(`Item ${itemSpawn.itemKey} can not be spawned; it has not yet been registered on the server.`); - } - }); - } - - public spawnGlobalNpcs(): void { - npcSpawns.forEach(npcSpawn => { - const npcDetails = findNpc(npcSpawn.npcKey) || null; - if (npcDetails && npcDetails.gameId !== undefined) { - this.registerNpc(new Npc(npcDetails, npcSpawn)); - } else { - logger.error(`NPC ${npcSpawn.npcKey} can not be spawned; it has not yet been registered on the server.`); - } - }); - } - - public async spawnNpc(npcKey: string | number, position: Position, face: Direction, - movementRadius: number = 0, instanceId: string = null): Promise { - if (!npcKey) { - return null; - } - - let npcData: NpcDetails | number = findNpc(npcKey); - if (!npcData) { - logger.warn(`NPC ${npcKey} not yet registered on the server.`); - - if (typeof npcKey === 'number') { - npcData = npcKey; - } else { - return null; - } - } - - const npc = new Npc(npcData, - new NpcSpawn(typeof npcData === 'number' ? `unknown_${npcData}` : npcData.key, - position, movementRadius, face), instanceId); - - await this.registerNpc(npc); - - return npc; - } - - public spawnScenery(): void { - this.scenerySpawns.forEach(locationObject => - this.globalInstance.spawnGameObject(locationObject)); - } - - public async setupWorldTick(): Promise { - await schedule(1); - this.worldTick(); - } - - public generateFakePlayers(): void { - const x: number = 3222; - const y: number = 3222; - let xOffset: number = 0; - let yOffset: number = 0; - - const spawnChunk = this.chunkManager.getChunkForWorldPosition(new Position(x, y, 0)); - - for (let i = 0; i < 1000; i++) { - const player = new Player(null, null, null, i, `test${i}`, 'abs', true); - this.registerPlayer(player); - player.interfaceState.closeAllSlots(); - - xOffset++; - - if (xOffset > 20) { - xOffset = 0; - yOffset--; - } - - player.position = new Position(x + xOffset, y + yOffset, 0); - const newChunk = this.chunkManager.getChunkForWorldPosition(player.position); - - if (!spawnChunk.equals(newChunk)) { - spawnChunk.removePlayer(player); - newChunk.addPlayer(player); - } - - player.initiateRandomMovement(); - } - } - - public async worldTick(): Promise { - const hrStart = Date.now(); - const activePlayers: Player[] = this.playerList.filter(player => player !== null); - - if (activePlayers.length === 0) { - return Promise.resolve().then(() => { - setTimeout(async () => this.worldTick(), World.TICK_LENGTH); //TODO: subtract processing time - }); - } - - const activeNpcs: Npc[] = this.npcList.filter(npc => npc !== null); - - await Promise.all([...activePlayers.map(async player => player.tick()), ...activeNpcs.map(async npc => npc.tick())]); - await Promise.all(activePlayers.map(async player => player.update())); - await Promise.all([...activePlayers.map(async player => player.reset()), ...activeNpcs.map(async npc => npc.reset())]); - - const hrEnd = Date.now(); - const duration = hrEnd - hrStart; - const delay = Math.max(World.TICK_LENGTH - duration, 0); - - if (this.debugCycleDuration) { - logger.info(`World tick completed in ${duration} ms, next tick in ${delay} ms.`); - } - - setTimeout(async () => this.worldTick(), delay); - return Promise.resolve(); - } - - public async scheduleNpcRespawn(npc: Npc): Promise { - await schedule(10); - return await this.registerNpc(npc); - } - - public findPlayer(playerUsername: string): Player { - playerUsername = playerUsername.toLowerCase(); - return this.playerList?.find(p => p !== null && p.username.toLowerCase() === playerUsername) || null; - } - - public playerOnline(player: Player | string): boolean { - if (typeof player === 'string') { - player = player.toLowerCase(); - return this.playerList.findIndex(p => p !== null && p.username.toLowerCase() === player) !== -1; - } else { - const foundPlayer = this.playerList[player.worldIndex]; - if (!foundPlayer) { - return false; - } - - return foundPlayer.equals(player); - } - } - - public registerPlayer(player: Player): boolean { - if (!player) { - return false; - } - - const index = this.playerList.findIndex(p => p === null); - - if (index === -1) { - logger.warn('World full!'); - return false; - } - - player.worldIndex = index; - this.playerList[index] = player; - return true; - } - - public deregisterPlayer(player: Player): void { - this.playerList[player.worldIndex] = null; - } - - public npcExists(npc: Npc): boolean { - const foundNpc = this.npcList[npc.worldIndex]; - if (!foundNpc || !foundNpc.exists) { - return false; - } - - return foundNpc.equals(npc); - } - - public async registerNpc(npc: Npc): Promise { - if (!npc) { - return false; - } - - const index = this.npcList.findIndex(n => n === null); - - if (index === -1) { - logger.warn('NPC list full!'); - return false; - } - - npc.worldIndex = index; - this.npcList[index] = npc; - await npc.init(); - return true; - } - - public deregisterNpc(npc: Npc): void { - npc.exists = false; - this.npcList[npc.worldIndex] = null; - } + public static readonly MAX_PLAYERS = 1600; + public static readonly MAX_NPCS = 30000; + public static readonly TICK_LENGTH = 600; + + public readonly playerList: Player[] = new Array(World.MAX_PLAYERS).fill(null); + public readonly npcList: Npc[] = new Array(World.MAX_NPCS).fill(null); + public readonly chunkManager: ChunkManager = new ChunkManager(); + public readonly examine: ExamineCache = new ExamineCache(); + public readonly scenerySpawns: LocationObject[]; + public readonly travelLocations: TravelLocations = new TravelLocations(); + public readonly playerTree: Quadtree; + public readonly npcTree: Quadtree; + public readonly globalInstance = new WorldInstance(); + + private readonly debugCycleDuration: boolean = process.argv.indexOf('-tickTime') !== -1; + + public constructor() { + this.scenerySpawns = parseScenerySpawns(); + this.playerTree = new Quadtree({ + width: 10000, + height: 10000 + }); + this.npcTree = new Quadtree({ + width: 10000, + height: 10000 + }); + + this.setupWorldTick(); + } + + public async init(): Promise { + await loadPlugins(); + await loadActionFiles(); + this.spawnGlobalNpcs(); + this.spawnWorldItems(); + this.spawnScenery(); + } + + /** + * Searched for an object by ID at the given position in any of the player's active instances. + * @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(actor: Actor, objectId: number, objectPosition: Position): { object: LocationObject, cacheOriginal: boolean } { + const x = objectPosition.x; + const y = objectPosition.y; + const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition); + let cacheOriginal = true; + + 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 locationObject = objectChunk.getCacheObject(objectId, objectPosition); + if(!locationObject) { + const tileObjects = [ ...tileModifications.mods.spawnedObjects ]; + + if(actor.isPlayer) { + tileObjects.push(...personalTileModifications.mods.spawnedObjects); + } + + locationObject = tileObjects.find(spawnedObject => + spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) || null; + + cacheOriginal = false; + + if(!locationObject) { + return { object: null, cacheOriginal: false }; + } + } + + 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) { + return { object: null, cacheOriginal: false }; + } + + return { + object: locationObject, + cacheOriginal + }; + } + + /** + * Saves player data for every active player within the game world. + */ + public saveOnlinePlayers(): void { + if(!this.playerList) { + return; + } + + logger.info(`Saving player data...`); + + this.playerList + .filter(player => player !== null) + .forEach(player => player.save()); + + logger.info(`Player data saved.`); + } + + /** + * Players a sound at a specific position for all players within range of that position. + * @param position The position to play the sound at. + * @param soundId The ID of the sound effect. + * @param volume The volume the sound should play at. + * @param distance The distance which the sound should reach. + */ + public playLocationSound(position: Position, soundId: number, volume: number, distance: number = 10): void { + this.findNearbyPlayers(position, distance).forEach(player => { + player.outgoingPackets.updateReferencePosition(position); + player.outgoingPackets.playSoundAtPosition( + soundId, + position.x, + position.y, + volume + ); + }); + } + + /** + * Finds all NPCs within the given distance from the given position that have the specified Npc ID. + * @param position The center position to search from. + * @param npcId The ID of the NPCs to find. + * @param distance The maximum distance to search for NPCs. + * @param instanceId The NPC's active instance. + */ + public findNearbyNpcsById(position: Position, npcId: number, distance: number, instanceId: string = null): Npc[] { + return this.npcTree.colliding({ + x: position.x - (distance / 2), + y: position.y - (distance / 2), + width: distance, + height: distance + }).map(quadree => quadree.actor as Npc).filter(npc => npc.id === npcId && npc.instanceId === instanceId); + } + + /** + * Finds all NPCs within the game world that have the specified Npc Key. + * @param npcKey The Key of the NPCs to find. + * @param instanceId The NPC's active instance. + */ + public findNpcsByKey(npcKey: string, instanceId: string = null): Npc[] { + return this.npcList.filter(npc => npc && npc.key === npcKey && npc.instanceId === instanceId); + } + + /** + * Finds all NPCs within the game world that have the specified Npc ID. + * @param npcId The ID of the NPCs to find. + * @param instanceId The NPC's active instance. + */ + public findNpcsById(npcId: number, instanceId: string = null): Npc[] { + return this.npcList.filter(npc => npc && npc.id === npcId && npc.instanceId === instanceId); + } + + /** + * Finds all NPCs within the specified instance. + * @param instanceId The NPC's active instance. + */ + public findNpcsByInstance(instanceId: string): Npc[] { + return this.npcList.filter(npc => npc && npc.instanceId === instanceId); + } + + /** + * Finds all NPCs within the given distance from the given position. + * @param position The center position to search from. + * @param distance The maximum distance to search for NPCs. + * @param instanceId The NPC's active instance. + */ + public findNearbyNpcs(position: Position, distance: number, instanceId: string = null): Npc[] { + return this.npcTree.colliding({ + x: position.x - (distance / 2), + y: position.y - (distance / 2), + width: distance, + height: distance + }).map(quadree => quadree.actor as Npc).filter(npc => npc.instanceId === instanceId); + } + + /** + * Finds all Players within the given distance from the given position. + * @param position The center position to search from. + * @param distance The maximum distance to search for Players. + * @param instanceId The player's active instance. + */ + public findNearbyPlayers(position: Position, distance: number, instanceId: string = null): Player[] { + return this.playerTree.colliding({ + x: position.x - (distance / 2), + y: position.y - (distance / 2), + width: distance, + height: distance + }) + .map(quadree => quadree.actor as Player) + .filter(player => player.personalInstance.instanceId === instanceId || + player.instance.instanceId === instanceId); + } + + /** + * Finds a logged in player via their username. + * @param username The player's username. + */ + public findActivePlayerByUsername(username: string): Player { + username = username.toLowerCase(); + return this.playerList.find(p => p && p.username.toLowerCase() === username); + } + + /** + * Spawns the list of pre-configured items into either the global instance or a player's personal instance. + * @param player [optional] The player to load the instanced items for. Uses the global world instance if not provided. + */ + public spawnWorldItems(player?: Player): void { + const instance = player ? player.personalInstance : this.globalInstance; + + itemSpawns.filter(spawn => player ? spawn.instance === 'player' : spawn.instance === 'global') + .forEach(itemSpawn => { + const itemDetails = findItem(itemSpawn.itemKey); + if(itemDetails && itemDetails.gameId !== undefined) { + instance.spawnWorldItem({ itemId: itemDetails.gameId, amount: itemSpawn.amount }, + itemSpawn.spawnPosition, { respawns: itemSpawn.respawn, owner: player || undefined }); + } else { + logger.error(`Item ${itemSpawn.itemKey} can not be spawned; it has not yet been registered on the server.`); + } + }); + } + + public spawnGlobalNpcs(): void { + npcSpawns.forEach(npcSpawn => { + const npcDetails = findNpc(npcSpawn.npcKey) || null; + if(npcDetails && npcDetails.gameId !== undefined) { + this.registerNpc(new Npc(npcDetails, npcSpawn)); + } else { + logger.error(`NPC ${npcSpawn.npcKey} can not be spawned; it has not yet been registered on the server.`); + } + }); + } + + public async spawnNpc(npcKey: string | number, position: Position, face: Direction, + movementRadius: number = 0, instanceId: string = null): Promise { + if(!npcKey) { + return null; + } + + let npcData: NpcDetails | number = findNpc(npcKey); + if(!npcData) { + logger.warn(`NPC ${npcKey} not yet registered on the server.`); + + if(typeof npcKey === 'number') { + npcData = npcKey; + } else { + return null; + } + } + + const npc = new Npc(npcData, + new NpcSpawn(typeof npcData === 'number' ? `unknown_${npcData}` : npcData.key, + position, movementRadius, face), instanceId); + + await this.registerNpc(npc); + + return npc; + } + + public spawnScenery(): void { + this.scenerySpawns.forEach(locationObject => + this.globalInstance.spawnGameObject(locationObject)); + } + + public async setupWorldTick(): Promise { + await schedule(1); + this.worldTick(); + } + + public generateFakePlayers(): void { + const x: number = 3222; + const y: number = 3222; + let xOffset: number = 0; + let yOffset: number = 0; + + const spawnChunk = this.chunkManager.getChunkForWorldPosition(new Position(x, y, 0)); + + for(let i = 0; i < 1000; i++) { + const player = new Player(null, null, null, i, `test${i}`, 'abs', true); + this.registerPlayer(player); + player.interfaceState.closeAllSlots(); + + xOffset++; + + if(xOffset > 20) { + xOffset = 0; + yOffset--; + } + + player.position = new Position(x + xOffset, y + yOffset, 0); + const newChunk = this.chunkManager.getChunkForWorldPosition(player.position); + + if(!spawnChunk.equals(newChunk)) { + spawnChunk.removePlayer(player); + newChunk.addPlayer(player); + } + + player.initiateRandomMovement(); + } + } + + public async worldTick(): Promise { + const hrStart = Date.now(); + const activePlayers: Player[] = this.playerList.filter(player => player !== null); + + if(activePlayers.length === 0) { + return Promise.resolve().then(() => { + setTimeout(async() => this.worldTick(), World.TICK_LENGTH); //TODO: subtract processing time + }); + } + + const activeNpcs: Npc[] = this.npcList.filter(npc => npc !== null); + + await Promise.all([ ...activePlayers.map(async player => player.tick()), ...activeNpcs.map(async npc => npc.tick()) ]); + await Promise.all(activePlayers.map(async player => player.update())); + await Promise.all([ ...activePlayers.map(async player => player.reset()), ...activeNpcs.map(async npc => npc.reset()) ]); + + const hrEnd = Date.now(); + const duration = hrEnd - hrStart; + const delay = Math.max(World.TICK_LENGTH - duration, 0); + + if(this.debugCycleDuration) { + logger.info(`World tick completed in ${duration} ms, next tick in ${delay} ms.`); + } + + setTimeout(async() => this.worldTick(), delay); + return Promise.resolve(); + } + + public async scheduleNpcRespawn(npc: Npc): Promise { + await schedule(10); + return await this.registerNpc(npc); + } + + public findPlayer(playerUsername: string): Player { + playerUsername = playerUsername.toLowerCase(); + return this.playerList?.find(p => p !== null && p.username.toLowerCase() === playerUsername) || null; + } + + public playerOnline(player: Player | string): boolean { + if(typeof player === 'string') { + player = player.toLowerCase(); + return this.playerList.findIndex(p => p !== null && p.username.toLowerCase() === player) !== -1; + } else { + const foundPlayer = this.playerList[player.worldIndex]; + if(!foundPlayer) { + return false; + } + + return foundPlayer.equals(player); + } + } + + public registerPlayer(player: Player): boolean { + if(!player) { + return false; + } + + const index = this.playerList.findIndex(p => p === null); + + if(index === -1) { + logger.warn('World full!'); + return false; + } + + player.worldIndex = index; + this.playerList[index] = player; + return true; + } + + public deregisterPlayer(player: Player): void { + this.playerList[player.worldIndex] = null; + } + + public npcExists(npc: Npc): boolean { + const foundNpc = this.npcList[npc.worldIndex]; + if(!foundNpc || !foundNpc.exists) { + return false; + } + + return foundNpc.equals(npc); + } + + public async registerNpc(npc: Npc): Promise { + if(!npc) { + return false; + } + + const index = this.npcList.findIndex(n => n === null); + + if(index === -1) { + logger.warn('NPC list full!'); + return false; + } + + npc.worldIndex = index; + this.npcList[index] = npc; + await npc.init(); + return true; + } + + public deregisterNpc(npc: Npc): void { + npc.exists = false; + this.npcList[npc.worldIndex] = null; + } } From 195c101d9a23ac4e42c12878fdebf448757a2f0c Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 11:54:33 -0500 Subject: [PATCH 08/16] Fixing all lint issues --- src/game-engine/world/action/hooks/task.ts | 14 ++++++------- .../world/action/item-swap.action.ts | 4 ++-- src/game-engine/world/actor/actor.ts | 2 +- .../skills/woodcutting/woodcutting.plugin.ts | 20 +++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/game-engine/world/action/hooks/task.ts b/src/game-engine/world/action/hooks/task.ts index 75f718ab6..832a733a5 100644 --- a/src/game-engine/world/action/hooks/task.ts +++ b/src/game-engine/world/action/hooks/task.ts @@ -45,13 +45,14 @@ export class TaskExecutor { this.running = true; if(!!this.task.delay || !!this.task.delayMs) { - await lastValueFrom(timer(!!this.task.delayMs ? this.task.delayMs : - (this.task.delay * World.TICK_LENGTH))); + 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 || (this.task.interval * World.TICK_LENGTH); + 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( @@ -61,11 +62,11 @@ export class TaskExecutor { resolve(); } }, - async error => { + error => { logger.error(error); resolve(); }, - async() => resolve()); + () => resolve()); }); } else { // Single execution task @@ -140,8 +141,7 @@ export class TaskExecutor { player: Player | undefined; npc: Npc | undefined; actionData: T; - session: TaskSessionData; - } { + session: TaskSessionData; } { const { type: { player, diff --git a/src/game-engine/world/action/item-swap.action.ts b/src/game-engine/world/action/item-swap.action.ts index 2eb8865c6..8066cd1be 100644 --- a/src/game-engine/world/action/item-swap.action.ts +++ b/src/game-engine/world/action/item-swap.action.ts @@ -44,8 +44,8 @@ export interface ItemSwapAction { * @param toSlot * @param widget */ -const itemSwapActionPipe = async (player: Player, fromSlot: number, toSlot: number, widget: - { widgetId: number, containerId: number }): Promise> => { +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)); diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index a625f64d6..88c6a8a8d 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -528,7 +528,7 @@ export abstract class Actor { return this instanceof Npc; } - public get type(): { player?: Player; npc?: 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/plugins/skills/woodcutting/woodcutting.plugin.ts b/src/plugins/skills/woodcutting/woodcutting.plugin.ts index 00e4e56ce..b07458785 100644 --- a/src/plugins/skills/woodcutting/woodcutting.plugin.ts +++ b/src/plugins/skills/woodcutting/woodcutting.plugin.ts @@ -80,18 +80,14 @@ const activate = (task: TaskExecutor, taskIteration: nu const targetName: string = cache.itemDefinitions.get(tree.itemId).name.toLowerCase(); if(actor.inventory.hasSpace()) { - let randomLoot = false; - let roll = randomBetween(1, 256); - if(roll === 1) { - randomLoot = true; + 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 }); - } - - const itemToAdd = tree.itemId; - - if(!randomLoot) { + } else { // Standard log chopper player?.sendMessage(`You manage to chop some ${targetName}.`); actor.giveItem(itemToAdd); } @@ -105,14 +101,16 @@ const activate = (task: TaskExecutor, taskIteration: nu return false; } } else { - player?.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true); + 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) { - player?.playSound(soundIds.axeSwing[Math.floor(Math.random() * soundIds.axeSwing.length)], 7, 0); + const randomSoundIdx = Math.floor(Math.random() * soundIds.axeSwing.length); + player?.playSound(soundIds.axeSwing[randomSoundIdx], 7, 0); } } From bbbc094bf395db4e98768d6dd411ec3a33bd13b6 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 12:17:10 -0500 Subject: [PATCH 09/16] Fixing the new walkTo api function --- src/game-engine/game-server.ts | 2 +- src/game-engine/world/action/index.ts | 12 +++++- src/game-engine/world/actor/actor.ts | 19 +++++++--- src/game-engine/world/position.ts | 54 +++++++++++++++------------ 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/src/game-engine/game-server.ts b/src/game-engine/game-server.ts index a518cd324..0e2441dfa 100644 --- a/src/game-engine/game-server.ts +++ b/src/game-engine/game-server.ts @@ -15,7 +15,7 @@ import { Player } from '@engine/world/actor/player/player'; import { Subject, timer } from 'rxjs'; import { Position } from '@engine/world/position'; import { ActionHook, sortActionHooks } from '@engine/world/action/hooks'; -import { ActionPipeline, ActionType } from '@engine/world/action'; +import { ActionType } from '@engine/world/action'; /** diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 103b561cb..edfed6fb4 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -6,6 +6,7 @@ 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 { LocationObject } from '@runejs/cache-parser'; /** @@ -146,7 +147,16 @@ export class ActionPipeline { } if(runnableHooks.actionPosition) { - await this.actor.waitForPathing(runnableHooks.actionPosition); + try { + const gameObject = runnableHooks.action['object'] || null; + await this.actor.waitForPathing( + !gameObject ? runnableHooks.actionPosition : (gameObject as LocationObject)); + } catch(error) { + logger.error(`Error pathing to hook target`, error); + return; + } + + logger.info(`Pathing successful`); } for(let i = 0; i < runnableHooks.hooks.length; i++) { diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index 88c6a8a8d..861a87ed3 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -83,26 +83,33 @@ export abstract class Actor { * @param gameObject The game object to wait for the actor to reach. */ public async waitForPathing(gameObject: LocationObject): Promise; - public async waitForPathing(position: Position | 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 { await new Promise((resolve, reject) => { - this.metadata.walkingTo = position; + 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(position)) { + if(!this.metadata.walkingTo || !this.metadata.walkingTo.equals(target)) { reject(); clearInterval(inter); return; } if(!this.walkingQueue.moving()) { - if(position instanceof Position) { - if(this.position.distanceBetween(position) > 1) { + if(target instanceof Position) { + if(this.position.distanceBetween(target) > 1) { reject(); } else { resolve(); } } else { - if(this.position.withinInteractionDistance(position)) { + if(this.position.withinInteractionDistance(target)) { resolve(); } else { reject(); diff --git a/src/game-engine/world/position.ts b/src/game-engine/world/position.ts index a97b3f35a..0fc8afa0d 100644 --- a/src/game-engine/world/position.ts +++ b/src/game-engine/world/position.ts @@ -43,33 +43,39 @@ export class Position { return new Position(this.x, this.y, this.level); } - public withinInteractionDistance(locationObject: LocationObject): boolean { - const definition = cache.locationObjectDefinitions.get(locationObject.objectId); - const occupantX = locationObject.x; - const occupantY = locationObject.y; - let width = definition.sizeX; - let height = definition.sizeY; - - 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: LocationObject, minimumDistance?: number): boolean; + public withinInteractionDistance(position: Position, minimumDistance?: number): boolean; + public withinInteractionDistance(target: LocationObject | 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 = cache.locationObjectDefinitions.get(target.objectId); + const occupantX = target.x; + const occupantY = target.y; + let width = definition.sizeX; + let height = definition.sizeY; + + 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; + } } } } From fc8dd78295a2d8928b468377cc72480340ef60ee Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 12:52:35 -0500 Subject: [PATCH 10/16] Making actions in the pipeline cancel when the actor initiates movement --- src/game-engine/world/action/hooks/task.ts | 2 +- src/game-engine/world/action/index.ts | 25 +++++++ src/game-engine/world/actor/actor.ts | 69 ++++++++------------ src/game-engine/world/actor/player/player.ts | 4 +- src/game-engine/world/actor/walking-queue.ts | 10 ++- 5 files changed, 67 insertions(+), 43 deletions(-) diff --git a/src/game-engine/world/action/hooks/task.ts b/src/game-engine/world/action/hooks/task.ts index 832a733a5..397f4d013 100644 --- a/src/game-engine/world/action/hooks/task.ts +++ b/src/game-engine/world/action/hooks/task.ts @@ -128,11 +128,11 @@ export class TaskExecutor { } public async stop(): Promise { + this.running = false; this.intervalSubscription?.unsubscribe(); await this.task?.onComplete(this, this.iteration); - this.running = false; this.session = null; } diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index edfed6fb4..3ae0fd807 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -7,6 +7,7 @@ import { Position } from '@engine/world/position'; import { Player } from '@engine/world/actor/player/player'; import { TaskExecutor } from '@engine/world/action/hooks/task'; import { LocationObject } from '@runejs/cache-parser'; +import { Subscription } from 'rxjs'; /** @@ -83,7 +84,11 @@ export class ActionPipeline { private runningTasks: TaskExecutor[] = []; + private movementSubscription: Subscription; + public constructor(public readonly actor: Actor) { + this.movementSubscription = this.actor.walkingQueue.movementQueued$ + .subscribe(() => this.handleMovement()); } public static getPipe(action: ActionType): Map { @@ -94,6 +99,10 @@ 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) { @@ -108,6 +117,22 @@ export class ActionPipeline { } } + private async handleMovement(): Promise { + console.log('movement'); + if(!this.runningTasks || this.runningTasks.length === 0) { + return; + } + + for(const runningTask of this.runningTasks) { + if(runningTask.running) { + await runningTask.stop(); + } + } + + // Remove all tasks + this.runningTasks = []; + } + private async cancelWeakerActions(newActionStrength: ActionStrength): Promise { if(!this.runningTasks || this.runningTasks.length === 0) { return; diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index 861a87ed3..273a41694 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -20,40 +20,42 @@ import { LocationObject } from '@runejs/cache-parser'; */ 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' { @@ -165,7 +167,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'), @@ -212,7 +214,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'), @@ -267,8 +269,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 { @@ -280,30 +281,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 { @@ -465,10 +465,6 @@ export abstract class Actor { this._worldIndex = value; } - public get walkingQueue(): WalkingQueue { - return this._walkingQueue; - } - public get walkDirection(): number { return this._walkDirection; } @@ -493,13 +489,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; } diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index e7b65533d..949c44295 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -279,7 +279,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); diff --git a/src/game-engine/world/actor/walking-queue.ts b/src/game-engine/world/actor/walking-queue.ts index 8c55fe5df..b2221e85e 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; } @@ -201,7 +209,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); From 30610c09b99835c292f45b6c7c6c9273d7e22fe5 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 13:00:58 -0500 Subject: [PATCH 11/16] Fixing a lint issue and adding a pipeline cancellation flag --- src/game-engine/world/action/index.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 3ae0fd807..9569026a7 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -83,12 +83,12 @@ 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(() => this.handleMovement()); + .subscribe(async () => this.handleMovement()); } public static getPipe(action: ActionType): Map { @@ -104,6 +104,10 @@ export class ActionPipeline { } public async call(action: ActionType, ...args: any[]): Promise { + if(this.canceling) { + return; + } + const actionHandler = ActionPipeline.pipes.get(action.toString()); if(actionHandler) { try { @@ -118,11 +122,12 @@ export class ActionPipeline { } private async handleMovement(): Promise { - console.log('movement'); - if(!this.runningTasks || this.runningTasks.length === 0) { + 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(); @@ -131,6 +136,7 @@ export class ActionPipeline { // Remove all tasks this.runningTasks = []; + this.canceling = false; } private async cancelWeakerActions(newActionStrength: ActionStrength): Promise { @@ -184,6 +190,10 @@ export class ActionPipeline { logger.info(`Pathing successful`); } + if(this.canceling) { + return; + } + for(let i = 0; i < runnableHooks.hooks.length; i++) { const hook = runnableHooks.hooks[i]; await this.runHook(hook, runnableHooks.action); From 88248867bf0ff60acf524eb611421e60c0bc7208 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 13:01:48 -0500 Subject: [PATCH 12/16] Removing one check --- src/game-engine/world/action/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 9569026a7..a073c6556 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -186,12 +186,6 @@ export class ActionPipeline { logger.error(`Error pathing to hook target`, error); return; } - - logger.info(`Pathing successful`); - } - - if(this.canceling) { - return; } for(let i = 0; i < runnableHooks.hooks.length; i++) { From cb90a5adc751f0d255c93018a53b6f549a2ba372 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 13:03:35 -0500 Subject: [PATCH 13/16] Oopsie v2 --- src/game-engine/world/action/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index a073c6556..1d621b68b 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -104,10 +104,6 @@ export class ActionPipeline { } public async call(action: ActionType, ...args: any[]): Promise { - if(this.canceling) { - return; - } - const actionHandler = ActionPipeline.pipes.get(action.toString()); if(actionHandler) { try { From e32017ebc9d3fb1338135238e3513b81ef3c5c84 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Sat, 20 Mar 2021 15:25:11 -0500 Subject: [PATCH 14/16] Running actor actions get cancelled when a new action is performed That does not mean all actions they are involved in are cancelled :) --- src/game-engine/world/action/index.ts | 64 ++++++++------------------- src/game-engine/world/actor/actor.ts | 4 ++ src/game-engine/world/position.ts | 1 + 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index 1d621b68b..0620024d8 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -88,7 +88,7 @@ export class ActionPipeline { public constructor(public readonly actor: Actor) { this.movementSubscription = this.actor.walkingQueue.movementQueued$ - .subscribe(async () => this.handleMovement()); + .subscribe(async () => this.cancelRunningTasks()); } public static getPipe(action: ActionType): Map { @@ -117,7 +117,7 @@ export class ActionPipeline { } } - private async handleMovement(): Promise { + public async cancelRunningTasks(): Promise { if(this.canceling || !this.runningTasks || this.runningTasks.length === 0) { return; } @@ -135,57 +135,33 @@ export class ActionPipeline { this.canceling = false; } - private async cancelWeakerActions(newActionStrength: ActionStrength): Promise { - if(!this.runningTasks || this.runningTasks.length === 0) { + private async runActionHandler(actionHandler: any, ...args: any[]): Promise { + const runnableHooks: RunnableHooks | null | undefined = actionHandler(...args); + + if(!runnableHooks?.hooks || runnableHooks.hooks.length === 0) { return; } - const pendingRemoval: string[] = []; - - for(const runningTask of this.runningTasks) { - if(!runningTask.running) { - pendingRemoval.push(runningTask.taskId); - continue; - } + for(let i = 0; i < runnableHooks.hooks.length; i++) { + const hook = runnableHooks.hooks[i]; - if(runningTask.strength === 'weak' || (runningTask.strength === 'normal' && newActionStrength === 'strong')) { - // Cancel obviously weaker tasks - await runningTask.stop(); - pendingRemoval.push(runningTask.taskId); + if(!hook) { continue; } - if(runningTask.strength === 'normal') { - // @TODO normal task handling - } else if(runningTask.strength === 'strong') { - // @TODO strong task handling - } - } - - // Remove all non-running and ceased tasks - this.runningTasks = this.runningTasks.filter(task => !pendingRemoval.includes(task.taskId)); - } - - private async runActionHandler(actionHandler: any, ...args: any[]): Promise { - const runnableHooks: RunnableHooks | null | undefined = actionHandler(...args); - - if(!runnableHooks?.hooks || runnableHooks.hooks.length === 0) { - return; - } + await this.cancelRunningTasks(); - if(runnableHooks.actionPosition) { - try { - const gameObject = runnableHooks.action['object'] || null; - await this.actor.waitForPathing( - !gameObject ? runnableHooks.actionPosition : (gameObject as LocationObject)); - } catch(error) { - logger.error(`Error pathing to hook target`, error); - return; + if(runnableHooks.actionPosition) { + try { + const gameObject = runnableHooks.action['object'] || null; + await this.actor.waitForPathing( + !gameObject ? runnableHooks.actionPosition : (gameObject as LocationObject)); + } catch(error) { + logger.error(`Error pathing to hook target`, error); + return; + } } - } - for(let i = 0; i < runnableHooks.hooks.length; i++) { - const hook = runnableHooks.hooks[i]; await this.runHook(hook, runnableHooks.action); if(!hook.multi) { // If the highest priority hook does not allow other hooks @@ -199,8 +175,6 @@ export class ActionPipeline { private async runHook(actionHook: ActionHook, action: any): Promise { const { handler, task } = actionHook; - await this.cancelWeakerActions(actionHook.strength || 'normal'); - if(task) { // Schedule task-based hook const taskExecutor = new TaskExecutor(this.actor, task, actionHook, action); diff --git a/src/game-engine/world/actor/actor.ts b/src/game-engine/world/actor/actor.ts index 273a41694..143755440 100644 --- a/src/game-engine/world/actor/actor.ts +++ b/src/game-engine/world/actor/actor.ts @@ -93,6 +93,10 @@ export abstract class Actor { */ 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); diff --git a/src/game-engine/world/position.ts b/src/game-engine/world/position.ts index 0fc8afa0d..b8b4c13dd 100644 --- a/src/game-engine/world/position.ts +++ b/src/game-engine/world/position.ts @@ -45,6 +45,7 @@ export class Position { public withinInteractionDistance(gameObject: LocationObject, minimumDistance?: number): boolean; public withinInteractionDistance(position: Position, minimumDistance?: number): boolean; + public withinInteractionDistance(target: LocationObject | Position, minimumDistance?: number): boolean; public withinInteractionDistance(target: LocationObject | Position, minimumDistance: number = 1): boolean { if(target instanceof Position) { return this.distanceBetween(target) <= minimumDistance; From 7ff0593c7e484a01221c94ad60f2e10da719b5e7 Mon Sep 17 00:00:00 2001 From: Tynarus Date: Tue, 20 Apr 2021 17:15:22 -0500 Subject: [PATCH 15/16] Nothing to see here --- src/game-engine/world/actor/player/player.ts | 2 +- src/plugins/skills/woodcutting/woodcutting.plugin.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index 452c91a81..dcccfc3d7 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -600,7 +600,7 @@ export class Player extends Actor { let showInConsole = false; if(typeof options === 'boolean') { showDialogue = true; - } else { + } else if(options) { showDialogue = options.dialogue || false; showInConsole = options.console || false; } diff --git a/src/plugins/skills/woodcutting/woodcutting.plugin.ts b/src/plugins/skills/woodcutting/woodcutting.plugin.ts index b07458785..dcd847563 100644 --- a/src/plugins/skills/woodcutting/woodcutting.plugin.ts +++ b/src/plugins/skills/woodcutting/woodcutting.plugin.ts @@ -9,10 +9,11 @@ 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 { cache, world } from '@engine/game-server'; +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 canActivate = (task: TaskExecutor, taskIteration: number): boolean => { @@ -77,7 +78,7 @@ const activate = (task: TaskExecutor, taskIteration: nu const percentNeeded = tree.baseChance + toolLevel + actor.skills.woodcutting.level; if(successChance <= percentNeeded) { - const targetName: string = cache.itemDefinitions.get(tree.itemId).name.toLowerCase(); + const targetName: string = findItem(tree.itemId).name.toLowerCase(); if(actor.inventory.hasSpace()) { const itemToAdd = tree.itemId; From 0aa4c6b4baf0e778a446863b9352c3ebf038104f Mon Sep 17 00:00:00 2001 From: Tynarus Date: Fri, 23 Apr 2021 18:13:03 -0500 Subject: [PATCH 16/16] why did I write that like that --- src/game-engine/world/action/hooks/task.ts | 2 +- src/game-engine/world/action/index.ts | 11 ++++++++++- .../world/actor/player/interface-state.ts | 14 ++++---------- src/game-engine/world/actor/player/player.ts | 12 +++++++----- src/game-engine/world/actor/walking-queue.ts | 9 --------- src/plugins/dialogue/dialogue-option.plugin.ts | 2 +- 6 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/game-engine/world/action/hooks/task.ts b/src/game-engine/world/action/hooks/task.ts index 397f4d013..d257f3ac2 100644 --- a/src/game-engine/world/action/hooks/task.ts +++ b/src/game-engine/world/action/hooks/task.ts @@ -54,7 +54,7 @@ export class TaskExecutor { const intervalMs = this.task.intervalMs !== undefined ? this.task.intervalMs : (this.task.interval * World.TICK_LENGTH); - await new Promise(resolve => { + await new Promise(resolve => { this.intervalSubscription = timer(0, intervalMs).subscribe( async() => { if(!await this.execute()) { diff --git a/src/game-engine/world/action/index.ts b/src/game-engine/world/action/index.ts index c2ddd0417..cdbeb186c 100644 --- a/src/game-engine/world/action/index.ts +++ b/src/game-engine/world/action/index.ts @@ -44,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. */ @@ -150,7 +156,10 @@ export class ActionPipeline { continue; } - await this.cancelRunningTasks(); + // Some actions are non-cancelling + if(gentleActions.indexOf(hook.type) === -1) { + await this.cancelRunningTasks(); + } if(runnableHooks.actionPosition) { try { diff --git a/src/game-engine/world/actor/player/interface-state.ts b/src/game-engine/world/actor/player/interface-state.ts index 27637258d..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; } @@ -131,21 +132,14 @@ export class InterfaceState { 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; } diff --git a/src/game-engine/world/actor/player/player.ts b/src/game-engine/world/actor/player/player.ts index f039a9e54..bc6e6a9e4 100644 --- a/src/game-engine/world/actor/player/player.ts +++ b/src/game-engine/world/actor/player/player.ts @@ -598,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) { diff --git a/src/game-engine/world/actor/walking-queue.ts b/src/game-engine/world/actor/walking-queue.ts index b2221e85e..db7e43c15 100644 --- a/src/game-engine/world/actor/walking-queue.ts +++ b/src/game-engine/world/actor/walking-queue.ts @@ -144,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(); } 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 {