mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
feat(client): walkTo reports out-of-range destinations explicitly
A destination beyond the 104x104 build area used to be silently clamped
to the scene edge and reported as a plain success, so a caller could not
tell a full route from a partial leg - the lite fleet worked around it
with a hand-rolled Traveller (bug report msjgy9cl). Both clients'
walkTo now return {moved, outOfRange}, and the ActionExecutor surfaces
reason 'out_of_range' / a leg-to-edge message instead of pretending the
route was complete.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
035c64a9ad
commit
697b43933d
5 files changed
+53
-19
No files matched your search
@@ -48,13 +48,22 @@ export class ActionExecutor {
|
||||
return { success: true, message: `Waiting ${action.ticks || 1} ticks` };
|
||||
|
||||
case 'walkTo': {
|
||||
const result = this.wrapBool(
|
||||
this.client.walkTo(action.x, action.z, action.running ?? true),
|
||||
`Walking to (${action.x}, ${action.z})`,
|
||||
'Failed to walk'
|
||||
);
|
||||
// walkTo already sets its own cross visual via projectTileToScreen
|
||||
return result;
|
||||
const walk = this.client.walkTo(action.x, action.z, action.running ?? true);
|
||||
if (!walk.moved) {
|
||||
return walk.outOfRange
|
||||
? { success: false, message: `(${action.x}, ${action.z}) is outside the current build area and no path leads toward its edge`, phase: 'validation', reason: 'out_of_range' }
|
||||
: { success: false, message: 'Failed to walk', phase: 'validation', reason: 'client_rejected' };
|
||||
}
|
||||
if (walk.outOfRange) {
|
||||
return {
|
||||
success: true,
|
||||
message: `Walking toward (${action.x}, ${action.z}) - outside the current build area, this is a leg to the scene edge`,
|
||||
phase: 'dispatch',
|
||||
data: { outOfRange: true }
|
||||
};
|
||||
}
|
||||
return { success: true, message: `Walking to (${action.x}, ${action.z})`, phase: 'dispatch' };
|
||||
}
|
||||
|
||||
case 'talkToNpc': {
|
||||
|
||||
@@ -158,6 +158,18 @@ export interface NearbyLoc {
|
||||
reachable?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of Client/LiteClient.walkTo. `moved` reports whether a MOVE packet
|
||||
* was emitted. `outOfRange` reports that the destination lies outside the
|
||||
* current 104x104 build area - the emitted path (if any) is then only a leg
|
||||
* toward the scene edge, not a route to the destination; crossing the edge
|
||||
* rebuilds the scene and the caller must dispatch the next leg itself.
|
||||
*/
|
||||
export interface WalkResult {
|
||||
moved: boolean;
|
||||
outOfRange: boolean;
|
||||
}
|
||||
|
||||
export interface MenuAction {
|
||||
option: string;
|
||||
actionCode: number;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { playWave, setWaveVolume } from '#3rdparty/audio.js';
|
||||
import { stopMidi, setMidiVolume, playMidi } from '#3rdparty/tinymidipcm.js';
|
||||
|
||||
import type { WalkResult } from '#/bot/types.js';
|
||||
|
||||
import ClientBuild from '#/client/ClientBuild.js';
|
||||
import { ClientCode } from '#/client/ClientCode.js';
|
||||
import GameShell from '#/client/GameShell.js';
|
||||
@@ -2131,11 +2133,13 @@ export class Client extends GameShell {
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk to a world coordinate using proper client-side pathfinding
|
||||
* Walk to a world coordinate using proper client-side pathfinding.
|
||||
* An out-of-scene destination is clamped to the edge and walked as a leg;
|
||||
* the result says so instead of pretending the whole route was found.
|
||||
*/
|
||||
walkTo(worldX: number, worldZ: number, running: boolean = false): boolean {
|
||||
walkTo(worldX: number, worldZ: number, running: boolean = false): WalkResult {
|
||||
if (!this.ingame || !this.out || !this.localPlayer) {
|
||||
return false;
|
||||
return { moved: false, outOfRange: false };
|
||||
}
|
||||
|
||||
let destSceneX = worldX - this.mapBuildBaseX;
|
||||
@@ -2182,15 +2186,15 @@ export class Client extends GameShell {
|
||||
this.crossCycle = 0;
|
||||
}
|
||||
|
||||
return success;
|
||||
return { moved: success, outOfRange: wasClamped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk relative to current position
|
||||
*/
|
||||
walkRelative(deltaX: number, deltaZ: number, running: boolean = false): boolean {
|
||||
walkRelative(deltaX: number, deltaZ: number, running: boolean = false): WalkResult {
|
||||
if (!this.ingame || !this.localPlayer) {
|
||||
return false;
|
||||
return { moved: false, outOfRange: false };
|
||||
}
|
||||
|
||||
const currentTileX = (this.localPlayer.x >> 7);
|
||||
|
||||
@@ -39,7 +39,7 @@ import { TypedArray1d, TypedArray3d } from '#/util/Arrays.js';
|
||||
|
||||
import { BotStateCollector } from '#/bot/StateCollector.js';
|
||||
import { ActionExecutor, type ActionResultOrPromise } from '#/bot/ActionExecutor.js';
|
||||
import type { BotAction, BotWorldState } from '#/bot/types.js';
|
||||
import type { BotAction, BotWorldState, WalkResult } from '#/bot/types.js';
|
||||
import type { Client } from '#/client/Client.js';
|
||||
|
||||
import { GameConnection } from './net/GameConnection.js';
|
||||
@@ -633,9 +633,9 @@ export class LiteClient {
|
||||
}
|
||||
|
||||
/** Send a MOVE_GAMECLICK with a client-computed path. See movement.ts. */
|
||||
walkTo(worldX: number, worldZ: number, running = false): boolean {
|
||||
walkTo(worldX: number, worldZ: number, running = false): WalkResult {
|
||||
if (!this.localPlayer) {
|
||||
return false;
|
||||
return { moved: false, outOfRange: false };
|
||||
}
|
||||
|
||||
// Match Client.walkTo: a long-distance BotActions route can hand the
|
||||
@@ -644,11 +644,17 @@ export class LiteClient {
|
||||
// after which BotActions observes progress and dispatches the next leg.
|
||||
// Keeping the one-tile border excluded also matches the browser's
|
||||
// collision bounds (tiles 0 and 103 are the blocked border ring).
|
||||
// `outOfRange` tells the caller this was only a leg, not a full route -
|
||||
// the silent-no-op era where a far destination looked like a success
|
||||
// with no signal is what bug report msjgy9cl was about.
|
||||
const minBound = 1;
|
||||
const maxBound = BuildArea.SIZE - 2;
|
||||
const destX = Math.max(minBound, Math.min(maxBound, worldX - this.mapBuildBaseX));
|
||||
const destZ = Math.max(minBound, Math.min(maxBound, worldZ - this.mapBuildBaseZ));
|
||||
return findPathToTile(this, destX, destZ, running);
|
||||
const rawX = worldX - this.mapBuildBaseX;
|
||||
const rawZ = worldZ - this.mapBuildBaseZ;
|
||||
const outOfRange = rawX < minBound || rawX > maxBound || rawZ < minBound || rawZ > maxBound;
|
||||
const destX = Math.max(minBound, Math.min(maxBound, rawX));
|
||||
const destZ = Math.max(minBound, Math.min(maxBound, rawZ));
|
||||
return { moved: findPathToTile(this, destX, destZ, running), outOfRange };
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ outgoing plumbing
|
||||
|
||||
@@ -267,7 +267,7 @@ describe('outgoing action encoding', () => {
|
||||
c.localPlayer!.routeZ[0] = 50;
|
||||
|
||||
const start = c.out.pos;
|
||||
expect(c.walkTo(4000, 3250, true)).toBe(true);
|
||||
expect(c.walkTo(4000, 3250, true)).toEqual({ moved: true, outOfRange: true });
|
||||
|
||||
const r = new Capture(c.out, start);
|
||||
expect(r.g1()).toBe(ClientProt.MOVE_GAMECLICK);
|
||||
@@ -276,6 +276,9 @@ describe('outgoing action encoding', () => {
|
||||
expect(r.g2()).toBe(3302); // baseX + build-area max interior tile
|
||||
expect(r.g2()).toBe(3250);
|
||||
expect(r.at).toBe(c.out.pos);
|
||||
|
||||
// An in-area destination reports outOfRange false.
|
||||
expect(c.walkTo(3251, 3251, true)).toEqual({ moved: true, outOfRange: false });
|
||||
});
|
||||
|
||||
test('an actor already inside an NPC footprint needs no movement packet', () => {
|
||||
|
||||
Reference in new issue
Block a user