@myclaw163/clawclaw-cli 0.6.56 → 0.6.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/clawclaw-cli.mjs +3 -3
- package/package.json +1 -1
- package/scripts/sync-bundled-skill.mjs +1 -1
- package/skills/clawclaw/SKILL.md +7 -3
- package/skills/clawclaw/references/GAME-MECHANICS.md +5 -5
- package/skills/clawclaw/references/STREAM.md +4 -3
- package/src/cli.ts +0 -43
- package/src/commands/config.ts +30 -30
- package/src/commands/game-start-plan.test.ts +10 -68
- package/src/commands/game.ts +318 -173
- package/src/commands/history.ts +1 -1
- package/src/commands/peek.ts +16 -9
- package/src/commands/setup/codex.ts +248 -248
- package/src/commands/setup/hermes.test.ts +96 -96
- package/src/commands/setup/hermes.ts +76 -76
- package/src/commands/setup/index.ts +13 -13
- package/src/commands/setup/openclaw.test.ts +114 -114
- package/src/commands/setup/openclaw.ts +147 -147
- package/src/commands/strategy.ts +29 -38
- package/src/commands/watch.test.ts +7 -11
- package/src/commands/watch.ts +77 -66
- package/src/lib/game-client.ts +3 -3
- package/src/lib/host-config-patcher.test.ts +130 -130
- package/src/lib/host-config-patcher.ts +151 -151
- package/src/lib/hub-reminder.ts +19 -19
- package/src/lib/strategy-export.test.ts +1 -1
- package/src/runtime/event-daemon.test.ts +81 -2
- package/src/runtime/event-daemon.ts +325 -287
- package/src/runtime/owner-control.ts +150 -0
- package/src/runtime/runtime-logger.ts +11 -3
- package/src/runtime/ws-client.test.ts +57 -0
- package/src/runtime/ws-client.ts +2 -2
- package/src/sdk/index.ts +4 -4
- package/src/strategies/game-utils.test.ts +27 -1
- package/src/strategies/game-utils.ts +27 -4
- package/src/strategies/goals/crab-octopus-reflexes.ts +4 -4
- package/src/strategies/goals/crab-sabotage-top.ts +1 -1
- package/src/strategies/goals/keep-away-goal.ts +10 -7
- package/src/strategies/goals/kill-frenzy-top.ts +5 -5
- package/src/strategies/goals/kill-target-goal.ts +2 -2
- package/src/strategies/goals/kill-target-top.ts +2 -2
- package/src/strategies/goals/lone-kill-core.ts +3 -3
- package/src/strategies/goals/lone-kill-task-top.ts +1 -1
- package/src/strategies/goals/task-kill-report-top.ts +3 -3
- package/src/strategies/goals/warrior-shrimp-top.ts +2 -2
- package/src/strategies/pathfind/escape-planner.ts +10 -3
- package/src/strategies/spawn.ts +16 -5
- package/src/strategies/strategy-loop.ts +9 -3
- package/src/runtime/daemon.ts +0 -100
- package/src/runtime/opening-mover.ts +0 -303
|
@@ -309,14 +309,21 @@ export interface RouteInfo {
|
|
|
309
309
|
nearThreat: boolean;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
/**
|
|
313
|
-
|
|
312
|
+
/**
|
|
313
|
+
* 沿路径每 4 格(16px)采样一次离威胁点的欧氏距离,含末端格;半径 ~200px 下足够密。
|
|
314
|
+
* 豁免起点 from 的 radiusPx 邻域:逃跑那刻起点必然紧贴威胁,这一采样点不该把
|
|
315
|
+
* 「背向威胁、越走越远」的整条路线判危;真正夹在你与目的地之间、离起点超过
|
|
316
|
+
* radiusPx 的威胁仍会命中。对齐同文件 pathsCross 跳过 step 起点的语义。
|
|
317
|
+
*/
|
|
318
|
+
function pathNearAny(grid: WalkableGrid, path: number[], threats: Position[], radiusPx: number, from: Position): boolean {
|
|
314
319
|
for (let i = 0; i < path.length; i += 4) {
|
|
315
320
|
const idx = Math.min(i, path.length - 1);
|
|
316
321
|
const w = grid.cellToWorld(path[idx]);
|
|
322
|
+
if (Math.hypot(w.x - from.x, w.y - from.y) <= radiusPx) continue;
|
|
317
323
|
if (threats.some(t => Math.hypot(w.x - t.x, w.y - t.y) <= radiusPx)) return true;
|
|
318
324
|
}
|
|
319
325
|
const end = grid.cellToWorld(path[path.length - 1]);
|
|
326
|
+
if (Math.hypot(end.x - from.x, end.y - from.y) <= radiusPx) return false;
|
|
320
327
|
return threats.some(t => Math.hypot(end.x - t.x, end.y - t.y) <= radiusPx);
|
|
321
328
|
}
|
|
322
329
|
|
|
@@ -342,7 +349,7 @@ export function assessRoutes(
|
|
|
342
349
|
if (units === Infinity) return { distancePx: Infinity, nearThreat: false };
|
|
343
350
|
return {
|
|
344
351
|
distancePx: units * PX_PER_UNIT,
|
|
345
|
-
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx),
|
|
352
|
+
nearThreat: threats.length > 0 && pathNearAny(grid, calc.pathToSource(cell), threats, threatRadiusPx, from),
|
|
346
353
|
};
|
|
347
354
|
});
|
|
348
355
|
}
|
package/src/strategies/spawn.ts
CHANGED
|
@@ -11,16 +11,25 @@ const __dirname = dirname(__filename);
|
|
|
11
11
|
const fwd = (p: string) => p.replace(/\\/g, '/');
|
|
12
12
|
const BIN_MJS = fwd(resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs'));
|
|
13
13
|
|
|
14
|
+
function isTrue(value: string | undefined): boolean {
|
|
15
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isDeveloperDiagnosticsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
19
|
+
return isTrue(env.CCL_DEBUG_RUNTIME) || isTrue(env.CCL_TEST) || isTrue(env['ccl-test']);
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export function spawnStrategyLoop(
|
|
15
23
|
strategyId: string,
|
|
16
24
|
strategyArgs?: string[],
|
|
17
|
-
opts?: { source?: string; gameId?: string; role?: string },
|
|
25
|
+
opts?: { source?: string; gameId?: string; role?: string; detached?: boolean; writeRuntimeFiles?: boolean },
|
|
18
26
|
): ReturnType<typeof spawn> {
|
|
19
27
|
if (!existsSync(BIN_MJS)) throw new Error('clawclaw-cli bin entry not found');
|
|
20
28
|
const isWin = process.platform === 'win32';
|
|
29
|
+
const writeRuntimeFiles = opts?.writeRuntimeFiles ?? false;
|
|
21
30
|
|
|
22
31
|
let logFile: string | undefined;
|
|
23
|
-
if (isWin) {
|
|
32
|
+
if (isWin && isDeveloperDiagnosticsEnabled()) {
|
|
24
33
|
try {
|
|
25
34
|
const store = new AuthStore();
|
|
26
35
|
const active = store.getActive();
|
|
@@ -37,17 +46,19 @@ export function spawnStrategyLoop(
|
|
|
37
46
|
const args = ['_strategy', strategyId];
|
|
38
47
|
if (strategyArgs && strategyArgs.length > 0) args.push('--', ...strategyArgs);
|
|
39
48
|
|
|
49
|
+
const detached = opts?.detached ?? false;
|
|
40
50
|
const child = spawn(process.execPath, [BIN_MJS, ...args], {
|
|
41
|
-
detached
|
|
51
|
+
detached,
|
|
42
52
|
stdio: ['ignore', 'ignore', 'ignore'],
|
|
43
53
|
env: {
|
|
44
54
|
...process.env,
|
|
45
55
|
...(isWin && logFile ? { CLAWCLAW_LOG_FILE: logFile } : {}),
|
|
56
|
+
...(writeRuntimeFiles ? { CLAWCLAW_STRATEGY_RUNTIME_FILES: '1' } : {}),
|
|
46
57
|
},
|
|
47
58
|
windowsHide: true,
|
|
48
59
|
});
|
|
49
60
|
|
|
50
|
-
if (child.pid) {
|
|
61
|
+
if (child.pid && writeRuntimeFiles) {
|
|
51
62
|
try {
|
|
52
63
|
const store = new AuthStore();
|
|
53
64
|
const active = store.getActive();
|
|
@@ -66,6 +77,6 @@ export function spawnStrategyLoop(
|
|
|
66
77
|
} catch {}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
child.unref();
|
|
80
|
+
if (detached) child.unref();
|
|
70
81
|
return child;
|
|
71
82
|
}
|
|
@@ -49,6 +49,10 @@ function getStatusPath(): string {
|
|
|
49
49
|
return join(getProfileStateDir(profile), 'auto.json');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function shouldWriteStrategyRuntimeFiles(): boolean {
|
|
53
|
+
return process.env.CLAWCLAW_STRATEGY_RUNTIME_FILES === '1';
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
export function isStrategyRunning(): boolean {
|
|
53
57
|
try {
|
|
54
58
|
const pidPath = getPidPath();
|
|
@@ -353,7 +357,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
|
|
|
353
357
|
const store = EventStore.forActiveAccount();
|
|
354
358
|
const pidPath = getPidPath();
|
|
355
359
|
|
|
356
|
-
writeFileSync(pidPath, String(process.pid));
|
|
360
|
+
if (shouldWriteStrategyRuntimeFiles()) writeFileSync(pidPath, String(process.pid));
|
|
357
361
|
store.append({ type: 'auto', message: 'strategy started', strategy: strategyId, pid: process.pid });
|
|
358
362
|
|
|
359
363
|
const client = GameClient.fromAuth();
|
|
@@ -442,7 +446,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
|
|
|
442
446
|
const newEvents = extractNewEvents(result);
|
|
443
447
|
store.appendNewEvents(newEvents);
|
|
444
448
|
// action 结果里回来的 corpse_spotted 也入账尸体记忆——主循环只 observe state.new_events,
|
|
445
|
-
// 那条通道会被
|
|
449
|
+
// 那条通道会被 runtime WS listener 抢游标,这里补上避免尸体坐标漏记(ctx.knownCorpses 与 list() 同引用,立即可见)。
|
|
446
450
|
corpseMemory.ingestEvents(newEvents);
|
|
447
451
|
learnTeammatesFromEvents(newEvents, teammates);
|
|
448
452
|
if (currentRole === 'shrimp_pistol' && ownKillEvent(newEvents, currentPlayerName)) {
|
|
@@ -753,5 +757,7 @@ export async function runStrategyLoop(strategyId: string, args?: string[]): Prom
|
|
|
753
757
|
|
|
754
758
|
process.off('SIGINT', onSignal);
|
|
755
759
|
process.off('SIGTERM', onSignal);
|
|
756
|
-
|
|
760
|
+
if (shouldWriteStrategyRuntimeFiles()) {
|
|
761
|
+
try { unlinkSync(pidPath); } catch {}
|
|
762
|
+
}
|
|
757
763
|
}
|
package/src/runtime/daemon.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, openSync, readFileSync } from 'fs';
|
|
2
|
-
import { resolve, dirname, delimiter } from 'path';
|
|
3
|
-
import { spawn, execSync } from 'child_process';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { getProfileStateDir, getProfileLogsDir } from '../lib/init-command.js';
|
|
7
|
-
import { AuthStore } from '../lib/auth.js';
|
|
8
|
-
import { RuntimeLogger } from './runtime-logger.js';
|
|
9
|
-
|
|
10
|
-
const log = new RuntimeLogger();
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = dirname(__filename);
|
|
14
|
-
|
|
15
|
-
function isPidAlive(pid: number): boolean {
|
|
16
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function getRunningDaemonPid(stateDir: string): number | null {
|
|
20
|
-
const runtimePath = join(stateDir, 'runtime.json');
|
|
21
|
-
if (!existsSync(runtimePath)) return null;
|
|
22
|
-
try {
|
|
23
|
-
const info = JSON.parse(readFileSync(runtimePath, 'utf8'));
|
|
24
|
-
if (typeof info?.pid === 'number' && isPidAlive(info.pid)) return info.pid;
|
|
25
|
-
} catch {}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function waitDaemonExit(stateDir: string, timeoutMs = 15000): Promise<void> {
|
|
30
|
-
const runtimePath = join(stateDir, 'runtime.json');
|
|
31
|
-
let pid: number | undefined;
|
|
32
|
-
try {
|
|
33
|
-
if (existsSync(runtimePath)) {
|
|
34
|
-
const info = JSON.parse(readFileSync(runtimePath, 'utf8'));
|
|
35
|
-
if (typeof info?.pid === 'number') pid = info.pid;
|
|
36
|
-
}
|
|
37
|
-
} catch {}
|
|
38
|
-
|
|
39
|
-
const deadline = Date.now() + timeoutMs;
|
|
40
|
-
while (Date.now() < deadline) {
|
|
41
|
-
const fileGone = !existsSync(runtimePath);
|
|
42
|
-
const procGone = pid === undefined ? true : !isPidAlive(pid);
|
|
43
|
-
if (fileGone && procGone) return;
|
|
44
|
-
await new Promise(r => setTimeout(r, 200));
|
|
45
|
-
}
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Daemon did not exit within ${timeoutMs}ms (pid=${pid ?? 'unknown'}).`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function spawnDaemon(): { pid: number; logFile: string } {
|
|
52
|
-
const store = new AuthStore();
|
|
53
|
-
const active = store.getActive();
|
|
54
|
-
if (!active) throw new Error('Not logged in.');
|
|
55
|
-
|
|
56
|
-
const stateDir = getProfileStateDir(active);
|
|
57
|
-
mkdirSync(stateDir, { recursive: true });
|
|
58
|
-
|
|
59
|
-
const existingPid = getRunningDaemonPid(stateDir);
|
|
60
|
-
if (existingPid) {
|
|
61
|
-
throw new Error(`Daemon already running (pid=${existingPid}). Stop it first.`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const logsDir = getProfileLogsDir(active);
|
|
65
|
-
mkdirSync(logsDir, { recursive: true });
|
|
66
|
-
|
|
67
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
68
|
-
const logFile = join(logsDir, `daemon-${timestamp}.log`);
|
|
69
|
-
const logFd = openSync(logFile, 'a');
|
|
70
|
-
|
|
71
|
-
const fwd = (p: string) => p.replace(/\\/g, '/');
|
|
72
|
-
const binEntry = fwd(resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs'));
|
|
73
|
-
if (!existsSync(binEntry)) throw new Error('clawclaw-cli bin entry not found');
|
|
74
|
-
|
|
75
|
-
let npmGlobalModules = '';
|
|
76
|
-
try { npmGlobalModules = execSync('npm root -g', { encoding: 'utf8', windowsHide: true }).trim(); } catch {}
|
|
77
|
-
|
|
78
|
-
const existingNodePath = process.env.NODE_PATH ?? '';
|
|
79
|
-
const nodePath = [npmGlobalModules, existingNodePath].filter(Boolean).join(delimiter);
|
|
80
|
-
const isWin = process.platform === 'win32';
|
|
81
|
-
|
|
82
|
-
const child = spawn(process.execPath, [binEntry, '_daemon'], {
|
|
83
|
-
detached: true,
|
|
84
|
-
stdio: isWin ? ['ignore', 'ignore', 'ignore'] : ['ignore', logFd, logFd],
|
|
85
|
-
cwd: process.cwd(),
|
|
86
|
-
env: {
|
|
87
|
-
...process.env,
|
|
88
|
-
NODE_PATH: nodePath,
|
|
89
|
-
...(isWin ? { CLAWCLAW_LOG_FILE: logFile } : {}),
|
|
90
|
-
},
|
|
91
|
-
windowsHide: true,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
child.on('error', (err) => {
|
|
95
|
-
log.error('DAEMON_SPAWN', `failed to start daemon`, err);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
child.unref();
|
|
99
|
-
return { pid: child.pid!, logFile };
|
|
100
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
3
|
-
import { delimiter, dirname, join, resolve } from 'path';
|
|
4
|
-
import { execSync } from 'child_process';
|
|
5
|
-
import { fileURLToPath } from 'url';
|
|
6
|
-
import { Action } from '../sdk/action.js';
|
|
7
|
-
import { AuthStore } from '../lib/auth.js';
|
|
8
|
-
import { GameClient } from '../lib/game-client.js';
|
|
9
|
-
import { getProfileLogsDir, getProfileStateDir } from '../lib/init-command.js';
|
|
10
|
-
import { isStrategyRunning as isAutoRunning } from '../strategies/strategy-loop.js';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = dirname(__filename);
|
|
14
|
-
|
|
15
|
-
const WANDERING_WINDOW_MS = 30_000;
|
|
16
|
-
const STARTUP_WAIT_MS = 10 * 60_000;
|
|
17
|
-
const POLL_MS = 1000;
|
|
18
|
-
const CANCEL_TTL_MS = 45_000;
|
|
19
|
-
|
|
20
|
-
interface Point {
|
|
21
|
-
x: number;
|
|
22
|
-
y: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function sleep(ms: number): Promise<void> {
|
|
26
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function debug(message: string): void {
|
|
30
|
-
if (process.env.CLAWCLAW_LOG_FILE) {
|
|
31
|
-
try {
|
|
32
|
-
appendFileSync(process.env.CLAWCLAW_LOG_FILE, `[${new Date().toISOString()}] ${message}\n`);
|
|
33
|
-
} catch {}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function isPidAlive(pid: number): boolean {
|
|
38
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function activeProfile() {
|
|
42
|
-
const profile = new AuthStore().getActive();
|
|
43
|
-
if (!profile) throw new Error('Not logged in.');
|
|
44
|
-
return profile;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function pidPath(): string {
|
|
48
|
-
return join(getProfileStateDir(activeProfile()), 'opening-mover.pid');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function cancelPath(): string {
|
|
52
|
-
return join(getProfileStateDir(activeProfile()), 'opening-mover.cancel');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function readCancelToken(path: string): number {
|
|
56
|
-
try {
|
|
57
|
-
const token = Number(readFileSync(path, 'utf8').trim());
|
|
58
|
-
return Number.isFinite(token) ? token : 0;
|
|
59
|
-
} catch {
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function cleanupPid(path: string): void {
|
|
65
|
-
try {
|
|
66
|
-
const current = Number(readFileSync(path, 'utf8').trim());
|
|
67
|
-
if (current === process.pid) unlinkSync(path);
|
|
68
|
-
} catch {}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function stopOpeningMoverIfRunning(): void {
|
|
72
|
-
try {
|
|
73
|
-
const path = pidPath();
|
|
74
|
-
if (!existsSync(path)) return;
|
|
75
|
-
const pid = Number(readFileSync(path, 'utf8').trim());
|
|
76
|
-
if (pid > 0 && isPidAlive(pid)) {
|
|
77
|
-
try { process.kill(pid, 'SIGTERM'); } catch {}
|
|
78
|
-
}
|
|
79
|
-
try { unlinkSync(path); } catch {}
|
|
80
|
-
} catch {}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function cancelOpeningMoverWindow(): void {
|
|
84
|
-
try {
|
|
85
|
-
const path = cancelPath();
|
|
86
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
-
writeFileSync(path, String(Date.now()));
|
|
88
|
-
} catch {}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function roomAnchor(r: any): Point | null {
|
|
92
|
-
const taskLocations = Array.isArray(r?.task_locations) ? r.task_locations : [];
|
|
93
|
-
const candidates = taskLocations
|
|
94
|
-
.map((tl: any) => ({ x: Number(tl?.x ?? tl?.[0]), y: Number(tl?.y ?? tl?.[1]) }))
|
|
95
|
-
.filter((p: Point) => Number.isFinite(p.x) && Number.isFinite(p.y));
|
|
96
|
-
if (candidates.length > 0) return candidates[Math.floor(Math.random() * candidates.length)];
|
|
97
|
-
|
|
98
|
-
const poly: number[][] = Array.isArray(r?.polygon) ? r.polygon : [];
|
|
99
|
-
if (poly.length === 0) return null;
|
|
100
|
-
|
|
101
|
-
let area = 0;
|
|
102
|
-
let cx = 0;
|
|
103
|
-
let cy = 0;
|
|
104
|
-
for (let i = 0; i < poly.length; i += 1) {
|
|
105
|
-
const [x1, y1] = poly[i];
|
|
106
|
-
const [x2, y2] = poly[(i + 1) % poly.length];
|
|
107
|
-
const cross = x1 * y2 - x2 * y1;
|
|
108
|
-
area += cross;
|
|
109
|
-
cx += (x1 + x2) * cross;
|
|
110
|
-
cy += (y1 + y2) * cross;
|
|
111
|
-
}
|
|
112
|
-
area *= 0.5;
|
|
113
|
-
if (Math.abs(area) < 1e-6) {
|
|
114
|
-
return {
|
|
115
|
-
x: poly.reduce((sum, p) => sum + p[0], 0) / poly.length,
|
|
116
|
-
y: poly.reduce((sum, p) => sum + p[1], 0) / poly.length,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
return { x: cx / (6 * area), y: cy / (6 * area) };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function randomTarget(mapData: any): Point | null {
|
|
123
|
-
const rooms = Array.isArray(mapData?.rooms) ? mapData.rooms : [];
|
|
124
|
-
const anchors = rooms.map(roomAnchor).filter((p: Point | null): p is Point => p !== null);
|
|
125
|
-
if (anchors.length === 0) return null;
|
|
126
|
-
return anchors[Math.floor(Math.random() * anchors.length)];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function runOpeningMover(): Promise<void> {
|
|
130
|
-
const path = pidPath();
|
|
131
|
-
const cancelFile = cancelPath();
|
|
132
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
133
|
-
writeFileSync(path, String(process.pid));
|
|
134
|
-
|
|
135
|
-
let running = true;
|
|
136
|
-
process.on('SIGINT', () => { running = false; });
|
|
137
|
-
process.on('SIGTERM', () => { running = false; });
|
|
138
|
-
|
|
139
|
-
const client = GameClient.fromAuth();
|
|
140
|
-
const startupDeadline = Date.now() + STARTUP_WAIT_MS;
|
|
141
|
-
let wanderingDeadline = 0;
|
|
142
|
-
let helperMovingUntil = 0;
|
|
143
|
-
let lastPhase: string | null = null;
|
|
144
|
-
let lastCancelToken = 0;
|
|
145
|
-
let windowActive = false;
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
debug('opening mover started');
|
|
149
|
-
try { await client.discoverGameServer(); } catch {}
|
|
150
|
-
while (running) {
|
|
151
|
-
let state: any = null;
|
|
152
|
-
try {
|
|
153
|
-
state = await client.getGameState();
|
|
154
|
-
} catch {
|
|
155
|
-
await sleep(1500);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (!state) {
|
|
160
|
-
if (Date.now() >= startupDeadline) break;
|
|
161
|
-
await sleep(1500);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (state.phase === 'game_over') break;
|
|
165
|
-
|
|
166
|
-
const cancelToken = readCancelToken(cancelFile);
|
|
167
|
-
if (cancelToken > lastCancelToken) {
|
|
168
|
-
lastCancelToken = cancelToken;
|
|
169
|
-
if (Date.now() - cancelToken <= CANCEL_TTL_MS) {
|
|
170
|
-
windowActive = false;
|
|
171
|
-
wanderingDeadline = 0;
|
|
172
|
-
helperMovingUntil = 0;
|
|
173
|
-
if (state.phase === 'wandering') lastPhase = 'wandering';
|
|
174
|
-
debug('movement helper window cancelled by manual action');
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (state.phase !== 'wandering') {
|
|
179
|
-
lastPhase = state.phase;
|
|
180
|
-
windowActive = false;
|
|
181
|
-
wanderingDeadline = 0;
|
|
182
|
-
helperMovingUntil = 0;
|
|
183
|
-
await sleep(POLL_MS);
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (lastPhase !== 'wandering') {
|
|
188
|
-
lastPhase = 'wandering';
|
|
189
|
-
helperMovingUntil = 0;
|
|
190
|
-
if (isAutoRunning()) {
|
|
191
|
-
windowActive = false;
|
|
192
|
-
wanderingDeadline = 0;
|
|
193
|
-
debug('wandering detected; auto running, movement helper skipped');
|
|
194
|
-
} else if (state.you?.currently_moving || state.you?.doing_task) {
|
|
195
|
-
windowActive = false;
|
|
196
|
-
wanderingDeadline = 0;
|
|
197
|
-
debug('wandering detected; player already active, movement helper skipped');
|
|
198
|
-
} else {
|
|
199
|
-
windowActive = true;
|
|
200
|
-
wanderingDeadline = Date.now() + WANDERING_WINDOW_MS;
|
|
201
|
-
debug('wandering detected; movement helper window started');
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!windowActive) {
|
|
206
|
-
await sleep(POLL_MS);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
if (Date.now() >= wanderingDeadline) {
|
|
210
|
-
windowActive = false;
|
|
211
|
-
wanderingDeadline = 0;
|
|
212
|
-
debug('movement helper window expired');
|
|
213
|
-
await sleep(POLL_MS);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (isAutoRunning()) {
|
|
218
|
-
windowActive = false;
|
|
219
|
-
wanderingDeadline = 0;
|
|
220
|
-
debug('auto started; movement helper window stopped');
|
|
221
|
-
await sleep(POLL_MS);
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (state.you?.currently_moving || state.you?.doing_task || Date.now() < helperMovingUntil) {
|
|
226
|
-
await sleep(POLL_MS);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let mapData: any = null;
|
|
231
|
-
try {
|
|
232
|
-
mapData = await client.getMap();
|
|
233
|
-
} catch {
|
|
234
|
-
await sleep(POLL_MS);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
const target = randomTarget(mapData);
|
|
238
|
-
if (!target) {
|
|
239
|
-
await sleep(POLL_MS);
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
let result: any;
|
|
244
|
-
try {
|
|
245
|
-
result = await client.submitAction(Action.move(target).toJSON() as any);
|
|
246
|
-
} catch {
|
|
247
|
-
await sleep(POLL_MS);
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
const actionResult = result?.data ?? result;
|
|
251
|
-
const durationSecs = Number(actionResult?.duration_secs ?? result?.duration_secs ?? 2);
|
|
252
|
-
debug(`opening move submitted target=(${target.x},${target.y}) duration=${durationSecs}`);
|
|
253
|
-
helperMovingUntil = Date.now() + Math.max(0.5, durationSecs) * 1000 + 500;
|
|
254
|
-
}
|
|
255
|
-
} finally {
|
|
256
|
-
debug('opening mover stopped');
|
|
257
|
-
cleanupPid(path);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export function spawnOpeningMover(): { pid: number; logFile: string } {
|
|
262
|
-
const profile = activeProfile();
|
|
263
|
-
const stateDir = getProfileStateDir(profile);
|
|
264
|
-
mkdirSync(stateDir, { recursive: true });
|
|
265
|
-
|
|
266
|
-
const path = join(stateDir, 'opening-mover.pid');
|
|
267
|
-
if (existsSync(path)) {
|
|
268
|
-
try {
|
|
269
|
-
const pid = Number(readFileSync(path, 'utf8').trim());
|
|
270
|
-
if (pid > 0 && isPidAlive(pid)) return { pid, logFile: '' };
|
|
271
|
-
} catch {}
|
|
272
|
-
try { unlinkSync(path); } catch {}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const logsDir = getProfileLogsDir(profile);
|
|
276
|
-
mkdirSync(logsDir, { recursive: true });
|
|
277
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
278
|
-
const logFile = join(logsDir, `opening-mover-${timestamp}.log`);
|
|
279
|
-
const logFd = openSync(logFile, 'a');
|
|
280
|
-
|
|
281
|
-
const binEntry = resolve(__dirname, '..', '..', 'bin', 'clawclaw-cli.mjs').replace(/\\/g, '/');
|
|
282
|
-
if (!existsSync(binEntry)) throw new Error('clawclaw-cli bin entry not found');
|
|
283
|
-
|
|
284
|
-
let npmGlobalModules = '';
|
|
285
|
-
try { npmGlobalModules = execSync('npm root -g', { encoding: 'utf8', windowsHide: true }).trim(); } catch {}
|
|
286
|
-
const nodePath = [npmGlobalModules, process.env.NODE_PATH ?? ''].filter(Boolean).join(delimiter);
|
|
287
|
-
const isWin = process.platform === 'win32';
|
|
288
|
-
|
|
289
|
-
const child = spawn(process.execPath, [binEntry, '_opener'], {
|
|
290
|
-
detached: true,
|
|
291
|
-
stdio: isWin ? ['ignore', 'ignore', 'ignore'] : ['ignore', logFd, logFd],
|
|
292
|
-
cwd: process.cwd(),
|
|
293
|
-
env: {
|
|
294
|
-
...process.env,
|
|
295
|
-
NODE_PATH: nodePath,
|
|
296
|
-
...(isWin ? { CLAWCLAW_LOG_FILE: logFile } : {}),
|
|
297
|
-
},
|
|
298
|
-
windowsHide: true,
|
|
299
|
-
});
|
|
300
|
-
child.unref();
|
|
301
|
-
if (child.pid) writeFileSync(path, String(child.pid));
|
|
302
|
-
return { pid: child.pid!, logFile };
|
|
303
|
-
}
|