diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index fe2b81f..8b8309b 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -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; diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index c6b7442..816a983 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -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";