Skip to content
Merged
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
27 changes: 27 additions & 0 deletions apps/mapgen-studio/src/app/StudioShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ export function StudioShell(props: StudioShellProps) {
// shapes are unchanged so `setupControlOptions` below consumes them as before.
const { savedSetupConfigs, setupCatalog } = useSetupDataQueries();
const [autoplayActionRunning, setAutoplayActionRunning] = useState(false);
const [exploreActionRunning, setExploreActionRunning] = useState(false);
const saveDeployRunning = saveDeployOperation?.status === "running";
const runInGameRunning = runInGameOperation?.status === "running";

Expand Down Expand Up @@ -1752,6 +1753,30 @@ export function StudioShell(props: StudioShellProps) {
}
}, [autoplayActionRunning, browserRunning, liveRuntime.autoplayActive, runInGameRunning, saveDeployRunning, toast]);

/**
* Explore (reveal the map) in the live game via the canonical
* `display.explore.request` control procedure — the studio's map-QA verb.
* The grant stays held (fog does not re-cover) for a disposable studio
* session; player 0 is the canonical local player, matching the CLI.
*/
const handleExplore = useCallback(async () => {
if (exploreActionRunning || browserRunning || runInGameRunning || saveDeployRunning) return;
setExploreActionRunning(true);
try {
const result = await liveControlPort.display.explore.request({ playerId: 0 });
toast(
result.classification === "already-explored"
? "Live map already fully revealed"
: `Live map revealed — ${result.grantedPlots} plots granted`,
{ variant: "success" },
);
} catch (err) {
toast(`Explore failed: ${err instanceof Error ? err.message : "live game unavailable"}`, { variant: "error" });
} finally {
setExploreActionRunning(false);
}
}, [browserRunning, exploreActionRunning, runInGameRunning, saveDeployRunning, toast]);

const copyRunInGameDiagnostics = useCallback(async () => {
if (!runInGameOperation) return;
try {
Expand Down Expand Up @@ -2302,6 +2327,8 @@ export function StudioShell(props: StudioShellProps) {
onSyncFromLiveGame={syncStudioFromLiveGame}
isAutoplayActionRunning={autoplayActionRunning}
onToggleAutoplay={handleToggleAutoplay}
onExplore={handleExplore}
isExploreActionRunning={exploreActionRunning}
operationControlsDisabled={browserRunning || runInGameRunning || saveDeployRunning}
isRunInGameRunning={runInGameRunning}
runInGameStatus={runInGameOperation}
Expand Down
35 changes: 35 additions & 0 deletions apps/mapgen-studio/src/lib/control/liveControlPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,40 @@ export type LiveControlReadinessResult = Awaited<
ReturnType<StudioCiv7ControlOrpcClient["readiness"]["current"]>
>;

export type LiveControlExploreRequestInput = Parameters<
StudioCiv7ControlOrpcClient["display"]["explore"]["request"]
>[0];

export type LiveControlExploreRequestResult = Awaited<
ReturnType<StudioCiv7ControlOrpcClient["display"]["explore"]["request"]>
>;

/**
* The studio's typed seam onto the canonical Civ7 control-oRPC surface.
*
* Why a port instead of passing the raw client around: control procedures
* are a pinned taxonomy (`display.*`, `view.*`, `readiness.*`) that must be
* called through the canonical router client — never re-exposed as bespoke
* studio routes. The port names exactly the procedures the shell consumes,
* mirrors the router's path shape, and keeps test seams trivial (hand a
* fake port, not a fake transport).
*/
export type LiveControlPort = Readonly<{
readiness: Readonly<{
current(): Promise<LiveControlReadinessResult>;
}>;
display: Readonly<{
explore: Readonly<{
/**
* Reveal the map for a player via a tracked visibility grant
* (`display.explore.request`): suspends the display queue with
* readback verification so discovery popups don't storm the UI,
* drains gameplay discovery, and reports before/after visibility
* probes plus an explored/already-explored/unverified classification.
*/
request(input: LiveControlExploreRequestInput): Promise<LiveControlExploreRequestResult>;
}>;
}>;
}>;

export function createBoundLiveControlPort(
Expand All @@ -20,6 +50,11 @@ export function createBoundLiveControlPort(
readiness: {
current: () => client.readiness.current({}),
},
display: {
explore: {
request: (input) => client.display.explore.request(input),
},
},
};
}

Expand Down
25 changes: 18 additions & 7 deletions apps/mapgen-studio/src/ui/components/GameConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ export interface GameConsoleProps {
/** Callback to start or stop Civ7 native autoplay */
onToggleAutoplay?: () => void;
/**
* Callback for the Explore command (tile visibility/exploration in the
* live game). The button is a disabled placeholder until this is wired.
* Callback for the Explore command: reveal the full map in the live game
* via the canonical `display.explore.request` control procedure (UI
* display queue suppressed while gameplay discovers the tiles). The
* button renders disabled until a handler is wired.
*/
onExplore?: () => void;
/** Whether a map explore (reveal) request is in flight */
isExploreActionRunning?: boolean;
/** Whether any studio/game operation is in flight (shared control gating) */
operationControlsDisabled: boolean;
/** Whether Civ7 Run in Game is currently running */
Expand Down Expand Up @@ -99,6 +103,7 @@ export const GameConsole: React.FC<GameConsoleProps> = ({
isAutoplayActionRunning = false,
onToggleAutoplay,
onExplore,
isExploreActionRunning = false,
operationControlsDisabled,
isRunInGameRunning,
runInGameStatus,
Expand Down Expand Up @@ -181,9 +186,11 @@ export const GameConsole: React.FC<GameConsoleProps> = ({
: liveRuntime?.autoplayActive
? `Stop Civ7 autoplay${liveRuntime.autoplayPaused ? " (paused)" : ""}`
: "Start Civ7 autoplay";
const exploreTitle = onExplore
? "Explore: toggle tile visibility in the live game"
: "Explore: tile visibility control is not yet available";
const exploreTitle = !onExplore
? "Explore: tile visibility control is not yet available"
: isExploreActionRunning
? "Explore request in flight"
: "Explore: reveal the full map in the live game";
const saveDeployLabel = saveDeployStatus ? formatMapConfigSaveDeployPhaseLabel(saveDeployStatus.phase) : null;
const saveDeployActive = saveDeployStatus?.status === "running";
const saveDeployDotClass =
Expand Down Expand Up @@ -300,12 +307,16 @@ export const GameConsole: React.FC<GameConsoleProps> = ({
variant="outline"
size="icon"
onClick={onExplore}
disabled={operationControlsDisabled || liveRuntime?.status !== "ok" || !onExplore}
disabled={operationControlsDisabled || isExploreActionRunning || liveRuntime?.status !== "ok" || !onExplore}
aria-label={exploreTitle}
title={exploreTitle}
className="shrink-0">

<ScanEye className="w-3.5 h-3.5" />
{isExploreActionRunning ? (
<LoaderCircle className="w-3.5 h-3.5 animate-spin" />
) : (
<ScanEye className="w-3.5 h-3.5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>{exploreTitle}</TooltipContent>
Expand Down
19 changes: 19 additions & 0 deletions apps/mapgen-studio/test/runInGame/GameConsole.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,25 @@ describe("GameConsole live runtime and save/deploy", () => {
expect(html).toContain("Explore: tile visibility control is not yet available");
});

it("renders the wired Explore reveal action when live and a handler exists", () => {
const html = renderConsole({
liveRuntime: { status: "ok", readiness: "ready" },
onExplore: vi.fn(),
});

expect(html).toContain("Explore: reveal the full map in the live game");
});

it("disables Explore and narrates the in-flight request while revealing", () => {
const html = renderConsole({
liveRuntime: { status: "ok", readiness: "ready" },
onExplore: vi.fn(),
isExploreActionRunning: true,
});

expect(html).toContain("Explore request in flight");
});

it("highlights live seed status when a proved live game is out of sync with Studio", () => {
const html = renderConsole({
liveRuntime: { status: "ok", turn: 12, seed: 123 },
Expand Down