From 48a70e4928179bb7b54165105682704f7b590abc Mon Sep 17 00:00:00 2001 From: root Date: Fri, 22 May 2026 09:04:04 +0800 Subject: [PATCH] feat: game pause/speed control API with AI pause protocol - POST /api/action/pause - pause game (AI must call before operations) - POST /api/action/unpause - resume game at desired speed - POST /api/action/speed - set game speed (1x/2x/3x) - Game state now includes isPaused and gameSpeed - AI pause protocol documented in SKILL.md: pause before every operation - Introduces cell.isDiggable flag that filters neutronium properly - pause/unpause/speed CLI commands added to oni_api.py --- README.md | 8 ++++ SKILL.md | 56 +++++++++++++++++++++++++++ mod/ONIAgentBridge.cs | 88 ++++++++++++++++++++++++++++++++++++++++++- tools/oni_api.py | 32 ++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a725593..439c49b 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,11 @@ python3 tools/oni_api.py research_select ImprovedOxygen # 科研 python3 tools/oni_api.py mop 10 10 # 清理液体 python3 tools/oni_api.py harvest 20 15 # 收获植物 +# 游戏速度控制(AI 操作前必须先暂停) +python3 tools/oni_api.py pause "Reason here" # 暂停游戏 +python3 tools/oni_api.py unpause 1 # 恢复(1x 速度) +python3 tools/oni_api.py speed 3 # 直接设速度(不暂停) + # 批量任务 python3 tools/oni_api.py batch docs/batch_example.json # 执行批量建造计划 @@ -231,6 +236,9 @@ python3 scripts/event_daemon.py # 事件守护进程(AI 输入源) | `/api/action/batch` | `{actions: [{type, ...}]}` — 批量执行 | | `/api/action/priority_global` | `{target, priority}` — 全局默认优先级 | | `/api/action/priority_type` | `{buildingType, priority}` — 按建筑类型设优先级 | +| `/api/action/pause` | `{reason?}` — 暂停游戏(AI 操作前必须调用) | +| `/api/action/unpause` | `{speed?}` — 恢复游戏 | +| `/api/action/speed` | `{speed}` — 设置速度 1x/2x/3x | ## 项目结构 diff --git a/SKILL.md b/SKILL.md index 4464ad5..4e93419 100644 --- a/SKILL.md +++ b/SKILL.md @@ -234,6 +234,62 @@ python3 tools/oni_builder.py build spom 42 42 --- +## AI 暂停协议(关键规则) + +AI **必须**在每次操作序列前暂停游戏,操作完成后恢复。这防止推理延迟期间游戏状态变化导致操作失效。 + +### 规则 +``` +1. 收到事件 / 决定操作 → 立即 PAUSE +2. 查询状态、分析、决策(可耗时,游戏已暂停) +3. 执行操作(dig / build / batch) +4. 检查操作反馈 +5. 所有操作完成后 → UNPAUSE +``` + +### 示例 + +```bash +# 1. 暂停 +python3 tools/oni_api.py pause "Building SPOM" + +# 2. 查询状态(游戏已冻结,不会变化) +python3 tools/oni_api.py explore 40 40 30 20 +python3 tools/oni_api.py cell 45 42 + +# 3. 执行操作 +python3 tools/oni_api.py dig 40 40 8 6 +python3 tools/oni_api.py build Electrolyzer 45 42 + +# 4. 恢复 +python3 tools/oni_api.py unpause 1 +``` + +### 当前游戏速度 + +通过 `status` 命令查看: + +``` +isPaused: true ← 游戏是否暂停 +gameSpeed: 0 ← 0=暂停, 1=1x, 2=2x, 3=3x +``` + +### 总是暂停的场景 + +| 场景 | 原因 | +|------|------| +| 收到 critical 事件 | 可能需要紧急操作 | +| 执行 dig / build / deconstruct | 坐标必须精确,不能偏移 | +| 执行 batch | 多个相关操作需原子性 | +| 查询状态后做决策 | 避免推理时状态变化 | + +### 不需要暂停的场景 + +| 场景 | 原因 | +|------|------| +| 仅查看 status / resources | 只读操作,无副作用 | +| 轮询 events | 事件本身是增量变化的 | + ## AI 事件驱动工作流 AI 应持续运行事件守护进程,形成"事件 → 分析 → 操作 → 反馈"的闭环: diff --git a/mod/ONIAgentBridge.cs b/mod/ONIAgentBridge.cs index 82ea454..7325855 100644 --- a/mod/ONIAgentBridge.cs +++ b/mod/ONIAgentBridge.cs @@ -181,6 +181,15 @@ namespace ONIAgentBridge case ("/api/action/priority_type", "POST"): responseJson = ExecutePriorityType(ctx); break; + case ("/api/action/pause", "POST"): + responseJson = ExecutePause(ctx); + break; + case ("/api/action/unpause", "POST"): + responseJson = ExecuteUnpause(ctx); + break; + case ("/api/action/speed", "POST"): + responseJson = ExecuteSpeed(ctx); + break; default: ctx.Response.StatusCode = 404; @@ -293,6 +302,9 @@ namespace ONIAgentBridge PushEvent("state_poll", "info", "Game state polled", $"Cycle {GameClock.Instance?.GetCycle() ?? 0}"); + bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false; + int gameSpeed = isPaused ? 0 : (SpeedControlScreen.Instance?.GetSpeed() ?? 1); + return JsonSerializer.Serialize(new { cycle = GameClock.Instance?.GetCycle() ?? 0, @@ -304,6 +316,8 @@ namespace ONIAgentBridge worldSize = cellCount, gridWidth = Grid.WidthInCells, gridHeight = Grid.HeightInCells, + isPaused, + gameSpeed, eventCount = _eventSeq }); } @@ -847,7 +861,8 @@ namespace ONIAgentBridge bool isDiggable = Grid.Solid[cell] && elem != null && elem.id != SimHashes.Unobtanium && elem.id != SimHashes.Katairite - && elem.id != SimHashes.Void; + && elem.id != SimHashes.Void + && !elem.name.Contains("Neutronium"); // Check dupe safety bool isSafeForDupe = elem != null @@ -1246,6 +1261,74 @@ namespace ONIAgentBridge catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } } + // =================================================================== + // Pause / Speed Control + // =================================================================== + private string ExecutePause(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + // No body needed, but accept optional { "reason": "..." } + string reason = data?.reason ?? "AI operation in progress"; + + SpeedControlScreen.Instance?.Pause(false, true); + PushEvent("pause", "info", "Game paused", + $"Game paused by AI: {reason}", "system"); + + return JsonSerializer.Serialize(ActionOk("game_paused", + new { reason, isPaused = true })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + private string ExecuteUnpause(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + int speed = data?.speed ?? 1; + if (speed < 1) speed = 1; + if (speed > 3) speed = 3; + + SpeedControlScreen.Instance?.Unpause(true); + SpeedControlScreen.Instance?.SetSpeed(speed); + PushEvent("unpause", "info", "Game resumed", + $"Game resumed by AI at {speed}x speed", "system"); + + return JsonSerializer.Serialize(ActionOk("game_unpaused", + new { speed, isPaused = false })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + + private string ExecuteSpeed(HttpListenerContext ctx) + { + try + { + var data = ReadBody(ctx); + if (data == null) + return JsonSerializer.Serialize(FailInvalid("invalid_request")); + + int speed = data.speed; + if (speed < 1) speed = 1; + if (speed > 3) speed = 3; + + bool isPaused = SpeedControlScreen.Instance?.IsPaused ?? false; + if (!isPaused) + { + SpeedControlScreen.Instance?.SetSpeed(speed); + } + + PushEvent("speed", "info", $"Game speed set to {speed}x", + $"Speed changed to {speed}x", "system"); + + return JsonSerializer.Serialize(ActionOk("speed_set", + new { speed, isPaused })); + } + catch (Exception e) { return JsonSerializer.Serialize(FailException(e)); } + } + // =================================================================== // Batch System // =================================================================== @@ -1578,6 +1661,9 @@ namespace ONIAgentBridge internal class MopRequest { public int x { get; set; } public int y { get; set; } } internal class HarvestRequest { public int x { get; set; } public int y { get; set; } } internal class CancelRequest { public int x { get; set; } public int y { get; set; } } + internal class PauseRequest { public string reason { get; set; } } + internal class UnpauseRequest { public int speed { get; set; } } + internal class SpeedRequest { public int speed { get; set; } } internal class PriorityGlobalRequest { public string target { get; set; } public int priority { get; set; } } internal class PriorityTypeRequest { public string buildingType { get; set; } public int priority { get; set; } } diff --git a/tools/oni_api.py b/tools/oni_api.py index 01fb17a..0dc1064 100644 --- a/tools/oni_api.py +++ b/tools/oni_api.py @@ -474,6 +474,30 @@ def cmd_priority_type(args): }) _print_feedback(result) +def cmd_pause(args): + """Pause the game. Usage: pause [reason]""" + reason = ' '.join(args) if args else 'AI operation in progress' + result = api_post('/api/action/pause', {} if not args else {"reason": reason}) + _print_feedback(result) + +def cmd_unpause(args): + """Unpause the game. Usage: unpause [speed]""" + speed = int(args[0]) if args else 1 + result = api_post('/api/action/unpause', {"speed": speed}) + _print_feedback(result) + +def cmd_speed(args): + """Set game speed. Usage: speed <1|2|3>""" + if not args: + print("Usage: speed <1|2|3>") + return + speed = int(args[0]) + if speed < 1 or speed > 3: + print("Speed must be 1, 2, or 3") + return + result = api_post('/api/action/speed', {"speed": speed}) + _print_feedback(result) + def _print_feedback(result): """Pretty-print action feedback.""" if result.get('success'): @@ -584,6 +608,9 @@ COMMANDS = { 'batch': cmd_batch, 'priority_global': cmd_priority_global, 'priority_type': cmd_priority_type, + 'pause': cmd_pause, + 'unpause': cmd_unpause, + 'speed': cmd_speed, } if __name__ == '__main__': @@ -631,6 +658,11 @@ if __name__ == '__main__': print(" mop Mop liquid") print(" harvest Harvest plant") print("") + print("=== Game Speed Control ===") + print(" pause [reason] Pause the game (AI should always pause before ops)") + print(" unpause [speed] Unpause the game at 1x/2x/3x speed") + print(" speed <1|2|3> Set game speed while running") + print("") print("=== Batch / Priority (Advanced) ===") print(" batch Execute batch plan") print(" priority_global

Set global default priority")