COMMIT readies the mission; LAUNCH belongs to the operator
The pod bay has a ritual: the mission is committed, every pod preps up to - but not past - the launch, and then the operator presses the button. The prep hold is not dead time. The MFDs and the map buttons are alive before the mission starts, so pilots use the window to set their maps and presets, and the wait itself is part of the game's social fabric. Cyd asked for the ritual back, and the engine turned out to have kept most of it: the console already feeds the eggs, watches every pod climb through loading to WaitingForLaunch, and held the run until the room was staged - it just fired RunMission itself the instant that was true. Now it arms instead of firing. The host's menu button reads COMMIT while hosting (a plain single-player launch is still a launch, and commits with no remote pods still start themselves - a lone pilot has nobody to wait for). Committing marshals everyone exactly as before, and a small commit board comes up over the 3D view: one row per pod, the host first, each saying what its machine is doing - connecting, waiting for egg, loading, READY - bright when ready, dim while not, so who the room is waiting on reads at a glance. When every system is ready the board's LAUNCH button lights, the operator presses it, the run goes out over the same wire and local delivery as always, and the board gets out of the way - the host has a race to fly. If a pod falls back out of readiness before the press, the button disarms rather than launching a room no longer whole. The board lives entirely on the game thread, created and clicked inside ConsoleTick, so the game's own message pump delivers its input - the same pump that keeps the MFDs alive through the hold is what makes the button work, which is fitting. WS_EX_NOACTIVATE keeps the game window focused throughout: PadRIO controls answer only while it is, and launching the race must not cost the operator their controls. The board repaints only when a state actually changes, and every teardown path destroys it - InstallCommon, the run transition, and the mission-ended-otherwise exit - so no board survives into a race or out of one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+326
-13
@@ -446,6 +446,245 @@ namespace
|
||||
return True;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The commit board: the pod bay ritual, on screen.
|
||||
//
|
||||
// In the pod bay the mission is COMMITTED first - every pod preps
|
||||
// up to, but not past, the launch - and then the operator presses
|
||||
// the launch button. The prep window is not dead time: the MFDs
|
||||
// and map buttons are alive before the mission starts, so pilots
|
||||
// use it to set maps and presets, and the wait is part of the
|
||||
// game's social fabric.
|
||||
//
|
||||
// This is that ritual for a lobby race. The host's menu button
|
||||
// says COMMIT; committing marshals everyone exactly as before, but
|
||||
// where the console used to fire RunMission the instant all pods
|
||||
// staged, it now arms a LAUNCH button on a small status board and
|
||||
// waits for the operator. The board lists every pod and what it is
|
||||
// doing - connecting, loading, READY - so the host can see who the
|
||||
// room is waiting on, and it gets out of the way the moment the
|
||||
// mission drops.
|
||||
//
|
||||
// Game thread throughout: created, painted, clicked and destroyed
|
||||
// inside ConsoleTick, so the game's own message pump (already
|
||||
// alive - that is why the MFDs work) delivers its input, and no
|
||||
// state crosses a thread. WS_EX_NOACTIVATE keeps the game window
|
||||
// focused: PadRIO controls answer only while it is, and a launch
|
||||
// click must not cost the host their controls.
|
||||
//---------------------------------------------------------------
|
||||
|
||||
HWND gStatusWindow = NULL;
|
||||
Logical gLaunchArmed = False; // everyone staged: button lit
|
||||
Logical gLaunchRequested = False; // the operator pressed it
|
||||
RECT gLaunchRect; // client coords, hit-tested
|
||||
int gShownStates[maxRemotePods + 2];
|
||||
|
||||
const COLORREF kBoardGreen = RGB(64, 255, 64);
|
||||
const COLORREF kBoardGreenDim = RGB(24, 140, 24);
|
||||
|
||||
const char *StateWord(int state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case -1: return "connecting";
|
||||
case Application::WaitingForEgg: return "waiting for egg";
|
||||
case Application::CreatingMission:
|
||||
case Application::LoadingMission: return "loading";
|
||||
case Application::WaitingForLaunch: return "READY";
|
||||
case Application::LaunchingMission:
|
||||
case Application::RunningMission: return "running";
|
||||
default: return "starting";
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK StatusBoardWndProc(
|
||||
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_MOUSEACTIVATE:
|
||||
// take the click, leave the focus with the game
|
||||
return MA_NOACTIVATE;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_LBUTTONDOWN:
|
||||
{
|
||||
int x = (int)(short) LOWORD(lParam);
|
||||
int y = (int)(short) HIWORD(lParam);
|
||||
|
||||
if (gLaunchArmed &&
|
||||
x >= gLaunchRect.left && x < gLaunchRect.right &&
|
||||
y >= gLaunchRect.top && y < gLaunchRect.bottom)
|
||||
{
|
||||
gLaunchRequested = True;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_PAINT:
|
||||
{
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hwnd, &ps);
|
||||
RECT client;
|
||||
GetClientRect(hwnd, &client);
|
||||
|
||||
HDC mem = CreateCompatibleDC(hdc);
|
||||
HBITMAP surface = CreateCompatibleBitmap(
|
||||
hdc, client.right, client.bottom);
|
||||
HGDIOBJ old_surface = SelectObject(mem, surface);
|
||||
|
||||
FillRect(mem, &client,
|
||||
(HBRUSH) GetStockObject(BLACK_BRUSH));
|
||||
HBRUSH frame = CreateSolidBrush(kBoardGreenDim);
|
||||
FrameRect(mem, &client, frame);
|
||||
DeleteObject(frame);
|
||||
|
||||
HFONT font = CreateFontA(-14, 0, 0, 0, FW_NORMAL,
|
||||
FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
|
||||
CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY,
|
||||
DEFAULT_PITCH | FF_MODERN, "Consolas");
|
||||
HGDIOBJ old_font = SelectObject(mem, font);
|
||||
SetBkMode(mem, TRANSPARENT);
|
||||
|
||||
int row_h = 20;
|
||||
RECT line;
|
||||
line.left = 10;
|
||||
line.right = client.right - 10;
|
||||
line.top = 8;
|
||||
line.bottom = line.top + row_h;
|
||||
|
||||
SetTextColor(mem, kBoardGreen);
|
||||
DrawTextA(mem, "MISSION COMMITTED", -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
line.top += row_h + 4;
|
||||
line.bottom += row_h + 4;
|
||||
|
||||
//
|
||||
// One row per pod: the host first, then the remotes in
|
||||
// [pilots] order. READY rows bright, the rest dim, so
|
||||
// who the room is waiting on reads at a glance.
|
||||
//
|
||||
char text[96];
|
||||
int local_state = (application != NULL)
|
||||
? application->GetApplicationState() : -1;
|
||||
|
||||
sprintf(text, "%-14s %s",
|
||||
(gPilotNameCount > 0) ? gPilotNames[0] : "HOST",
|
||||
StateWord(local_state));
|
||||
SetTextColor(mem,
|
||||
(local_state == Application::WaitingForLaunch)
|
||||
? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem, text, -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
line.top += row_h;
|
||||
line.bottom += row_h;
|
||||
|
||||
const char *name = (i + 1 < gPilotNameCount)
|
||||
? gPilotNames[i + 1] : gRemotePods[i].address;
|
||||
|
||||
sprintf(text, "%-14s %s", name,
|
||||
StateWord(gRemotePods[i].state));
|
||||
SetTextColor(mem,
|
||||
(gRemotePods[i].state == Application::WaitingForLaunch)
|
||||
? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem, text, -1, &line,
|
||||
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
|
||||
}
|
||||
|
||||
//
|
||||
// The operator's button. Lit only when every system is
|
||||
// ready - the same condition that used to fire the run
|
||||
// automatically.
|
||||
//
|
||||
gLaunchRect.left = 10;
|
||||
gLaunchRect.right = client.right - 10;
|
||||
gLaunchRect.bottom = client.bottom - 8;
|
||||
gLaunchRect.top = gLaunchRect.bottom - 26;
|
||||
|
||||
HBRUSH button = CreateSolidBrush(
|
||||
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
||||
FrameRect(mem, &gLaunchRect, button);
|
||||
DeleteObject(button);
|
||||
SetTextColor(mem,
|
||||
gLaunchArmed ? kBoardGreen : kBoardGreenDim);
|
||||
DrawTextA(mem,
|
||||
gLaunchArmed ? "L A U N C H" : "waiting for pods...",
|
||||
-1, &gLaunchRect,
|
||||
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
|
||||
|
||||
BitBlt(hdc, 0, 0, client.right, client.bottom,
|
||||
mem, 0, 0, SRCCOPY);
|
||||
SelectObject(mem, old_font);
|
||||
DeleteObject(font);
|
||||
SelectObject(mem, old_surface);
|
||||
DeleteObject(surface);
|
||||
DeleteDC(mem);
|
||||
EndPaint(hwnd, &ps);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcA(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void CreateStatusBoard()
|
||||
{
|
||||
static Logical class_registered = False;
|
||||
|
||||
if (!class_registered)
|
||||
{
|
||||
class_registered = True;
|
||||
WNDCLASSA window_class;
|
||||
memset(&window_class, 0, sizeof(window_class));
|
||||
window_class.lpfnWndProc = StatusBoardWndProc;
|
||||
window_class.hInstance = GetModuleHandleA(NULL);
|
||||
window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
||||
window_class.lpszClassName = "RPCommitBoard";
|
||||
RegisterClassA(&window_class);
|
||||
}
|
||||
|
||||
int height = 8 + 24 + (gRemotePodCount + 1) * 20 + 12 + 26 + 8;
|
||||
int width = 280;
|
||||
|
||||
//
|
||||
// Top-right of the game window, inset a little - over the 3D
|
||||
// view, clear of the instrument panes along the edges.
|
||||
//
|
||||
RECT game;
|
||||
GetWindowRect(ghWnd, &game);
|
||||
|
||||
gStatusWindow = CreateWindowExA(
|
||||
WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW,
|
||||
"RPCommitBoard", "", WS_POPUP,
|
||||
game.right - width - 48, game.top + 64,
|
||||
width, height,
|
||||
ghWnd, NULL, GetModuleHandleA(NULL), NULL);
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
ShowWindow(gStatusWindow, SW_SHOWNOACTIVATE);
|
||||
}
|
||||
for (int i = 0; i < maxRemotePods + 2; ++i)
|
||||
{
|
||||
gShownStates[i] = -999;
|
||||
}
|
||||
}
|
||||
|
||||
void DestroyStatusBoard()
|
||||
{
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
DestroyWindow(gStatusWindow);
|
||||
gStatusWindow = NULL;
|
||||
}
|
||||
gLaunchArmed = False;
|
||||
gLaunchRequested = False;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// The game-thread tick: state reporting + engine-safe execution
|
||||
//---------------------------------------------------------------
|
||||
@@ -486,32 +725,104 @@ namespace
|
||||
}
|
||||
|
||||
//
|
||||
// Everyone staged: launch the race everywhere
|
||||
// The commit board, from the moment there is anything to
|
||||
// show. It repaints only when a state actually changes -
|
||||
// this ticks every frame.
|
||||
//
|
||||
if (gRemotePodCount > 0 && !gRunSent)
|
||||
{
|
||||
if (gStatusWindow == NULL)
|
||||
{
|
||||
CreateStatusBoard();
|
||||
}
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
Logical changed = (gShownStates[0] != state);
|
||||
|
||||
gShownStates[0] = state;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
if (gShownStates[i + 1] != gRemotePods[i].state)
|
||||
{
|
||||
gShownStates[i + 1] = gRemotePods[i].state;
|
||||
changed = True;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Everyone staged: ARM. The console used to fire the run
|
||||
// itself right here; the run belongs to the operator now,
|
||||
// pod bay fashion, and the prep hold is the point - the
|
||||
// MFDs and map buttons are alive, so pilots set maps and
|
||||
// presets before the drop.
|
||||
//
|
||||
if (!gRunSent &&
|
||||
state == Application::WaitingForLaunch &&
|
||||
AllRemotesInState(Application::WaitingForLaunch))
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: all pods staged - RUN\n" << std::flush;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
if (!gLaunchArmed)
|
||||
{
|
||||
Application::RunMissionMessage run;
|
||||
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
||||
&run, (int) run.messageLength);
|
||||
gLaunchArmed = True;
|
||||
DEBUG_STREAM << "LocalConsole: all pods staged - "
|
||||
<< "committed, LAUNCH is the operator's\n"
|
||||
<< std::flush;
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// A solo commit (no remote pods) launches itself: with
|
||||
// nobody to wait for there is no board, and the old
|
||||
// instant start is what a lone pilot expects.
|
||||
//
|
||||
if (gLaunchRequested || gRemotePodCount == 0)
|
||||
{
|
||||
DEBUG_STREAM << "LocalConsole: RUN\n" << std::flush;
|
||||
for (int i = 0; i < gRemotePodCount; ++i)
|
||||
{
|
||||
Application::RunMissionMessage run;
|
||||
SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID,
|
||||
&run, (int) run.messageLength);
|
||||
}
|
||||
Application::RunMissionMessage local_run;
|
||||
DeliverLocal(
|
||||
NetworkClient::ApplicationClientID,
|
||||
&local_run,
|
||||
(int) local_run.messageLength
|
||||
);
|
||||
gRunSent = True;
|
||||
|
||||
// out of the way: the host has a race to fly
|
||||
DestroyStatusBoard();
|
||||
}
|
||||
}
|
||||
else if (gLaunchArmed && !gRunSent)
|
||||
{
|
||||
//
|
||||
// Somebody fell back out of readiness (a reload, a
|
||||
// drop). Disarm rather than launching a room that is
|
||||
// no longer whole.
|
||||
//
|
||||
gLaunchArmed = False;
|
||||
if (gStatusWindow != NULL)
|
||||
{
|
||||
InvalidateRect(gStatusWindow, NULL, FALSE);
|
||||
}
|
||||
Application::RunMissionMessage local_run;
|
||||
DeliverLocal(
|
||||
NetworkClient::ApplicationClientID,
|
||||
&local_run,
|
||||
(int) local_run.messageLength
|
||||
);
|
||||
gRunSent = True;
|
||||
}
|
||||
}
|
||||
|
||||
if (state == Application::RunningMission)
|
||||
{
|
||||
gPhase = PhaseRunning;
|
||||
DestroyStatusBoard(); // however the run began
|
||||
gWatchedApp = application;
|
||||
gResultCount = 0;
|
||||
InterlockedExchange(&gRunStartTick, (LONG) GetTickCount());
|
||||
@@ -537,6 +848,7 @@ namespace
|
||||
// mission ended some other way (pilot exit etc.)
|
||||
InterlockedExchange(&gMissionRunning, 0);
|
||||
gPhase = PhaseStopped;
|
||||
DestroyStatusBoard();
|
||||
DisconnectRemotes();
|
||||
}
|
||||
else if (gStopRequested)
|
||||
@@ -616,6 +928,7 @@ namespace
|
||||
gRunSent = False;
|
||||
gRemoteStopsSent = False;
|
||||
gLocalEggFed = False;
|
||||
DestroyStatusBoard(); // no board survives into a new race
|
||||
|
||||
// game-thread execution point
|
||||
gPerFrameHook = &ConsoleTick;
|
||||
|
||||
+13
-2
@@ -1086,8 +1086,19 @@ namespace
|
||||
case GroupRole: return kRoles[index].name;
|
||||
case GroupRecord: return kRecording[index].name;
|
||||
case GroupLaunch:
|
||||
return (gFE != NULL && gFE->memberLive)
|
||||
? "WAITING FOR THE HOST" : "L A U N C H G A M E";
|
||||
//
|
||||
// Hosting a lobby, the button is the pod bay's COMMIT: it
|
||||
// readies the mission everywhere - eggs fed, pods prepped,
|
||||
// MFDs alive - and the actual launch belongs to the commit
|
||||
// board that appears once everyone is staged. A plain
|
||||
// single-player launch is still a launch.
|
||||
//
|
||||
if (gFE != NULL && gFE->memberLive)
|
||||
{
|
||||
return "WAITING FOR THE HOST";
|
||||
}
|
||||
return (gFE != NULL && gFE->hostingLive)
|
||||
? "C O M M I T" : "L A U N C H G A M E";
|
||||
case GroupSteamHost:
|
||||
return (gFE != NULL && gFE->hostingLive)
|
||||
? "CLOSE LOBBY" : "HOST STEAM GAME";
|
||||
|
||||
Reference in New Issue
Block a user