Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 85 additions & 9 deletions McpMod.Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object?>
{
["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<string, object?> { ["status"] = "ok", ["message"] = "No more epochs to advance", ["done"] = true };
}
Expand Down Expand Up @@ -1388,6 +1385,85 @@ public static partial class McpMod
return Error("Not on a menu screen");
}

private static Dictionary<string, object?> TryRevealVisibleTimelineEpoch(
NTimelineScreen timelineScreen,
List<string> unrevealedEpochs)
{
var pendingSet = new HashSet<string>(unrevealedEpochs, System.StringComparer.OrdinalIgnoreCase);
var revealableSlots = FindAll<NEpochSlot>(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<string, object?>
{
["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<string, object?>
{
["status"] = "ok",
["message"] = $"Revealing timeline epoch {epochId}",
["revealed_epoch_id"] = epochId,
["pending_epoch_ids"] = unrevealedEpochs
};
}
catch (System.ObjectDisposedException)
{
return new Dictionary<string, object?>
{
["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<string, object?> TimelineUnlocksNeedManualRevealFromOpenTimeline(List<string> unrevealedEpochs)
{
return new Dictionary<string, object?>
{
["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<string, object?> TimelineUnlocksNeedManualReveal(List<string> unrevealedEpochs)
{
return new Dictionary<string, object?>
Expand Down
5 changes: 5 additions & 0 deletions docs/raw-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down
2 changes: 1 addition & 1 deletion docs/raw-simplified.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<index>` / `join_<player_id>`. 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_<index>` / `join_<player_id>`. 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

Expand Down
5 changes: 5 additions & 0 deletions mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_<index> /
Expand Down