mirror of
https://github.com/bdnugget/rs-sdk.git
synced 2026-11-07 09:07:30 +00:00
feat(lite): in-process fast-KBD attack/prayer hooks in the gateway runner
Policy-gated fast path for King Black Dragon fights: the lite runner emits the KBD attack op (and protect-prayer on/off flips) in-process on the same publication that reveals the spawn/removal, instead of round-tripping through the gateway controller. Enabled only when bots/logs/ fleet-runtime.mode contains the fast-kbd mode string; state publications carry fastKbdAttackTick/fastKbdPrayerOn/fastKbdPrayerOff so the controller can observe what was already dispatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d447f76077
commit
dfac800e21
7 files changed
+320
-3
No files matched your search
@@ -500,6 +500,8 @@ interface BotWorldState {
|
||||
tick: number;
|
||||
/** Tick on which an updated Lite runner already emitted policy-gated KBD op2. */
|
||||
fastKbdAttackTick?: number;
|
||||
/** Prayer-on packets emitted in-process immediately before first-spawn KBD op2. */
|
||||
fastKbdPrayerOn?: { tick: number; prayerIndices: number[] };
|
||||
/** Prayer-off packets emitted in-process on the KBD-removal publication. */
|
||||
fastKbdPrayerOff?: { tick: number; prayerIndices: number[] };
|
||||
/** Monotonic state publication cursor; advances even for multiple publications in one game tick. */
|
||||
|
||||
@@ -496,6 +496,12 @@ export interface OpFeedbackState {
|
||||
|
||||
export interface BotWorldState {
|
||||
tick: number;
|
||||
/** Tick on which an updated Lite runner already emitted policy-gated KBD op2. */
|
||||
fastKbdAttackTick?: number;
|
||||
/** Prayer-on packets emitted in-process immediately before first-spawn KBD op2. */
|
||||
fastKbdPrayerOn?: { tick: number; prayerIndices: number[] };
|
||||
/** Prayer-off packets emitted in-process on the KBD-removal publication. */
|
||||
fastKbdPrayerOff?: { tick: number; prayerIndices: number[] };
|
||||
/** Monotonic state publication cursor; advances even for multiple publications in one game tick. */
|
||||
revision?: number;
|
||||
inGame: boolean;
|
||||
|
||||
@@ -405,6 +405,12 @@ export interface BotState {
|
||||
|
||||
// Extended world state interface for agent (includes extra debug info)
|
||||
export interface BotWorldState extends BotState {
|
||||
/** Tick on which a policy-gated Lite runner already emitted KBD op2. */
|
||||
fastKbdAttackTick?: number;
|
||||
/** Prayer-on packets emitted in-process immediately before first-spawn KBD op2. */
|
||||
fastKbdPrayerOn?: { tick: number; prayerIndices: number[] };
|
||||
/** Prayer-off packets emitted in-process on the KBD-removal publication. */
|
||||
fastKbdPrayerOff?: { tick: number; prayerIndices: number[] };
|
||||
dialog: DialogState & {
|
||||
allComponents?: Array<{ id: number; type: number; buttonType: number; option: string; text: string }>;
|
||||
};
|
||||
|
||||
@@ -726,6 +726,16 @@ export class LiteClient {
|
||||
interactNpc(npcIndex: number, optionIndex: number): ClientActionResult {
|
||||
return A.interactNpc(this, npcIndex, optionIndex);
|
||||
}
|
||||
|
||||
/** Exact raw-table lookup for latency-critical, policy-gated hooks. */
|
||||
findNpcIndexByExactName(name: string): number | undefined {
|
||||
for (let i = 0; i < this.npcCount; i++) {
|
||||
const index = this.npcIds[i];
|
||||
const npc = this.npc[index];
|
||||
if (npc?.type?.name === name) return index;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
interactPlayer(playerIndex: number, optionIndex: number): ClientActionResult {
|
||||
return A.interactPlayer(this, playerIndex, optionIndex);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import type { BotWorldState } from '#/bot/types.js';
|
||||
import {
|
||||
findFastKbdAttackTarget,
|
||||
findFastKbdPrayerOffIndices,
|
||||
findFastKbdPrayerOnIndices,
|
||||
isFastKbdPolicyState,
|
||||
} from './fast-kbd.js';
|
||||
|
||||
function state(overrides: Record<string, unknown> = {}): BotWorldState {
|
||||
return {
|
||||
player: { worldX: 2718, worldZ: 9819, isDead: false },
|
||||
modalOpen: false,
|
||||
dialog: { isOpen: false },
|
||||
equipment: [{ name: 'Rune 2h sword' }],
|
||||
inventory: [],
|
||||
nearbyNpcs: [{ name: 'King black dragon', index: 2771, reachable: true }],
|
||||
prayers: { activePrayers: Array(15).fill(false), prayerPoints: 70, prayerLevel: 70 },
|
||||
...overrides,
|
||||
} as unknown as BotWorldState;
|
||||
}
|
||||
|
||||
describe('runner-side KBD fast attack eligibility', () => {
|
||||
test('selects the exact KBD from the one-item lair state', () => {
|
||||
expect(findFastKbdAttackTarget(state())).toBe(2771);
|
||||
});
|
||||
|
||||
test('allows two retained valuable slots but not a due bank return', () => {
|
||||
expect(findFastKbdAttackTarget(state({
|
||||
inventory: [{ name: 'Dragonhide' }, { name: 'Coal' }],
|
||||
}))).toBe(2771);
|
||||
expect(findFastKbdAttackTarget(state({
|
||||
inventory: [{ name: 'Dragonhide' }, { name: 'Coal' }, { name: 'Yew logs' }],
|
||||
}))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('rejects forbidden loot, blocking UI, and positions outside KBD', () => {
|
||||
expect(findFastKbdAttackTarget(state({ inventory: [{ name: 'Coins' }] }))).toBeUndefined();
|
||||
expect(findFastKbdAttackTarget(state({ modalOpen: true }))).toBeUndefined();
|
||||
expect(findFastKbdAttackTarget(state({
|
||||
player: { worldX: 3094, worldZ: 3957, isDead: false },
|
||||
}))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('recognises the reusable one-item policy without a live dragon', () => {
|
||||
expect(isFastKbdPolicyState(state({ nearbyNpcs: [] }))).toBe(true);
|
||||
});
|
||||
|
||||
test('switches active prayers off only during a policy-gated post-KBD phase', () => {
|
||||
const activePrayers = Array(15).fill(false);
|
||||
activePrayers[10] = true;
|
||||
activePrayers[11] = true;
|
||||
activePrayers[12] = true;
|
||||
const removed = state({
|
||||
nearbyNpcs: [],
|
||||
prayers: { activePrayers, prayerPoints: 60, prayerLevel: 70 },
|
||||
});
|
||||
expect(findFastKbdPrayerOffIndices(removed, true)).toEqual([10, 11, 12]);
|
||||
expect(findFastKbdPrayerOffIndices(removed, false)).toEqual([]);
|
||||
expect(findFastKbdPrayerOffIndices(state({
|
||||
prayers: { activePrayers, prayerPoints: 60, prayerLevel: 70 },
|
||||
}), true)).toEqual([]);
|
||||
});
|
||||
|
||||
test('switches damage prayers on for the spawn tick and adds bounded protection', () => {
|
||||
expect(findFastKbdPrayerOnIndices(state())).toEqual([10, 11]);
|
||||
expect(findFastKbdPrayerOnIndices(state({
|
||||
player: { worldX: 2718, worldZ: 9819, isDead: false, hp: 60 },
|
||||
inventory: [{ name: 'Dragonhide' }],
|
||||
}))).toEqual([10, 11, 12, 8]);
|
||||
});
|
||||
|
||||
test('does not re-toggle active or unavailable spawn prayers', () => {
|
||||
const activePrayers = Array(15).fill(false);
|
||||
activePrayers[10] = true;
|
||||
activePrayers[11] = true;
|
||||
expect(findFastKbdPrayerOnIndices(state({
|
||||
prayers: { activePrayers, prayerPoints: 60, prayerLevel: 70 },
|
||||
}))).toEqual([]);
|
||||
expect(findFastKbdPrayerOnIndices(state({
|
||||
prayers: { activePrayers: Array(15).fill(false), prayerPoints: 0, prayerLevel: 70 },
|
||||
}))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { BotWorldState } from '#/bot/types.js';
|
||||
|
||||
export const FAST_KBD_RUNTIME_MODE = 'kbd-fast-attack-v4';
|
||||
|
||||
const RUNE_2H = /^rune 2h sword$/i;
|
||||
const BONES = /bones$/i;
|
||||
const IGNORED_KBD_LOOT = /^(?:coins|gold ring|uncut (?:sapphire|emerald|ruby|diamond))$/i;
|
||||
const KBD_PROTECT_MAGIC_HP_THRESHOLD = 65;
|
||||
|
||||
function valuableKbdLootSlots(state: BotWorldState): number {
|
||||
return state.inventory.filter(item =>
|
||||
!BONES.test(item.name) && !IGNORED_KBD_LOOT.test(item.name)
|
||||
).length;
|
||||
}
|
||||
|
||||
/** The durable one-item policy conditions shared by the attack and prayer-off hooks. */
|
||||
export function isFastKbdPolicyState(state: BotWorldState): boolean {
|
||||
const player = state.player;
|
||||
if (!player
|
||||
|| player.isDead
|
||||
|| player.worldX < 2688 || player.worldX >= 2752
|
||||
|| player.worldZ < 9792 || player.worldZ >= 9856
|
||||
|| state.modalOpen
|
||||
|| state.dialog.isOpen
|
||||
|| state.equipment.length !== 1
|
||||
|| !RUNE_2H.test(state.equipment[0].name)
|
||||
|| state.inventory.some(item => IGNORED_KBD_LOOT.test(item.name))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return valuableKbdLootSlots(state) < 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the one NPC index the Lite runner may attack before publishing state.
|
||||
* This mirrors the durable fleet policy narrowly enough that the generic
|
||||
* runner cannot start combat anywhere else or while a bank return is due.
|
||||
*/
|
||||
export function findFastKbdAttackTarget(state: BotWorldState): number | undefined {
|
||||
if (!isFastKbdPolicyState(state)) return undefined;
|
||||
|
||||
return state.nearbyNpcs.find(npc =>
|
||||
npc.name === 'King black dragon' && npc.reachable !== false
|
||||
)?.index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactive prayers to switch on immediately before the first KBD op2 packet.
|
||||
* Interface-button packets are decoded before player interactions in the next
|
||||
* server cycle, so these boosts apply to that same first swing without paying
|
||||
* several pre-spawn drain ticks.
|
||||
*/
|
||||
export function findFastKbdPrayerOnIndices(state: BotWorldState): number[] {
|
||||
const player = state.player;
|
||||
if (!player || !isFastKbdPolicyState(state) || state.prayers.prayerPoints <= 0) return [];
|
||||
|
||||
const level = state.prayers.prayerLevel;
|
||||
const indices: number[] = [];
|
||||
if (level >= 31) indices.push(10); // Ultimate Strength
|
||||
else if (level >= 13) indices.push(4); // Superhuman Strength
|
||||
else if (level >= 4) indices.push(1); // Burst of Strength
|
||||
|
||||
if (level >= 34) indices.push(11); // Incredible Reflexes
|
||||
else if (level >= 16) indices.push(5); // Improved Reflexes
|
||||
else if (level >= 7) indices.push(2); // Clarity of Thought
|
||||
|
||||
if ((player.hp ?? Number.MAX_SAFE_INTEGER) <= KBD_PROTECT_MAGIC_HP_THRESHOLD
|
||||
&& level >= 37) {
|
||||
indices.push(12); // Protect from Magic
|
||||
}
|
||||
if (valuableKbdLootSlots(state) > 0 && level >= 25) {
|
||||
indices.push(8); // Protect Item
|
||||
}
|
||||
|
||||
return indices.filter(index => !state.prayers.activePrayers[index]);
|
||||
}
|
||||
|
||||
/** Active prayers the runner may disable during a confirmed post-KBD off phase. */
|
||||
export function findFastKbdPrayerOffIndices(
|
||||
state: BotWorldState,
|
||||
prayerOffPending: boolean,
|
||||
): number[] {
|
||||
if (!prayerOffPending
|
||||
|| !isFastKbdPolicyState(state)
|
||||
|| state.nearbyNpcs.some(npc => npc.name === 'King black dragon')) {
|
||||
return [];
|
||||
}
|
||||
return state.prayers.activePrayers.flatMap((active, index) => active ? [index] : []);
|
||||
}
|
||||
@@ -13,8 +13,15 @@
|
||||
import { startSession, type LiteSession } from './session.js';
|
||||
import { type ActionResult } from '#/bot/ActionExecutor.js';
|
||||
import { BotActionQueue, type QueuedBotAction } from '#/bot/ActionQueue.js';
|
||||
import type { BotAction } from '#/bot/types.js';
|
||||
import type { BotAction, BotWorldState } from '#/bot/types.js';
|
||||
import type { LiteClient } from './LiteClient.js';
|
||||
import {
|
||||
FAST_KBD_RUNTIME_MODE,
|
||||
findFastKbdAttackTarget,
|
||||
findFastKbdPrayerOffIndices,
|
||||
findFastKbdPrayerOnIndices,
|
||||
isFastKbdPolicyState,
|
||||
} from './fast-kbd.js';
|
||||
|
||||
const TICK_MS = 20; // matches the browser draw loop that drives BotOverlay.tick()
|
||||
const RECONNECT_MS = 3000;
|
||||
@@ -24,6 +31,15 @@ class LiteGatewayRunner {
|
||||
private actionQueue = new BotActionQueue();
|
||||
private waitTicks = 0;
|
||||
private serverTick = 0;
|
||||
private lastFastKbdAttackTick = -1;
|
||||
private fastKbdPolicyEligible = false;
|
||||
private fastKbdWasVisible = false;
|
||||
private fastKbdRawWasVisible = false;
|
||||
private fastKbdLastState: BotWorldState | null = null;
|
||||
private lastFastKbdPrayerOnTick = -1;
|
||||
private lastFastKbdPrayerOnIndices: number[] = [];
|
||||
private fastKbdPrayerOffPending = false;
|
||||
private lastFastKbdPrayerOffDispatchTick = -1;
|
||||
|
||||
private ws: WebSocket | null = null;
|
||||
private wsConnected = false;
|
||||
@@ -33,13 +49,31 @@ class LiteGatewayRunner {
|
||||
|
||||
constructor(
|
||||
private session: LiteSession,
|
||||
private gatewayUrl: string
|
||||
private gatewayUrl: string,
|
||||
private fastKbdAttackEnabled = false,
|
||||
) {
|
||||
this.client = session.client;
|
||||
// State collection and action execution go through the client, which
|
||||
// activates this bot's interface table first - see LiteClient.activate.
|
||||
this.client.setOnGameTickCallback(() => {
|
||||
this.serverTick++;
|
||||
// Reuse the previous authoritative one-item policy snapshot and
|
||||
// probe the newly decoded raw NPC table before full state
|
||||
// collection. This puts KBD op2 ahead of reachability, nearby
|
||||
// player/loc/item scans, and UI serialization.
|
||||
if (this.fastKbdAttackEnabled) {
|
||||
const targetIndex = this.fastKbdPolicyEligible
|
||||
? this.client.findNpcIndexByExactName('King black dragon')
|
||||
: undefined;
|
||||
if (targetIndex !== undefined
|
||||
&& this.lastFastKbdAttackTick !== this.serverTick) {
|
||||
if (!this.fastKbdRawWasVisible && this.fastKbdLastState) {
|
||||
this.dispatchFastKbdPrayerOn(this.fastKbdLastState);
|
||||
}
|
||||
this.dispatchFastKbdAttack(targetIndex);
|
||||
}
|
||||
this.fastKbdRawWasVisible = targetIndex !== undefined;
|
||||
}
|
||||
this.sendState();
|
||||
});
|
||||
|
||||
@@ -218,10 +252,88 @@ class LiteGatewayRunner {
|
||||
this.send({ type: 'actionResult', actionId, result });
|
||||
}
|
||||
|
||||
private dispatchFastKbdAttack(targetIndex: number): void {
|
||||
const result = this.client.interactNpc(targetIndex, 2);
|
||||
if (result.success) this.lastFastKbdAttackTick = this.serverTick;
|
||||
}
|
||||
|
||||
private dispatchFastKbdPrayerOn(state: BotWorldState): void {
|
||||
const emitted: number[] = [];
|
||||
for (const prayerIndex of findFastKbdPrayerOnIndices(state)) {
|
||||
const result = this.client.executeBotAction({
|
||||
type: 'togglePrayer',
|
||||
prayerIndex,
|
||||
reason: 'runner-fast KBD publication',
|
||||
});
|
||||
if (!(result instanceof Promise) && result.success) emitted.push(prayerIndex);
|
||||
}
|
||||
if (emitted.length > 0) {
|
||||
this.lastFastKbdPrayerOnTick = this.serverTick;
|
||||
this.lastFastKbdPrayerOnIndices = emitted;
|
||||
}
|
||||
}
|
||||
|
||||
private sendState(): void {
|
||||
if (!this.wsConnected) return;
|
||||
const state = this.client.collectBotState(this.serverTick);
|
||||
if (!state) return;
|
||||
if (this.fastKbdAttackEnabled) {
|
||||
const targetIndex = findFastKbdAttackTarget(state);
|
||||
if (targetIndex !== undefined && this.lastFastKbdAttackTick !== this.serverTick) {
|
||||
// This is intentionally in-process and before state publication:
|
||||
// the ordinary SDK path must traverse the gateway twice before
|
||||
// the same packet reaches this client.
|
||||
this.dispatchFastKbdPrayerOn(state);
|
||||
this.dispatchFastKbdAttack(targetIndex);
|
||||
}
|
||||
if (this.lastFastKbdAttackTick === this.serverTick) {
|
||||
state.fastKbdAttackTick = this.serverTick;
|
||||
}
|
||||
if (this.lastFastKbdPrayerOnTick === this.serverTick) {
|
||||
state.fastKbdPrayerOn = {
|
||||
tick: this.serverTick,
|
||||
prayerIndices: this.lastFastKbdPrayerOnIndices,
|
||||
};
|
||||
}
|
||||
|
||||
const fastKbdVisible = targetIndex !== undefined;
|
||||
const policyEligible = isFastKbdPolicyState(state);
|
||||
if (this.fastKbdWasVisible && !fastKbdVisible && policyEligible) {
|
||||
this.fastKbdPrayerOffPending = true;
|
||||
}
|
||||
if (fastKbdVisible || !policyEligible) {
|
||||
this.fastKbdPrayerOffPending = false;
|
||||
}
|
||||
const prayerOffIndices = this.lastFastKbdPrayerOffDispatchTick === this.serverTick
|
||||
? []
|
||||
: findFastKbdPrayerOffIndices(state, this.fastKbdPrayerOffPending);
|
||||
const emittedPrayerOffIndices: number[] = [];
|
||||
for (const prayerIndex of prayerOffIndices) {
|
||||
const result = this.client.executeBotAction({
|
||||
type: 'togglePrayer',
|
||||
prayerIndex,
|
||||
reason: 'runner-fast KBD removal',
|
||||
});
|
||||
if (!(result instanceof Promise) && result.success) {
|
||||
emittedPrayerOffIndices.push(prayerIndex);
|
||||
}
|
||||
}
|
||||
if (emittedPrayerOffIndices.length > 0) {
|
||||
this.lastFastKbdPrayerOffDispatchTick = this.serverTick;
|
||||
state.fastKbdPrayerOff = {
|
||||
tick: this.serverTick,
|
||||
prayerIndices: emittedPrayerOffIndices,
|
||||
};
|
||||
}
|
||||
if (this.fastKbdPrayerOffPending
|
||||
&& !state.prayers.activePrayers.some(active => active)) {
|
||||
this.fastKbdPrayerOffPending = false;
|
||||
}
|
||||
this.fastKbdPolicyEligible = policyEligible;
|
||||
this.fastKbdWasVisible = fastKbdVisible;
|
||||
this.fastKbdRawWasVisible = fastKbdVisible;
|
||||
this.fastKbdLastState = state;
|
||||
}
|
||||
this.send({ type: 'state', state });
|
||||
}
|
||||
}
|
||||
@@ -271,7 +383,14 @@ console.log(`[lite-runner] '${env.BOT_USERNAME}' logged into ${host}`);
|
||||
// local hosts with an explicit web port (localhost:8888 is the engine, not the
|
||||
// gateway). GATEWAY_URL (bot.env or process env) overrides the derivation.
|
||||
const gatewayUrl = env.GATEWAY_URL || process.env.GATEWAY_URL || deriveGatewayUrl(host);
|
||||
const runner = new LiteGatewayRunner(session, gatewayUrl);
|
||||
const runtimeMode = await Bun.file(`${repoRoot}bots/logs/fleet-runtime.mode`).text()
|
||||
.then(value => value.trim())
|
||||
.catch(() => '');
|
||||
const fastKbdAttackEnabled = runtimeMode === FAST_KBD_RUNTIME_MODE;
|
||||
if (fastKbdAttackEnabled) {
|
||||
console.log('[lite-runner] in-process KBD op2 enabled');
|
||||
}
|
||||
const runner = new LiteGatewayRunner(session, gatewayUrl, fastKbdAttackEnabled);
|
||||
runner.connect();
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
|
||||
Reference in new issue
Block a user