From 39c163fd4e5634a466d962857c15126efe20aa8f Mon Sep 17 00:00:00 2001 From: romgenie <5861166+romgenie@users.noreply.github.com> Date: Sun, 10 May 2026 16:13:53 -0400 Subject: [PATCH] Allow Timeline advance to reveal obtained epochs --- McpMod.Actions.cs | 94 ++++++++++++++++++++++++++++++++++++++---- docs/raw-full.md | 5 +++ docs/raw-simplified.md | 2 +- mcp/server.py | 5 +++ 4 files changed, 96 insertions(+), 10 deletions(-) diff --git a/McpMod.Actions.cs b/McpMod.Actions.cs index 7a537665..7e95514a 100644 --- a/McpMod.Actions.cs +++ b/McpMod.Actions.cs @@ -1221,16 +1221,13 @@ public static partial class McpMod if (queuedUnlockResult != null) return queuedUnlockResult; - var unrevealedEpochs = GetProgressEpochIdsByState("Obtained", "ObtainedNoSlot"); + var unrevealedEpochs = GetProgressEpochIdsByState("Obtained"); if (unrevealedEpochs.Count > 0) - return new Dictionary - { - ["status"] = "ok", - ["message"] = "Epoch unlocks are obtained but not revealed; not forcing timeline reveal from automation", - ["pending_epoch_ids"] = unrevealedEpochs, - ["manual_action_required"] = true, - ["done"] = true - }; + return TryRevealVisibleTimelineEpoch(timelineScreen, unrevealedEpochs); + + var noSlotEpochs = GetProgressEpochIdsByState("ObtainedNoSlot"); + if (noSlotEpochs.Count > 0) + return TimelineUnlocksNeedManualRevealFromOpenTimeline(noSlotEpochs); return new Dictionary { ["status"] = "ok", ["message"] = "No more epochs to advance", ["done"] = true }; } @@ -1388,6 +1385,85 @@ public static partial class McpMod return Error("Not on a menu screen"); } + private static Dictionary TryRevealVisibleTimelineEpoch( + NTimelineScreen timelineScreen, + List unrevealedEpochs) + { + var pendingSet = new HashSet(unrevealedEpochs, System.StringComparer.OrdinalIgnoreCase); + var revealableSlots = FindAll(timelineScreen) + .Where(slot => + { + try + { + var epochId = slot.model?.Id; + return slot.State == EpochSlotState.Obtained && + epochId != null && + pendingSet.Contains(epochId) && + slot.HasSpawned && + IsNodeVisible(slot) && + slot is NClickableControl; + } + catch (System.ObjectDisposedException) + { + return false; + } + }) + .OrderBy(slot => slot.GlobalPosition.Y) + .ThenBy(slot => slot.GlobalPosition.X) + .ToList(); + + if (revealableSlots.Count == 0) + { + var message = IsTimelineScreenBusy(timelineScreen) + ? "Timeline has pending epoch reveals, but the revealable epoch slot is still spawning; retry after the next state poll" + : "Timeline has pending epoch reveals, but no actionable obtained epoch slot is visible yet; retry after the next state poll"; + + return new Dictionary + { + ["status"] = "ok", + ["message"] = message, + ["pending_epoch_ids"] = unrevealedEpochs, + ["retry"] = true + }; + } + + var slotToReveal = revealableSlots[0]; + var epochId = slotToReveal.model.Id; + try + { + ((NClickableControl)slotToReveal).ForceClick(); + return new Dictionary + { + ["status"] = "ok", + ["message"] = $"Revealing timeline epoch {epochId}", + ["revealed_epoch_id"] = epochId, + ["pending_epoch_ids"] = unrevealedEpochs + }; + } + catch (System.ObjectDisposedException) + { + return new Dictionary + { + ["status"] = "ok", + ["message"] = "Timeline changed before the epoch reveal could be clicked; retry after the next state poll", + ["pending_epoch_ids"] = unrevealedEpochs, + ["retry"] = true + }; + } + } + + private static Dictionary TimelineUnlocksNeedManualRevealFromOpenTimeline(List unrevealedEpochs) + { + return new Dictionary + { + ["status"] = "ok", + ["message"] = "Timeline has obtained epochs without visible slots; no revealable epoch slot is available through automation", + ["pending_epoch_ids"] = unrevealedEpochs, + ["manual_action_required"] = true, + ["done"] = true + }; + } + private static Dictionary TimelineUnlocksNeedManualReveal(List unrevealedEpochs) { return new Dictionary diff --git a/docs/raw-full.md b/docs/raw-full.md index b01ab1f6..8269cb78 100644 --- a/docs/raw-full.md +++ b/docs/raw-full.md @@ -951,6 +951,11 @@ Select an option from the main menu, a menu submenu, profile select, character s `game_over` advertises only `main_menu`. `continue` is not actionable on that screen and returns an error. If `timeline` is blocked by pending obtained epochs, `menu_select` returns an error with `manual_action_required: true` and `pending_epoch_ids` instead of opening Timeline. +When the game is already on `menu_screen: "timeline"`, `menu_select` with `option: "advance"` progresses safe Timeline UI steps: +tutorial/confirm/proceed buttons, queued unlock screens, epoch inspect close buttons, and visible obtained epoch slots. +If the Timeline is still animating, or the obtained epoch slot has not spawned yet, the response includes `retry: true` with `pending_epoch_ids`. +Pending `ObtainedNoSlot` epochs still return `manual_action_required: true` and `done: true` if no queued Timeline expansion screen or visible slot is available. +The main-menu `timeline` guard remains in place because entering Timeline from that state can still trigger invalid game unlock-state errors. --- diff --git a/docs/raw-simplified.md b/docs/raw-simplified.md index 1ff7b9ba..5d8a1200 100644 --- a/docs/raw-simplified.md +++ b/docs/raw-simplified.md @@ -56,7 +56,7 @@ All POST requests use JSON body with `"action"` field. All responses include `{ | Action | Parameters | When to Use | |---|---|---| -| `menu_select` | `option`: string, `seed`?: string | Choose an advertised menu option. Options are case-insensitive. Submenus include `back` where visible, including `profile_select` options `profile_1`, `profile_2`, `profile_3`, and `back`. Blocking popups expose normalized button labels such as `ignore` or `back`. `game_over` supports `main_menu` only; `continue` returns an error. Supplying `seed` in unsupported contexts such as standard singleplayer character select returns an error and does not start a run. If Timeline has pending obtained epochs that require manual reveal, it may appear in `blocked_options`; selecting `timeline` returns `manual_action_required: true` with `pending_epoch_ids` instead of opening Timeline. Multiplayer flow: on `multiplayer_join` use `refresh` / `back` / `join_` / `join_`. On `multiplayer_load_lobby` use `confirm` (or `embark`) to ready up, `unready` to retract, `back` to leave. On `character_select` while in MP, an additional `unready` option becomes available after readying, plus a `lobby` block in state lists ascension, all_ready, and per-player roster. | +| `menu_select` | `option`: string, `seed`?: string | Choose an advertised menu option. Options are case-insensitive. Submenus include `back` where visible, including `profile_select` options `profile_1`, `profile_2`, `profile_3`, and `back`. Blocking popups expose normalized button labels such as `ignore` or `back`. `game_over` supports `main_menu` only; `continue` returns an error. Supplying `seed` in unsupported contexts such as standard singleplayer character select returns an error and does not start a run. If Timeline has pending obtained epochs that require manual reveal, it may appear in `blocked_options`; selecting `timeline` returns `manual_action_required: true` with `pending_epoch_ids` instead of opening Timeline. Once already on `menu_screen: "timeline"`, `advance` can reveal a visible obtained epoch slot; animation or spawn waits return `retry: true` with `pending_epoch_ids`. Pending `ObtainedNoSlot` epochs still return `manual_action_required: true` if no queued expansion or visible slot is available. Multiplayer flow: on `multiplayer_join` use `refresh` / `back` / `join_` / `join_`. On `multiplayer_load_lobby` use `confirm` (or `embark`) to ready up, `unready` to retract, `back` to leave. On `character_select` while in MP, an additional `unready` option becomes available after readying, plus a `lobby` block in state lists ascension, all_ready, and per-player roster. | ### Profiles diff --git a/mcp/server.py b/mcp/server.py index 5716310a..e50634a7 100644 --- a/mcp/server.py +++ b/mcp/server.py @@ -161,6 +161,11 @@ async def menu_select(option: str, seed: str | None = None) -> str: multiplayer load lobby (resume saved co-op run), character select for SP and MP (with `unready` once readied in MP), profile switching, timeline controls, tutorial prompts, blocking popups, and game-over main-menu return. + Timeline entry from the main menu may still return `manual_action_required` + when obtained epochs are pending, but `advance` on an already-open Timeline + can reveal visible obtained epoch slots and may return `retry` while the + screen is animating. Pending epochs without visible slots still return + `manual_action_required`. Multiplayer flow tips: - On menu_screen "multiplayer_join", use refresh / back / join_ /