#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; }
void RPL4Lobby_PushRaceResults()					{ }
void RPL4Lobby_PullRaceResults()					{ }

#else

#include "rpl4fe.h"
#include "rpl4console.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
	int			gShownResultsNonce = 0;	// score sheets we already displayed
	const char	kResultsKey[] = "res";
	const char	kScenarioKey[] = "sc";

	// 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");
		}
	}

	Logical IsOwner()
	{
		return gInLobby &&
			SteamMatchmaking()->GetLobbyOwner(gLobby) == SteamUser()->GetSteamID();
	}

	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);

		// football: this member's own team and position pick
		SteamMatchmaking()->SetLobbyMemberData(gLobby, "tm",
			RPL4FrontEnd_TeamKey(RPL4FrontEnd_GetTeamIndex()));
		SteamMatchmaking()->SetLobbyMemberData(gLobby, "ps",
			RPL4FrontEnd_PositionKey(RPL4FrontEnd_GetPositionIndex()));

		// the owner also publishes the scenario, so members know
		// whether their team/position pick matters
		if (IsOwner())
		{
			SteamMatchmaking()->SetLobbyData(gLobby, kScenarioKey,
				RPL4FrontEnd_IsFootballSelected() ? "football" : "race");
		}
	}

	Logical FootballLobby()
	{
		if (!gInLobby)
		{
			return False;
		}
		const char *scenario = SteamMatchmaking()->GetLobbyData(gLobby, kScenarioKey);
		return (scenario != NULL && strcmp(scenario, "football") == 0);
	}

	//---------------------------------------------------------------
	// 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];
		char		team[32];		// football pick
		char		position[16];
		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);
			strncpy(member->team,
				SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "tm"),
				sizeof(member->team) - 1);
			strncpy(member->position,
				SteamMatchmaking()->GetLobbyMemberData(gLobby, member->id, "ps"),
				sizeof(member->position) - 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,
					members[i].id.ConvertToUint64());
			}
		}
	}

	// 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);
			strncpy(pilot->team, members[i].team, sizeof(pilot->team) - 1);
			strncpy(pilot->position, members[i].position, sizeof(pilot->position) - 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;
		RECT		teamRect;		// football: cycle my team
		RECT		positionRect;	// football: cycle my position
		Logical		launchClicked;
		Logical		leaveClicked;
		Logical		pickChanged;
		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,
			FootballLobby() ? "STEAM FOOTBALL LOBBY" : "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 (FootballLobby())
			{
				// football: the roster is a team sheet
				if (member->team[0] != '\0')
				{
					const char *position_name = "?";
					for (int i = 0; i < RPL4FrontEnd_PositionCount(); ++i)
					{
						if (strcmp(member->position, RPL4FrontEnd_PositionKey(i)) == 0)
						{
							position_name = RPL4FrontEnd_PositionName(i);
							break;
						}
					}
					sprintf(text, "%s  -  %s", member->team, position_name);
					DrawTextA(mem, text, -1, &row, DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
				}
			}
			else 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);

		//
		// Football: my own team and position, click to cycle
		//
		if (FootballLobby())
		{
			FrameRect(mem, &room->teamRect, frame);
			SetTextColor(mem, kGreenBright);
			sprintf(text, "MY TEAM:  %s",
				RPL4FrontEnd_TeamName(RPL4FrontEnd_GetTeamIndex()));
			DrawTextA(mem, text, -1, &room->teamRect,
				DT_CENTER | DT_VCENTER | DT_SINGLELINE);

			FrameRect(mem, &room->positionRect, frame);
			sprintf(text, "MY POSITION:  %s",
				RPL4FrontEnd_PositionName(RPL4FrontEnd_GetPositionIndex()));
			DrawTextA(mem, text, -1, &room->positionRect,
				DT_CENTER | DT_VCENTER | DT_SINGLELINE);
		}
		if (IsOwner())
		{
			FrameRect(mem, &room->launchRect, frame);
			SetTextColor(mem, kGreenBright);
			DrawTextA(mem, "L A U N C H   G A M 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;
				}
				else if (FootballLobby() && PtInRect(&room->teamRect, point))
				{
					RPL4FrontEnd_SetTeamIndex(
						(RPL4FrontEnd_GetTeamIndex() + 1) % RPL4FrontEnd_TeamCount());
					room->pickChanged = True;
				}
				else if (FootballLobby() && PtInRect(&room->positionRect, point))
				{
					RPL4FrontEnd_SetPositionIndex(
						(RPL4FrontEnd_GetPositionIndex() + 1) % RPL4FrontEnd_PositionCount());
					room->pickChanged = 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;

		// football pick buttons, above the launch button
		room.teamRect.left = (client.right - col_w) / 2;
		room.teamRect.right = room.teamRect.left + col_w;
		room.teamRect.top = client.bottom - 12 * row_h;
		room.teamRect.bottom = room.teamRect.top + 2 * row_h;
		room.positionRect.left = room.teamRect.left;
		room.positionRect.right = room.teamRect.right;
		room.positionRect.top = client.bottom - 9 * row_h;
		room.positionRect.bottom = room.positionRect.top + 2 * 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;
			}
			//
			// A pick changed: republish so everyone's roster updates
			//
			if (room.pickChanged)
			{
				room.pickChanged = False;
				PublishMemberData();
				room.memberCount = CollectMembers(room.members);
				InvalidateRect(room.window, NULL, FALSE);
				RedrawWindow(room.window, NULL, NULL, RDW_UPDATENOW);
			}

			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[800];
					sprintf(go, "%d:", gLastGoNonce);
					for (int i = 0; i < room.memberCount; ++i)
					{
						char entry[96];
						sprintf(entry, "%s|%d|%d|%I64u;", room.members[i].ip,
							room.members[i].consolePort, room.members[i].gamePort,
							room.members[i].id.ConvertToUint64());
						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;
							unsigned __int64 steam_id = 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 != ';') ++cursor;
							}
							if (*cursor == '|')
							{
								steam_id = _strtoui64(++cursor, NULL, 10);
							}
							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, steam_id);
							}
						}
						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);
	// the default lobby search is distance-filtered (roughly same
	// region) - RP412 races are worldwide
	SteamMatchmaking()->AddRequestLobbyListDistanceFilter(
		k_ELobbyDistanceFilterWorldwide);
	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);
}

void
	RPL4Lobby_PushRaceResults()
{
	if (!gInLobby || !IsOwner() || RPL4LocalConsole_ResultCount() == 0)
	{
		return;
	}
	SteamAPI_RunCallbacks();

	char sheet[600];
	sprintf(sheet, "%d:", gLastGoNonce);
	for (int i = 0; i < RPL4LocalConsole_ResultCount(); ++i)
	{
		int host_ID, score;
		if (!RPL4LocalConsole_GetResult(i, &host_ID, &score))
		{
			continue;
		}
		const char *name = RPL4LocalConsole_GetResultName(host_ID);
		char entry[80];
		sprintf(entry, "%d|%d|%.30s;", host_ID, score,
			(name != NULL) ? name : "");
		if (strlen(sheet) + strlen(entry) < sizeof(sheet))
		{
			strcat(sheet, entry);
		}
	}
	SteamMatchmaking()->SetLobbyData(gLobby, kResultsKey, sheet);
	gShownResultsNonce = gLastGoNonce;		// the owner sees its own screen
	DEBUG_STREAM << "Lobby: results published (" << sheet << ")\n" << std::flush;
}

void
	RPL4Lobby_PullRaceResults()
{
	if (!gInLobby || IsOwner() || gLastGoNonce == gShownResultsNonce)
	{
		return;
	}

	//
	// The owner publishes right after its race teardown - ours may
	// finish first, so give the sheet a moment to arrive
	//
	const char *sheet = NULL;
	DWORD deadline = GetTickCount() + 8 * 1000;
	for (;;)
	{
		SteamAPI_RunCallbacks();
		sheet = SteamMatchmaking()->GetLobbyData(gLobby, kResultsKey);
		if (sheet != NULL && sheet[0] != '\0' && atoi(sheet) == gLastGoNonce)
		{
			break;
		}
		if ((LONG)(GetTickCount() - deadline) >= 0)
		{
			return;		// no sheet for this race - skip the screen
		}
		Sleep(100);
	}
	gShownResultsNonce = gLastGoNonce;

	RPL4LocalConsole_ClearResults();
	const char *cursor = strchr(sheet, ':');
	cursor = (cursor != NULL) ? cursor + 1 : sheet;
	while (*cursor != '\0')
	{
		int host_ID = atoi(cursor);
		int score = 0;
		char name[32] = "";
		while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor;
		if (*cursor == '|')
		{
			score = atoi(++cursor);
			while (*cursor != '\0' && *cursor != '|' && *cursor != ';') ++cursor;
		}
		if (*cursor == '|')
		{
			++cursor;
			int n = 0;
			while (cursor[n] != '\0' && cursor[n] != ';' &&
				n < (int) sizeof(name) - 1)
			{
				name[n] = cursor[n];
				++n;
			}
			name[n] = '\0';
			cursor += n;
		}
		while (*cursor != '\0' && *cursor != ';') ++cursor;
		if (*cursor == ';') ++cursor;

		if (host_ID > 0)
		{
			RPL4LocalConsole_InjectResult(host_ID, score, name);
		}
	}
	DEBUG_STREAM << "Lobby: results pulled for race " << gLastGoNonce << "\n" << std::flush;
}

#endif	// RP412_STEAM
