From b1d60852ecc44053c9e46a0bb29e097e07952c25 Mon Sep 17 00:00:00 2001 From: markb5 Date: Tue, 17 Feb 2026 19:48:45 -0500 Subject: [PATCH 01/18] fix: break pathing --- src/engine/World.ts | 33 +++++-------------- src/engine/entity/Player.ts | 30 ++++++++++------- .../game/client/handler/MoveClickHandler.ts | 18 ++++------ 3 files changed, 34 insertions(+), 47 deletions(-) diff --git a/src/engine/World.ts b/src/engine/World.ts index 5e8e25c12..1a0ed5e37 100644 --- a/src/engine/World.ts +++ b/src/engine/World.ts @@ -93,8 +93,6 @@ import Environment from '#/util/Environment.js'; import { fromBase37, toBase37, toSafeName } from '#/util/JString.js'; import LinkList from '#/datastruct/LinkList.js'; import { printDebug, printError, printInfo } from '#/util/Logger.js'; -import { WalkTriggerSetting } from '#/engine/entity/WalkTriggerSetting.js'; - import OnDemand from './OnDemand.js'; import { ObjDelayedRequest } from './entity/ObjDelayedRequest.js'; import DbTableIndex from '#/cache/config/DbTableIndex.js'; @@ -610,7 +608,6 @@ class World { player.processInputTracking(); if (isClientConnected(player) && player.decodeIn()) { - const followingPlayer = player.targetOp === ServerTriggerType.APPLAYER3 || player.targetOp === ServerTriggerType.OPPLAYER3; if (player.userPath.length > 0 || player.opcalled) { if (player.delayed) { player.unsetMapFlag(); @@ -627,19 +624,6 @@ class World { } else { player.moveClickRequest = true; } - - if (!followingPlayer && player.opcalled && (player.userPath.length === 0 || !Environment.NODE_CLIENT_ROUTEFINDER)) { - player.pathToTarget(); - continue; - } - - if (Environment.NODE_WALKTRIGGER_SETTING !== WalkTriggerSetting.PLAYERPACKET) { - player.pathToMoveClick(player.userPath, !Environment.NODE_CLIENT_ROUTEFINDER); - - if (Environment.NODE_WALKTRIGGER_SETTING === WalkTriggerSetting.PLAYERSETUP && !player.opcalled && player.hasWaypoints()) { - player.processWalktrigger(); - } - } } } @@ -906,11 +890,13 @@ class World { player.client.state = 1; - player.client.send(Uint8Array.from([ - 2, - Math.min(player.staffModLevel, 2), - 1 // mouse tracking can only be enabled on login - ])); + player.client.send( + Uint8Array.from([ + 2, + Math.min(player.staffModLevel, 2), + 1 // mouse tracking can only be enabled on login + ]) + ); const remote = player.client.remoteAddress; if (remote.indexOf('.') !== -1) { @@ -1874,10 +1860,7 @@ class World { } else if (reply === 10) { // hop timer const { remaining } = msg; - client.send(Uint8Array.from([ - 21, - Math.min(255, remaining! / 1000) - ])); + client.send(Uint8Array.from([21, Math.min(255, remaining! / 1000)])); client.close(); return; } diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 47722bbaf..fbcd6b114 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -409,9 +409,17 @@ export default class Player extends PathingEntity { constructor(username: string, username37: bigint, hash64: bigint) { super( - 0, 3094, 3106, // tutorial island - 1, 1, - EntityLifeCycle.FOREVER, MoveRestrict.NORMAL, BlockWalk.NPC, MoveStrategy.SMART, PlayerInfoProt.FACE_COORD, PlayerInfoProt.FACE_ENTITY + 0, + 3094, + 3106, // tutorial island + 1, + 1, + EntityLifeCycle.FOREVER, + MoveRestrict.NORMAL, + BlockWalk.NPC, + MoveStrategy.NAIVE, + PlayerInfoProt.FACE_COORD, + PlayerInfoProt.FACE_ENTITY ); this.username = username; @@ -1222,11 +1230,6 @@ export default class Player extends PathingEntity { return; } - // Run the optrigger, but applayer3 should not run this - if (!followOp) { - this.processWalktrigger(); - } - interacted = this.tryInteract(false); } @@ -1747,7 +1750,7 @@ export default class Player extends PathingEntity { const { basevar, startbit, endbit } = varbit; const mask = Packet.bitmask[endbit - startbit + 1]; - return this.vars[basevar] >> startbit & mask; + return (this.vars[basevar] >> startbit) & mask; } setVarBit(id: number, value: number) { @@ -1764,7 +1767,7 @@ export default class Player extends PathingEntity { } mask <<= startbit; - this.setVar(basevar, mask & value << startbit | this.vars[basevar] & ~mask); + this.setVar(basevar, (mask & (value << startbit)) | (this.vars[basevar] & ~mask)); } private writeVarp(id: number, value: number): void { @@ -1946,7 +1949,12 @@ export default class Player extends PathingEntity { // todo: make compiler do this at pack time playSong(name: string) { // todo: don't rely on MidiPack (server should be runnable using only packed content) - const id = MidiPack.getByName(name.toLowerCase().replaceAll(' ', '_').replace(/[^a-z0-9_-]/g, '')); + const id = MidiPack.getByName( + name + .toLowerCase() + .replaceAll(' ', '_') + .replace(/[^a-z0-9_-]/g, '') + ); if (id !== -1) { this.write(new MidiSong(id)); } diff --git a/src/network/game/client/handler/MoveClickHandler.ts b/src/network/game/client/handler/MoveClickHandler.ts index 20e39d2ea..86134f254 100644 --- a/src/network/game/client/handler/MoveClickHandler.ts +++ b/src/network/game/client/handler/MoveClickHandler.ts @@ -40,20 +40,16 @@ export default class MoveClickHandler extends ClientGameMessageHandler Date: Tue, 17 Feb 2026 20:11:13 -0500 Subject: [PATCH 02/18] fix: remove bad code --- src/engine/entity/PathingEntity.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index 6224b1501..49b88bff9 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -409,16 +409,11 @@ export default abstract class PathingEntity extends Entity { } pathToMoveClick(input: number[], needsfinding: boolean): void { - if (this.moveStrategy === MoveStrategy.SMART) { - if (needsfinding) { - const { x, z } = CoordGrid.unpackCoord(input[0]); - this.queueWaypoints(findPath(this.level, this.x, this.z, x, z)); - } else { - this.queueWaypoints(input); - } + if (needsfinding) { + const { x, z } = CoordGrid.unpackCoord(input[0]); + this.queueWaypoints(findPath(this.level, this.x, this.z, x, z)); } else { - const { x, z } = CoordGrid.unpackCoord(input[input.length - 1]); - this.queueWaypoint(x, z); + this.queueWaypoints(input); } } From 9a3f2cab953386aa7e060cecd4365da4307e07bb Mon Sep 17 00:00:00 2001 From: markb5 Date: Thu, 19 Feb 2026 15:43:39 -0500 Subject: [PATCH 03/18] fix:Authenticity changes and env variables --- .env.example | 1 - src/engine/GameMap.ts | 78 ++++++++++++++++++- src/engine/entity/Npc.ts | 27 +++++-- src/engine/entity/PathingEntity.ts | 36 ++++----- src/engine/entity/Player.ts | 46 ++++++++--- .../game/client/handler/MoveClickHandler.ts | 42 +++++----- 6 files changed, 166 insertions(+), 64 deletions(-) diff --git a/.env.example b/.env.example index 6a3c38904..5e1acfb95 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,6 @@ # NODE_STAFF=pazaz # NODE_CLIENT_ROUTEFINDER=true # NODE_SOCKET_TIMEOUT=true -# NODE_WALKTRIGGER_SETTING=0 # NODE_PROFILE=main # NODE_MAX_PLAYERS=2047 # NODE_MAX_NPCS=8191 diff --git a/src/engine/GameMap.ts b/src/engine/GameMap.ts index 5174c4bad..a8dcc46fc 100644 --- a/src/engine/GameMap.ts +++ b/src/engine/GameMap.ts @@ -19,6 +19,8 @@ import Packet from '#/io/Packet.js'; import Environment from '#/util/Environment.js'; import { printDebug, printFatalError, printWarning } from '#/util/Logger.js'; +export type RouteCoordinates = { x: number; z: number }; + export default class GameMap { private static readonly OPEN: number = 0x0; private static readonly BLOCK_MAP_SQUARE: number = 0x1; @@ -340,6 +342,78 @@ export function changeLocCollision(shape: number, angle: number, blockrange: boo } } +export function findNaivePath(level: number, srcX: number, srcZ: number, destX: number, destZ: number, srcWidth: number, srcHeight: number, destWidth: number, destHeight: number, extraFlag: number, collision: CollisionType): Uint32Array { + return rsmod.findNaivePath(level, srcX, srcZ, destX, destZ, srcWidth, srcHeight, destWidth, destHeight, extraFlag, collision); +} + +function translate(coord: RouteCoordinates, dx: number, dz: number): RouteCoordinates { + return { x: coord.x + dx, z: coord.z + dz }; +} + +// Keep these as your existing implementations. +// The Kotlin rotate(...) as used here appears to mean "pick width vs length based on angle". +// If you have a different rotate behavior, swap it in. +export function rotate(angle: number, dimensionA: number, dimensionB: number): number { + return (angle & 0x1) !== 0 ? dimensionB : dimensionA; +} +export function naiveDestination( + sourceX: number, + sourceZ: number, + sourceWidth: number, + sourceLength: number, + targetX: number, + targetZ: number, + targetWidth: number, + targetLength: number, + targetAngle: number = 0, + reversePriority: boolean = false // NEW +): RouteCoordinates { + const diagonal = sourceX - targetX + (sourceZ - targetZ); + const anti = sourceX - targetX - (sourceZ - targetZ); + + const rotatedWidth = rotate(targetAngle, targetWidth, targetLength); + const rotatedLength = rotate(targetAngle, targetLength, targetWidth); + + const nwBoundary = rotatedLength - 1 - (sourceWidth - 1); + const seBoundary = rotatedWidth - 1 - (sourceLength - 1); + const neBoundary = sourceWidth - sourceLength; + + // Default behavior (reversePriority=false): N/S wins ties. + // Reverse behavior (reversePriority=true): E/W wins ties by flipping equality ownership. + const southWestClockwise = reversePriority ? anti <= 0 : anti < 0; + const northWestClockwise = reversePriority ? diagonal > nwBoundary : diagonal >= nwBoundary; + const northEastClockwise = reversePriority ? anti >= neBoundary : anti > neBoundary; + const southEastClockwise = reversePriority ? diagonal < seBoundary : diagonal <= seBoundary; + + const target: RouteCoordinates = { x: targetX, z: targetZ }; + + if (southWestClockwise && !northWestClockwise) { + // West + const offZ = diagonal >= -sourceWidth ? Math.min(diagonal + sourceWidth, rotatedLength - 1) : anti > -sourceWidth ? -(sourceWidth + anti) : 0; + + return translate(target, -sourceWidth, offZ); + } else if (northWestClockwise && !northEastClockwise) { + // North + const offX = anti >= -rotatedLength ? Math.min(anti + rotatedLength, rotatedWidth - 1) : diagonal < rotatedLength ? Math.max(diagonal - rotatedLength, -(sourceWidth - 1)) : 0; + + return translate(target, offX, rotatedLength); + } else if (northEastClockwise && !southEastClockwise) { + // East + const offZ = anti <= rotatedWidth ? rotatedLength - anti : diagonal < rotatedWidth ? Math.max(diagonal - rotatedWidth, -(sourceLength - 1)) : 0; + + return translate(target, rotatedWidth, offZ); + } else { + if (!(southEastClockwise && !southWestClockwise)) { + throw new Error('Invariant failed: expected southEastClockwise && !southWestClockwise'); + } + + // South + const offX = diagonal > -sourceLength ? Math.min(diagonal + sourceLength, rotatedWidth - 1) : anti < sourceLength ? Math.max(anti - sourceLength, -(sourceLength - 1)) : 0; + + return translate(target, offX, -sourceLength); + } +} + /** * Change collision at a specified Position for npcs. * @param size The size square of this npc. (1x1, 2x2, etc). @@ -387,10 +461,6 @@ export function findPathToLoc(level: number, srcX: number, srcZ: number, destX: return rsmod.findPath(level, srcX, srcZ, destX, destZ, srcSize, destWidth, destHeight, angle, shape, true, blockAccessFlags, 25, CollisionType.NORMAL); } -export function findNaivePath(level: number, srcX: number, srcZ: number, destX: number, destZ: number, srcWidth: number, srcHeight: number, destWidth: number, destHeight: number, extraFlag: number, collision: CollisionType): Uint32Array { - return rsmod.findNaivePath(level, srcX, srcZ, destX, destZ, srcWidth, srcHeight, destWidth, destHeight, extraFlag, collision); -} - export function reachedEntity(level: number, srcX: number, srcZ: number, destX: number, destZ: number, destWidth: number, destHeight: number, srcSize: number): boolean { return rsmod.reached(level, srcX, srcZ, destX, destZ, destWidth, destHeight, srcSize, 0, -2, 0); } diff --git a/src/engine/entity/Npc.ts b/src/engine/entity/Npc.ts index 5d0ff0ec7..a744a81e8 100644 --- a/src/engine/entity/Npc.ts +++ b/src/engine/entity/Npc.ts @@ -1,6 +1,6 @@ import { NpcInfoProt } from '@2004scape/rsbuf'; import * as rsbuf from '@2004scape/rsbuf'; -import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; import HuntType from '#/cache/config/HuntType.js'; import NpcType from '#/cache/config/NpcType.js'; @@ -26,7 +26,7 @@ import { NpcQueueRequest } from '#/engine/entity/NpcQueueRequest.js'; import { NpcStat } from '#/engine/entity/NpcStat.js'; import PathingEntity from '#/engine/entity/PathingEntity.js'; import Player from '#/engine/entity/Player.js'; -import { isFlagged, findNaivePath } from '#/engine/GameMap.js'; +import { isFlagged, naiveDestination } from '#/engine/GameMap.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import { HuntIterator } from '#/engine/script/ScriptIterators.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -326,7 +326,7 @@ export default class Npc extends PathingEntity { } if (CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { - this.queueWaypoints(findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, 0, CollisionType.NORMAL)); + this.randomWalk(); return; } @@ -432,7 +432,7 @@ export default class Npc extends PathingEntity { this.uid = (type << 16) | this.nid; this.resetOnRevert = reset; - if(reset) { + if (reset) { const npcType = NpcType.get(type); for (let index = 0; index < npcType.stats.length; index++) { const level = npcType.stats[index]; @@ -562,6 +562,19 @@ export default class Npc extends PathingEntity { } } + naivePathToTarget() { + if (!this.target) { + return; + } + + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + const coord = naiveDestination(this.x, this.z, this.width, this.length, this.target?.x, this.target?.z, this.target?.width, this.target?.length, angle); + this.queueWaypoint(coord.x, coord.z); + } + private processMovementInteraction() { if (this.delayed || !this.isActive) { return; @@ -682,7 +695,7 @@ export default class Npc extends PathingEntity { return true; } - private randomWalk(range: number) { + private wander(range: number) { const dx = Math.round(Math.random() * (range * 2) - range); const dz = Math.round(Math.random() * (range * 2) - range); const destX = this.startX + dx; @@ -702,7 +715,7 @@ export default class Npc extends PathingEntity { // 1/8 chance to move every tick (even if they already have a destination) if (type.moverestrict !== MoveRestrict.NOMOVE && Math.random() < 0.125) { - this.randomWalk(type.wanderrange); + this.wander(type.wanderrange); } this.updateMovement(); @@ -989,7 +1002,7 @@ export default class Npc extends PathingEntity { // --- Other - private getTrigger(type : NpcType): ScriptFile | null { + private getTrigger(type: NpcType): ScriptFile | null { const trigger: ServerTriggerType | null = this.getTriggerForMode(this.targetOp); if (trigger) { return ScriptProvider.getByTrigger(trigger, this.type, type.category) ?? null; diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index 49b88bff9..16247982b 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -15,10 +15,9 @@ import Npc from '#/engine/entity/Npc.js'; import { NpcMode } from '#/engine/entity/NpcMode.js'; import Obj from '#/engine/entity/Obj.js'; import Player from '#/engine/entity/Player.js'; -import { canTravel, changeNpcCollision, changePlayerCollision, findNaivePath, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; -import Environment from '#/util/Environment.js'; type TargetSubject = { type: number; @@ -122,6 +121,7 @@ export default abstract class PathingEntity extends Entity { abstract updateMovement(): boolean; abstract blockWalkFlag(): CollisionFlag; abstract defaultMoveSpeed(): MoveSpeed; + abstract naivePathToTarget(): void; /** * Process movement function for a PathingEntity to use. @@ -374,7 +374,7 @@ export default abstract class PathingEntity extends Entity { /* * Returns if this PathingEntity is at the last waypoint or has no waypoint. */ - isLastOrNoWaypoint(): boolean { + isLastWaypoint(): boolean { return this.waypointIndex <= 0; } @@ -408,13 +408,15 @@ export default abstract class PathingEntity extends Entity { return CoordGrid.distanceTo(this, target) <= range && isApproached(this.level, this.x, this.z, target.x, target.z, this.width, this.length, target.width, target.length); } - pathToMoveClick(input: number[], needsfinding: boolean): void { - if (needsfinding) { - const { x, z } = CoordGrid.unpackCoord(input[0]); - this.queueWaypoints(findPath(this.level, this.x, this.z, x, z)); + randomWalk(): void { + let x = this.x, + z = this.z; + if (Math.random() < 0.5) { + x += Math.random() < 0.5 ? -1 : 1; } else { - this.queueWaypoints(input); + z += Math.random() < 0.5 ? -1 : 1; } + this.queueWaypoint(x, z); } pathToPathingTarget(): void { @@ -427,16 +429,12 @@ export default abstract class PathingEntity extends Entity { return; } - if ( - !(this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3) && - Environment.NODE_CLIENT_ROUTEFINDER && - CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length) - ) { - this.queueWaypoints(findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, 0, CollisionType.NORMAL)); + if (!(this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3) && CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { + this.randomWalk(); return; } - if (!this.isLastOrNoWaypoint()) { + if (!this.isLastWaypoint()) { return; } @@ -459,11 +457,7 @@ export default abstract class PathingEntity extends Entity { if (this.moveStrategy === MoveStrategy.SMART) { if (this.target instanceof PathingEntity) { - if (Environment.NODE_CLIENT_ROUTEFINDER && CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { - this.queueWaypoints(findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, 0, CollisionType.NORMAL)); - } else { - this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length)); - } + this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length)); } else if (this.target instanceof Loc) { const forceapproach = LocType.get(this.target.type).forceapproach; this.queueWaypoints(findPathToLoc(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length, this.target.angle, this.target.shape, forceapproach)); @@ -485,7 +479,7 @@ export default abstract class PathingEntity extends Entity { return; } if (this.target instanceof PathingEntity) { - this.queueWaypoints(findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, extraFlag, collisionStrategy)); + this.naivePathToTarget(); } else { this.queueWaypoint(this.target.x, this.target.z); } diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index fbcd6b114..aaf4dc26b 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import { PlayerInfoProt, Visibility } from '@2004scape/rsbuf'; -import { CollisionType, CollisionFlag } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; import Component from '#/cache/config/Component.js'; import FontType from '#/cache/config/FontType.js'; @@ -36,7 +36,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, findNaivePath, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj, naiveDestination } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -324,6 +324,7 @@ export default class Player extends PathingEntity { allowDesign: boolean = false; afkEventReady: boolean = false; moveClickRequest: boolean = false; + lastMoveClick: number = 0; requestLogout: boolean = false; requestIdleLogout: boolean = false; @@ -417,7 +418,7 @@ export default class Player extends PathingEntity { EntityLifeCycle.FOREVER, MoveRestrict.NORMAL, BlockWalk.NPC, - MoveStrategy.NAIVE, + Environment.NODE_CLIENT_ROUTEFINDER ? MoveStrategy.NAIVE : MoveStrategy.SMART, PlayerInfoProt.FACE_COORD, PlayerInfoProt.FACE_ENTITY ); @@ -1046,12 +1047,29 @@ export default class Player extends PathingEntity { return ScriptProvider.getByTrigger(this.targetOp, typeId, categoryId) ?? null; } + setLastMoveClick() { + this.lastMoveClick = World.currentTick; + } + + naivePathToTarget() { + if (!this.target) { + return; + } + + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + const coord = naiveDestination(this.x, this.z, this.width, this.length, this.target?.x, this.target?.z, this.target?.width, this.target?.length, angle, true); + this.queueWaypoint(coord.x, coord.z); + } + pathToPathingTarget(): void { if (!(this.target instanceof PathingEntity)) { return; } - if (this.isLastOrNoWaypoint() && (this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3)) { + if (this.isLastWaypoint() && (this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3)) { this.queueWaypoint(this.target.followX, this.target.followZ); return; } @@ -1060,11 +1078,18 @@ export default class Player extends PathingEntity { return; } - if (Environment.NODE_CLIENT_ROUTEFINDER && CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { - this.queueWaypoints(findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, 0, CollisionType.NORMAL)); - return; - } - if (this.isLastOrNoWaypoint()) { + // Different mechanics for naive and smart paths + if (this.moveStrategy === MoveStrategy.NAIVE) { + const underTarget = CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length); + if (underTarget) { + this.randomWalk(); + return; + } + + if (this.isLastWaypoint() && this.lastMoveClick < World.currentTick) { + this.pathToTarget(); + } + } else if (this.isLastWaypoint()) { this.pathToTarget(); } } @@ -1223,6 +1248,9 @@ export default class Player extends PathingEntity { // If there is a target and p_access is available, try to interact before movement if (this.target && this.canAccess()) { + if (Environment.NODE_CLIENT_ROUTEFINDER && !followOp) { + this.processWalktrigger(); + } // Clear the interaction if target validation does not pass if (!this.validateTarget()) { this.clearInteraction(); diff --git a/src/network/game/client/handler/MoveClickHandler.ts b/src/network/game/client/handler/MoveClickHandler.ts index 86134f254..c74e26071 100644 --- a/src/network/game/client/handler/MoveClickHandler.ts +++ b/src/network/game/client/handler/MoveClickHandler.ts @@ -4,7 +4,8 @@ import ClientGameMessageHandler from '#/network/game/client/ClientGameMessageHan import MoveClick from '#/network/game/client/model/MoveClick.js'; import UnsetMapFlag from '#/network/game/server/model/UnsetMapFlag.js'; import Environment from '#/util/Environment.js'; -import { WalkTriggerSetting } from '#/engine/entity/WalkTriggerSetting.js'; + +import { findPath } from '#/engine/GameMap.js'; export default class MoveClickHandler extends ClientGameMessageHandler { handle(message: MoveClick, player: NetworkPlayer): boolean { @@ -14,41 +15,38 @@ export default class MoveClickHandler extends ClientGameMessageHandler 1 || CoordGrid.distanceToSW(player, { x: start.x, z: start.z }) > 104) { player.unsetMapFlag(); player.userPath = []; return false; } - if (Environment.NODE_CLIENT_ROUTEFINDER) { - if (message.path.length === 1 && start.x === player.x && start.z === player.z) { - // this check ignores setting the path when the player is clicking on their current tile - player.userPath = []; - } else { - player.userPath = []; - - for (let i = 0; i < message.path.length; i++) { - player.userPath[i] = CoordGrid.packCoord(player.level, message.path[i].x, message.path[i].z); - } - } - } else { - const dest = message.path[message.path.length - 1]; - player.userPath = [CoordGrid.packCoord(player.level, dest.x, dest.z)]; - } - - if (Environment.NODE_WALKTRIGGER_SETTING === WalkTriggerSetting.PLAYERPACKET) { - player.pathToMoveClick(player.userPath, !Environment.NODE_CLIENT_ROUTEFINDER); - } - + // Clear previous interaction player.clearPendingAction(); + // Handle ctrl run if (player.runenergy < 100 && message.ctrlHeld === 1) { player.tempRun = 0; } else { player.tempRun = message.ctrlHeld; } - player.processWalktrigger(); + // Set new path + if (Environment.NODE_CLIENT_ROUTEFINDER) { + player.userPath = []; + + for (let i = 0; i < message.path.length; i++) { + player.userPath[i] = CoordGrid.packCoord(player.level, message.path[i].x, message.path[i].z); + } + player.queueWaypoints(player.userPath); + player.setLastMoveClick(); + player.processWalktrigger(); + } else { + const dest = message.path[message.path.length - 1]; + player.queueWaypoints(findPath(player.level, player.x, player.z, dest.x, dest.z)); + } return true; } From d2b7c209b1b118186cb5a9622cf9e0a43abffc6c Mon Sep 17 00:00:00 2001 From: markb5 Date: Thu, 19 Feb 2026 15:49:00 -0500 Subject: [PATCH 04/18] fix: reorder walktrigger --- src/engine/entity/Player.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index aaf4dc26b..a590894f4 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1248,9 +1248,6 @@ export default class Player extends PathingEntity { // If there is a target and p_access is available, try to interact before movement if (this.target && this.canAccess()) { - if (Environment.NODE_CLIENT_ROUTEFINDER && !followOp) { - this.processWalktrigger(); - } // Clear the interaction if target validation does not pass if (!this.validateTarget()) { this.clearInteraction(); @@ -1258,6 +1255,10 @@ export default class Player extends PathingEntity { return; } + if (Environment.NODE_CLIENT_ROUTEFINDER && !followOp) { + this.processWalktrigger(); + } + interacted = this.tryInteract(false); } From aafdc2eb22e78ebe0ffbec7bba1160122ed50113 Mon Sep 17 00:00:00 2001 From: markb5 Date: Thu, 19 Feb 2026 15:53:13 -0500 Subject: [PATCH 05/18] cleanup: remove unused function --- src/engine/entity/PathingEntity.ts | 31 ------------------------------ 1 file changed, 31 deletions(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index 16247982b..e23c58705 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -419,37 +419,6 @@ export default abstract class PathingEntity extends Entity { this.queueWaypoint(x, z); } - pathToPathingTarget(): void { - if (!this.target) { - return; - } - - if (!(this.target instanceof PathingEntity)) { - this.pathToTarget(); - return; - } - - if (!(this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3) && CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { - this.randomWalk(); - return; - } - - if (!this.isLastWaypoint()) { - return; - } - - if (this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3) { - this.queueWaypoint(this.target.followX, this.target.followZ); - return; - } - - /*if (this.targetX === this.target.x && this.targetZ === this.target.z && !Position.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length)) { - return; - }*/ - - this.pathToTarget(); - } - pathToTarget(): void { if (!this.target) { return; From ddd0180862aa7fc8efce2d4347b847a3f91f5643 Mon Sep 17 00:00:00 2001 From: markb5 Date: Fri, 20 Feb 2026 17:28:48 -0500 Subject: [PATCH 06/18] fix: player naive tiebreaker --- src/engine/GameMap.ts | 4 ++++ src/engine/entity/PathingEntity.ts | 1 + src/engine/entity/Player.ts | 1 - 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/GameMap.ts b/src/engine/GameMap.ts index a8dcc46fc..c9600b86f 100644 --- a/src/engine/GameMap.ts +++ b/src/engine/GameMap.ts @@ -389,6 +389,8 @@ export function naiveDestination( if (southWestClockwise && !northWestClockwise) { // West + if (reversePriority && anti === 0) return translate(target, -sourceWidth, 0); // FIX + const offZ = diagonal >= -sourceWidth ? Math.min(diagonal + sourceWidth, rotatedLength - 1) : anti > -sourceWidth ? -(sourceWidth + anti) : 0; return translate(target, -sourceWidth, offZ); @@ -399,6 +401,8 @@ export function naiveDestination( return translate(target, offX, rotatedLength); } else if (northEastClockwise && !southEastClockwise) { // East + if (reversePriority && anti === 0) return translate(target, rotatedWidth, 0); // FIX + const offZ = anti <= rotatedWidth ? rotatedLength - anti : diagonal < rotatedWidth ? Math.max(diagonal - rotatedWidth, -(sourceLength - 1)) : 0; return translate(target, rotatedWidth, offZ); diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index e23c58705..de23275e3 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -209,6 +209,7 @@ export default abstract class PathingEntity extends Entity { } if (dir === -1) { this.waypointIndex--; + if (this.waypointIndex != -1) { return this.validateAndAdvanceStep(); } diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index a590894f4..1eecded9b 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1278,7 +1278,6 @@ export default class Player extends PathingEntity { } this.updateMovement(); - // If there's a target and p_access is available, try to interact after moving if (this.target && this.canAccess() && !followOp) { interacted = this.tryInteract(this.stepsTaken === 0); From 2a63f470a0eea1d4792e827b23f74cd4f9432b52 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 22 Feb 2026 00:23:04 -0500 Subject: [PATCH 07/18] fix: fix pathing again --- src/engine/GameMap.ts | 72 ------------------- src/engine/entity/MoveGeneratedFrom.ts | 5 ++ src/engine/entity/Npc.ts | 29 ++++---- src/engine/entity/PathingEntity.ts | 21 ++++-- src/engine/entity/Player.ts | 25 ++++--- .../game/client/handler/MoveClickHandler.ts | 17 +++-- 6 files changed, 56 insertions(+), 113 deletions(-) create mode 100644 src/engine/entity/MoveGeneratedFrom.ts diff --git a/src/engine/GameMap.ts b/src/engine/GameMap.ts index c9600b86f..fe1f8d3b0 100644 --- a/src/engine/GameMap.ts +++ b/src/engine/GameMap.ts @@ -346,78 +346,6 @@ export function findNaivePath(level: number, srcX: number, srcZ: number, destX: return rsmod.findNaivePath(level, srcX, srcZ, destX, destZ, srcWidth, srcHeight, destWidth, destHeight, extraFlag, collision); } -function translate(coord: RouteCoordinates, dx: number, dz: number): RouteCoordinates { - return { x: coord.x + dx, z: coord.z + dz }; -} - -// Keep these as your existing implementations. -// The Kotlin rotate(...) as used here appears to mean "pick width vs length based on angle". -// If you have a different rotate behavior, swap it in. -export function rotate(angle: number, dimensionA: number, dimensionB: number): number { - return (angle & 0x1) !== 0 ? dimensionB : dimensionA; -} -export function naiveDestination( - sourceX: number, - sourceZ: number, - sourceWidth: number, - sourceLength: number, - targetX: number, - targetZ: number, - targetWidth: number, - targetLength: number, - targetAngle: number = 0, - reversePriority: boolean = false // NEW -): RouteCoordinates { - const diagonal = sourceX - targetX + (sourceZ - targetZ); - const anti = sourceX - targetX - (sourceZ - targetZ); - - const rotatedWidth = rotate(targetAngle, targetWidth, targetLength); - const rotatedLength = rotate(targetAngle, targetLength, targetWidth); - - const nwBoundary = rotatedLength - 1 - (sourceWidth - 1); - const seBoundary = rotatedWidth - 1 - (sourceLength - 1); - const neBoundary = sourceWidth - sourceLength; - - // Default behavior (reversePriority=false): N/S wins ties. - // Reverse behavior (reversePriority=true): E/W wins ties by flipping equality ownership. - const southWestClockwise = reversePriority ? anti <= 0 : anti < 0; - const northWestClockwise = reversePriority ? diagonal > nwBoundary : diagonal >= nwBoundary; - const northEastClockwise = reversePriority ? anti >= neBoundary : anti > neBoundary; - const southEastClockwise = reversePriority ? diagonal < seBoundary : diagonal <= seBoundary; - - const target: RouteCoordinates = { x: targetX, z: targetZ }; - - if (southWestClockwise && !northWestClockwise) { - // West - if (reversePriority && anti === 0) return translate(target, -sourceWidth, 0); // FIX - - const offZ = diagonal >= -sourceWidth ? Math.min(diagonal + sourceWidth, rotatedLength - 1) : anti > -sourceWidth ? -(sourceWidth + anti) : 0; - - return translate(target, -sourceWidth, offZ); - } else if (northWestClockwise && !northEastClockwise) { - // North - const offX = anti >= -rotatedLength ? Math.min(anti + rotatedLength, rotatedWidth - 1) : diagonal < rotatedLength ? Math.max(diagonal - rotatedLength, -(sourceWidth - 1)) : 0; - - return translate(target, offX, rotatedLength); - } else if (northEastClockwise && !southEastClockwise) { - // East - if (reversePriority && anti === 0) return translate(target, rotatedWidth, 0); // FIX - - const offZ = anti <= rotatedWidth ? rotatedLength - anti : diagonal < rotatedWidth ? Math.max(diagonal - rotatedWidth, -(sourceLength - 1)) : 0; - - return translate(target, rotatedWidth, offZ); - } else { - if (!(southEastClockwise && !southWestClockwise)) { - throw new Error('Invariant failed: expected southEastClockwise && !southWestClockwise'); - } - - // South - const offX = diagonal > -sourceLength ? Math.min(diagonal + sourceLength, rotatedWidth - 1) : anti < sourceLength ? Math.max(anti - sourceLength, -(sourceLength - 1)) : 0; - - return translate(target, offX, -sourceLength); - } -} - /** * Change collision at a specified Position for npcs. * @param size The size square of this npc. (1x1, 2x2, etc). diff --git a/src/engine/entity/MoveGeneratedFrom.ts b/src/engine/entity/MoveGeneratedFrom.ts new file mode 100644 index 000000000..a9d461ea6 --- /dev/null +++ b/src/engine/entity/MoveGeneratedFrom.ts @@ -0,0 +1,5 @@ +export const enum AllowRepath { + BEFOREDEST, + ATDEST, + NONE +} diff --git a/src/engine/entity/Npc.ts b/src/engine/entity/Npc.ts index a744a81e8..696c444a9 100644 --- a/src/engine/entity/Npc.ts +++ b/src/engine/entity/Npc.ts @@ -1,6 +1,6 @@ import { NpcInfoProt } from '@2004scape/rsbuf'; import * as rsbuf from '@2004scape/rsbuf'; -import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; import HuntType from '#/cache/config/HuntType.js'; import NpcType from '#/cache/config/NpcType.js'; @@ -26,7 +26,7 @@ import { NpcQueueRequest } from '#/engine/entity/NpcQueueRequest.js'; import { NpcStat } from '#/engine/entity/NpcStat.js'; import PathingEntity from '#/engine/entity/PathingEntity.js'; import Player from '#/engine/entity/Player.js'; -import { isFlagged, naiveDestination } from '#/engine/GameMap.js'; +import { findNaivePath, isFlagged } from '#/engine/GameMap.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import { HuntIterator } from '#/engine/script/ScriptIterators.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -37,6 +37,7 @@ import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; import LinkList from '#/datastruct/LinkList.js'; import { printError } from '#/util/Logger.js'; +import { AllowRepath } from './MoveGeneratedFrom.js'; export default class Npc extends PathingEntity { // constructor properties @@ -314,6 +315,17 @@ export default class Npc extends PathingEntity { super.resetPathingEntity(); } } + naivePathToTarget() { + if (!this.target) { + return; + } + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); + this.queueWaypoints(waypoints, AllowRepath.BEFOREDEST); + } pathToTarget(): void { if (!this.target) { @@ -562,19 +574,6 @@ export default class Npc extends PathingEntity { } } - naivePathToTarget() { - if (!this.target) { - return; - } - - let angle = 0; - if (this.target instanceof Loc) { - angle = this.target.angle; - } - const coord = naiveDestination(this.x, this.z, this.width, this.length, this.target?.x, this.target?.z, this.target?.width, this.target?.length, angle); - this.queueWaypoint(coord.x, coord.z); - } - private processMovementInteraction() { if (this.delayed || !this.isActive) { return; diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index de23275e3..6e4a00670 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -7,6 +7,7 @@ import Entity from '#/engine/entity/Entity.js'; import { EntityLifeCycle } from '#/engine/entity/EntityLifeCycle.js'; import { Interaction } from '#/engine/entity/Interaction.js'; import Loc from '#/engine/entity/Loc.js'; +import { AllowRepath } from './MoveGeneratedFrom.js'; import { MoveRestrict } from '#/engine/entity/MoveRestrict.js'; import { MoveSpeed } from '#/engine/entity/MoveSpeed.js'; import { MoveStrategy } from '#/engine/entity/MoveStrategy.js'; @@ -53,6 +54,7 @@ export default abstract class PathingEntity extends Entity { lastInt: number = -1; // resume_p_countdialog, ai_queue lastCrawl: boolean = false; lastMovement: number = 0; + lastMoveGeneratedFrom: AllowRepath = AllowRepath.NONE; walktrigger: number = -1; walktriggerArg: number = 0; // used for npcs @@ -240,21 +242,23 @@ export default abstract class PathingEntity extends Entity { * @param x The x position of the step. * @param z The z position of the step. */ - queueWaypoint(x: number, z: number): void { + queueWaypoint(x: number, z: number, from: AllowRepath = AllowRepath.NONE): void { this.waypoints[0] = CoordGrid.packCoord(0, x, z); // level doesn't matter here this.waypointIndex = 0; + this.lastMoveGeneratedFrom = from; } /** * Queue waypoints to this PathingEntity. * @param waypoints The waypoints to queue. */ - queueWaypoints(waypoints: ArrayLike): void { + queueWaypoints(waypoints: ArrayLike, from: AllowRepath = AllowRepath.NONE): void { let index: number = -1; for (let input: number = waypoints.length - 1, output: number = 0; input >= 0 && output < this.waypoints.length; input--, output++) { this.waypoints[output] = waypoints[input]; index++; } + this.lastMoveGeneratedFrom = from; this.waypointIndex = index; } @@ -417,7 +421,7 @@ export default abstract class PathingEntity extends Entity { } else { z += Math.random() < 0.5 ? -1 : 1; } - this.queueWaypoint(x, z); + this.queueWaypoint(x, z, AllowRepath.BEFOREDEST); } pathToTarget(): void { @@ -427,14 +431,14 @@ export default abstract class PathingEntity extends Entity { if (this.moveStrategy === MoveStrategy.SMART) { if (this.target instanceof PathingEntity) { - this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length)); + this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length), AllowRepath.BEFOREDEST); } else if (this.target instanceof Loc) { const forceapproach = LocType.get(this.target.type).forceapproach; this.queueWaypoints(findPathToLoc(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length, this.target.angle, this.target.shape, forceapproach)); } else if (this.target instanceof Obj && this.x === this.target.x && this.z === this.target.z) { - this.queueWaypoint(this.target.x, this.target.z); // work around because our findpath() returns 0, 0 if coord and target coord are the same + this.queueWaypoint(this.target.x, this.target.z, AllowRepath.BEFOREDEST); // work around because our findpath() returns 0, 0 if coord and target coord are the same } else { - this.queueWaypoints(findPath(this.level, this.x, this.z, this.target.x, this.target.z)); + this.queueWaypoints(findPath(this.level, this.x, this.z, this.target.x, this.target.z), AllowRepath.BEFOREDEST); } } else if (this.moveStrategy === MoveStrategy.NAIVE) { const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); @@ -451,7 +455,7 @@ export default abstract class PathingEntity extends Entity { if (this.target instanceof PathingEntity) { this.naivePathToTarget(); } else { - this.queueWaypoint(this.target.x, this.target.z); + this.queueWaypoint(this.target.x, this.target.z, AllowRepath.ATDEST); } } else { const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); @@ -487,6 +491,9 @@ export default abstract class PathingEntity extends Entity { this.targetSubject.type = -1; } + // Allow repath + this.lastMoveGeneratedFrom = AllowRepath.BEFOREDEST; + this.focus(CoordGrid.fine(target.x, target.width), CoordGrid.fine(target.z, target.length), target instanceof NonPathingEntity && interaction === Interaction.ENGINE); if (target instanceof Player) { diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 1eecded9b..2962b92ff 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import { PlayerInfoProt, Visibility } from '@2004scape/rsbuf'; -import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; import Component from '#/cache/config/Component.js'; import FontType from '#/cache/config/FontType.js'; @@ -24,6 +24,7 @@ import { EntityTimer, PlayerTimerType } from '#/engine/entity/EntityTimer.js'; import HeroPoints from '#/engine/entity/HeroPoints.js'; import Loc from '#/engine/entity/Loc.js'; import { ModalState } from '#/engine/entity/ModalState.js'; +import { AllowRepath } from './MoveGeneratedFrom.js'; import { MoveRestrict } from '#/engine/entity/MoveRestrict.js'; import { MoveSpeed } from '#/engine/entity/MoveSpeed.js'; import { MoveStrategy } from '#/engine/entity/MoveStrategy.js'; @@ -36,7 +37,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj, naiveDestination } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, findNaivePath, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -324,7 +325,6 @@ export default class Player extends PathingEntity { allowDesign: boolean = false; afkEventReady: boolean = false; moveClickRequest: boolean = false; - lastMoveClick: number = 0; requestLogout: boolean = false; requestIdleLogout: boolean = false; @@ -1047,21 +1047,16 @@ export default class Player extends PathingEntity { return ScriptProvider.getByTrigger(this.targetOp, typeId, categoryId) ?? null; } - setLastMoveClick() { - this.lastMoveClick = World.currentTick; - } - naivePathToTarget() { if (!this.target) { return; } - let angle = 0; if (this.target instanceof Loc) { angle = this.target.angle; } - const coord = naiveDestination(this.x, this.z, this.width, this.length, this.target?.x, this.target?.z, this.target?.width, this.target?.length, angle, true); - this.queueWaypoint(coord.x, coord.z); + const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); + this.queueWaypoints(waypoints, AllowRepath.ATDEST); } pathToPathingTarget(): void { @@ -1070,7 +1065,7 @@ export default class Player extends PathingEntity { } if (this.isLastWaypoint() && (this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3)) { - this.queueWaypoint(this.target.followX, this.target.followZ); + this.queueWaypoint(this.target.followX, this.target.followZ, AllowRepath.BEFOREDEST); return; } @@ -1080,14 +1075,18 @@ export default class Player extends PathingEntity { // Different mechanics for naive and smart paths if (this.moveStrategy === MoveStrategy.NAIVE) { + // This logic is redundant with some stuff in pathToTarget and findNaivePath, + // But for maintainability it's nice to split it out... It's pretty hard to match correct mechanics const underTarget = CoordGrid.intersects(this.x, this.z, this.width, this.length, this.target.x, this.target.z, this.target.width, this.target.length); if (underTarget) { this.randomWalk(); return; } - if (this.isLastWaypoint() && this.lastMoveClick < World.currentTick) { - this.pathToTarget(); + if (this.isLastWaypoint() && this.lastMoveGeneratedFrom === AllowRepath.BEFOREDEST) { + this.naivePathToTarget(); + } else if (this.waypointIndex === -1 && this.lastMoveGeneratedFrom === AllowRepath.ATDEST) { + this.naivePathToTarget(); } } else if (this.isLastWaypoint()) { this.pathToTarget(); diff --git a/src/network/game/client/handler/MoveClickHandler.ts b/src/network/game/client/handler/MoveClickHandler.ts index c74e26071..1052ab9bc 100644 --- a/src/network/game/client/handler/MoveClickHandler.ts +++ b/src/network/game/client/handler/MoveClickHandler.ts @@ -1,6 +1,7 @@ import { CoordGrid } from '#/engine/CoordGrid.js'; import { NetworkPlayer } from '#/engine/entity/NetworkPlayer.js'; import ClientGameMessageHandler from '#/network/game/client/ClientGameMessageHandler.js'; +import { AllowRepath } from '#/engine/entity/MoveGeneratedFrom.js'; import MoveClick from '#/network/game/client/model/MoveClick.js'; import UnsetMapFlag from '#/network/game/server/model/UnsetMapFlag.js'; import Environment from '#/util/Environment.js'; @@ -36,16 +37,20 @@ export default class MoveClickHandler extends ClientGameMessageHandler Date: Sun, 22 Feb 2026 00:26:57 -0500 Subject: [PATCH 08/18] cleanup: move naivepath back to pathingentity --- src/engine/entity/Npc.ts | 16 ++-------------- src/engine/entity/PathingEntity.ts | 15 +++++++++++++-- src/engine/entity/Player.ts | 16 ++-------------- 3 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/engine/entity/Npc.ts b/src/engine/entity/Npc.ts index 696c444a9..8441959db 100644 --- a/src/engine/entity/Npc.ts +++ b/src/engine/entity/Npc.ts @@ -1,6 +1,6 @@ import { NpcInfoProt } from '@2004scape/rsbuf'; import * as rsbuf from '@2004scape/rsbuf'; -import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; import HuntType from '#/cache/config/HuntType.js'; import NpcType from '#/cache/config/NpcType.js'; @@ -26,7 +26,7 @@ import { NpcQueueRequest } from '#/engine/entity/NpcQueueRequest.js'; import { NpcStat } from '#/engine/entity/NpcStat.js'; import PathingEntity from '#/engine/entity/PathingEntity.js'; import Player from '#/engine/entity/Player.js'; -import { findNaivePath, isFlagged } from '#/engine/GameMap.js'; +import { isFlagged } from '#/engine/GameMap.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import { HuntIterator } from '#/engine/script/ScriptIterators.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -37,7 +37,6 @@ import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; import LinkList from '#/datastruct/LinkList.js'; import { printError } from '#/util/Logger.js'; -import { AllowRepath } from './MoveGeneratedFrom.js'; export default class Npc extends PathingEntity { // constructor properties @@ -315,17 +314,6 @@ export default class Npc extends PathingEntity { super.resetPathingEntity(); } } - naivePathToTarget() { - if (!this.target) { - return; - } - let angle = 0; - if (this.target instanceof Loc) { - angle = this.target.angle; - } - const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); - this.queueWaypoints(waypoints, AllowRepath.BEFOREDEST); - } pathToTarget(): void { if (!this.target) { diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index 6e4a00670..bde2be005 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -16,7 +16,7 @@ import Npc from '#/engine/entity/Npc.js'; import { NpcMode } from '#/engine/entity/NpcMode.js'; import Obj from '#/engine/entity/Obj.js'; import Player from '#/engine/entity/Player.js'; -import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj, findNaivePath } from '#/engine/GameMap.js'; import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; @@ -123,7 +123,6 @@ export default abstract class PathingEntity extends Entity { abstract updateMovement(): boolean; abstract blockWalkFlag(): CollisionFlag; abstract defaultMoveSpeed(): MoveSpeed; - abstract naivePathToTarget(): void; /** * Process movement function for a PathingEntity to use. @@ -424,6 +423,18 @@ export default abstract class PathingEntity extends Entity { this.queueWaypoint(x, z, AllowRepath.BEFOREDEST); } + naivePathToTarget() { + if (!this.target) { + return; + } + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); + this.queueWaypoints(waypoints, AllowRepath.ATDEST); + } + pathToTarget(): void { if (!this.target) { return; diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 2962b92ff..d80ce0d1f 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import { PlayerInfoProt, Visibility } from '@2004scape/rsbuf'; -import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; import Component from '#/cache/config/Component.js'; import FontType from '#/cache/config/FontType.js'; @@ -37,7 +37,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, findNaivePath, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -1047,18 +1047,6 @@ export default class Player extends PathingEntity { return ScriptProvider.getByTrigger(this.targetOp, typeId, categoryId) ?? null; } - naivePathToTarget() { - if (!this.target) { - return; - } - let angle = 0; - if (this.target instanceof Loc) { - angle = this.target.angle; - } - const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); - this.queueWaypoints(waypoints, AllowRepath.ATDEST); - } - pathToPathingTarget(): void { if (!(this.target instanceof PathingEntity)) { return; From a3eca7c558547c033d39f8e6c425ce139fc339e6 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 22 Feb 2026 00:30:07 -0500 Subject: [PATCH 09/18] cleanup: change var name --- src/engine/entity/PathingEntity.ts | 8 ++++---- src/engine/entity/Player.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index bde2be005..d8957dbdd 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -54,7 +54,7 @@ export default abstract class PathingEntity extends Entity { lastInt: number = -1; // resume_p_countdialog, ai_queue lastCrawl: boolean = false; lastMovement: number = 0; - lastMoveGeneratedFrom: AllowRepath = AllowRepath.NONE; + allowRepath: AllowRepath = AllowRepath.NONE; walktrigger: number = -1; walktriggerArg: number = 0; // used for npcs @@ -244,7 +244,7 @@ export default abstract class PathingEntity extends Entity { queueWaypoint(x: number, z: number, from: AllowRepath = AllowRepath.NONE): void { this.waypoints[0] = CoordGrid.packCoord(0, x, z); // level doesn't matter here this.waypointIndex = 0; - this.lastMoveGeneratedFrom = from; + this.allowRepath = from; } /** @@ -257,7 +257,7 @@ export default abstract class PathingEntity extends Entity { this.waypoints[output] = waypoints[input]; index++; } - this.lastMoveGeneratedFrom = from; + this.allowRepath = from; this.waypointIndex = index; } @@ -503,7 +503,7 @@ export default abstract class PathingEntity extends Entity { } // Allow repath - this.lastMoveGeneratedFrom = AllowRepath.BEFOREDEST; + this.allowRepath = AllowRepath.BEFOREDEST; this.focus(CoordGrid.fine(target.x, target.width), CoordGrid.fine(target.z, target.length), target instanceof NonPathingEntity && interaction === Interaction.ENGINE); diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index d80ce0d1f..98fee1b40 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1071,9 +1071,9 @@ export default class Player extends PathingEntity { return; } - if (this.isLastWaypoint() && this.lastMoveGeneratedFrom === AllowRepath.BEFOREDEST) { + if (this.isLastWaypoint() && this.allowRepath === AllowRepath.BEFOREDEST) { this.naivePathToTarget(); - } else if (this.waypointIndex === -1 && this.lastMoveGeneratedFrom === AllowRepath.ATDEST) { + } else if (this.waypointIndex === -1 && this.allowRepath === AllowRepath.ATDEST) { this.naivePathToTarget(); } } else if (this.isLastWaypoint()) { From e7a7f6404234a4e780fdd0928bd98cea7a961d8a Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 22 Feb 2026 15:42:57 -0500 Subject: [PATCH 10/18] fix: fix pathing_final_final_2 --- src/engine/entity/AllowRepath.ts | 5 ++++ src/engine/entity/MoveGeneratedFrom.ts | 7 ++--- src/engine/entity/PathingEntity.ts | 28 +++++++++++-------- src/engine/entity/Player.ts | 21 ++++++++++++-- .../game/client/handler/MoveClickHandler.ts | 12 ++++---- 5 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 src/engine/entity/AllowRepath.ts diff --git a/src/engine/entity/AllowRepath.ts b/src/engine/entity/AllowRepath.ts new file mode 100644 index 000000000..a9d461ea6 --- /dev/null +++ b/src/engine/entity/AllowRepath.ts @@ -0,0 +1,5 @@ +export const enum AllowRepath { + BEFOREDEST, + ATDEST, + NONE +} diff --git a/src/engine/entity/MoveGeneratedFrom.ts b/src/engine/entity/MoveGeneratedFrom.ts index a9d461ea6..d8959e4ed 100644 --- a/src/engine/entity/MoveGeneratedFrom.ts +++ b/src/engine/entity/MoveGeneratedFrom.ts @@ -1,5 +1,4 @@ -export const enum AllowRepath { - BEFOREDEST, - ATDEST, - NONE +export const enum MoveGeneratedFrom { + CLIENT, + SERVER } diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index d8957dbdd..f761caaf1 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -7,7 +7,7 @@ import Entity from '#/engine/entity/Entity.js'; import { EntityLifeCycle } from '#/engine/entity/EntityLifeCycle.js'; import { Interaction } from '#/engine/entity/Interaction.js'; import Loc from '#/engine/entity/Loc.js'; -import { AllowRepath } from './MoveGeneratedFrom.js'; +import { AllowRepath } from './AllowRepath.js'; import { MoveRestrict } from '#/engine/entity/MoveRestrict.js'; import { MoveSpeed } from '#/engine/entity/MoveSpeed.js'; import { MoveStrategy } from '#/engine/entity/MoveStrategy.js'; @@ -54,7 +54,7 @@ export default abstract class PathingEntity extends Entity { lastInt: number = -1; // resume_p_countdialog, ai_queue lastCrawl: boolean = false; lastMovement: number = 0; - allowRepath: AllowRepath = AllowRepath.NONE; + allowRepath: AllowRepath = AllowRepath.BEFOREDEST; walktrigger: number = -1; walktriggerArg: number = 0; // used for npcs @@ -241,24 +241,28 @@ export default abstract class PathingEntity extends Entity { * @param x The x position of the step. * @param z The z position of the step. */ - queueWaypoint(x: number, z: number, from: AllowRepath = AllowRepath.NONE): void { + queueWaypoint(x: number, z: number): void { this.waypoints[0] = CoordGrid.packCoord(0, x, z); // level doesn't matter here this.waypointIndex = 0; - this.allowRepath = from; + this.setAllowRepath(AllowRepath.BEFOREDEST); } /** * Queue waypoints to this PathingEntity. * @param waypoints The waypoints to queue. */ - queueWaypoints(waypoints: ArrayLike, from: AllowRepath = AllowRepath.NONE): void { + queueWaypoints(waypoints: ArrayLike): void { let index: number = -1; for (let input: number = waypoints.length - 1, output: number = 0; input >= 0 && output < this.waypoints.length; input--, output++) { this.waypoints[output] = waypoints[input]; index++; } - this.allowRepath = from; this.waypointIndex = index; + this.setAllowRepath(AllowRepath.BEFOREDEST); + } + + setAllowRepath(value: AllowRepath) { + this.allowRepath = value; } clearWaypoints(): void { @@ -420,7 +424,7 @@ export default abstract class PathingEntity extends Entity { } else { z += Math.random() < 0.5 ? -1 : 1; } - this.queueWaypoint(x, z, AllowRepath.BEFOREDEST); + this.queueWaypoint(x, z); } naivePathToTarget() { @@ -432,7 +436,7 @@ export default abstract class PathingEntity extends Entity { angle = this.target.angle; } const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); - this.queueWaypoints(waypoints, AllowRepath.ATDEST); + this.queueWaypoints(waypoints); } pathToTarget(): void { @@ -442,14 +446,14 @@ export default abstract class PathingEntity extends Entity { if (this.moveStrategy === MoveStrategy.SMART) { if (this.target instanceof PathingEntity) { - this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length), AllowRepath.BEFOREDEST); + this.queueWaypoints(findPathToEntity(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length)); } else if (this.target instanceof Loc) { const forceapproach = LocType.get(this.target.type).forceapproach; this.queueWaypoints(findPathToLoc(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.target.width, this.target.length, this.target.angle, this.target.shape, forceapproach)); } else if (this.target instanceof Obj && this.x === this.target.x && this.z === this.target.z) { - this.queueWaypoint(this.target.x, this.target.z, AllowRepath.BEFOREDEST); // work around because our findpath() returns 0, 0 if coord and target coord are the same + this.queueWaypoint(this.target.x, this.target.z); // work around because our findpath() returns 0, 0 if coord and target coord are the same } else { - this.queueWaypoints(findPath(this.level, this.x, this.z, this.target.x, this.target.z), AllowRepath.BEFOREDEST); + this.queueWaypoints(findPath(this.level, this.x, this.z, this.target.x, this.target.z)); } } else if (this.moveStrategy === MoveStrategy.NAIVE) { const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); @@ -466,7 +470,7 @@ export default abstract class PathingEntity extends Entity { if (this.target instanceof PathingEntity) { this.naivePathToTarget(); } else { - this.queueWaypoint(this.target.x, this.target.z, AllowRepath.ATDEST); + this.queueWaypoint(this.target.x, this.target.z); } } else { const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 98fee1b40..3f43416a6 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -24,7 +24,8 @@ import { EntityTimer, PlayerTimerType } from '#/engine/entity/EntityTimer.js'; import HeroPoints from '#/engine/entity/HeroPoints.js'; import Loc from '#/engine/entity/Loc.js'; import { ModalState } from '#/engine/entity/ModalState.js'; -import { AllowRepath } from './MoveGeneratedFrom.js'; +import { AllowRepath } from './AllowRepath.js'; +import { MoveGeneratedFrom } from './MoveGeneratedFrom.js'; import { MoveRestrict } from '#/engine/entity/MoveRestrict.js'; import { MoveSpeed } from '#/engine/entity/MoveSpeed.js'; import { MoveStrategy } from '#/engine/entity/MoveStrategy.js'; @@ -325,6 +326,7 @@ export default class Player extends PathingEntity { allowDesign: boolean = false; afkEventReady: boolean = false; moveClickRequest: boolean = false; + moveGeneratedFrom: MoveGeneratedFrom = MoveGeneratedFrom.SERVER; requestLogout: boolean = false; requestIdleLogout: boolean = false; @@ -1047,13 +1049,23 @@ export default class Player extends PathingEntity { return ScriptProvider.getByTrigger(this.targetOp, typeId, categoryId) ?? null; } + queueWaypoint(x: number, z: number): void { + super.queueWaypoint(x, z); + this.moveGeneratedFrom = MoveGeneratedFrom.SERVER; + } + + queueWaypoints(waypoints: ArrayLike): void { + super.queueWaypoints(waypoints); + this.moveGeneratedFrom = MoveGeneratedFrom.SERVER; + } + pathToPathingTarget(): void { if (!(this.target instanceof PathingEntity)) { return; } if (this.isLastWaypoint() && (this.targetOp === ServerTriggerType.APPLAYER3 || this.targetOp === ServerTriggerType.OPPLAYER3)) { - this.queueWaypoint(this.target.followX, this.target.followZ, AllowRepath.BEFOREDEST); + this.queueWaypoint(this.target.followX, this.target.followZ); return; } @@ -1073,6 +1085,11 @@ export default class Player extends PathingEntity { if (this.isLastWaypoint() && this.allowRepath === AllowRepath.BEFOREDEST) { this.naivePathToTarget(); + + // If path is generated from client, force player to reach their next dest before repathing + if (this.moveGeneratedFrom === MoveGeneratedFrom.CLIENT) { + this.setAllowRepath(AllowRepath.ATDEST); + } } else if (this.waypointIndex === -1 && this.allowRepath === AllowRepath.ATDEST) { this.naivePathToTarget(); } diff --git a/src/network/game/client/handler/MoveClickHandler.ts b/src/network/game/client/handler/MoveClickHandler.ts index 1052ab9bc..c2722c2cc 100644 --- a/src/network/game/client/handler/MoveClickHandler.ts +++ b/src/network/game/client/handler/MoveClickHandler.ts @@ -1,8 +1,9 @@ import { CoordGrid } from '#/engine/CoordGrid.js'; import { NetworkPlayer } from '#/engine/entity/NetworkPlayer.js'; import ClientGameMessageHandler from '#/network/game/client/ClientGameMessageHandler.js'; -import { AllowRepath } from '#/engine/entity/MoveGeneratedFrom.js'; +import { AllowRepath } from '#/engine/entity/AllowRepath.js'; import MoveClick from '#/network/game/client/model/MoveClick.js'; +import { MoveGeneratedFrom } from '#/engine/entity/MoveGeneratedFrom.js'; import UnsetMapFlag from '#/network/game/server/model/UnsetMapFlag.js'; import Environment from '#/util/Environment.js'; @@ -39,18 +40,19 @@ export default class MoveClickHandler extends ClientGameMessageHandler Date: Sun, 22 Feb 2026 16:28:14 -0500 Subject: [PATCH 11/18] fix: finish player pathing --- src/engine/entity/Player.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 3f43416a6..876a479e1 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1084,11 +1084,12 @@ export default class Player extends PathingEntity { } if (this.isLastWaypoint() && this.allowRepath === AllowRepath.BEFOREDEST) { - this.naivePathToTarget(); - // If path is generated from client, force player to reach their next dest before repathing if (this.moveGeneratedFrom === MoveGeneratedFrom.CLIENT) { + this.naivePathToTarget(); this.setAllowRepath(AllowRepath.ATDEST); + } else { + this.naivePathToTarget(); } } else if (this.waypointIndex === -1 && this.allowRepath === AllowRepath.ATDEST) { this.naivePathToTarget(); From 54faf7b1cc41dfa69572e164b58994e436764e3e Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 22 Feb 2026 16:51:45 -0500 Subject: [PATCH 12/18] fix: path fix again --- src/engine/entity/PathingEntity.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index f761caaf1..075300064 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -262,6 +262,9 @@ export default abstract class PathingEntity extends Entity { } setAllowRepath(value: AllowRepath) { + if (this instanceof Player) { + console.log(`setting allow repath to ${value}`, World.currentTick); + } this.allowRepath = value; } @@ -506,8 +509,10 @@ export default abstract class PathingEntity extends Entity { this.targetSubject.type = -1; } - // Allow repath - this.allowRepath = AllowRepath.BEFOREDEST; + if (interaction === Interaction.SCRIPT) { + // Allow repath + this.allowRepath = AllowRepath.BEFOREDEST; + } this.focus(CoordGrid.fine(target.x, target.width), CoordGrid.fine(target.z, target.length), target instanceof NonPathingEntity && interaction === Interaction.ENGINE); From 86b4794e90e9315f1c0953a3a1430242d96c7003 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 22 Feb 2026 17:22:53 -0500 Subject: [PATCH 13/18] fix: aprange match case + remove console.log --- src/engine/entity/PathingEntity.ts | 3 --- src/engine/entity/Player.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index 075300064..da3200d61 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -262,9 +262,6 @@ export default abstract class PathingEntity extends Entity { } setAllowRepath(value: AllowRepath) { - if (this instanceof Player) { - console.log(`setting allow repath to ${value}`, World.currentTick); - } this.allowRepath = value; } diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 876a479e1..0d5481004 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1288,7 +1288,7 @@ export default class Player extends PathingEntity { interacted = this.tryInteract(this.stepsTaken === 0); // If Player did not interact, has no path, and did not move this cycle, terminate the interaction - if (!interacted && !this.hasWaypoints() && this.stepsTaken === 0) { + if (!interacted && !this.apRangeCalled && !this.hasWaypoints() && this.stepsTaken === 0) { this.messageGame("I can't reach that!"); this.clearInteraction(); } From 9e30037156c02fb198c4d17ce904bed671e5a6f8 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sat, 14 Mar 2026 21:34:58 -0400 Subject: [PATCH 14/18] some reversion to earlier --- src/engine/GameMap.ts | 71 +++++++++++++++++++++++++- src/engine/entity/AllowRepath.ts | 1 - src/engine/entity/MoveGeneratedFrom.ts | 4 -- src/engine/entity/PathingEntity.ts | 6 +-- src/engine/entity/Player.ts | 26 +++++----- 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/src/engine/GameMap.ts b/src/engine/GameMap.ts index fe1f8d3b0..2637b5a00 100644 --- a/src/engine/GameMap.ts +++ b/src/engine/GameMap.ts @@ -342,10 +342,77 @@ export function changeLocCollision(shape: number, angle: number, blockrange: boo } } -export function findNaivePath(level: number, srcX: number, srcZ: number, destX: number, destZ: number, srcWidth: number, srcHeight: number, destWidth: number, destHeight: number, extraFlag: number, collision: CollisionType): Uint32Array { - return rsmod.findNaivePath(level, srcX, srcZ, destX, destZ, srcWidth, srcHeight, destWidth, destHeight, extraFlag, collision); +function translate(coord: RouteCoordinates, dx: number, dz: number): RouteCoordinates { + return { x: coord.x + dx, z: coord.z + dz }; } +// Keep these as your existing implementations. +// The Kotlin rotate(...) as used here appears to mean "pick width vs length based on angle". +// If you have a different rotate behavior, swap it in. +export function rotate(angle: number, dimensionA: number, dimensionB: number): number { + return (angle & 0x1) !== 0 ? dimensionB : dimensionA; +} +export function naiveDestination( + sourceX: number, + sourceZ: number, + sourceWidth: number, + sourceLength: number, + targetX: number, + targetZ: number, + targetWidth: number, + targetLength: number, + targetAngle: number = 0, + reversePriority: boolean = false // NEW +): RouteCoordinates { + const diagonal = sourceX - targetX + (sourceZ - targetZ); + const anti = sourceX - targetX - (sourceZ - targetZ); + + const rotatedWidth = rotate(targetAngle, targetWidth, targetLength); + const rotatedLength = rotate(targetAngle, targetLength, targetWidth); + + const nwBoundary = rotatedLength - 1 - (sourceWidth - 1); + const seBoundary = rotatedWidth - 1 - (sourceLength - 1); + const neBoundary = sourceWidth - sourceLength; + + // Default behavior (reversePriority=false): N/S wins ties. + // Reverse behavior (reversePriority=true): E/W wins ties by flipping equality ownership. + const southWestClockwise = reversePriority ? anti <= 0 : anti < 0; + const northWestClockwise = reversePriority ? diagonal > nwBoundary : diagonal >= nwBoundary; + const northEastClockwise = reversePriority ? anti >= neBoundary : anti > neBoundary; + const southEastClockwise = reversePriority ? diagonal < seBoundary : diagonal <= seBoundary; + + const target: RouteCoordinates = { x: targetX, z: targetZ }; + + if (southWestClockwise && !northWestClockwise) { + // West + if (reversePriority && anti === 0) return translate(target, -sourceWidth, 0); // FIX + + const offZ = diagonal >= -sourceWidth ? Math.min(diagonal + sourceWidth, rotatedLength - 1) : anti > -sourceWidth ? -(sourceWidth + anti) : 0; + + return translate(target, -sourceWidth, offZ); + } else if (northWestClockwise && !northEastClockwise) { + // North + const offX = anti >= -rotatedLength ? Math.min(anti + rotatedLength, rotatedWidth - 1) : diagonal < rotatedLength ? Math.max(diagonal - rotatedLength, -(sourceWidth - 1)) : 0; + + return translate(target, offX, rotatedLength); + } else if (northEastClockwise && !southEastClockwise) { + // East + if (reversePriority && anti === 0) return translate(target, rotatedWidth, 0); // FIX + + const offZ = anti <= rotatedWidth ? rotatedLength - anti : diagonal < rotatedWidth ? Math.max(diagonal - rotatedWidth, -(sourceLength - 1)) : 0; + + return translate(target, rotatedWidth, offZ); + } else { + if (!(southEastClockwise && !southWestClockwise)) { + throw new Error('Invariant failed: expected southEastClockwise && !southWestClockwise'); + } + + // South + const offX = diagonal > -sourceLength ? Math.min(diagonal + sourceLength, rotatedWidth - 1) : anti < sourceLength ? Math.max(anti - sourceLength, -(sourceLength - 1)) : 0; + + return translate(target, offX, -sourceLength); + } +} /** * Change collision at a specified Position for npcs. * @param size The size square of this npc. (1x1, 2x2, etc). diff --git a/src/engine/entity/AllowRepath.ts b/src/engine/entity/AllowRepath.ts index a9d461ea6..b7d3061be 100644 --- a/src/engine/entity/AllowRepath.ts +++ b/src/engine/entity/AllowRepath.ts @@ -1,5 +1,4 @@ export const enum AllowRepath { BEFOREDEST, - ATDEST, NONE } diff --git a/src/engine/entity/MoveGeneratedFrom.ts b/src/engine/entity/MoveGeneratedFrom.ts index d8959e4ed..e69de29bb 100644 --- a/src/engine/entity/MoveGeneratedFrom.ts +++ b/src/engine/entity/MoveGeneratedFrom.ts @@ -1,4 +0,0 @@ -export const enum MoveGeneratedFrom { - CLIENT, - SERVER -} diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index da3200d61..b11cf6b59 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -16,7 +16,7 @@ import Npc from '#/engine/entity/Npc.js'; import { NpcMode } from '#/engine/entity/NpcMode.js'; import Obj from '#/engine/entity/Obj.js'; import Player from '#/engine/entity/Player.js'; -import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj, findNaivePath } from '#/engine/GameMap.js'; +import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj, naiveDestination } from '#/engine/GameMap.js'; import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; @@ -435,8 +435,8 @@ export default abstract class PathingEntity extends Entity { if (this.target instanceof Loc) { angle = this.target.angle; } - const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); - this.queueWaypoints(waypoints); + const waypoint = naiveDestination(this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle); + this.queueWaypoint(waypoint.x, waypoint.z); } pathToTarget(): void { diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 0d5481004..ffc1a40e7 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -25,7 +25,6 @@ import HeroPoints from '#/engine/entity/HeroPoints.js'; import Loc from '#/engine/entity/Loc.js'; import { ModalState } from '#/engine/entity/ModalState.js'; import { AllowRepath } from './AllowRepath.js'; -import { MoveGeneratedFrom } from './MoveGeneratedFrom.js'; import { MoveRestrict } from '#/engine/entity/MoveRestrict.js'; import { MoveSpeed } from '#/engine/entity/MoveSpeed.js'; import { MoveStrategy } from '#/engine/entity/MoveStrategy.js'; @@ -38,7 +37,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, naiveDestination, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -326,7 +325,6 @@ export default class Player extends PathingEntity { allowDesign: boolean = false; afkEventReady: boolean = false; moveClickRequest: boolean = false; - moveGeneratedFrom: MoveGeneratedFrom = MoveGeneratedFrom.SERVER; requestLogout: boolean = false; requestIdleLogout: boolean = false; @@ -1051,12 +1049,22 @@ export default class Player extends PathingEntity { queueWaypoint(x: number, z: number): void { super.queueWaypoint(x, z); - this.moveGeneratedFrom = MoveGeneratedFrom.SERVER; } queueWaypoints(waypoints: ArrayLike): void { super.queueWaypoints(waypoints); - this.moveGeneratedFrom = MoveGeneratedFrom.SERVER; + } + + naivePathToTarget() { + if (!this.target) { + return; + } + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + const waypoint = naiveDestination(this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, true); + this.queueWaypoint(waypoint.x, waypoint.z); } pathToPathingTarget(): void { @@ -1084,14 +1092,6 @@ export default class Player extends PathingEntity { } if (this.isLastWaypoint() && this.allowRepath === AllowRepath.BEFOREDEST) { - // If path is generated from client, force player to reach their next dest before repathing - if (this.moveGeneratedFrom === MoveGeneratedFrom.CLIENT) { - this.naivePathToTarget(); - this.setAllowRepath(AllowRepath.ATDEST); - } else { - this.naivePathToTarget(); - } - } else if (this.waypointIndex === -1 && this.allowRepath === AllowRepath.ATDEST) { this.naivePathToTarget(); } } else if (this.isLastWaypoint()) { From a0118206b8e950033056761e8aaacae8ae9747c7 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 15 Mar 2026 17:50:23 -0400 Subject: [PATCH 15/18] more pathing --- src/engine/GameMap.ts | 72 +-------- src/engine/entity/MoveGeneratedFrom.ts | 0 src/engine/entity/PathingEntity.ts | 144 +++++++++--------- src/engine/entity/Player.ts | 27 +--- .../game/client/handler/MoveClickHandler.ts | 2 - 5 files changed, 73 insertions(+), 172 deletions(-) delete mode 100644 src/engine/entity/MoveGeneratedFrom.ts diff --git a/src/engine/GameMap.ts b/src/engine/GameMap.ts index 7cdc89336..ef754dc32 100644 --- a/src/engine/GameMap.ts +++ b/src/engine/GameMap.ts @@ -341,78 +341,10 @@ export function changeLocCollision(shape: number, angle: number, blockrange: boo } } } - -function translate(coord: RouteCoordinates, dx: number, dz: number): RouteCoordinates { - return { x: coord.x + dx, z: coord.z + dz }; -} - -// Keep these as your existing implementations. -// The Kotlin rotate(...) as used here appears to mean "pick width vs length based on angle". -// If you have a different rotate behavior, swap it in. -export function rotate(angle: number, dimensionA: number, dimensionB: number): number { - return (angle & 0x1) !== 0 ? dimensionB : dimensionA; +export function findNaivePath(level: number, srcX: number, srcZ: number, destX: number, destZ: number, srcWidth: number, srcHeight: number, destWidth: number, destHeight: number, extraFlag: number, collision: CollisionType): Uint32Array { + return rsmod.findNaivePath(level, srcX, srcZ, destX, destZ, srcWidth, srcHeight, destWidth, destHeight, extraFlag, collision); } -export function naiveDestination( - sourceX: number, - sourceZ: number, - sourceWidth: number, - sourceLength: number, - targetX: number, - targetZ: number, - targetWidth: number, - targetLength: number, - targetAngle: number = 0, - reversePriority: boolean = false // NEW -): RouteCoordinates { - const diagonal = sourceX - targetX + (sourceZ - targetZ); - const anti = sourceX - targetX - (sourceZ - targetZ); - - const rotatedWidth = rotate(targetAngle, targetWidth, targetLength); - const rotatedLength = rotate(targetAngle, targetLength, targetWidth); - - const nwBoundary = rotatedLength - 1 - (sourceWidth - 1); - const seBoundary = rotatedWidth - 1 - (sourceLength - 1); - const neBoundary = sourceWidth - sourceLength; - - // Default behavior (reversePriority=false): N/S wins ties. - // Reverse behavior (reversePriority=true): E/W wins ties by flipping equality ownership. - const southWestClockwise = reversePriority ? anti <= 0 : anti < 0; - const northWestClockwise = reversePriority ? diagonal > nwBoundary : diagonal >= nwBoundary; - const northEastClockwise = reversePriority ? anti >= neBoundary : anti > neBoundary; - const southEastClockwise = reversePriority ? diagonal < seBoundary : diagonal <= seBoundary; - - const target: RouteCoordinates = { x: targetX, z: targetZ }; - - if (southWestClockwise && !northWestClockwise) { - // West - if (reversePriority && anti === 0) return translate(target, -sourceWidth, 0); // FIX - - const offZ = diagonal >= -sourceWidth ? Math.min(diagonal + sourceWidth, rotatedLength - 1) : anti > -sourceWidth ? -(sourceWidth + anti) : 0; - - return translate(target, -sourceWidth, offZ); - } else if (northWestClockwise && !northEastClockwise) { - // North - const offX = anti >= -rotatedLength ? Math.min(anti + rotatedLength, rotatedWidth - 1) : diagonal < rotatedLength ? Math.max(diagonal - rotatedLength, -(sourceWidth - 1)) : 0; - - return translate(target, offX, rotatedLength); - } else if (northEastClockwise && !southEastClockwise) { - // East - if (reversePriority && anti === 0) return translate(target, rotatedWidth, 0); // FIX - - const offZ = anti <= rotatedWidth ? rotatedLength - anti : diagonal < rotatedWidth ? Math.max(diagonal - rotatedWidth, -(sourceLength - 1)) : 0; - - return translate(target, rotatedWidth, offZ); - } else { - if (!(southEastClockwise && !southWestClockwise)) { - throw new Error('Invariant failed: expected southEastClockwise && !southWestClockwise'); - } - // South - const offX = diagonal > -sourceLength ? Math.min(diagonal + sourceLength, rotatedWidth - 1) : anti < sourceLength ? Math.max(anti - sourceLength, -(sourceLength - 1)) : 0; - - return translate(target, offX, -sourceLength); - } -} /** * Change collision at a specified Position for npcs. * @param size The size square of this npc. (1x1, 2x2, etc). diff --git a/src/engine/entity/MoveGeneratedFrom.ts b/src/engine/entity/MoveGeneratedFrom.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index f65791c44..b4ec2a479 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -16,7 +16,7 @@ import Npc from '#/engine/entity/Npc.js'; import { NpcMode } from '#/engine/entity/NpcMode.js'; import Obj from '#/engine/entity/Obj.js'; import Player from '#/engine/entity/Player.js'; -import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj, naiveDestination } from '#/engine/GameMap.js'; +import { canTravel, changeNpcCollision, changePlayerCollision, findPath, findPathToEntity, findPathToLoc, isApproached, isZoneAllocated, reachedEntity, reachedLoc, reachedObj, findNaivePath } from '#/engine/GameMap.js'; import ServerTriggerType from '#/engine/script/ServerTriggerType.js'; import World from '#/engine/World.js'; @@ -162,25 +162,22 @@ export default abstract class PathingEntity extends Entity { * @param previousLevel Their previous recorded level position before movement. This one is important for teleport. */ private refreshZonePresence(previousX: number, previousZ: number, previousLevel: number): void { - // only update collision map when the entity moves. - if (this.x != previousX || this.z !== previousZ || this.level !== previousLevel) { - // update collision map - // players and npcs both can change this collision - switch (this.blockWalk) { - case BlockWalk.NPC: - changeNpcCollision(this.width, previousX, previousZ, previousLevel, false); - changeNpcCollision(this.width, this.x, this.z, this.level, true); - break; - case BlockWalk.ALL: - changeNpcCollision(this.width, previousX, previousZ, previousLevel, false); - changeNpcCollision(this.width, this.x, this.z, this.level, true); - changePlayerCollision(this.width, previousX, previousZ, previousLevel, false); - changePlayerCollision(this.width, this.x, this.z, this.level, true); - break; - } - this.lastStepX = previousX; - this.lastStepZ = previousZ; - } + // update collision map + // players and npcs both can change this collision + switch (this.blockWalk) { + case BlockWalk.NPC: + changeNpcCollision(this.width, previousX, previousZ, previousLevel, false); + changeNpcCollision(this.width, this.x, this.z, this.level, true); + break; + case BlockWalk.ALL: + changeNpcCollision(this.width, previousX, previousZ, previousLevel, false); + changeNpcCollision(this.width, this.x, this.z, this.level, true); + changePlayerCollision(this.width, previousX, previousZ, previousLevel, false); + changePlayerCollision(this.width, this.x, this.z, this.level, true); + break; + } + this.lastStepX = previousX; + this.lastStepZ = previousZ; if (CoordGrid.zone(previousX) !== CoordGrid.zone(this.x) || CoordGrid.zone(previousZ) !== CoordGrid.zone(this.z) || previousLevel != this.level) { World.gameMap.getZone(previousX, previousZ, previousLevel).leave(this); @@ -204,28 +201,33 @@ export default abstract class PathingEntity extends Entity { * Returns the final validated step direction. */ private validateAndAdvanceStep(): number { - const dir: number | null = this.takeStep(); - if (dir === null) { - return -1; + const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); + const extraFlag: CollisionFlag = this.blockWalkFlag(); + + // Clear waypoints if no movement is allowed + if (collisionStrategy === null || extraFlag === CollisionFlag.NULL) { + this.waypointIndex = -1; } - if (dir === -1) { - this.waypointIndex--; - if (this.waypointIndex != -1) { - return this.validateAndAdvanceStep(); - } + // If no waypoints, return + if (this.waypointIndex === -1) { return -1; } - const previousX: number = this.x; - const previousZ: number = this.z; - this.x = CoordGrid.moveX(this.x, dir); - this.z = CoordGrid.moveZ(this.z, dir); - const moveX: number = CoordGrid.moveX(this.x, dir); - const moveZ: number = CoordGrid.moveZ(this.z, dir); - this.focus(CoordGrid.fine(moveX, this.width), CoordGrid.fine(moveZ, this.length), false); - this.stepsTaken++; - this.refreshZonePresence(previousX, previousZ, this.level); + // If we have a valid collision strategy and extra flag, attempt to take step. + const delta: [number, number] = this.takeStep(); + + const srcX = this.x; + const srcZ = this.z; + + // Move entity + this.x = this.x + delta[0]; + this.z = this.z + delta[1]; + + // Refresh zone presence if we had a waypoint, even if we didn't move + this.refreshZonePresence(srcX, srcZ, this.level); + + // Update waypoint index if we reached the current waypoint if (this.waypointIndex !== -1) { const coord: CoordGrid = CoordGrid.unpackCoord(this.waypoints[this.waypointIndex]); if (coord.x === this.x && coord.z === this.z) { @@ -233,7 +235,17 @@ export default abstract class PathingEntity extends Entity { } } - return dir; + // If we actually moved, update orientation and steps taken. + if (this.x !== srcX || this.z !== srcZ) { + // Focus the tile in front + const focusX: number = this.x + delta[0]; + const focusZ: number = this.z + delta[1]; + this.focus(CoordGrid.fine(focusX, this.width), CoordGrid.fine(focusZ, this.length), false); + this.stepsTaken++; + return CoordGrid.face(srcX, srcZ, this.x, this.z); + } + + return -1; } /** @@ -435,8 +447,8 @@ export default abstract class PathingEntity extends Entity { if (this.target instanceof Loc) { angle = this.target.angle; } - const waypoint = naiveDestination(this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle); - this.queueWaypoint(waypoint.x, waypoint.z); + const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); + this.queueWaypoints(waypoints); } pathToTarget(): void { @@ -608,24 +620,20 @@ export default abstract class PathingEntity extends Entity { this.setFaceEntity(); } - private takeStep(): number | null { + private takeStep(): [number, number] { // dir -1 means we reached the destination. // dir null means nothing happened if (this.waypointIndex === -1) { // failsafe check - return null; + return [0, 0]; } const collisionStrategy: CollisionType | null = this.getCollisionStrategy(); - if (collisionStrategy === null) { - // nomove moverestrict returns as null = no walking allowed. - return -1; - } - const extraFlag: CollisionFlag = this.blockWalkFlag(); - if (extraFlag === CollisionFlag.NULL) { - // nomove moverestrict returns as null = no walking allowed. - return -1; + + if (collisionStrategy === null) { + // failsafe check + return [0, 0]; } const srcX: number = this.x; @@ -633,46 +641,30 @@ export default abstract class PathingEntity extends Entity { const { x, z } = CoordGrid.unpackCoord(this.waypoints[this.waypointIndex]); - if (this.width > 1) { - const tryDirX = CoordGrid.face(srcX, 0, x, 0); - if (canTravel(this.level, srcX, srcZ, CoordGrid.deltaX(tryDirX), 0, this.width, extraFlag, collisionStrategy)) { - return tryDirX; - } - const tryDirZ = CoordGrid.face(0, srcZ, 0, z); - if (canTravel(this.level, srcX, srcZ, 0, CoordGrid.deltaZ(tryDirZ), this.width, extraFlag, collisionStrategy)) { - return tryDirZ; - } - return null; - } - const dir: number = CoordGrid.face(srcX, srcZ, x, z); const dx: number = CoordGrid.deltaX(dir); const dz: number = CoordGrid.deltaZ(dir); - // check if moved off current pos. - if (dx == 0 && dz == 0) { - return -1; - } - + // Noclip stuff, I guess. God mode if (this.moveStrategy === MoveStrategy.FLY) { - return dir; + return [dx, dz]; } - // check current direction if can travel to chosen dest. - if (canTravel(this.level, this.x, this.z, dx, dz, this.width, extraFlag, collisionStrategy)) { - return dir; + // Move diagonal + if (this.width === 1 && canTravel(this.level, this.x, this.z, dx, dz, this.width, extraFlag, collisionStrategy)) { + return [dx, dz]; } - // check another direction if can travel to chosen dest on current z-axis. + // Move E/W if (dx != 0 && canTravel(this.level, this.x, this.z, dx, 0, this.width, extraFlag, collisionStrategy)) { - return CoordGrid.face(srcX, srcZ, x, srcZ); + return [dx, 0]; } - // check another direction if can travel to chosen dest on current x-axis. + // Move N/S if (dz != 0 && canTravel(this.level, this.x, this.z, 0, dz, this.width, extraFlag, collisionStrategy)) { - return CoordGrid.face(srcX, srcZ, srcX, z); + return [0, dz]; } // https://x.com/JagexAsh/status/1727609489954664502 - return null; + return [0, 0]; } } diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index b06022421..36b86ff17 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -37,7 +37,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, naiveDestination, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -1062,18 +1062,6 @@ export default class Player extends PathingEntity { super.queueWaypoints(waypoints); } - naivePathToTarget() { - if (!this.target) { - return; - } - let angle = 0; - if (this.target instanceof Loc) { - angle = this.target.angle; - } - const waypoint = naiveDestination(this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, true); - this.queueWaypoint(waypoint.x, waypoint.z); - } - pathToPathingTarget(): void { if (!(this.target instanceof PathingEntity)) { return; @@ -1988,17 +1976,8 @@ export default class Player extends PathingEntity { } // todo: make compiler do this at pack time - playSong(name: string) { - // todo: don't rely on MidiPack (server should be runnable using only packed content) - const id = MidiPack.getByName( - name - .toLowerCase() - .replaceAll(' ', '_') - .replace(/[^a-z0-9_-]/g, '') - ); - if (id !== -1) { - this.write(new MidiSong(id)); - } + playSong(id: number) { + this.write(new MidiSong(id)); } playJingle(id: number, delay: number): void { diff --git a/src/network/game/client/handler/MoveClickHandler.ts b/src/network/game/client/handler/MoveClickHandler.ts index c2722c2cc..fafca4360 100644 --- a/src/network/game/client/handler/MoveClickHandler.ts +++ b/src/network/game/client/handler/MoveClickHandler.ts @@ -3,7 +3,6 @@ import { NetworkPlayer } from '#/engine/entity/NetworkPlayer.js'; import ClientGameMessageHandler from '#/network/game/client/ClientGameMessageHandler.js'; import { AllowRepath } from '#/engine/entity/AllowRepath.js'; import MoveClick from '#/network/game/client/model/MoveClick.js'; -import { MoveGeneratedFrom } from '#/engine/entity/MoveGeneratedFrom.js'; import UnsetMapFlag from '#/network/game/server/model/UnsetMapFlag.js'; import Environment from '#/util/Environment.js'; @@ -48,7 +47,6 @@ export default class MoveClickHandler extends ClientGameMessageHandler Date: Sun, 15 Mar 2026 18:52:42 -0400 Subject: [PATCH 16/18] pathing fix again --- src/engine/entity/Player.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 36b86ff17..0596c3ba0 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1,7 +1,7 @@ import 'dotenv/config'; import { PlayerInfoProt, Visibility } from '@2004scape/rsbuf'; -import { CollisionFlag } from '@2004scape/rsmod-pathfinder'; +import { CollisionFlag, CollisionType } from '@2004scape/rsmod-pathfinder'; import Component from '#/cache/config/Component.js'; import FontType from '#/cache/config/FontType.js'; @@ -37,7 +37,7 @@ import { PlayerQueueRequest, PlayerQueueType, QueueType, ScriptArgument } from ' import { PlayerStat, PlayerStatEnabled, PlayerStatFree, PlayerStatNameMap } from '#/engine/entity/PlayerStat.js'; import InputTracking from '#/engine/entity/tracking/InputTracking.js'; import { WealthEventParams } from '#/engine/entity/tracking/WealthEvent.js'; -import { changeNpcCollision, changePlayerCollision, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; +import { changeNpcCollision, changePlayerCollision, findNaivePath, reachedEntity, reachedLoc, reachedObj } from '#/engine/GameMap.js'; import { Inventory, InventoryListener } from '#/engine/Inventory.js'; import ScriptFile from '#/engine/script/ScriptFile.js'; import ScriptPointer from '#/engine/script/ScriptPointer.js'; @@ -1094,6 +1094,24 @@ export default class Player extends PathingEntity { } } + naivePathToTarget() { + if (!this.target) { + return; + } + let angle = 0; + if (this.target instanceof Loc) { + angle = this.target.angle; + } + + const { x, z } = CoordGrid.unpackCoord(this.waypoints[0]); + + // If no waypoint, or waypoint is further than 1 tile from target, set new dest + if (this.waypointIndex === -1 || Math.abs(this.target.x - x) > 1 || Math.abs(this.target.z - z) > 1) { + const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); + this.queueWaypoints(waypoints); + } + } + // https://youtu.be/_NmFftkMm0I?si=xSgb8GCydgUXUayR&t=79 // to allow p_walk (sets player destination tile) during walktriggers // we process walktriggers from regular movement in client input, From 2ff38b878eccbd56cd6ec4f172eba74857d4913b Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 15 Mar 2026 18:56:11 -0400 Subject: [PATCH 17/18] remove unused methods --- src/engine/entity/Player.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/engine/entity/Player.ts b/src/engine/entity/Player.ts index 0596c3ba0..45d80bb83 100644 --- a/src/engine/entity/Player.ts +++ b/src/engine/entity/Player.ts @@ -1054,14 +1054,6 @@ export default class Player extends PathingEntity { return ScriptProvider.getByTrigger(this.targetOp, typeId, categoryId) ?? null; } - queueWaypoint(x: number, z: number): void { - super.queueWaypoint(x, z); - } - - queueWaypoints(waypoints: ArrayLike): void { - super.queueWaypoints(waypoints); - } - pathToPathingTarget(): void { if (!(this.target instanceof PathingEntity)) { return; From 578d9f63230bacf45935726a402a84045bf06711 Mon Sep 17 00:00:00 2001 From: markb5 Date: Sun, 15 Mar 2026 19:06:39 -0400 Subject: [PATCH 18/18] don't queue naive waypoint if already there --- src/engine/entity/PathingEntity.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/entity/PathingEntity.ts b/src/engine/entity/PathingEntity.ts index b4ec2a479..7982adbf7 100644 --- a/src/engine/entity/PathingEntity.ts +++ b/src/engine/entity/PathingEntity.ts @@ -448,7 +448,11 @@ export default abstract class PathingEntity extends Entity { angle = this.target.angle; } const waypoints = findNaivePath(this.level, this.x, this.z, this.target.x, this.target.z, this.width, this.length, this.target.width, this.target.length, angle, CollisionType.NORMAL); - this.queueWaypoints(waypoints); + + const { x, z } = CoordGrid.unpackCoord(waypoints[0]); + if (x !== this.x || z !== this.z) { + this.queueWaypoints(waypoints); + } } pathToTarget(): void {