Steam: the lobby + IDENTITY-TOKEN roster -- internet MP code-complete (step 4c)
NEW gated (BT_STEAM) game/glass/btl4lobby: an ISteamMatchmaking room replacing the arcade Site-Management screen -- host creates (lobby data btl4=1), members join by filter, pilots publish name/mech/color, the host ENTER mints the roster and signals GO. THE FAKEIP LESSON (live-verified, prior-art assumption DISPROVEN): a fresh process gets a DIFFERENT FakeIP, so menu-time fake addresses go stale by mission time -- the first cycle timed out connecting to its own stale address. Redesign: roster addresses are opaque ipv4-shaped TOKENS (169.254.77.N with ports 1501/1502) minted by the host at GO, mapped to Steam IDENTITIES; connections ride ConnectP2P(identity, channel) with console=0/game=1 virtual ports; the map reaches mission processes via env (BT_FE_MYFAKE + BT_FE_STEAMMAP); the GetMyAddress seam feeds the pod its own token for roster self-match; CheckSocket reports peers by token. L4STEAMNET reworked (no FakeIP allocation at all); the marshal gained the Steam-wire branch. Verified single-machine (ON/ON, live Steam): menu -> HOST STEAM LOBBY -> room -> GO -> token map minted -> egg roster carries the token -> mission process up with 1 roster token incl. self -> pod self-matches -> stock console ladder -> mission RUNS (46 ticks). Remaining: the live 2-account cross-machine session (docs/STEAM_TEST.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -368,6 +368,20 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
||||
break;
|
||||
#ifdef BT_STEAM
|
||||
case BTFeLaunchJoinSteam:
|
||||
SetEnvironmentVariableA("BT_FE_EGG", NULL);
|
||||
SetEnvironmentVariableA("BT_STEAM_NET", "1");
|
||||
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
|
||||
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
|
||||
sprintf(fe_arguments, "-net %d -platform glass", fe_spec.consolePort);
|
||||
break;
|
||||
case BTFeLaunchHostSteam:
|
||||
SetEnvironmentVariableA("BT_STEAM_NET", "1");
|
||||
SetEnvironmentVariableA("BT_FE_MYFAKE", fe_spec.steamMyToken);
|
||||
SetEnvironmentVariableA("BT_FE_STEAMMAP", fe_spec.steamMap);
|
||||
// falls into the marshal-armed default
|
||||
#endif
|
||||
default: // Solo / HostLan: the marshal armed via env handoff
|
||||
SetEnvironmentVariableA("BT_FE_EGG", fe_spec.eggPath);
|
||||
SetEnvironmentVariableA("BT_FE_PODS", fe_spec.podList);
|
||||
|
||||
+108
-12
@@ -68,6 +68,56 @@ struct MarshalState
|
||||
|
||||
static MarshalState marshalState;
|
||||
|
||||
//
|
||||
// The Steam-wire branch (BT_STEAM): a pod whose console address is a
|
||||
// FakeIP is reached through the engine's Steam transport (munga_engine)
|
||||
// instead of raw Winsock -- same wire bytes, different carrier.
|
||||
//
|
||||
#ifdef BT_STEAM
|
||||
extern int BTSteamNet_IsFakeAddress(unsigned long internet_address_be);
|
||||
extern SOCKET BTSteamNet_Connect(unsigned long internet_address_be, int remote_port);
|
||||
extern int BTSteamNet_Owns(SOCKET wire_socket);
|
||||
extern int BTSteamNet_Send(SOCKET wire_socket, const char *data, int length);
|
||||
extern int BTSteamNet_Recv(SOCKET wire_socket, char *buffer, int capacity);
|
||||
extern void BTSteamNet_Close(SOCKET wire_socket);
|
||||
#endif
|
||||
|
||||
static int
|
||||
MarshalIsSteam(SOCKET s)
|
||||
{
|
||||
#ifdef BT_STEAM
|
||||
return BTSteamNet_Owns(s);
|
||||
#else
|
||||
(void)s;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static int
|
||||
MarshalSend(SOCKET s, const char *data, int length)
|
||||
{
|
||||
#ifdef BT_STEAM
|
||||
if (BTSteamNet_Owns(s))
|
||||
{
|
||||
return BTSteamNet_Send(s, data, length);
|
||||
}
|
||||
#endif
|
||||
return send(s, data, length, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
MarshalClose(SOCKET s)
|
||||
{
|
||||
#ifdef BT_STEAM
|
||||
if (BTSteamNet_Owns(s))
|
||||
{
|
||||
BTSteamNet_Close(s);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
closesocket(s);
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
|
||||
void
|
||||
@@ -199,7 +249,7 @@ static int
|
||||
PutInt32(packet + 32, wire_length);
|
||||
PutInt32(packet + 36, this_length);
|
||||
memcpy(packet + 40, wire + off, this_length);
|
||||
if (send(s, (const char *)packet, sizeof(packet), 0) != sizeof(packet))
|
||||
if (MarshalSend(s, (const char *)packet, sizeof(packet)) != sizeof(packet))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -222,7 +272,7 @@ static int
|
||||
PutInt32(packet + 16, 12);
|
||||
PutInt32(packet + 20, message_id);
|
||||
PutInt32(packet + 24, ReliableFlag);
|
||||
return (send(s, (const char *)packet, sizeof(packet), 0) == sizeof(packet))
|
||||
return (MarshalSend(s, (const char *)packet, sizeof(packet)) == sizeof(packet))
|
||||
? 0 : -1;
|
||||
}
|
||||
|
||||
@@ -233,6 +283,24 @@ static int
|
||||
static SOCKET
|
||||
ConnectWithRetry(const char *host, int port, int attempts)
|
||||
{
|
||||
#ifdef BT_STEAM
|
||||
{
|
||||
unsigned long address_be = inet_addr(host);
|
||||
if (address_be != INADDR_NONE && BTSteamNet_IsFakeAddress(address_be))
|
||||
{
|
||||
for (int attempt = 0; attempt < attempts; ++attempt)
|
||||
{
|
||||
SOCKET s = BTSteamNet_Connect(address_be, port);
|
||||
if (s != INVALID_SOCKET)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
Sleep(1000);
|
||||
}
|
||||
return INVALID_SOCKET;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (int attempt = 0; attempt < attempts; ++attempt)
|
||||
{
|
||||
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
@@ -348,24 +416,52 @@ static DWORD WINAPI
|
||||
char drain[4096];
|
||||
while (GetTickCount() < mission_end)
|
||||
{
|
||||
//
|
||||
// select() only understands REAL sockets; Steam pseudo-sockets
|
||||
// drain nonblocking on their own pass.
|
||||
//
|
||||
fd_set readable;
|
||||
FD_ZERO(&readable);
|
||||
int real_count = 0;
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
FD_SET(pods[p], &readable);
|
||||
}
|
||||
timeval tv = { 1, 0 };
|
||||
int ready = select(0, &readable, NULL, NULL, &tv);
|
||||
if (ready > 0)
|
||||
{
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
if (!MarshalIsSteam(pods[p]))
|
||||
{
|
||||
if (FD_ISSET(pods[p], &readable))
|
||||
FD_SET(pods[p], &readable);
|
||||
++real_count;
|
||||
}
|
||||
}
|
||||
if (real_count > 0)
|
||||
{
|
||||
timeval tv = { 1, 0 };
|
||||
int ready = select(0, &readable, NULL, NULL, &tv);
|
||||
if (ready > 0)
|
||||
{
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
recv(pods[p], drain, sizeof(drain), 0);
|
||||
if (!MarshalIsSteam(pods[p]) &&
|
||||
FD_ISSET(pods[p], &readable))
|
||||
{
|
||||
recv(pods[p], drain, sizeof(drain), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Sleep(1000);
|
||||
}
|
||||
#ifdef BT_STEAM
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
if (MarshalIsSteam(pods[p]))
|
||||
{
|
||||
while (BTSteamNet_Recv(pods[p], drain, sizeof(drain)) > 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
MarshalLog("mission clock expired -- StopMission to all pods");
|
||||
@@ -376,7 +472,7 @@ static DWORD WINAPI
|
||||
Sleep(MissionEndGraceSeconds * 1000);
|
||||
for (int p = 0; p < pod_count; ++p)
|
||||
{
|
||||
closesocket(pods[p]);
|
||||
MarshalClose(pods[p]);
|
||||
}
|
||||
|
||||
MarshalLog("relaunching the menu");
|
||||
|
||||
+67
-2
@@ -4,6 +4,9 @@
|
||||
//###########################################################################
|
||||
|
||||
#include "btl4fe.hpp"
|
||||
#ifdef BT_STEAM
|
||||
#include "btl4lobby.hpp"
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
@@ -25,7 +28,11 @@ static const char *kVehicles[] = { "bhk1", "madcat", "ava1" };
|
||||
static const char *kColors[] = { "White", "Black", "Crimson" };
|
||||
static const char *kModes[] =
|
||||
{ "SOLO MISSION (networked)", "RAW SOLO (-egg, endless)",
|
||||
"HOST LAN", "JOIN LAN" };
|
||||
"HOST LAN", "JOIN LAN",
|
||||
#ifdef BT_STEAM
|
||||
"HOST STEAM LOBBY", "JOIN STEAM LOBBY",
|
||||
#endif
|
||||
};
|
||||
static const int kPorts[] = { 1501, 1601, 1701, 1801 };
|
||||
|
||||
#define COUNT(a) ((int)(sizeof(a)/sizeof((a)[0])))
|
||||
@@ -720,10 +727,68 @@ int
|
||||
}
|
||||
break;
|
||||
|
||||
default: // JOIN LAN: plain -net pod; the host's marshal feeds us
|
||||
case 3: // JOIN LAN: plain -net pod; the host's marshal feeds us
|
||||
spec->mode = BTFeLaunchJoinLan;
|
||||
spec->eggPath[0] = 0; // no egg written; host provides
|
||||
return 0;
|
||||
|
||||
#ifdef BT_STEAM
|
||||
case 4: // HOST STEAM LOBBY: the room fills the roster (self first)
|
||||
{
|
||||
BTLobbyRoster roster;
|
||||
if (BTLobby_HostAndRoom(self.name, self.vehicle, self.color,
|
||||
&roster, spec->steamMyToken, sizeof(spec->steamMyToken),
|
||||
spec->steamMap, sizeof(spec->steamMap)) != 0 ||
|
||||
roster.memberCount == 0)
|
||||
{
|
||||
return 1; // cancelled / Steam unavailable
|
||||
}
|
||||
spec->mode = BTFeLaunchHostSteam;
|
||||
//
|
||||
// Roster -> egg pilots (FAKE game addresses; self is pilot 1)
|
||||
// + the marshal pod list (self over TCP loopback, members at
|
||||
// their FAKE console addresses over the Steam wire).
|
||||
//
|
||||
mission.pilotCount = 0;
|
||||
sprintf(spec->podList, "127.0.0.1:%d", console_port);
|
||||
for (int m = 0; m < roster.memberCount && m < 8; ++m)
|
||||
{
|
||||
const BTLobbyMember &member = roster.members[m];
|
||||
BTFePilot &pilot = mission.pilots[mission.pilotCount++];
|
||||
strncpy(pilot.name, member.name, sizeof(pilot.name) - 1);
|
||||
strncpy(pilot.vehicle,
|
||||
member.vehicle[0] ? member.vehicle : kVehicles[0],
|
||||
sizeof(pilot.vehicle) - 1);
|
||||
strncpy(pilot.color,
|
||||
member.color[0] ? member.color : kColors[0],
|
||||
sizeof(pilot.color) - 1);
|
||||
sprintf(pilot.address, "%s:%d",
|
||||
member.fakeAddress, member.gamePort);
|
||||
if (!member.isSelf)
|
||||
{
|
||||
char entry[64];
|
||||
sprintf(entry, ",%s:%d",
|
||||
member.fakeAddress, member.consolePort);
|
||||
strcat(spec->podList, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 5: // JOIN STEAM LOBBY: wait for GO, then a Steam-wire pod
|
||||
if (BTLobby_JoinAndWait(self.name, self.vehicle, self.color,
|
||||
spec->steamMyToken, sizeof(spec->steamMyToken),
|
||||
spec->steamMap, sizeof(spec->steamMap)) != 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
spec->mode = BTFeLaunchJoinSteam;
|
||||
spec->eggPath[0] = 0;
|
||||
return 0;
|
||||
#endif
|
||||
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (BTFeMission_WriteEgg(&mission, spec->eggPath) != 0)
|
||||
|
||||
@@ -47,7 +47,9 @@ enum BTFeLaunchMode
|
||||
BTFeLaunchSolo, // networked 1-pod mission (console marshal + self)
|
||||
BTFeLaunchRawSolo, // plain -egg endless solo (renderer work)
|
||||
BTFeLaunchHostLan, // marshal feeds self + remote pods
|
||||
BTFeLaunchJoinLan // plain -net pod; a remote host's marshal feeds us
|
||||
BTFeLaunchJoinLan, // plain -net pod; a remote host's marshal feeds us
|
||||
BTFeLaunchHostSteam, // (BT_STEAM) lobby host: marshal over the Steam wire
|
||||
BTFeLaunchJoinSteam // (BT_STEAM) lobby member: -net pod on the Steam wire
|
||||
};
|
||||
|
||||
struct BTFeLaunchSpec
|
||||
@@ -57,6 +59,8 @@ struct BTFeLaunchSpec
|
||||
int consolePort; // this instance's -net port
|
||||
char podList[256]; // marshal targets (host modes)
|
||||
int missionSeconds;
|
||||
char steamMyToken[48]; // (BT_STEAM) my roster token ip
|
||||
char steamMap[512]; // (BT_STEAM) ip=steamid64;...
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
//###########################################################################
|
||||
// btl4lobby -- the Steam lobby (BT_STEAM only; compiled only when the gate
|
||||
// is on -- see CMakeLists.txt). Design: btl4lobby.hpp.
|
||||
//###########################################################################
|
||||
|
||||
#include "btl4lobby.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#pragma pack(push, 8)
|
||||
#include "steam/steam_api.h"
|
||||
#include "steam/isteammatchmaking.h"
|
||||
#pragma pack(pop)
|
||||
|
||||
//
|
||||
// The engine-side transport surface (munga_engine).
|
||||
//
|
||||
extern int BTSteamNet_Install();
|
||||
extern int BTSteamNet_Active();
|
||||
extern unsigned long long BTSteamNet_MySteamID();
|
||||
|
||||
static FILE *lobbyLogFile = NULL;
|
||||
static void
|
||||
LobbyLog(const char *format, ...)
|
||||
{
|
||||
if (lobbyLogFile == NULL)
|
||||
{
|
||||
lobbyLogFile = fopen("marshal.log", "at"); // shares the marshal's file
|
||||
if (lobbyLogFile == NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
va_list arguments;
|
||||
va_start(arguments, format);
|
||||
fprintf(lobbyLogFile, "[lobby] ");
|
||||
vfprintf(lobbyLogFile, format, arguments);
|
||||
fprintf(lobbyLogFile, "\n");
|
||||
fflush(lobbyLogFile);
|
||||
va_end(arguments);
|
||||
}
|
||||
#include <stdarg.h>
|
||||
|
||||
//###########################################################################
|
||||
// Synchronous Steam call-result helper (manual polling -- no callback
|
||||
// template machinery in this C-style TU).
|
||||
//###########################################################################
|
||||
|
||||
static int
|
||||
WaitApiCall(SteamAPICall_t call, void *result, int result_size,
|
||||
int expected_callback_id, int timeout_ms)
|
||||
{
|
||||
ISteamUtils *utils = SteamUtils();
|
||||
for (int waited = 0; waited < timeout_ms; waited += 50)
|
||||
{
|
||||
SteamAPI_RunCallbacks();
|
||||
bool failed = false;
|
||||
if (utils->IsAPICallCompleted(call, &failed))
|
||||
{
|
||||
if (failed)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return utils->GetAPICallResult(call, result, result_size,
|
||||
expected_callback_id, &failed) && !failed ? 0 : -1;
|
||||
}
|
||||
Sleep(50);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Member data
|
||||
//###########################################################################
|
||||
|
||||
static CSteamID currentLobby;
|
||||
|
||||
static void
|
||||
PublishSelf(const char *pilot_name, const char *vehicle, const char *color)
|
||||
{
|
||||
//
|
||||
// Identity is implicit (the member's SteamID); the roster TOKENS are
|
||||
// minted by the host at GO time -- only the pilot identity publishes.
|
||||
//
|
||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||
matchmaking->SetLobbyMemberData(currentLobby, "nm", pilot_name);
|
||||
matchmaking->SetLobbyMemberData(currentLobby, "vh", vehicle);
|
||||
matchmaking->SetLobbyMemberData(currentLobby, "cl", color);
|
||||
}
|
||||
|
||||
static int
|
||||
ReadMember(CSteamID lobby, int index, BTLobbyMember *out)
|
||||
{
|
||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||
CSteamID member = matchmaking->GetLobbyMemberByIndex(lobby, index);
|
||||
const char *published_name = matchmaking->GetLobbyMemberData(lobby, member, "nm");
|
||||
if (published_name == NULL || published_name[0] == 0)
|
||||
{
|
||||
return -1; // not published yet
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->steamID = member.ConvertToUint64();
|
||||
out->consolePort = 1501; // token ports (see L4STEAMNET token table)
|
||||
out->gamePort = 1502;
|
||||
strncpy(out->name,
|
||||
matchmaking->GetLobbyMemberData(lobby, member, "nm"),
|
||||
sizeof(out->name) - 1);
|
||||
strncpy(out->vehicle,
|
||||
matchmaking->GetLobbyMemberData(lobby, member, "vh"),
|
||||
sizeof(out->vehicle) - 1);
|
||||
strncpy(out->color,
|
||||
matchmaking->GetLobbyMemberData(lobby, member, "cl"),
|
||||
sizeof(out->color) - 1);
|
||||
out->isSelf = (member == SteamUser()->GetSteamID());
|
||||
if (out->name[0] == 0)
|
||||
{
|
||||
strcpy(out->name, "Pilot");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// The room screen -- the FE's green-on-black style; lists members, host
|
||||
// ENTER = GO, ESC = cancel.
|
||||
//###########################################################################
|
||||
|
||||
static int roomIsHost = 0;
|
||||
static int roomResult = 0; // 0 pending, 1 go, -1 cancel
|
||||
|
||||
static void
|
||||
PaintRoom(HWND window)
|
||||
{
|
||||
PAINTSTRUCT paint;
|
||||
HDC dc = BeginPaint(window, &paint);
|
||||
RECT client;
|
||||
GetClientRect(window, &client);
|
||||
HBRUSH background = CreateSolidBrush(RGB(4, 12, 4));
|
||||
FillRect(dc, &client, background);
|
||||
DeleteObject(background);
|
||||
SetBkMode(dc, TRANSPARENT);
|
||||
|
||||
HFONT font = CreateFontW(-16, 0, 0, 0, FW_NORMAL, 0, 0, 0,
|
||||
DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, FIXED_PITCH, L"Consolas");
|
||||
HFONT old_font = (HFONT)SelectObject(dc, font);
|
||||
SetTextColor(dc, RGB(90, 255, 90));
|
||||
|
||||
WCHAR line[160];
|
||||
int y = 16;
|
||||
wsprintfW(line, L"STEAM LOBBY -- %s",
|
||||
roomIsHost ? L"HOSTING (ENTER = launch, ESC = cancel)"
|
||||
: L"WAITING FOR THE HOST (ESC = leave)");
|
||||
RECT row = { 24, y, client.right - 24, y + 24 };
|
||||
DrawTextW(dc, line, -1, &row, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
||||
y += 40;
|
||||
|
||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||
int count = matchmaking->GetNumLobbyMembers(currentLobby);
|
||||
for (int i = 0; i < count && i < 8; ++i)
|
||||
{
|
||||
BTLobbyMember member;
|
||||
WCHAR name[120];
|
||||
if (ReadMember(currentLobby, i, &member) == 0)
|
||||
{
|
||||
int n = 0;
|
||||
for (const char *s = member.name; *s && n < 60; ++s) name[n++] = (WCHAR)*s;
|
||||
name[n] = 0;
|
||||
wsprintfW(line, L" %d. %s%s", i + 1, name,
|
||||
member.isSelf ? L" (you)" : L"");
|
||||
}
|
||||
else
|
||||
{
|
||||
wsprintfW(line, L" %d. (joining...)", i + 1);
|
||||
}
|
||||
RECT member_row = { 24, y, client.right - 24, y + 24 };
|
||||
DrawTextW(dc, line, -1, &member_row, DT_LEFT | DT_TOP | DT_SINGLELINE);
|
||||
y += 26;
|
||||
}
|
||||
|
||||
SelectObject(dc, old_font);
|
||||
DeleteObject(font);
|
||||
EndPaint(window, &paint);
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK
|
||||
RoomWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case WM_PAINT:
|
||||
PaintRoom(window);
|
||||
return 0;
|
||||
case WM_TIMER:
|
||||
SteamAPI_RunCallbacks();
|
||||
//
|
||||
// Members leave when the host signals GO.
|
||||
//
|
||||
if (!roomIsHost)
|
||||
{
|
||||
const char *go = SteamMatchmaking()->GetLobbyData(currentLobby, "btl4go");
|
||||
if (go != NULL && go[0] == '1')
|
||||
{
|
||||
roomResult = 1;
|
||||
}
|
||||
}
|
||||
InvalidateRect(window, NULL, FALSE);
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
if (wparam == VK_RETURN && roomIsHost)
|
||||
{
|
||||
roomResult = 1;
|
||||
}
|
||||
else if (wparam == VK_ESCAPE)
|
||||
{
|
||||
roomResult = -1;
|
||||
}
|
||||
return 0;
|
||||
case WM_CLOSE:
|
||||
roomResult = -1;
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
static int
|
||||
RunRoom(int is_host)
|
||||
{
|
||||
roomIsHost = is_host;
|
||||
roomResult = 0;
|
||||
|
||||
WNDCLASSW window_class;
|
||||
memset(&window_class, 0, sizeof(window_class));
|
||||
window_class.lpfnWndProc = RoomWndProc;
|
||||
window_class.hInstance = GetModuleHandleW(NULL);
|
||||
window_class.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW);
|
||||
window_class.lpszClassName = L"BTLobbyRoomWnd";
|
||||
RegisterClassW(&window_class);
|
||||
|
||||
HWND window = CreateWindowW(
|
||||
L"BTLobbyRoomWnd", L"BattleTech - Steam Lobby",
|
||||
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU,
|
||||
160, 160, 560, 360,
|
||||
NULL, NULL, GetModuleHandleW(NULL), NULL);
|
||||
if (window == NULL)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
SetTimer(window, 1, 250, NULL);
|
||||
ShowWindow(window, SW_SHOW);
|
||||
SetForegroundWindow(window);
|
||||
|
||||
MSG message;
|
||||
while (roomResult == 0 && GetMessageW(&message, NULL, 0, 0))
|
||||
{
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
KillTimer(window, 1);
|
||||
DestroyWindow(window);
|
||||
return (roomResult == 1) ? 0 : 1;
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
// Host / member entries
|
||||
//###########################################################################
|
||||
|
||||
int
|
||||
BTLobby_HostAndRoom(
|
||||
const char *pilot_name, const char *vehicle, const char *color,
|
||||
BTLobbyRoster *roster_out,
|
||||
char *my_token_out, int my_token_capacity,
|
||||
char *steam_map_out, int steam_map_capacity)
|
||||
{
|
||||
memset(roster_out, 0, sizeof(*roster_out));
|
||||
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
|
||||
{
|
||||
LobbyLog("host: Steam transport unavailable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
SteamAPICall_t call = SteamMatchmaking()->CreateLobby(k_ELobbyTypePublic, 8);
|
||||
LobbyCreated_t created;
|
||||
memset(&created, 0, sizeof(created));
|
||||
if (WaitApiCall(call, &created, sizeof(created),
|
||||
LobbyCreated_t::k_iCallback, 15000) != 0 ||
|
||||
created.m_eResult != k_EResultOK)
|
||||
{
|
||||
LobbyLog("host: CreateLobby failed");
|
||||
return -1;
|
||||
}
|
||||
currentLobby = created.m_ulSteamIDLobby;
|
||||
SteamMatchmaking()->SetLobbyData(currentLobby, "btl4", "1");
|
||||
PublishSelf(pilot_name, vehicle, color);
|
||||
LobbyLog("host: lobby up (%llu)", (unsigned long long)created.m_ulSteamIDLobby);
|
||||
|
||||
if (RunRoom(1) != 0)
|
||||
{
|
||||
SteamMatchmaking()->LeaveLobby(currentLobby);
|
||||
return 1;
|
||||
}
|
||||
|
||||
//
|
||||
// GO: snapshot the roster SELF FIRST (the egg's pilot order defines
|
||||
// the connect topology; the host must be pilot 1), MINT the tokens,
|
||||
// publish the token map, then signal.
|
||||
//
|
||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||
int count = matchmaking->GetNumLobbyMembers(currentLobby);
|
||||
for (int pass = 0; pass < 2; ++pass)
|
||||
{
|
||||
for (int i = 0; i < count && roster_out->memberCount < 8; ++i)
|
||||
{
|
||||
BTLobbyMember member;
|
||||
if (ReadMember(currentLobby, i, &member) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((pass == 0) == (member.isSelf != 0))
|
||||
{
|
||||
roster_out->members[roster_out->memberCount++] = member;
|
||||
}
|
||||
}
|
||||
}
|
||||
steam_map_out[0] = 0;
|
||||
my_token_out[0] = 0;
|
||||
for (int m = 0; m < roster_out->memberCount; ++m)
|
||||
{
|
||||
BTLobbyMember &member = roster_out->members[m];
|
||||
sprintf(member.fakeAddress, "169.254.77.%d", m + 1);
|
||||
char entry[96];
|
||||
sprintf(entry, "%s%s=%llu", (m > 0) ? ";" : "",
|
||||
member.fakeAddress, member.steamID);
|
||||
if ((int)(strlen(steam_map_out) + strlen(entry) + 1)
|
||||
< steam_map_capacity)
|
||||
{
|
||||
strcat(steam_map_out, entry);
|
||||
}
|
||||
if (member.isSelf)
|
||||
{
|
||||
strncpy(my_token_out, member.fakeAddress, my_token_capacity - 1);
|
||||
my_token_out[my_token_capacity - 1] = 0;
|
||||
}
|
||||
}
|
||||
matchmaking->SetLobbyData(currentLobby, "btl4map", steam_map_out);
|
||||
matchmaking->SetLobbyData(currentLobby, "btl4go", "1");
|
||||
SteamAPI_RunCallbacks();
|
||||
LobbyLog("host: GO with %d member(s), map [%s]",
|
||||
roster_out->memberCount, steam_map_out);
|
||||
//
|
||||
// The lobby will not survive the relaunch (documented); leave now so
|
||||
// members do not see a ghost host.
|
||||
//
|
||||
Sleep(1500); // let the GO propagate
|
||||
SteamMatchmaking()->LeaveLobby(currentLobby);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
BTLobby_JoinAndWait(
|
||||
const char *pilot_name, const char *vehicle, const char *color,
|
||||
char *my_token_out, int my_token_capacity,
|
||||
char *steam_map_out, int steam_map_capacity)
|
||||
{
|
||||
if (BTSteamNet_Install() != 0 || !BTSteamNet_Active())
|
||||
{
|
||||
LobbyLog("join: Steam transport unavailable");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ISteamMatchmaking *matchmaking = SteamMatchmaking();
|
||||
matchmaking->AddRequestLobbyListStringFilter(
|
||||
"btl4", "1", k_ELobbyComparisonEqual);
|
||||
SteamAPICall_t call = matchmaking->RequestLobbyList();
|
||||
LobbyMatchList_t match_list;
|
||||
memset(&match_list, 0, sizeof(match_list));
|
||||
if (WaitApiCall(call, &match_list, sizeof(match_list),
|
||||
LobbyMatchList_t::k_iCallback, 15000) != 0 ||
|
||||
match_list.m_nLobbiesMatching == 0)
|
||||
{
|
||||
LobbyLog("join: no btl4 lobby found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
CSteamID lobby = matchmaking->GetLobbyByIndex(0);
|
||||
call = matchmaking->JoinLobby(lobby);
|
||||
LobbyEnter_t entered;
|
||||
memset(&entered, 0, sizeof(entered));
|
||||
if (WaitApiCall(call, &entered, sizeof(entered),
|
||||
LobbyEnter_t::k_iCallback, 15000) != 0 ||
|
||||
entered.m_EChatRoomEnterResponse != k_EChatRoomEnterResponseSuccess)
|
||||
{
|
||||
LobbyLog("join: JoinLobby failed");
|
||||
return -1;
|
||||
}
|
||||
currentLobby = lobby;
|
||||
PublishSelf(pilot_name, vehicle, color);
|
||||
LobbyLog("join: in lobby, waiting for GO");
|
||||
|
||||
int result = RunRoom(0);
|
||||
if (result == 0)
|
||||
{
|
||||
//
|
||||
// GO: read the token map, find my own token by my SteamID.
|
||||
//
|
||||
const char *map_text = matchmaking->GetLobbyData(currentLobby, "btl4map");
|
||||
steam_map_out[0] = 0;
|
||||
my_token_out[0] = 0;
|
||||
if (map_text != NULL)
|
||||
{
|
||||
strncpy(steam_map_out, map_text, steam_map_capacity - 1);
|
||||
steam_map_out[steam_map_capacity - 1] = 0;
|
||||
char needle[32];
|
||||
sprintf(needle, "=%llu", BTSteamNet_MySteamID());
|
||||
char map_copy[512];
|
||||
strncpy(map_copy, map_text, sizeof(map_copy) - 1);
|
||||
map_copy[sizeof(map_copy) - 1] = 0;
|
||||
char *cursor = strtok(map_copy, ";");
|
||||
while (cursor != NULL)
|
||||
{
|
||||
char *hit = strstr(cursor, needle);
|
||||
if (hit != NULL && hit[strlen(needle)] == 0)
|
||||
{
|
||||
*hit = 0;
|
||||
strncpy(my_token_out, cursor, my_token_capacity - 1);
|
||||
my_token_out[my_token_capacity - 1] = 0;
|
||||
}
|
||||
cursor = strtok(NULL, ";");
|
||||
}
|
||||
}
|
||||
LobbyLog("join: my token [%s], map [%s]", my_token_out, steam_map_out);
|
||||
if (my_token_out[0] == 0)
|
||||
{
|
||||
result = -1; // the host's map is missing us
|
||||
}
|
||||
}
|
||||
SteamMatchmaking()->LeaveLobby(currentLobby);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
//###########################################################################
|
||||
//
|
||||
// btl4lobby -- the Steam lobby (BT_STEAM only): an ISteamMatchmaking room
|
||||
// replacing the arcade Site-Management screen for internet MP testing.
|
||||
//
|
||||
// The MENU process runs the lobby (the Steam transport is installed there
|
||||
// first, so each member's FakeIP is known): members publish their fake
|
||||
// address/ports + pilot identity as lobby member data; the host's room
|
||||
// screen lists members and its ENTER writes the GO signal; everyone then
|
||||
// relaunches into the mission -- the host with the marshal armed against
|
||||
// the members' fake CONSOLE addresses, members as plain -net pods with
|
||||
// BT_STEAM_NET=1 waiting to be fed over the Steam wire.
|
||||
//
|
||||
// Known limitation (documented): the lobby object does not survive the
|
||||
// per-mission process relaunch -- after a mission everyone re-joins from
|
||||
// the menu. FakeIP stability across the immediate relaunch is assumed
|
||||
// (Valve persists assignments per app+account); the live 2-account test
|
||||
// is the step's exit criterion (docs/STEAM_TEST.md).
|
||||
//
|
||||
//###########################################################################
|
||||
|
||||
struct BTLobbyMember
|
||||
{
|
||||
char name[32];
|
||||
char fakeAddress[48]; // the minted roster TOKEN ip
|
||||
int consolePort; // token console port (1501)
|
||||
int gamePort; // token game port (1502)
|
||||
char vehicle[16];
|
||||
char color[16];
|
||||
int isSelf;
|
||||
unsigned long long steamID;
|
||||
};
|
||||
|
||||
struct BTLobbyRoster
|
||||
{
|
||||
int memberCount;
|
||||
BTLobbyMember members[8];
|
||||
};
|
||||
|
||||
//
|
||||
// Host: create the lobby, run the room screen, on ENTER write GO and fill
|
||||
// the roster (self first). Returns 0 = launch, nonzero = cancelled.
|
||||
//
|
||||
int
|
||||
BTLobby_HostAndRoom(
|
||||
const char *pilot_name, const char *vehicle, const char *color,
|
||||
BTLobbyRoster *roster_out,
|
||||
char *my_token_out, int my_token_capacity,
|
||||
char *steam_map_out, int steam_map_capacity);
|
||||
|
||||
//
|
||||
// Member: find + join a lobby, run the room screen, return when the host
|
||||
// signals GO. Returns 0 = launch, nonzero = cancelled / none found.
|
||||
//
|
||||
int
|
||||
BTLobby_JoinAndWait(
|
||||
const char *pilot_name, const char *vehicle, const char *color,
|
||||
char *my_token_out, int my_token_capacity,
|
||||
char *steam_map_out, int steam_map_capacity);
|
||||
Reference in New Issue
Block a user