From 5892af318e0d4fa8f6483428b5feaacd90a537ab Mon Sep 17 00:00:00 2001 From: Cyd Date: Sun, 12 Jul 2026 21:26:31 -0500 Subject: [PATCH] Steam lobby: host, join, room, and launch into a marshaled race RPL4LOBBY implements the multiplayer front door on ISteamMatchmaking. The setup menu grows HOST STEAM RACE / JOIN STEAM RACE buttons when the Steam wire is live; hosting creates a tagged public lobby, joining finds one. Every member publishes FakeIP + fake ports + persona + loadout as member data; the room screen lists members (host marked) and gives the owner a launch button. Launching writes a nonced go-roster into lobby data. Each pod registers every peer with the Steam transport (two-port peer table: engine console/game ports map to Steam fake ports on connect) and enters the race: the owner through the hosted-race path - it builds the multi-pilot egg from real personas and loadouts and its console marshals everyone - and members as network pods that boot straight into WaitingForEgg for the owner to feed over the wire. The lobby outlives races: members loop back through WinMain into the room (no local console needed - MissionCompleted is waived for member races), and the owner returns to the room after its results screen. Leaving the lobby clears the hosted-race priming. Verified on this box: menu buttons appear under RP412STEAM=1, hosting creates a lobby on the Steam backend, the room runs and leaves back to the menu; single-player cycling and the LAN hosted race both still pass. Full three-account mesh test is next, on real hardware. Co-Authored-By: Claude Fable 5 --- MUNGA_L4/L4STEAMTRANSPORT.cpp | 56 ++- MUNGA_L4/L4STEAMTRANSPORT.h | 16 +- RP_L4/RPL4.CPP | 83 ++-- RP_L4/RPL4FE.cpp | 434 +++++++++++++++------ RP_L4/RPL4FE.h | 34 ++ RP_L4/RPL4LOBBY.cpp | 690 ++++++++++++++++++++++++++++++++++ RP_L4/RPL4LOBBY.h | 48 +++ RP_L4/RP_L4.vcxproj | 2 + 8 files changed, 1191 insertions(+), 172 deletions(-) create mode 100644 RP_L4/RPL4LOBBY.cpp create mode 100644 RP_L4/RPL4LOBBY.h diff --git a/MUNGA_L4/L4STEAMTRANSPORT.cpp b/MUNGA_L4/L4STEAMTRANSPORT.cpp index 9e49307..2cccc35 100644 --- a/MUNGA_L4/L4STEAMTRANSPORT.cpp +++ b/MUNGA_L4/L4STEAMTRANSPORT.cpp @@ -58,9 +58,13 @@ namespace struct PeerRecord { unsigned long fakeIP; // host order + unsigned short fakeConsolePort; // host order unsigned short fakeGamePort; // host order }; + // engine-side port convention (lobby default; game port is +1) + unsigned short gEngineConsolePort = 1501; + Logical gSteamReady = False; unsigned long gLocalFakeIP = 0; // host order unsigned short gLocalFakePorts[2] = { 0, 0 }; @@ -136,14 +140,17 @@ namespace } } + // engine port in a SOCKADDR_IN -> the peer's matching fake port unsigned short - LookupPeerFakeGamePort(unsigned long fake_ip_host) + LookupPeerFakePort(unsigned long fake_ip_host, unsigned short engine_port) { for (int i = 0; i < gPeerCount; ++i) { if (gPeers[i].fakeIP == fake_ip_host) { - return gPeers[i].fakeGamePort; + return (engine_port == gEngineConsolePort) + ? gPeers[i].fakeConsolePort + : gPeers[i].fakeGamePort; } } return 0; @@ -272,8 +279,9 @@ namespace ) { unsigned long remote_fake_ip = ntohl(remote->sin_addr.S_un.S_addr); - unsigned short fake_game_port = LookupPeerFakeGamePort(remote_fake_ip); - if (fake_game_port == 0) + unsigned short fake_port = + LookupPeerFakePort(remote_fake_ip, ntohs(remote->sin_port)); + if (fake_port == 0) { DEBUG_STREAM << "SteamNetTransport: no registered peer for " << inet_ntoa(remote->sin_addr) << " - lobby did not feed it\n" << std::flush; @@ -281,8 +289,8 @@ namespace } DEBUG_STREAM << "SteamNetTransport: connecting to " - << inet_ntoa(remote->sin_addr) << " (fake port " - << fake_game_port << ")...\n" << std::flush; + << inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port) + << " (fake port " << fake_port << ")...\n" << std::flush; SteamNetworkingConfigValue_t option; option.SetPtr( @@ -291,7 +299,7 @@ namespace SteamNetworkingIPAddr target; target.Clear(); - target.SetIPv4(remote_fake_ip, fake_game_port); + target.SetIPv4(remote_fake_ip, fake_port); // mirror the TCP retry-while-refused loop, bounded: the // egg-ACK ordering means the peer may not be listening yet @@ -620,32 +628,56 @@ const char * return gLocalFakeAddressString; } +int + SteamNetTransport_GetFakeConsolePort() +{ + return gLocalFakePorts[0]; +} + int SteamNetTransport_GetFakeGamePort() { return gLocalFakePorts[1]; } +void + SteamNetTransport_SetEnginePorts(int console_port) +{ + gEngineConsolePort = (unsigned short) console_port; +} + void SteamNetTransport_RegisterPeer( const char *fake_ip, + int fake_console_port, int fake_game_port ) { - if (gPeerCount >= maxPeers) - { - return; - } SteamNetworkingIPAddr parsed; if (!parsed.ParseString(fake_ip)) { return; } + for (int i = 0; i < gPeerCount; ++i) + { + if (gPeers[i].fakeIP == parsed.GetIPv4()) + { + gPeers[i].fakeConsolePort = (unsigned short) fake_console_port; + gPeers[i].fakeGamePort = (unsigned short) fake_game_port; + return; + } + } + if (gPeerCount >= maxPeers) + { + return; + } gPeers[gPeerCount].fakeIP = parsed.GetIPv4(); + gPeers[gPeerCount].fakeConsolePort = (unsigned short) fake_console_port; gPeers[gPeerCount].fakeGamePort = (unsigned short) fake_game_port; ++gPeerCount; DEBUG_STREAM << "SteamNetTransport: peer registered: " << fake_ip - << " game port " << fake_game_port << "\n" << std::flush; + << " (console " << fake_console_port << ", game " + << fake_game_port << ")\n" << std::flush; } #endif // RP412_STEAM diff --git a/MUNGA_L4/L4STEAMTRANSPORT.h b/MUNGA_L4/L4STEAMTRANSPORT.h index 4b892c6..f6f3b8a 100644 --- a/MUNGA_L4/L4STEAMTRANSPORT.h +++ b/MUNGA_L4/L4STEAMTRANSPORT.h @@ -68,15 +68,25 @@ Logical const char * SteamNetTransport_GetFakeAddressString(); -// Our Steam-assigned fake game port (lobby member data). +// Our Steam-assigned fake ports (lobby member data): the console +// channel and the game mesh. +int + SteamNetTransport_GetFakeConsolePort(); int SteamNetTransport_GetFakeGamePort(); -// The lobby feeds every member's FakeIP + fake game port here before -// StartConnecting runs. +// The engine-side port convention for this session (default 1501; +// game port is +1). Connect() maps an engine port in a SOCKADDR_IN to +// the peer's matching Steam fake port. +void + SteamNetTransport_SetEnginePorts(int console_port); + +// The lobby feeds every member's FakeIP + fake ports here before +// anything dials out. Registering is idempotent per IP. void SteamNetTransport_RegisterPeer( const char *fake_ip, + int fake_console_port, int fake_game_port ); diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index e5634b3..fe37a4f 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -223,6 +223,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine L4Application::GetNetworkCommonFlatAddress() == 0 && !L4Application::GetEggNotationFileName(); // no egg on the command line + int last_launch_mode = FELaunchSingle; + for (;;) { if (front_end_mode) @@ -250,40 +252,55 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine { break; // player closed the menu without launching } - L4Application::SetEggNotationFileName(frontend_egg); + last_launch_mode = RPL4FrontEnd_LastLaunchMode(); Exit_Code = 0; - // Front-end games are marshaled by the in-process console: it - // ends the race when the selected time expires. (Hand-fed -egg - // runs stay unmarshaled - the developer shortcut.) - // - // RP412HOSTPODS turns the launch into a hosted network race: - // this pod switches to network mode (it meshes like any pod) - // and the console also marshals the listed member pods over - // the wire. (The Steam lobby will feed this same path.) - const char *host_pods = getenv("RP412HOSTPODS"); - if (host_pods != NULL && host_pods[0] != '\0') + if (last_launch_mode == FELaunchMember) { - int host_port = 1501; - const char *port_override = getenv("RP412HOSTPORT"); - if (port_override != NULL && atoi(port_override) > 0) - { - host_port = atoi(port_override); - } - L4Application::SetNetworkCommonFlatAddress(host_port); - if (!RPL4LocalConsole_InstallNetworkRace( - RPL4FrontEnd_LastMissionSeconds(), frontend_egg, - host_pods, RPL4FrontEnd_LastPilotNames())) - { - DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush; - L4Application::SetNetworkCommonFlatAddress(0); - continue; - } + // + // Lobby member: enter the race as a network pod. The + // owner's console connects over the wire, feeds the egg, + // and marshals us - no local egg, no local console. + // + L4Application::SetNetworkCommonFlatAddress(1501); } else { - L4Application::SetNetworkCommonFlatAddress(0); - RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); + L4Application::SetEggNotationFileName(frontend_egg); + + // Front-end games are marshaled by the in-process console: + // it ends the race when the selected time expires. + // (Hand-fed -egg runs stay unmarshaled - the developer + // shortcut.) + // + // RP412HOSTPODS turns the launch into a hosted network + // race: this pod switches to network mode (it meshes like + // any pod) and the console also marshals the listed member + // pods over the wire. The Steam lobby primes the same path. + const char *host_pods = getenv("RP412HOSTPODS"); + if (host_pods != NULL && host_pods[0] != '\0') + { + int host_port = 1501; + const char *port_override = getenv("RP412HOSTPORT"); + if (port_override != NULL && atoi(port_override) > 0) + { + host_port = atoi(port_override); + } + L4Application::SetNetworkCommonFlatAddress(host_port); + if (!RPL4LocalConsole_InstallNetworkRace( + RPL4FrontEnd_LastMissionSeconds(), frontend_egg, + host_pods, RPL4FrontEnd_LastPilotNames())) + { + DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush; + L4Application::SetNetworkCommonFlatAddress(0); + continue; + } + } + else + { + L4Application::SetNetworkCommonFlatAddress(0); + RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); + } } } @@ -381,12 +398,14 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // //------------------------------------------------------------------------- - // Single-binary race loop: if the local console ended this mission, - // go back to the setup screen for the next one; anything else (user - // quit, -egg / -net / -mr single pass) falls out. + // Single-binary race loop: if the local console ended this mission + // (or we raced as a lobby member under the owner's console), go + // back to the setup screen / lobby room for the next one; anything + // else (user quit, -egg / -net / -mr single pass) falls out. //------------------------------------------------------------------------- // - if (!front_end_mode || !RPL4LocalConsole_MissionCompleted()) + if (!front_end_mode || + (last_launch_mode != FELaunchMember && !RPL4LocalConsole_MissionCompleted())) { break; } diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 2352f78..701d5bf 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -3,6 +3,7 @@ #include "rpl4fe.h" #include "rpl4console.h" +#include "rpl4lobby.h" #include #include @@ -125,6 +126,8 @@ namespace GroupWeather, GroupLength, GroupLaunch, + GroupSteamHost, // buttons, not selections (Steam builds only) + GroupSteamJoin, GroupCount }; @@ -150,6 +153,7 @@ namespace HBRUSH editBrush; Logical launched; Logical closed; + int steamAction; // 1 = host lobby, 2 = join lobby }; FEState *gFE = NULL; @@ -178,12 +182,36 @@ namespace { char address[64]; // mesh (game port) address for [pilots] char name[32]; - const char *vehicle; - const char *color; + char vehicle[24]; + char color[16]; + char badge[24]; }; + // lobby-fed override (real personas + loadouts); empty = env parsing + FEHostedPilot gHostedPilots[maxExtraPilots]; + int gHostedPilotCount = 0; + char gHostedOwnerAddress[64] = ""; + int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address) { + if (gHostedPilotCount > 0) + { + sprintf(owner_address, "%s:1502", gHostedOwnerAddress); + for (int i = 0; i < gHostedPilotCount; ++i) + { + strcpy(extras[i].address, gHostedPilots[i].address); + strcpy(extras[i].name, + gHostedPilots[i].name[0] ? gHostedPilots[i].name : "Pilot"); + strcpy(extras[i].vehicle, + gHostedPilots[i].vehicle[0] ? gHostedPilots[i].vehicle : "speck"); + strcpy(extras[i].color, + gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red"); + strcpy(extras[i].badge, + gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None"); + } + return gHostedPilotCount; + } + const char *host_pods = getenv("RP412HOSTPODS"); if (host_pods == NULL || host_pods[0] == '\0') { @@ -233,10 +261,11 @@ namespace ExtraPilot *pilot = &extras[extra_count]; sprintf(pilot->address, "%s:%d", entry, console_port + 1); sprintf(pilot->name, "PILOT %d", extra_count + 2); - pilot->vehicle = kVehicles[ - (fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key; - pilot->color = kColors[ - (fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key; + strcpy(pilot->vehicle, kVehicles[ + (fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key); + strcpy(pilot->color, kColors[ + (fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key); + strcpy(pilot->badge, "None"); ++extra_count; } return extra_count; @@ -304,6 +333,26 @@ namespace launch->rect.right = col3 + col_w; launch->rect.bottom = client_h - row_h; + // Steam lobby buttons (only when the Steam wire is live) + if (RPL4Lobby_Available()) + { + FEItem *host = &fe->items[fe->itemCount++]; + host->group = GroupSteamHost; + host->index = 0; + host->rect.left = col3; + host->rect.top = client_h - 6 * row_h; + host->rect.right = col3 + col_w; + host->rect.bottom = client_h - 5 * row_h; + + FEItem *join = &fe->items[fe->itemCount++]; + join->group = GroupSteamJoin; + join->index = 0; + join->rect.left = col3; + join->rect.top = client_h - (9 * row_h) / 2; + join->rect.right = col3 + col_w; + join->rect.bottom = client_h - (7 * row_h) / 2; + } + // pilot name edit sits at the top of column 3 if (fe->nameEdit != NULL) { @@ -339,6 +388,8 @@ namespace case GroupWeather: return kWeather[index].name; case GroupLength: return kLengths[index].name; case GroupLaunch: return "L A U N C H R A C E"; + case GroupSteamHost: return "HOST STEAM RACE"; + case GroupSteamJoin: return "JOIN STEAM RACE"; } return ""; } @@ -377,7 +428,7 @@ namespace for (int i = 0; i < fe->itemCount; ++i) { const FEItem *item = &fe->items[i]; - if (item->group != previous_group && item->group != GroupLaunch) + if (item->group != previous_group && item->group < GroupLaunch) { previous_group = item->group; RECT header = item->rect; @@ -389,11 +440,11 @@ namespace } Logical selected = - (item->group == GroupLaunch) || + (item->group >= GroupLaunch) || (fe->selection[item->group] == item->index); RECT row = item->rect; - if (item->group == GroupLaunch) + if (item->group >= GroupLaunch) { HBRUSH launch_brush = CreateSolidBrush(kGreenBright); FrameRect(mem, &row, launch_brush); @@ -462,6 +513,11 @@ namespace // SendMessage never passes through the queue) PostMessageA(fe->menuWindow, WM_NULL, 0, 0); } + else if (item->group == GroupSteamHost || item->group == GroupSteamJoin) + { + fe->steamAction = (item->group == GroupSteamHost) ? 1 : 2; + PostMessageA(fe->menuWindow, WM_NULL, 0, 0); + } else { fe->selection[item->group] = item->index; @@ -673,7 +729,8 @@ namespace egg += line; sprintf(line, "color=%s\n", extras[p].color); egg += line; - egg += "badge=None\n"; + sprintf(line, "badge=%s\n", extras[p].badge); + egg += line; } egg += "[largebitmap]\n"; @@ -734,13 +791,14 @@ namespace //############################ RPL4FrontEnd_Run ########################## //######################################################################## -Logical - RPL4FrontEnd_Run( - HINSTANCE instance, - HWND main_window, - char *egg_path_out, - int egg_path_size - ) +static int gLastLaunchMode = FELaunchSingle; + +//--------------------------------------------------------------- +// Build frontend.egg from the persisted loadout (the menu persists +// on every exit; lobby launches can happen without a live menu) +//--------------------------------------------------------------- +static Logical + BuildEggFromPersisted(char *egg_path_out, int egg_path_size) { FEState fe; memset(&fe, 0, sizeof(fe)); @@ -753,138 +811,225 @@ Logical { fe.selection[GroupLength] = 2; // 5:00 } - gFE = &fe; + gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds; - //--------------------------------------------------------------- - // Window class + fonts - //--------------------------------------------------------------- - static Logical class_registered = False; - if (!class_registered) + std::string egg; + egg.reserve(16384); + BuildEggText(&fe, egg); + + strncpy(egg_path_out, "frontend.egg", egg_path_size - 1); + egg_path_out[egg_path_size - 1] = '\0'; + + FILE *file = fopen(egg_path_out, "wb"); + if (file == NULL) { - class_registered = True; - WNDCLASSA window_class; - memset(&window_class, 0, sizeof(window_class)); - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.lpfnWndProc = FrontEndWndProc; - window_class.hInstance = instance; - window_class.hCursor = LoadCursor(NULL, IDC_ARROW); - window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); - window_class.lpszClassName = "RPFrontEnd"; - RegisterClassA(&window_class); - } - - RECT client; - GetClientRect(main_window, &client); - - fe.menuWindow = CreateWindowExA( - 0, "RPFrontEnd", "", - WS_CHILD | WS_VISIBLE, - 0, 0, client.right, client.bottom, - main_window, NULL, instance, NULL); - if (fe.menuWindow == NULL) - { - gFE = NULL; + DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out + << "\n" << std::flush; return False; } + fwrite(egg.data(), 1, egg.size(), file); + fclose(file); + DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " (" + << (int) egg.size() << " bytes)\n" << std::flush; + return True; +} - int text_h = client.bottom / 40; - if (text_h < 16) text_h = 16; - if (text_h > 24) text_h = 24; - fe.textFont = CreateFontA(-text_h, 0, 0, 0, FW_NORMAL, - FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, - CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, - "Consolas"); - fe.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD, - FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, - CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, - "Consolas"); - fe.editBrush = CreateSolidBrush(kBlack); - - fe.nameEdit = CreateWindowExA( - 0, "EDIT", fe.pilotName, - WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, - 0, 0, 10, 10, - fe.menuWindow, NULL, instance, NULL); - SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE); - SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0); - - LayoutMenu(&fe, client.right, client.bottom); - InvalidateRect(fe.menuWindow, NULL, TRUE); +Logical + RPL4FrontEnd_Run( + HINSTANCE instance, + HWND main_window, + char *egg_path_out, + int egg_path_size + ) +{ + gLastLaunchMode = FELaunchSingle; //--------------------------------------------------------------- - // Modal loop until the player launches or closes the window + // Coming back from a race while still in a lobby: straight to + // the room (the lobby outlives races - single binary payoff) //--------------------------------------------------------------- - MSG msg; - while (!fe.launched && !fe.closed) + if (RPL4Lobby_InRoom()) { - BOOL result = GetMessageA(&msg, NULL, 0, 0); - if (result <= 0) + int outcome = RPL4Lobby_Room(instance, main_window); + if (outcome == LobbyRoomClosed) { - fe.closed = True; - break; + return False; } - if (msg.message == WM_QUIT || - (msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE)) + if (outcome == LobbyLaunchMember) { - fe.closed = True; + gLastLaunchMode = FELaunchMember; + egg_path_out[0] = '\0'; + return True; } - // closing the main window ends the front end too - if (msg.message == WM_CLOSE && msg.hwnd == main_window) + if (outcome == LobbyLaunchHost) { - fe.closed = True; - } - TranslateMessage(&msg); - DispatchMessageA(&msg); - if (!IsWindow(main_window)) - { - fe.closed = True; + gLastLaunchMode = FELaunchHost; + return BuildEggFromPersisted(egg_path_out, egg_path_size); } + // LobbyRoomLeft: fall through to the setup menu } - Logical launched = fe.launched; - if (launched) + for (;;) { - gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds; - - GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1); - if (fe.pilotName[0] == '\0') + FEState fe; + memset(&fe, 0, sizeof(fe)); + strcpy(fe.pilotName, gLastPilotName); + if (gHavePersist) { - strcpy(fe.pilotName, "Pilot"); - } - strcpy(gLastPilotName, fe.pilotName); - memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection)); - gHavePersist = True; - - std::string egg; - egg.reserve(16384); - BuildEggText(&fe, egg); - - strncpy(egg_path_out, "frontend.egg", egg_path_size - 1); - egg_path_out[egg_path_size - 1] = '\0'; - - FILE *file = fopen(egg_path_out, "wb"); - if (file != NULL) - { - fwrite(egg.data(), 1, egg.size(), file); - fclose(file); - DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " (" - << (int) egg.size() << " bytes)\n" << std::flush; + memcpy(fe.selection, gPersistSelection, sizeof(fe.selection)); } else { - DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out - << "\n" << std::flush; - launched = False; + fe.selection[GroupLength] = 2; // 5:00 } + gFE = &fe; + + //--------------------------------------------------------------- + // Window class + fonts + //--------------------------------------------------------------- + static Logical class_registered = False; + if (!class_registered) + { + class_registered = True; + WNDCLASSA window_class; + memset(&window_class, 0, sizeof(window_class)); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = FrontEndWndProc; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = "RPFrontEnd"; + RegisterClassA(&window_class); + } + + RECT client; + GetClientRect(main_window, &client); + + fe.menuWindow = CreateWindowExA( + 0, "RPFrontEnd", "", + WS_CHILD | WS_VISIBLE, + 0, 0, client.right, client.bottom, + main_window, NULL, instance, NULL); + if (fe.menuWindow == NULL) + { + gFE = NULL; + return False; + } + + int text_h = client.bottom / 40; + if (text_h < 16) text_h = 16; + if (text_h > 24) text_h = 24; + fe.textFont = CreateFontA(-text_h, 0, 0, 0, FW_NORMAL, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + fe.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + fe.editBrush = CreateSolidBrush(kBlack); + + fe.nameEdit = CreateWindowExA( + 0, "EDIT", fe.pilotName, + WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, + 0, 0, 10, 10, + fe.menuWindow, NULL, instance, NULL); + SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE); + SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0); + + LayoutMenu(&fe, client.right, client.bottom); + InvalidateRect(fe.menuWindow, NULL, TRUE); + + //--------------------------------------------------------------- + // Modal loop until launch, lobby button, or close + //--------------------------------------------------------------- + MSG msg; + while (!fe.launched && !fe.closed && fe.steamAction == 0) + { + BOOL result = GetMessageA(&msg, NULL, 0, 0); + if (result <= 0) + { + fe.closed = True; + break; + } + if (msg.message == WM_QUIT || + (msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE)) + { + fe.closed = True; + } + // closing the main window ends the front end too + if (msg.message == WM_CLOSE && msg.hwnd == main_window) + { + fe.closed = True; + } + TranslateMessage(&msg); + DispatchMessageA(&msg); + if (!IsWindow(main_window)) + { + fe.closed = True; + } + } + + //--------------------------------------------------------------- + // Harvest the loadout whichever way we leave the menu - the + // lobby publishes it as member data, launches build from it + //--------------------------------------------------------------- + if (!fe.closed) + { + GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1); + if (fe.pilotName[0] == '\0') + { + strcpy(fe.pilotName, "Pilot"); + } + strcpy(gLastPilotName, fe.pilotName); + memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection)); + gHavePersist = True; + } + + Logical launched = fe.launched; + Logical closed = fe.closed; + int steam_action = fe.steamAction; + + DestroyWindow(fe.menuWindow); + DeleteObject(fe.textFont); + DeleteObject(fe.titleFont); + DeleteObject(fe.editBrush); + gFE = NULL; + + if (closed) + { + return False; + } + if (launched) + { + // plain launch: single player, or a LAN-hosted race when + // RP412HOSTPODS is set in the environment + return BuildEggFromPersisted(egg_path_out, egg_path_size); + } + + //--------------------------------------------------------------- + // Steam lobby (host or join), then back here if they leave + //--------------------------------------------------------------- + int outcome = (steam_action == 1) + ? RPL4Lobby_Host(instance, main_window) + : RPL4Lobby_Join(instance, main_window); + if (outcome == LobbyRoomClosed) + { + return False; + } + if (outcome == LobbyLaunchMember) + { + gLastLaunchMode = FELaunchMember; + egg_path_out[0] = '\0'; + return True; + } + if (outcome == LobbyLaunchHost) + { + gLastLaunchMode = FELaunchHost; + return BuildEggFromPersisted(egg_path_out, egg_path_size); + } + // LobbyRoomLeft: show the menu again } - - DestroyWindow(fe.menuWindow); - DeleteObject(fe.textFont); - DeleteObject(fe.titleFont); - DeleteObject(fe.editBrush); - gFE = NULL; - - return launched; } int @@ -899,6 +1044,45 @@ const char * return gLastPilotNamesCsv; } +int + RPL4FrontEnd_LastLaunchMode() +{ + return gLastLaunchMode; +} + +void + RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge) +{ + int v = gHavePersist ? gPersistSelection[GroupVehicle] : 0; + int c = gHavePersist ? gPersistSelection[GroupColor] : 0; + int b = gHavePersist ? gPersistSelection[GroupBadge] : 0; + strcpy(vehicle, kVehicles[v].key); + strcpy(color, kColors[c].key); + strcpy(badge, kBadges[b].key); +} + +void + RPL4FrontEnd_SetHostedPilots( + const char *owner_address, + const FEHostedPilot *pilots, + int count + ) +{ + if (count > maxExtraPilots) + { + count = maxExtraPilots; + } + gHostedPilotCount = (pilots != NULL) ? count : 0; + for (int i = 0; i < gHostedPilotCount; ++i) + { + gHostedPilots[i] = pilots[i]; + } + strncpy(gHostedOwnerAddress, + (owner_address != NULL) ? owner_address : "", + sizeof(gHostedOwnerAddress) - 1); + gHostedOwnerAddress[sizeof(gHostedOwnerAddress) - 1] = '\0'; +} + //######################################################################## //######################## RPL4FrontEnd_ShowResults ###################### //######################################################################## diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h index e1746bf..a8ccd8f 100644 --- a/RP_L4/RPL4FE.h +++ b/RP_L4/RPL4FE.h @@ -34,6 +34,40 @@ int const char * RPL4FrontEnd_LastPilotNames(); +// How the last Run() ended: a plain race, hosting a network race (the +// console marshals the other pods), or entering one as a member pod +// (the lobby owner's console marshals us; no local egg, no console). +enum FELaunchMode +{ + FELaunchSingle = 0, + FELaunchHost, + FELaunchMember +}; +int + RPL4FrontEnd_LastLaunchMode(); + +// The player's persisted loadout (lobby member data). Buffers: 24/16/24. +void + RPL4FrontEnd_GetLoadout(char *vehicle, char *color, char *badge); + +// Hosted-race pilots fed by the Steam lobby: overrides the +// RP412HOSTPODS parsing with real personas and loadouts. owner_address +// is this pod's mesh IP (the FakeIP); count 0 clears the override. +struct FEHostedPilot +{ + char address[64]; // mesh (game port) address for [pilots] + char name[32]; + char vehicle[24]; + char color[16]; + char badge[24]; +}; +void + RPL4FrontEnd_SetHostedPilots( + const char *owner_address, + const FEHostedPilot *pilots, + int count + ); + // The between-races results screen: shows the last mission's final // scores (from the local console) with a CONTINUE button. Returns // immediately when there are no results (first boot). False only if diff --git a/RP_L4/RPL4LOBBY.cpp b/RP_L4/RPL4LOBBY.cpp new file mode 100644 index 0000000..cbbc42b --- /dev/null +++ b/RP_L4/RPL4LOBBY.cpp @@ -0,0 +1,690 @@ +#include "..\munga_l4\mungal4.h" +#pragma hdrstop + +#include "rpl4lobby.h" + +#ifndef RP412_STEAM + +//######################################################################## +// No Steam SDK in this build: the lobby is unavailable, everything +// else (single player, LAN -net races) works as always. +//######################################################################## + +Logical RPL4Lobby_Available() { return False; } +Logical RPL4Lobby_InRoom() { return False; } +int RPL4Lobby_Host(HINSTANCE, HWND) { return LobbyRoomLeft; } +int RPL4Lobby_Join(HINSTANCE, HWND) { return LobbyRoomLeft; } +int RPL4Lobby_Room(HINSTANCE, HWND) { return LobbyRoomLeft; } + +#else + +#include "rpl4fe.h" +#include "..\munga_l4\l4steamtransport.h" + +#pragma pack(push, 8) +#include "steam/steam_api.h" +#pragma pack(pop) + +#include + +//######################################################################## +// Lobby state - one lobby at a time, alive across races +//######################################################################## + +namespace +{ + enum { kMaxLobbyMembers = 4 }; + const char kLobbyTagKey[] = "rp412"; + const char kGoKey[] = "go"; + + CSteamID gLobby; + Logical gInLobby = False; + int gLastGoNonce = 0; // launches we already answered + + // async call-result plumbing + Logical gCallDone = False; + Logical gCallOK = False; + CSteamID gCallLobby; + + class LobbyCalls + { + public: + CCallResult createResult; + CCallResult listResult; + CCallResult enterResult; + + void OnCreated(LobbyCreated_t *result, bool io_failure) + { + gCallOK = !io_failure && result->m_eResult == k_EResultOK; + gCallLobby = CSteamID(result->m_ulSteamIDLobby); + gCallDone = True; + } + void OnList(LobbyMatchList_t *result, bool io_failure) + { + gCallOK = !io_failure && result->m_nLobbiesMatching > 0; + if (gCallOK) + { + gCallLobby = SteamMatchmaking()->GetLobbyByIndex(0); + } + gCallDone = True; + } + void OnEnter(LobbyEnter_t *result, bool io_failure) + { + gCallOK = !io_failure && + result->m_EChatRoomEnterResponse == k_EChatRoomEnterResponseSuccess; + gCallLobby = CSteamID(result->m_ulSteamIDLobby); + gCallDone = True; + } + }; + LobbyCalls gCalls; + + Logical WaitForCall(int timeout_ms) + { + DWORD deadline = GetTickCount() + timeout_ms; + while (!gCallDone) + { + SteamAPI_RunCallbacks(); + if ((LONG)(GetTickCount() - deadline) >= 0) + { + return False; + } + Sleep(50); + } + return gCallOK; + } + + //--------------------------------------------------------------- + // Member data: what every pod publishes on entering the room + //--------------------------------------------------------------- + void SanitizeName(const char *in, char *out, int out_size) + { + int n = 0; + for (int i = 0; in[i] != '\0' && n < out_size - 1; ++i) + { + char c = in[i]; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_') + { + out[n++] = c; + } + } + out[n] = '\0'; + if (n == 0) + { + strcpy(out, "Pilot"); + } + } + + void PublishMemberData() + { + char value[64]; + + SteamMatchmaking()->SetLobbyMemberData(gLobby, "ip", + SteamNetTransport_GetFakeAddressString()); + sprintf(value, "%d", SteamNetTransport_GetFakeConsolePort()); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "cp", value); + sprintf(value, "%d", SteamNetTransport_GetFakeGamePort()); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "gp", value); + + char name[32]; + SanitizeName(SteamFriends()->GetPersonaName(), name, sizeof(name)); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "nm", name); + + char vehicle[24], color[16], badge[24]; + RPL4FrontEnd_GetLoadout(vehicle, color, badge); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "vh", vehicle); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "cl", color); + SteamMatchmaking()->SetLobbyMemberData(gLobby, "bd", badge); + } + + Logical IsOwner() + { + return gInLobby && + SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID(); + } + + //--------------------------------------------------------------- + // Launch: roster into lobby data, peers into the transport, and + // the hosted-race path primed on the owner + //--------------------------------------------------------------- + struct MemberInfo + { + CSteamID id; + char ip[32]; + int consolePort; + int gamePort; + char name[32]; + char vehicle[24]; + char color[16]; + char badge[24]; + Logical published; + }; + + int CollectMembers(MemberInfo *members) + { + int count = SteamMatchmaking()->GetNumLobbyMembers(gLobby); + if (count > kMaxLobbyMembers) + { + count = kMaxLobbyMembers; + } + for (int i = 0; i < count; ++i) + { + MemberInfo *member = &members[i]; + memset(member, 0, sizeof(*member)); + member->id = SteamMatchmaking()->GetLobbyMemberByIndex(gLobby, i); + strncpy(member->ip, + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ip"), + sizeof(member->ip) - 1); + member->consolePort = atoi( + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cp")); + member->gamePort = atoi( + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "gp")); + strncpy(member->name, + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "nm"), + sizeof(member->name) - 1); + strncpy(member->vehicle, + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "vh"), + sizeof(member->vehicle) - 1); + strncpy(member->color, + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "cl"), + sizeof(member->color) - 1); + strncpy(member->badge, + SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "bd"), + sizeof(member->badge) - 1); + member->published = + member->ip[0] != '\0' && member->consolePort > 0 && member->gamePort > 0; + } + return count; + } + + void RegisterRoster(MemberInfo *members, int count) + { + SteamNetTransport_SetEnginePorts(1501); + for (int i = 0; i < count; ++i) + { + if (members[i].published) + { + SteamNetTransport_RegisterPeer( + members[i].ip, members[i].consolePort, members[i].gamePort); + } + } + } + + // Owner side: prime RPL4FE + WinMain's hosted-race path + void PrimeHostedRace(MemberInfo *members, int count) + { + static char pods_env[512]; + static char addr_env[64]; + static char port_env[32]; + + CSteamID self = SteamUser()->GetSteamID(); + FEHostedPilot pilots[kMaxLobbyMembers]; + int pilot_count = 0; + char pods[512] = ""; + + for (int i = 0; i < count; ++i) + { + if (members[i].id == self || !members[i].published) + { + continue; + } + FEHostedPilot *pilot = &pilots[pilot_count++]; + memset(pilot, 0, sizeof(*pilot)); + sprintf(pilot->address, "%s:1502", members[i].ip); + strncpy(pilot->name, members[i].name, sizeof(pilot->name) - 1); + strncpy(pilot->vehicle, members[i].vehicle, sizeof(pilot->vehicle) - 1); + strncpy(pilot->color, members[i].color, sizeof(pilot->color) - 1); + strncpy(pilot->badge, members[i].badge, sizeof(pilot->badge) - 1); + + if (pods[0] != '\0') + { + strcat(pods, ","); + } + strcat(pods, members[i].ip); + strcat(pods, ":1501"); + } + + RPL4FrontEnd_SetHostedPilots( + SteamNetTransport_GetFakeAddressString(), pilots, pilot_count); + + sprintf(pods_env, "RP412HOSTPODS=%s", pods); + putenv(pods_env); + sprintf(addr_env, "RP412HOSTADDR=%s", SteamNetTransport_GetFakeAddressString()); + putenv(addr_env); + strcpy(port_env, "RP412HOSTPORT=1501"); + putenv(port_env); + } + + //--------------------------------------------------------------- + // The room screen (front-end style: green on black) + //--------------------------------------------------------------- + const COLORREF kGreenBright = RGB(64, 255, 64); + const COLORREF kGreenDim = RGB(24, 140, 24); + + struct RoomState + { + HWND window; + HFONT textFont; + HFONT titleFont; + RECT launchRect; // owner only + RECT leaveRect; + Logical launchClicked; + Logical leaveClicked; + Logical closed; + MemberInfo members[kMaxLobbyMembers]; + int memberCount; + }; + + RoomState *gRoom = NULL; + + void PaintRoom(RoomState *room) + { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(room->window, &ps); + + RECT client; + GetClientRect(room->window, &client); + + HDC mem = CreateCompatibleDC(hdc); + HBITMAP surface = CreateCompatibleBitmap(hdc, client.right, client.bottom); + HBITMAP old_surface = (HBITMAP) SelectObject(mem, surface); + + FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH)); + SetBkMode(mem, TRANSPARENT); + + SelectObject(mem, room->titleFont); + SetTextColor(mem, kGreenBright); + RECT title = client; + title.top = client.bottom / 28; + title.bottom = title.top + client.bottom / 10; + DrawTextA(mem, "STEAM RACE LOBBY", -1, &title, + DT_CENTER | DT_TOP | DT_SINGLELINE); + + SelectObject(mem, room->textFont); + int row_h = client.bottom / 16; + int top = client.bottom / 4; + int left = client.right / 4; + int right = 3 * client.right / 4; + char text[128]; + for (int i = 0; i < room->memberCount; ++i) + { + MemberInfo *member = &room->members[i]; + RECT row; + row.left = left; + row.right = right; + row.top = top + i * row_h; + row.bottom = row.top + row_h; + + Logical is_owner_row = + SteamMatchmaking()->GetLobbyOwner(gLobby) == member->id; + SetTextColor(mem, member->published ? kGreenBright : kGreenDim); + sprintf(text, "%s%s", member->name[0] ? member->name : "(joining...)", + is_owner_row ? " [HOST]" : ""); + DrawTextA(mem, text, -1, &row, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + if (member->vehicle[0] != '\0') + { + sprintf(text, "%s / %s", member->vehicle, member->color); + DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); + } + } + + SetTextColor(mem, kGreenDim); + RECT hint; + hint.left = left; + hint.right = right; + hint.top = top + kMaxLobbyMembers * row_h + row_h / 2; + hint.bottom = hint.top + row_h; + DrawTextA(mem, + IsOwner() ? "You are the console: launch when everyone is in." + : "Waiting for the host to launch...", + -1, &hint, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + HBRUSH frame = CreateSolidBrush(kGreenBright); + if (IsOwner()) + { + FrameRect(mem, &room->launchRect, frame); + SetTextColor(mem, kGreenBright); + DrawTextA(mem, "L A U N C H R A C E", -1, &room->launchRect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + FrameRect(mem, &room->leaveRect, frame); + SetTextColor(mem, kGreenBright); + DrawTextA(mem, "LEAVE LOBBY", -1, &room->leaveRect, + DT_CENTER | DT_VCENTER | DT_SINGLELINE); + DeleteObject(frame); + + BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY); + + SelectObject(mem, old_surface); + DeleteObject(surface); + DeleteDC(mem); + EndPaint(room->window, &ps); + } + + LRESULT CALLBACK + RoomWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) + { + RoomState *room = gRoom; + + switch (message) + { + case WM_PAINT: + if (room != NULL) + { + PaintRoom(room); + return 0; + } + break; + + case WM_LBUTTONDOWN: + if (room != NULL) + { + POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) }; + if (IsOwner() && PtInRect(&room->launchRect, point)) + { + room->launchClicked = True; + } + else if (PtInRect(&room->leaveRect, point)) + { + room->leaveClicked = True; + } + return 0; + } + break; + + case WM_ERASEBKGND: + return 1; + } + return DefWindowProcA(hwnd, message, wParam, lParam); + } + + //--------------------------------------------------------------- + // The room loop: pump Steam + Windows, watch for launch/leave + //--------------------------------------------------------------- + int RunRoom(HINSTANCE instance, HWND main_window) + { + static Logical class_registered = False; + if (!class_registered) + { + class_registered = True; + WNDCLASSA window_class; + memset(&window_class, 0, sizeof(window_class)); + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.lpfnWndProc = RoomWndProc; + window_class.hInstance = instance; + window_class.hCursor = LoadCursor(NULL, IDC_ARROW); + window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); + window_class.lpszClassName = "RPLobby"; + RegisterClassA(&window_class); + } + + RECT client; + GetClientRect(main_window, &client); + + RoomState room; + memset(&room, 0, sizeof(room)); + room.window = CreateWindowExA( + 0, "RPLobby", "", + WS_CHILD | WS_VISIBLE, + 0, 0, client.right, client.bottom, + main_window, NULL, instance, NULL); + if (room.window == NULL) + { + return LobbyRoomClosed; + } + + int text_h = client.bottom / 40; + if (text_h < 16) text_h = 16; + if (text_h > 24) text_h = 24; + room.textFont = CreateFontA(-(text_h * 3 / 2), 0, 0, 0, FW_NORMAL, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + room.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD, + FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN, + "Consolas"); + + int row_h = client.bottom / 36; + if (row_h < 18) row_h = 18; + if (row_h > 30) row_h = 30; + int col_w = client.right / 4; + room.launchRect.left = (client.right - col_w) / 2; + room.launchRect.right = room.launchRect.left + col_w; + room.launchRect.top = client.bottom - 6 * row_h; + room.launchRect.bottom = room.launchRect.top + 2 * row_h; + room.leaveRect.left = (client.right - col_w / 2) / 2; + room.leaveRect.right = room.leaveRect.left + col_w / 2; + room.leaveRect.top = client.bottom - 3 * row_h; + room.leaveRect.bottom = client.bottom - row_h; + + gRoom = &room; + PublishMemberData(); + + int outcome = -1; + DWORD last_refresh = 0; + while (outcome < 0) + { + MSG msg; + while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) + { + if (msg.message == WM_QUIT) + { + room.closed = True; + } + TranslateMessage(&msg); + DispatchMessageA(&msg); + } + SteamAPI_RunCallbacks(); + + if (!IsWindow(main_window) || room.closed) + { + outcome = LobbyRoomClosed; + break; + } + if (room.leaveClicked) + { + SteamMatchmaking()->LeaveLobby(gLobby); + gInLobby = False; + // plain menu launches must not inherit lobby hosting + RPL4FrontEnd_SetHostedPilots(NULL, NULL, 0); + static char clear_env[] = "RP412HOSTPODS="; + putenv(clear_env); + outcome = LobbyRoomLeft; + break; + } + + // + // Owner launch: roster into lobby data + local prime + // + if (room.launchClicked) + { + room.launchClicked = False; + room.memberCount = CollectMembers(room.members); + Logical all_published = True; + for (int i = 0; i < room.memberCount; ++i) + { + if (!room.members[i].published) + { + all_published = False; + } + } + if (all_published && room.memberCount >= 1) + { + ++gLastGoNonce; + char go[600]; + sprintf(go, "%d:", gLastGoNonce); + for (int i = 0; i < room.memberCount; ++i) + { + char entry[64]; + sprintf(entry, "%s|%d|%d;", room.members[i].ip, + room.members[i].consolePort, room.members[i].gamePort); + if (strlen(go) + strlen(entry) < sizeof(go)) + { + strcat(go, entry); + } + } + SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, go); + RegisterRoster(room.members, room.memberCount); + PrimeHostedRace(room.members, room.memberCount); + outcome = LobbyLaunchHost; + break; + } + DEBUG_STREAM << "Lobby: not everyone is ready yet\n" << std::flush; + } + + // + // Member: watch for the owner's go signal + // + if (!IsOwner()) + { + const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); + if (go != NULL && go[0] != '\0') + { + int nonce = atoi(go); + if (nonce > gLastGoNonce) + { + gLastGoNonce = nonce; + // register every rostered peer with the transport + SteamNetTransport_SetEnginePorts(1501); + const char *cursor = strchr(go, ':'); + cursor = (cursor != NULL) ? cursor + 1 : go; + while (*cursor != '\0') + { + char ip[32]; + int console_port = 0, game_port = 0; + int n = 0; + while (cursor[n] != '\0' && cursor[n] != '|' && + n < (int) sizeof(ip) - 1) + { + ip[n] = cursor[n]; + ++n; + } + ip[n] = '\0'; + cursor += n; + if (*cursor == '|') + { + console_port = atoi(++cursor); + while (*cursor != '\0' && *cursor != '|') ++cursor; + } + if (*cursor == '|') + { + game_port = atoi(++cursor); + } + while (*cursor != '\0' && *cursor != ';') ++cursor; + if (*cursor == ';') ++cursor; + + if (ip[0] != '\0' && console_port > 0 && game_port > 0) + { + SteamNetTransport_RegisterPeer(ip, console_port, game_port); + } + } + outcome = LobbyLaunchMember; + break; + } + } + } + + // + // Refresh the member list once a second + // + DWORD now = GetTickCount(); + if ((LONG)(now - last_refresh) >= 1000) + { + last_refresh = now; + room.memberCount = CollectMembers(room.members); + InvalidateRect(room.window, NULL, FALSE); + RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW); + } + Sleep(50); + } + + DestroyWindow(room.window); + DeleteObject(room.textFont); + DeleteObject(room.titleFont); + gRoom = NULL; + return outcome; + } +} + +//######################################################################## +// Public API +//######################################################################## + +Logical + RPL4Lobby_Available() +{ + return SteamNetTransport_GetFakeAddressString()[0] != '\0'; +} + +Logical + RPL4Lobby_InRoom() +{ + return gInLobby; +} + +int + RPL4Lobby_Host(HINSTANCE instance, HWND main_window) +{ + gCallDone = False; + SteamAPICall_t call = + SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, kMaxLobbyMembers); + gCalls.createResult.Set(call, &gCalls, &LobbyCalls::OnCreated); + if (!WaitForCall(15000)) + { + DEBUG_STREAM << "Lobby: CreateLobby failed\n" << std::flush; + return LobbyRoomLeft; + } + gLobby = gCallLobby; + gInLobby = True; + gLastGoNonce = 0; + SteamMatchmaking()->SetLobbyData(gLobby, kLobbyTagKey, "1"); + SteamMatchmaking()->SetLobbyData(gLobby, kGoKey, ""); + DEBUG_STREAM << "Lobby: hosting " << gLobby.ConvertToUint64() << "\n" << std::flush; + return RunRoom(instance, main_window); +} + +int + RPL4Lobby_Join(HINSTANCE instance, HWND main_window) +{ + gCallDone = False; + SteamMatchmaking()->AddRequestLobbyListStringFilter( + kLobbyTagKey, "1", k_ELobbyComparisonEqual); + SteamAPICall_t call = SteamMatchmaking()->RequestLobbyList(); + gCalls.listResult.Set(call, &gCalls, &LobbyCalls::OnList); + if (!WaitForCall(15000)) + { + DEBUG_STREAM << "Lobby: no open race lobby found\n" << std::flush; + return LobbyRoomLeft; + } + + CSteamID target = gCallLobby; + gCallDone = False; + call = SteamMatchmaking()->JoinLobby(target); + gCalls.enterResult.Set(call, &gCalls, &LobbyCalls::OnEnter); + if (!WaitForCall(15000)) + { + DEBUG_STREAM << "Lobby: join failed\n" << std::flush; + return LobbyRoomLeft; + } + gLobby = gCallLobby; + gInLobby = True; + // answer only launches newer than anything already in the lobby + const char *go = SteamMatchmaking()->GetLobbyData(gLobby, kGoKey); + gLastGoNonce = (go != NULL) ? atoi(go) : 0; + DEBUG_STREAM << "Lobby: joined " << gLobby.ConvertToUint64() << "\n" << std::flush; + return RunRoom(instance, main_window); +} + +int + RPL4Lobby_Room(HINSTANCE instance, HWND main_window) +{ + if (!gInLobby) + { + return LobbyRoomLeft; + } + return RunRoom(instance, main_window); +} + +#endif // RP412_STEAM diff --git a/RP_L4/RPL4LOBBY.h b/RP_L4/RPL4LOBBY.h new file mode 100644 index 0000000..845b910 --- /dev/null +++ b/RP_L4/RPL4LOBBY.h @@ -0,0 +1,48 @@ +#pragma once + +#include "..\munga\style.h" + +//######################################################################## +//########################### RPL4 Steam Lobby ########################### +//######################################################################## +// +// The multiplayer front door (docs/RP412-FRONTEND-DESIGN.md section 3): +// an ISteamMatchmaking lobby stands in for the arcade's Site Management +// screen. Every member publishes its FakeIP + fake ports + persona + +// loadout as lobby member data; the owner is the console. Launching +// writes a "go" roster into lobby data - each pod registers every +// peer with the Steam transport and enters the race, the owner +// through the hosted-race path (it builds the egg and marshals), the +// members as network pods waiting for the owner's console connection. +// +// The lobby OBJECT outlives races - that is the point of the single +// binary. After a race everyone lands back in the room for the next +// launch. +// +// Compiled to stubs without RP412_STEAM. +// +enum RPL4LobbyOutcome +{ + LobbyRoomLeft = 0, // player left the lobby - back to the menu + LobbyRoomClosed, // window went away - quit + LobbyLaunchHost, // owner launched: hosted-race path is primed + LobbyLaunchMember // owner launched: enter the race as a pod +}; + +// True when the Steam transport is up (lobby UI is offered). +Logical + RPL4Lobby_Available(); + +// True while we sit in a lobby (races return to the room). +Logical + RPL4Lobby_InRoom(); + +// Create a lobby / find-and-join one, then run the room screen. +int + RPL4Lobby_Host(HINSTANCE instance, HWND main_window); +int + RPL4Lobby_Join(HINSTANCE instance, HWND main_window); + +// Re-enter the room of the lobby we are already in (post-race). +int + RPL4Lobby_Room(HINSTANCE instance, HWND main_window); diff --git a/RP_L4/RP_L4.vcxproj b/RP_L4/RP_L4.vcxproj index e0a3c3f..8740dfb 100644 --- a/RP_L4/RP_L4.vcxproj +++ b/RP_L4/RP_L4.vcxproj @@ -109,6 +109,7 @@ + @@ -146,6 +147,7 @@ +