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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
+31
-12
@@ -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,17 +252,31 @@ 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.)
|
||||
if (last_launch_mode == FELaunchMember)
|
||||
{
|
||||
//
|
||||
// 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.)
|
||||
// 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::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')
|
||||
{
|
||||
@@ -286,6 +302,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
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;
|
||||
}
|
||||
|
||||
+223
-39
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "rpl4fe.h"
|
||||
#include "rpl4console.h"
|
||||
#include "rpl4lobby.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
@@ -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,6 +791,49 @@ namespace
|
||||
//############################ RPL4FrontEnd_Run ##########################
|
||||
//########################################################################
|
||||
|
||||
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));
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
if (gHavePersist)
|
||||
{
|
||||
memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
|
||||
}
|
||||
else
|
||||
{
|
||||
fe.selection[GroupLength] = 2; // 5:00
|
||||
}
|
||||
gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds;
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
Logical
|
||||
RPL4FrontEnd_Run(
|
||||
HINSTANCE instance,
|
||||
@@ -742,6 +842,35 @@ Logical
|
||||
int egg_path_size
|
||||
)
|
||||
{
|
||||
gLastLaunchMode = FELaunchSingle;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Coming back from a race while still in a lobby: straight to
|
||||
// the room (the lobby outlives races - single binary payoff)
|
||||
//---------------------------------------------------------------
|
||||
if (RPL4Lobby_InRoom())
|
||||
{
|
||||
int outcome = RPL4Lobby_Room(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: fall through to the setup menu
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
FEState fe;
|
||||
memset(&fe, 0, sizeof(fe));
|
||||
strcpy(fe.pilotName, gLastPilotName);
|
||||
@@ -812,10 +941,10 @@ Logical
|
||||
InvalidateRect(fe.menuWindow, NULL, TRUE);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Modal loop until the player launches or closes the window
|
||||
// Modal loop until launch, lobby button, or close
|
||||
//---------------------------------------------------------------
|
||||
MSG msg;
|
||||
while (!fe.launched && !fe.closed)
|
||||
while (!fe.launched && !fe.closed && fe.steamAction == 0)
|
||||
{
|
||||
BOOL result = GetMessageA(&msg, NULL, 0, 0);
|
||||
if (result <= 0)
|
||||
@@ -841,11 +970,12 @@ Logical
|
||||
}
|
||||
}
|
||||
|
||||
Logical launched = fe.launched;
|
||||
if (launched)
|
||||
//---------------------------------------------------------------
|
||||
// Harvest the loadout whichever way we leave the menu - the
|
||||
// lobby publishes it as member data, launches build from it
|
||||
//---------------------------------------------------------------
|
||||
if (!fe.closed)
|
||||
{
|
||||
gLastMissionSeconds = kLengths[fe.selection[GroupLength]].seconds;
|
||||
|
||||
GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
|
||||
if (fe.pilotName[0] == '\0')
|
||||
{
|
||||
@@ -854,37 +984,52 @@ Logical
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out
|
||||
<< "\n" << std::flush;
|
||||
launched = False;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return launched;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 ######################
|
||||
//########################################################################
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <stdio.h>
|
||||
|
||||
//########################################################################
|
||||
// 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<LobbyCalls, LobbyCreated_t> createResult;
|
||||
CCallResult<LobbyCalls, LobbyMatchList_t> listResult;
|
||||
CCallResult<LobbyCalls, LobbyEnter_t> 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
|
||||
@@ -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);
|
||||
@@ -109,6 +109,7 @@
|
||||
<ClCompile Include=".\RPL4APP.cpp" />
|
||||
<ClCompile Include=".\RPL4CONSOLE.cpp" />
|
||||
<ClCompile Include=".\RPL4FE.cpp" />
|
||||
<ClCompile Include=".\RPL4LOBBY.cpp" />
|
||||
<ClCompile Include=".\RPL4ARND.cpp" />
|
||||
<ClCompile Include=".\RPL4GAUG.cpp" />
|
||||
<ClCompile Include=".\RPL4GRND.cpp" />
|
||||
@@ -146,6 +147,7 @@
|
||||
<ClInclude Include=".\RPL4APP.h" />
|
||||
<ClInclude Include=".\RPL4CONSOLE.h" />
|
||||
<ClInclude Include=".\RPL4FE.h" />
|
||||
<ClInclude Include=".\RPL4LOBBY.h" />
|
||||
<ClInclude Include=".\RPL4ARND.h" />
|
||||
<ClInclude Include=".\RPL4GAUG.h" />
|
||||
<ClInclude Include=".\RPL4GRND.h" />
|
||||
|
||||
Reference in New Issue
Block a user