#include "..\munga_l4\mungal4.h"
#pragma hdrstop

#include "rpl4fe.h"
#include "rpl4console.h"
#include "rpl4lobby.h"

#include <stdio.h>
#include <string>

//########################################################################
// The Death Race catalog (from TeslaConsole's RedPlanet/RPConfig.xml;
// 'headmf' is excluded for the race scenario there).
//########################################################################

namespace
{
	struct CatalogEntry
	{
		const char	*key;
		const char	*name;
	};

	const CatalogEntry kMaps[] =
	{
		{ "wise",		"Wiseguy's Wake" },
		{ "lyzlane",	"Lyz's Lane" },
		{ "yip",		"Yip's Yahoorama" },
		{ "blade",		"Blade's Edge" },
		{ "otto",		"Berserker's Bahnzai" },
		{ "frstrm",		"Firestorm's Fury" },
		{ "burnt",		"Burnt Cookies" },
		{ "brewers",	"Brewer's Bane" },
		{ "pain",		"Paingod's Passage" },
		{ "headoff",	"Freezemoon's Freeway" },
	};

	// Football excludes pain/headoff and adds the football build of
	// Freezemoon's Freeway (RPConfig.xml per-scenario invalid lists).
	const CatalogEntry kFootballMaps[] =
	{
		{ "wise",		"Wiseguy's Wake" },
		{ "lyzlane",	"Lyz's Lane" },
		{ "yip",		"Yip's Yahoorama" },
		{ "blade",		"Blade's Edge" },
		{ "otto",		"Berserker's Bahnzai" },
		{ "frstrm",		"Firestorm's Fury" },
		{ "burnt",		"Burnt Cookies" },
		{ "brewers",	"Brewer's Bane" },
		{ "headmf",		"Freezemoon's Freeway" },
	};

	const CatalogEntry kScenarios[] =
	{
		{ "race",		"Death Race" },
		{ "football",	"Football" },
	};

	// Two-color teams: crushers/blockers wear the team color, the
	// runner wears the runner color (that is how you spot the ball).
	struct TeamEntry
	{
		const char	*key;
		const char	*name;
		const char	*teamColor;
		const char	*runnerColor;
	};

	const TeamEntry kTeams[] =
	{
		{ "Red/Pink",		"Red / Pink",		"Red",		"Pink" },
		{ "Blue/Aqua",		"Blue / Aqua",		"Blue",		"Aqua" },
		{ "Green/Yellow",	"Green / Yellow",	"Green",	"Yellow" },
		{ "Black/Purple",	"Black / Purple",	"Black",	"Purple" },
	};

	const CatalogEntry kPositions[] =
	{
		{ "runner",		"Runner" },
		{ "crusher",	"Crusher" },
		{ "blocker",	"Blocker" },
	};

	const CatalogEntry kVehicles[] =
	{
		{ "quark",		"Quark" },
		{ "lepton",		"Lepton" },
		{ "proton",		"Proton" },
		{ "bug",		"Bug" },
		{ "speck",		"Speck" },
		{ "blkspk",		"Black Speck" },
		{ "chigger",	"Chigger" },
		{ "flea",		"Flea" },
		{ "roach",		"Roach" },
		{ "skeeter",	"Skeeter" },
		{ "wasp",		"Wasp" },
		{ "barge",		"Barge" },
		{ "broccoli",	"Screaming Broccoli" },
		{ "burro",		"Burro" },
		{ "mule",		"Mule" },
		{ "bttlbrg",	"Battle Barge" },
		{ "gator",		"Gator" },
		{ "grunt",		"Grunt" },
		{ "vole",		"Vole" },
		{ "bull",		"Bull" },
		{ "tarntula",	"Tarantula" },
		{ "mantis",		"Mantis" },
		{ "ripper",		"Ripper" },
		{ "roadblk",	"Road Block" },
		{ "spitter",	"Spitter" },
		{ "puck",		"Armadillo" },
		{ "dragon",		"Dragon" },
	};

	const CatalogEntry kColors[] =
	{
		{ "Red", "Red" }, { "Blue", "Blue" }, { "Green", "Green" },
		{ "White", "White" }, { "Pink", "Pink" }, { "Aqua", "Aqua" },
		{ "Yellow", "Yellow" }, { "Purple", "Purple" }, { "Black", "Black" },
	};

	const CatalogEntry kBadges[] =
	{
		{ "None", "None" }, { "Celtic", "Celtic" }, { "Desert", "Desert" },
		{ "Flames", "Flames" }, { "Giraffe", "Giraffe" }, { "Hawaii", "Hawaii" },
		{ "Korean", "Korean Camo" }, { "Lightning", "Lightning" },
		{ "Razzle Dazzle", "Razzle Dazzle" }, { "Rising Sun", "Rising Sun" },
		{ "Tiger Stripe", "Tiger Stripes" },
	};

	const CatalogEntry kTimes[] =
	{
		{ "day", "Day" }, { "night", "Night" },
	};

	const CatalogEntry kWeather[] =
	{
		{ "clear", "Clear" }, { "fog", "Light Fog" }, { "soup", "Heavy Fog" },
	};

	struct LengthEntry
	{
		int			seconds;	// 0 = endless (length line omitted)
		const char	*name;
	};

	const LengthEntry kLengths[] =
	{
		{ 0,	"Endless" },
		{ 180,	"3:00" },
		{ 300,	"5:00" },
		{ 480,	"8:00" },
		{ 600,	"10:00" },
	};

	// Standalone pilot address, as used by TEST.EGG: single-user mode
	// adopts the first pilot entry without resolving it.
	const char kLocalPilotAddress[] = "200.0.0.255";

	#define FE_COUNT(t)	((int)(sizeof(t)/sizeof((t)[0])))

	//---------------------------------------------------------------
	// Menu model
	//---------------------------------------------------------------
	enum FEGroup
	{
		GroupMap = 0,
		GroupVehicle,
		GroupColor,
		GroupBadge,
		GroupTime,
		GroupWeather,
		GroupLength,
		GroupScenario,
		GroupTeam,			// football only
		GroupPosition,		// football only
		GroupLaunch,
		GroupSteamHost,		// buttons, not selections (Steam builds only)
		GroupSteamJoin,
		GroupCount
	};

	Logical IsFootball(const int *selection)
	{
		return selection[GroupScenario] == 1;
	}

	const CatalogEntry *ActiveMaps(const int *selection, int *count)
	{
		if (IsFootball(selection))
		{
			*count = (int)(sizeof(kFootballMaps) / sizeof(kFootballMaps[0]));
			return kFootballMaps;
		}
		*count = (int)(sizeof(kMaps) / sizeof(kMaps[0]));
		return kMaps;
	}

	struct FEItem
	{
		int		group;
		int		index;
		RECT	rect;
	};

	struct FEState
	{
		int		selection[GroupCount];
		char	pilotName[24];

		FEItem	items[128];
		int		itemCount;

		HWND	menuWindow;
		HWND	nameEdit;
		HFONT	textFont;
		HFONT	titleFont;
		HBRUSH	editBrush;
		Logical	launched;
		Logical	closed;
		int		steamAction;	// 1 = host lobby, 2 = join lobby
	};

	FEState *gFE = NULL;
	int gLastMissionSeconds = 0;

	// carried across races so cycling back to the menu keeps the
	// player's loadout (and names the results screen's score rows)
	int		gPersistSelection[GroupCount];
	Logical	gHavePersist = False;
	char	gLastPilotName[24] = "Pilot";

	// [pilots]-order names of the last launched race (owner first),
	// comma separated - the network console labels results with them
	char	gLastPilotNamesCsv[256] = "";

	//---------------------------------------------------------------
	// Multiplayer host mode (lobby stand-in): RP412HOSTPODS lists the
	// member pods' console channels ("ip[:port]" comma separated) and
	// the egg gains one pilot per member. RP412HOSTPORT is the
	// owner's console port (default 1501; game port is +1) and
	// RP412HOSTADDR the owner's mesh address (default 127.0.0.1 -
	// the Steam lobby supplies the FakeIP instead).
	//---------------------------------------------------------------
	enum { maxExtraPilots = 8 };
	struct ExtraPilot
	{
		char		address[64];	// mesh (game port) address for [pilots]
		char		name[32];
		char		vehicle[24];
		char		color[16];
		char		badge[24];
		char		team[32];		// football pick, "" = assign for them
		char		position[16];
	};

	// 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");
				strcpy(extras[i].team, gHostedPilots[i].team);
				strcpy(extras[i].position, gHostedPilots[i].position);
			}
			return gHostedPilotCount;
		}

		const char *host_pods = getenv("RP412HOSTPODS");
		if (host_pods == NULL || host_pods[0] == '\0')
		{
			return -1;		// single player: standalone address
		}

		int host_port = 1501;
		const char *port_override = getenv("RP412HOSTPORT");
		if (port_override != NULL && atoi(port_override) > 0)
		{
			host_port = atoi(port_override);
		}
		const char *host_address = getenv("RP412HOSTADDR");
		if (host_address == NULL || host_address[0] == '\0')
		{
			host_address = "127.0.0.1";
		}
		sprintf(owner_address, "%s:%d", host_address, host_port + 1);

		int extra_count = 0;
		const char *cursor = host_pods;
		while (*cursor != '\0' && extra_count < maxExtraPilots)
		{
			char entry[64];
			int length = 0;
			while (cursor[length] != '\0' && cursor[length] != ',' &&
				length < (int) sizeof(entry) - 1)
			{
				entry[length] = cursor[length];
				++length;
			}
			entry[length] = '\0';
			cursor += length;
			if (*cursor == ',')
			{
				++cursor;
			}

			int console_port = 1501;
			char *colon = strrchr(entry, ':');
			if (colon != NULL)
			{
				*colon = '\0';
				console_port = atoi(colon + 1);
			}

			ExtraPilot *pilot = &extras[extra_count];
			memset(pilot, 0, sizeof(*pilot));
			sprintf(pilot->address, "%s:%d", entry, console_port + 1);
			sprintf(pilot->name, "PILOT %d", extra_count + 2);
			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;
	}

	const COLORREF kGreenBright	= RGB(64, 255, 64);
	const COLORREF kGreenDim	= RGB(24, 140, 24);
	const COLORREF kBlack		= RGB(0, 0, 0);

	//---------------------------------------------------------------
	// Layout: three columns of lists + the launch button
	//---------------------------------------------------------------
	void AddGroupItems(
		FEState *fe, int group, int count,
		int x, int *y, int row_h, int width)
	{
		for (int i = 0; i < count; ++i)
		{
			FEItem *item = &fe->items[fe->itemCount++];
			item->group = group;
			item->index = i;
			item->rect.left = x;
			item->rect.top = *y;
			item->rect.right = x + width;
			item->rect.bottom = *y + row_h;
			*y += row_h;
		}
		*y += row_h;		// gap below the group
	}

	void LayoutMenu(FEState *fe, int client_w, int client_h)
	{
		fe->itemCount = 0;

		int row_h = client_h / 36;
		if (row_h < 18)	row_h = 18;
		if (row_h > 30)	row_h = 30;

		int col_w = client_w / 5;
		int col1 = client_w / 14;
		int col2 = col1 + col_w + client_w / 28;
		int col3 = col2 + col_w + client_w / 28;
		int top = client_h / 7;

		int y = top;
		AddGroupItems(fe, GroupScenario, FE_COUNT(kScenarios), col1, &y, row_h, col_w);
		int map_count;
		ActiveMaps(fe->selection, &map_count);
		AddGroupItems(fe, GroupMap, map_count, col1, &y, row_h, col_w);
		AddGroupItems(fe, GroupTime, FE_COUNT(kTimes), col1, &y, row_h, col_w);
		AddGroupItems(fe, GroupWeather, FE_COUNT(kWeather), col1, &y, row_h, col_w);
		AddGroupItems(fe, GroupLength, FE_COUNT(kLengths), col1, &y, row_h, col_w);

		y = top;
		AddGroupItems(fe, GroupVehicle, FE_COUNT(kVehicles), col2, &y, row_h, col_w);

		y = top + 2 * row_h;	// leave room for the name edit + header
		if (IsFootball(fe->selection))
		{
			AddGroupItems(fe, GroupTeam, FE_COUNT(kTeams), col3, &y, row_h, col_w);
			AddGroupItems(fe, GroupPosition, FE_COUNT(kPositions), col3, &y, row_h, col_w);
		}
		else
		{
			AddGroupItems(fe, GroupColor, FE_COUNT(kColors), col3, &y, row_h, col_w);
			AddGroupItems(fe, GroupBadge, FE_COUNT(kBadges), col3, &y, row_h, col_w);
		}

		// launch button
		FEItem *launch = &fe->items[fe->itemCount++];
		launch->group = GroupLaunch;
		launch->index = 0;
		launch->rect.left = col3;
		launch->rect.top = client_h - 3 * row_h;
		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)
		{
			MoveWindow(fe->nameEdit,
				col3, top - row_h / 4, col_w, row_h, TRUE);
		}
	}

	const char *GroupTitle(int group)
	{
		switch (group)
		{
		case GroupScenario:	return "SCENARIO";
		case GroupMap:		return "TRACK";
		case GroupVehicle:	return "VEHICLE";
		case GroupColor:	return "COLOR";
		case GroupBadge:	return "BADGE";
		case GroupTeam:		return "TEAM";
		case GroupPosition:	return "POSITION";
		case GroupTime:		return "TIME OF DAY";
		case GroupWeather:	return "WEATHER";
		case GroupLength:	return "GAME LENGTH";
		}
		return "";
	}

	// the map rows need the active scenario's list
	const int *gItemNameSelection = NULL;

	const char *ItemName(int group, int index)
	{
		switch (group)
		{
		case GroupScenario:	return kScenarios[index].name;
		case GroupMap:
			if (gItemNameSelection != NULL && IsFootball(gItemNameSelection))
			{
				return kFootballMaps[index].name;
			}
			return kMaps[index].name;
		case GroupVehicle:	return kVehicles[index].name;
		case GroupColor:	return kColors[index].name;
		case GroupBadge:	return kBadges[index].name;
		case GroupTeam:		return kTeams[index].name;
		case GroupPosition:	return kPositions[index].name;
		case GroupTime:		return kTimes[index].name;
		case GroupWeather:	return kWeather[index].name;
		case GroupLength:	return kLengths[index].name;
		case GroupLaunch:	return "L A U N C H   G A M E";
		case GroupSteamHost:	return "HOST STEAM GAME";
		case GroupSteamJoin:	return "JOIN STEAM GAME";
		}
		return "";
	}

	//---------------------------------------------------------------
	// Painting (double buffered, pod green on black)
	//---------------------------------------------------------------
	void PaintMenu(FEState *fe)
	{
		PAINTSTRUCT ps;
		HDC hdc = BeginPaint(fe->menuWindow, &ps);

		RECT client;
		GetClientRect(fe->menuWindow, &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);

		// title
		gItemNameSelection = fe->selection;
		SelectObject(mem, fe->titleFont);
		SetTextColor(mem, kGreenBright);
		RECT title = client;
		title.top = client.bottom / 28;
		title.bottom = title.top + client.bottom / 10;
		DrawTextA(mem,
			IsFootball(fe->selection)
				? "RED PLANET  -  MARTIAN FOOTBALL"
				: "RED PLANET  -  MARTIAN DEATH RACE",
			-1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE);

		SelectObject(mem, fe->textFont);

		// group headers (drawn above each group's first item)
		int previous_group = -1;
		for (int i = 0; i < fe->itemCount; ++i)
		{
			const FEItem *item = &fe->items[i];
			if (item->group != previous_group && item->group < GroupLaunch)
			{
				previous_group = item->group;
				RECT header = item->rect;
				header.top -= (item->rect.bottom - item->rect.top);
				header.bottom = item->rect.top;
				SetTextColor(mem, kGreenBright);
				DrawTextA(mem, GroupTitle(item->group), -1, &header,
					DT_LEFT | DT_VCENTER | DT_SINGLELINE);
			}

			Logical selected =
				(item->group >= GroupLaunch) ||
				(fe->selection[item->group] == item->index);

			RECT row = item->rect;
			if (item->group >= GroupLaunch)
			{
				HBRUSH launch_brush = CreateSolidBrush(kGreenBright);
				FrameRect(mem, &row, launch_brush);
				DeleteObject(launch_brush);
				SetTextColor(mem, kGreenBright);
				DrawTextA(mem, ItemName(item->group, item->index), -1, &row,
					DT_CENTER | DT_VCENTER | DT_SINGLELINE);
				continue;
			}

			if (selected)
			{
				HBRUSH sel_brush = CreateSolidBrush(kGreenDim);
				RECT fill = row;
				FillRect(mem, &fill, sel_brush);
				DeleteObject(sel_brush);
				SetTextColor(mem, kGreenBright);
			}
			else
			{
				SetTextColor(mem, kGreenDim);
			}
			RECT text = row;
			text.left += 6;
			DrawTextA(mem, ItemName(item->group, item->index), -1, &text,
				DT_LEFT | DT_VCENTER | DT_SINGLELINE);
		}

		// pilot-name header
		if (fe->nameEdit != NULL)
		{
			RECT edit_rect;
			GetWindowRect(fe->nameEdit, &edit_rect);
			POINT corner = { edit_rect.left, edit_rect.top };
			ScreenToClient(fe->menuWindow, &corner);
			RECT header;
			header.left = corner.x;
			header.right = corner.x + 300;
			header.bottom = corner.y;
			header.top = corner.y - 26;
			SetTextColor(mem, kGreenBright);
			DrawTextA(mem, "PILOT NAME", -1, &header,
				DT_LEFT | DT_VCENTER | DT_SINGLELINE);
		}

		BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);

		SelectObject(mem, old_surface);
		DeleteObject(surface);
		DeleteDC(mem);
		EndPaint(fe->menuWindow, &ps);
	}

	void MenuClick(FEState *fe, int x, int y)
	{
		for (int i = 0; i < fe->itemCount; ++i)
		{
			const FEItem *item = &fe->items[i];
			if (x >= item->rect.left && x < item->rect.right &&
				y >= item->rect.top && y < item->rect.bottom)
			{
				if (item->group == GroupLaunch)
				{
					fe->launched = True;
					// wake the modal loop (a click delivered via
					// 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;
					if (item->group == GroupScenario)
					{
						// the scenario swaps the map list and the
						// color/badge vs team/position columns
						int map_count;
						ActiveMaps(fe->selection, &map_count);
						if (fe->selection[GroupMap] >= map_count)
						{
							fe->selection[GroupMap] = 0;
						}
						RECT client;
						GetClientRect(fe->menuWindow, &client);
						LayoutMenu(fe, client.right, client.bottom);
					}
					InvalidateRect(fe->menuWindow, NULL, FALSE);
				}
				return;
			}
		}
	}

	LRESULT CALLBACK
		FrontEndWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
	{
		FEState *fe = gFE;

		switch (message)
		{
		case WM_PAINT:
			if (fe != NULL)
			{
				PaintMenu(fe);
				return 0;
			}
			break;

		case WM_LBUTTONDOWN:
			if (fe != NULL)
			{
				MenuClick(fe, (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam));
				return 0;
			}
			break;

		case WM_CTLCOLOREDIT:
			if (fe != NULL)
			{
				SetTextColor((HDC) wParam, kGreenBright);
				SetBkColor((HDC) wParam, kBlack);
				return (LRESULT) fe->editBrush;
			}
			break;

		case WM_ERASEBKGND:
			return 1;
		}
		return DefWindowProcA(hwnd, message, wParam, lParam);
	}

	//---------------------------------------------------------------
	// Plasma name bitmaps: white text auto-fit into WxH, thresholded
	// to 1bpp hex rows - the C++ twin of the console's PlasmaBitmaps.
	//---------------------------------------------------------------
	void AppendNameBitmap(std::string &egg, int width, int height, const char *name)
	{
		BITMAPINFO info;
		memset(&info, 0, sizeof(info));
		info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
		info.bmiHeader.biWidth = width;
		info.bmiHeader.biHeight = -height;		// top-down
		info.bmiHeader.biPlanes = 1;
		info.bmiHeader.biBitCount = 32;
		info.bmiHeader.biCompression = BI_RGB;

		void *bits = NULL;
		HDC dc = CreateCompatibleDC(NULL);
		HBITMAP bitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &bits, NULL, 0);
		HBITMAP old_bitmap = (HBITMAP) SelectObject(dc, bitmap);

		memset(bits, 0, width * height * 4);
		SetBkMode(dc, TRANSPARENT);
		SetTextColor(dc, RGB(255, 255, 255));

		// shrink the font until the name fits (console: 24pt downward)
		int point_size = 24;
		for (;;)
		{
			HFONT font = CreateFontA(
				-MulDiv(point_size, GetDeviceCaps(dc, LOGPIXELSY), 72),
				0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
				ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
				NONANTIALIASED_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
				"Microsoft Sans Serif");
			HFONT old_font = (HFONT) SelectObject(dc, font);

			SIZE extent;
			GetTextExtentPoint32A(dc, name, (int) strlen(name), &extent);
			if ((extent.cx <= width && extent.cy <= height) || point_size <= 1)
			{
				RECT rect = { 0, 0, width, height };
				DrawTextA(dc, name, -1, &rect,
					DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
				SelectObject(dc, old_font);
				DeleteObject(font);
				break;
			}
			SelectObject(dc, old_font);
			DeleteObject(font);
			point_size = (point_size <= 2) ? (point_size / 2) : (point_size - 2);
		}

		GdiFlush();
		const unsigned long *pixels = (const unsigned long *) bits;
		char hex[8];
		for (int row = 0; row < height; ++row)
		{
			egg += "bitmap=";
			for (int byte_index = 0; byte_index < width / 8; ++byte_index)
			{
				unsigned char packed = 0;
				for (int bit = 0; bit < 8; ++bit)
				{
					unsigned long pixel = pixels[row * width + byte_index * 8 + bit];
					if ((pixel & 0xFF) >= 128)		// white text on black
					{
						packed |= (unsigned char)(128 >> bit);
					}
				}
				sprintf(hex, "%02X", packed);
				egg += hex;
			}
			egg += "\n";
		}
		sprintf(hex, "%d", width);
		egg += "x="; egg += hex; egg += "\n";
		sprintf(hex, "%d", height);
		egg += "y="; egg += hex; egg += "\n";

		SelectObject(dc, old_bitmap);
		DeleteObject(bitmap);
		DeleteDC(dc);
	}

	// The 1st-4th place plasma graphics, verbatim from the console
	// (RPMission.AppendOrdinals) / TEST.EGG.
	extern const char *kOrdinalsBlock;

	//---------------------------------------------------------------
	// Egg assembly (console RPMission.ToEggString / RPFootballMission).
	// Single player uses the standalone address; a hosted network game
	// lists the owner first plus one pilot per member pod. Football
	// adds team=/position= per pilot and the [teams] blocks; the
	// owner's team/position picks seed a deterministic assignment
	// across two teams (each team's first member without a runner
	// becomes one; the rest alternate crusher/blocker; the runner
	// wears the team's runner color, everyone else the team color).
	//---------------------------------------------------------------
	void BuildEggText(const FEState *fe, std::string &egg)
	{
		char line[256];
		Logical football = IsFootball(fe->selection);

		ExtraPilot extras[maxExtraPilots];
		char owner_address[64];
		strcpy(owner_address, kLocalPilotAddress);
		int extra_count = CollectExtraPilots(fe, extras, owner_address);
		if (extra_count < 0)
		{
			extra_count = 0;		// single player
		}

		//
		// One flat pilot table: the owner then the extras
		//
		struct PilotEntry
		{
			const char	*address;
			const char	*name;
			const char	*vehicle;
			const char	*color;
			const char	*badge;
			int			team;		// kTeams index, -1 outside football
			const char	*position;
			Logical		chosePosition;	// picked it themselves
		};
		PilotEntry pilots[maxExtraPilots + 1];
		int pilot_count = 0;

		PilotEntry *owner = &pilots[pilot_count++];
		owner->address = owner_address;
		owner->name = fe->pilotName;
		owner->vehicle = kVehicles[fe->selection[GroupVehicle]].key;
		owner->color = kColors[fe->selection[GroupColor]].key;
		owner->badge = kBadges[fe->selection[GroupBadge]].key;
		owner->team = -1;
		owner->position = NULL;
		owner->chosePosition = False;
		for (int p = 0; p < extra_count; ++p)
		{
			PilotEntry *pilot = &pilots[pilot_count++];
			pilot->address = extras[p].address;
			pilot->name = extras[p].name;
			pilot->vehicle = extras[p].vehicle;
			pilot->color = extras[p].color;
			pilot->badge = extras[p].badge;
			pilot->team = -1;
			pilot->position = NULL;
			pilot->chosePosition = False;
		}

		//
		// Football: every pilot's own team/position pick is honored;
		// unset picks get filled in, then each team is normalized to
		// exactly one runner (the console's rule).
		//
		if (football)
		{
			//
			// 1. Teams: the host's pick, then each member's, then
			//    alternate whoever did not choose across the two
			//    most-used teams so the sides stay balanced.
			//
			owner->team = fe->selection[GroupTeam];
			for (int p = 1; p < pilot_count; ++p)
			{
				for (int t = 0; t < FE_COUNT(kTeams); ++t)
				{
					if (strcmp(extras[p - 1].team, kTeams[t].key) == 0)
					{
						pilots[p].team = t;
						break;
					}
				}
			}

			int fallback_team = (owner->team + 1) % FE_COUNT(kTeams);
			int unassigned = 0;
			for (int p = 1; p < pilot_count; ++p)
			{
				if (pilots[p].team < 0)
				{
					pilots[p].team = (unassigned++ % 2) ? owner->team : fallback_team;
				}
			}

			//
			// 2. Positions: honor each pick, default the rest to the
			//    line, then give every team exactly one runner.
			//
			for (int p = 1; p < pilot_count; ++p)
			{
				const char *pick = extras[p - 1].position;
				for (int i = 0; i < FE_COUNT(kPositions); ++i)
				{
					if (strcmp(pick, kPositions[i].key) == 0)
					{
						pilots[p].position = kPositions[i].key;
						pilots[p].chosePosition = True;
						break;
					}
				}
			}
			owner->position = kPositions[fe->selection[GroupPosition]].key;
			owner->chosePosition = True;

			for (int t = 0; t < FE_COUNT(kTeams); ++t)
			{
				int members = 0, runner = -1, spare = -1, line = 0;
				for (int p = 0; p < pilot_count; ++p)
				{
					if (pilots[p].team != t)
					{
						continue;
					}
					++members;
					if (spare < 0 && !pilots[p].chosePosition)
					{
						spare = p;		// nobody's plans to disturb
					}
					if (pilots[p].position != NULL &&
						strcmp(pilots[p].position, "runner") == 0)
					{
						if (runner < 0)
						{
							runner = p;		// first runner keeps the ball
						}
						else
						{
							// two pilots claimed it - the later one lines up
							pilots[p].position = NULL;
							pilots[p].chosePosition = False;
						}
					}
				}
				if (members == 0)
				{
					continue;
				}
				if (runner < 0 && spare >= 0)
				{
					// no volunteer: the first pilot without a pick of
					// their own runs (explicit picks are never overridden)
					pilots[spare].position = "runner";
					runner = spare;
				}
				for (int p = 0; p < pilot_count; ++p)
				{
					if (pilots[p].team == t && p != runner &&
						pilots[p].position == NULL)
					{
						pilots[p].position = (line++ % 2) ? "crusher" : "blocker";
					}
				}
			}

			//
			// 3. Colors follow team and position (runner wears the
			//    team's runner color - that is how you spot the ball)
			//
			for (int p = 0; p < pilot_count; ++p)
			{
				const TeamEntry *team = &kTeams[pilots[p].team];
				pilots[p].color =
					(strcmp(pilots[p].position, "runner") == 0)
						? team->runnerColor : team->teamColor;
				pilots[p].badge = "None";
			}
		}

		//
		// [mission]
		//
		int map_count;
		const CatalogEntry *maps = ActiveMaps(fe->selection, &map_count);

		egg += "[mission]\n";
		egg += "adventure=Red Planet\n";
		sprintf(line, "map=%s\n", maps[fe->selection[GroupMap]].key);
		egg += line;
		sprintf(line, "scenario=%s\n", kScenarios[fe->selection[GroupScenario]].key);
		egg += line;
		sprintf(line, "time=%s\n", kTimes[fe->selection[GroupTime]].key);
		egg += line;
		sprintf(line, "weather=%s\n", kWeather[fe->selection[GroupWeather]].key);
		egg += line;
		egg += "temperature=0\n";
		egg += "compression=0\n";
		int seconds = kLengths[fe->selection[GroupLength]].seconds;
		if (seconds > 0)
		{
			sprintf(line, "length=%d\n", seconds);
			egg += line;
		}

		//
		// [pilots] + per-pilot sections
		//
		egg += "[pilots]\n";
		for (int p = 0; p < pilot_count; ++p)
		{
			sprintf(line, "pilot=%s\n", pilots[p].address);
			egg += line;
		}

		for (int p = 0; p < pilot_count; ++p)
		{
			sprintf(line, "[%s]\n", pilots[p].address);
			egg += line;
			egg += "hostType=0\n";
			egg += "dropzone=one\n";
			sprintf(line, "name=%s\n", pilots[p].name);
			egg += line;
			sprintf(line, "bitmapindex=%d\n", p + 1);
			egg += line;
			egg += "loadzones=1\n";
			sprintf(line, "vehicle=%s\n", pilots[p].vehicle);
			egg += line;
			sprintf(line, "color=%s\n", pilots[p].color);
			egg += line;
			sprintf(line, "badge=%s\n", pilots[p].badge);
			egg += line;
			if (football)
			{
				sprintf(line, "team=team::%s\n", kTeams[pilots[p].team].key);
				egg += line;
				sprintf(line, "position=%s\n", pilots[p].position);
				egg += line;
			}
		}

		//
		// Football team blocks (console RPFootballMission layout)
		//
		if (football)
		{
			std::string teams_list = "[teams]\n";
			std::string team_sections, pilots_sections;
			std::string teambitmap_list = "[teambitmap]\n";
			std::string teambitmap_sections;
			int bitmap_index = 1;
			for (int side = 0; side < FE_COUNT(kTeams); ++side)
			{
				Logical team_present = False;
				for (int p = 0; p < pilot_count; ++p)
				{
					if (pilots[p].team == side)
					{
						team_present = True;
					}
				}
				if (!team_present)
				{
					continue;
				}
				const char *team_key = kTeams[side].key;
				sprintf(line, "team=team::%s\n", team_key);
				teams_list += line;
				sprintf(line, "[team::%s]\nbitmapindex=%d\npilots=pilots::%s\n",
					team_key, bitmap_index++, team_key);
				team_sections += line;
				sprintf(line, "[pilots::%s]\n", team_key);
				pilots_sections += line;
				for (int p = 0; p < pilot_count; ++p)
				{
					if (pilots[p].team == side)
					{
						sprintf(line, "pilot=%s\n", pilots[p].address);
						pilots_sections += line;
					}
				}
				sprintf(line, "bitmap=BitMap::Small::team::%s\n", team_key);
				teambitmap_list += line;
				sprintf(line, "[BitMap::Small::team::%s]\n", team_key);
				teambitmap_sections += line;
				AppendNameBitmap(teambitmap_sections, 64, 16, team_key);
				teambitmap_sections += "width=4\n";
			}
			egg += teams_list;
			egg += team_sections;
			egg += pilots_sections;
			egg += teambitmap_list;
			egg += teambitmap_sections;
		}

		//
		// Name bitmaps + ordinals
		//
		egg += "[largebitmap]\n";
		for (int p = 0; p < pilot_count; ++p)
		{
			sprintf(line, "bitmap=BitMap::Large::%s\n", pilots[p].name);
			egg += line;
		}
		egg += "[smallbitmap]\n";
		for (int p = 0; p < pilot_count; ++p)
		{
			sprintf(line, "bitmap=BitMap::Small::%s\n", pilots[p].name);
			egg += line;
		}
		for (int p = 0; p < pilot_count; ++p)
		{
			sprintf(line, "[BitMap::Large::%s]\n", pilots[p].name);
			egg += line;
			AppendNameBitmap(egg, 128, 32, pilots[p].name);
			egg += "width=8\n";
			sprintf(line, "[BitMap::Small::%s]\n", pilots[p].name);
			egg += line;
			AppendNameBitmap(egg, 64, 16, pilots[p].name);
			egg += "width=4\n";
		}

		egg += kOrdinalsBlock;

		// [pilots]-order names for the network console's results
		strcpy(gLastPilotNamesCsv, fe->pilotName);
		for (int p = 1; p < pilot_count; ++p)
		{
			if (strlen(gLastPilotNamesCsv) + strlen(pilots[p].name) + 2 <
				sizeof(gLastPilotNamesCsv))
			{
				strcat(gLastPilotNamesCsv, ",");
				strcat(gLastPilotNamesCsv, pilots[p].name);
			}
		}
	}
}

//########################################################################
//############################ 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,
		HWND main_window,
		char *egg_path_out,
		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);
		if (gHavePersist)
		{
			memcpy(fe.selection, gPersistSelection, sizeof(fe.selection));
		}
		else
		{
			fe.selection[GroupLength] = 2;		// 5:00
		}
		gFE = &fe;

		//---------------------------------------------------------------
		// Window class + fonts
		//---------------------------------------------------------------
		static Logical class_registered = False;
		if (!class_registered)
		{
			class_registered = True;
			WNDCLASSA window_class;
			memset(&window_class, 0, sizeof(window_class));
			window_class.style = CS_HREDRAW | CS_VREDRAW;
			window_class.lpfnWndProc = FrontEndWndProc;
			window_class.hInstance = instance;
			window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
			window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
			window_class.lpszClassName = "RPFrontEnd";
			RegisterClassA(&window_class);
		}

		RECT client;
		GetClientRect(main_window, &client);

		fe.menuWindow = CreateWindowExA(
			0, "RPFrontEnd", "",
			WS_CHILD | WS_VISIBLE,
			0, 0, client.right, client.bottom,
			main_window, NULL, instance, NULL);
		if (fe.menuWindow == NULL)
		{
			gFE = NULL;
			return False;
		}

		int text_h = client.bottom / 40;
		if (text_h < 16)	text_h = 16;
		if (text_h > 24)	text_h = 24;
		fe.textFont = CreateFontA(-text_h, 0, 0, 0, FW_NORMAL,
			FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
			CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
			"Consolas");
		fe.titleFont = CreateFontA(-(text_h * 2), 0, 0, 0, FW_BOLD,
			FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
			CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH | FF_MODERN,
			"Consolas");
		fe.editBrush = CreateSolidBrush(kBlack);

		fe.nameEdit = CreateWindowExA(
			0, "EDIT", fe.pilotName,
			WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
			0, 0, 10, 10,
			fe.menuWindow, NULL, instance, NULL);
		SendMessageA(fe.nameEdit, WM_SETFONT, (WPARAM) fe.textFont, TRUE);
		SendMessageA(fe.nameEdit, EM_SETLIMITTEXT, sizeof(fe.pilotName) - 2, 0);

		LayoutMenu(&fe, client.right, client.bottom);
		InvalidateRect(fe.menuWindow, NULL, TRUE);

		//---------------------------------------------------------------
		// Modal loop until launch, lobby button, or close
		//---------------------------------------------------------------
		MSG msg;
		while (!fe.launched && !fe.closed && fe.steamAction == 0)
		{
			BOOL result = GetMessageA(&msg, NULL, 0, 0);
			if (result <= 0)
			{
				fe.closed = True;
				break;
			}
			if (msg.message == WM_QUIT ||
				(msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE))
			{
				fe.closed = True;
			}
			// closing the main window ends the front end too
			if (msg.message == WM_CLOSE && msg.hwnd == main_window)
			{
				fe.closed = True;
			}
			TranslateMessage(&msg);
			DispatchMessageA(&msg);
			if (!IsWindow(main_window))
			{
				fe.closed = True;
			}
		}

		//---------------------------------------------------------------
		// Harvest the loadout whichever way we leave the menu - the
		// lobby publishes it as member data, launches build from it
		//---------------------------------------------------------------
		if (!fe.closed)
		{
			GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1);
			if (fe.pilotName[0] == '\0')
			{
				strcpy(fe.pilotName, "Pilot");
			}
			strcpy(gLastPilotName, fe.pilotName);
			memcpy(gPersistSelection, fe.selection, sizeof(gPersistSelection));
			gHavePersist = True;
		}

		Logical launched = fe.launched;
		Logical closed = fe.closed;
		int steam_action = fe.steamAction;

		DestroyWindow(fe.menuWindow);
		DeleteObject(fe.textFont);
		DeleteObject(fe.titleFont);
		DeleteObject(fe.editBrush);
		gFE = NULL;

		if (closed)
		{
			return False;
		}
		if (launched)
		{
			// plain launch: single player, or a LAN-hosted race when
			// RP412HOSTPODS is set in the environment
			return BuildEggFromPersisted(egg_path_out, egg_path_size);
		}

		//---------------------------------------------------------------
		// Steam lobby (host or join), then back here if they leave
		//---------------------------------------------------------------
		int outcome = (steam_action == 1)
			? RPL4Lobby_Host(instance, main_window)
			: RPL4Lobby_Join(instance, main_window);
		if (outcome == LobbyRoomClosed)
		{
			return False;
		}
		if (outcome == LobbyLaunchMember)
		{
			gLastLaunchMode = FELaunchMember;
			egg_path_out[0] = '\0';
			return True;
		}
		if (outcome == LobbyLaunchHost)
		{
			gLastLaunchMode = FELaunchHost;
			return BuildEggFromPersisted(egg_path_out, egg_path_size);
		}
		// LobbyRoomLeft: show the menu again
	}
}

int
	RPL4FrontEnd_LastMissionSeconds()
{
	return gLastMissionSeconds;
}

const char *
	RPL4FrontEnd_LastPilotNames()
{
	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);
}

//########################################################################
// Football team / position picks (the lobby publishes and cycles these)
//########################################################################

int
	RPL4FrontEnd_TeamCount()
{
	return FE_COUNT(kTeams);
}

const char *
	RPL4FrontEnd_TeamName(int index)
{
	if (index < 0 || index >= FE_COUNT(kTeams))
	{
		return "";
	}
	return kTeams[index].name;
}

const char *
	RPL4FrontEnd_TeamKey(int index)
{
	if (index < 0 || index >= FE_COUNT(kTeams))
	{
		return "";
	}
	return kTeams[index].key;
}

int
	RPL4FrontEnd_PositionCount()
{
	return FE_COUNT(kPositions);
}

const char *
	RPL4FrontEnd_PositionName(int index)
{
	if (index < 0 || index >= FE_COUNT(kPositions))
	{
		return "";
	}
	return kPositions[index].name;
}

const char *
	RPL4FrontEnd_PositionKey(int index)
{
	if (index < 0 || index >= FE_COUNT(kPositions))
	{
		return "";
	}
	return kPositions[index].key;
}

int
	RPL4FrontEnd_GetTeamIndex()
{
	return gHavePersist ? gPersistSelection[GroupTeam] : 0;
}

void
	RPL4FrontEnd_SetTeamIndex(int index)
{
	if (index < 0 || index >= FE_COUNT(kTeams))
	{
		return;
	}
	gPersistSelection[GroupTeam] = index;
	gHavePersist = True;
}

int
	RPL4FrontEnd_GetPositionIndex()
{
	return gHavePersist ? gPersistSelection[GroupPosition] : 0;
}

void
	RPL4FrontEnd_SetPositionIndex(int index)
{
	if (index < 0 || index >= FE_COUNT(kPositions))
	{
		return;
	}
	gPersistSelection[GroupPosition] = index;
	gHavePersist = True;
}

Logical
	RPL4FrontEnd_IsFootballSelected()
{
	return gHavePersist && IsFootball(gPersistSelection);
}

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 ######################
//########################################################################

namespace
{
	struct ResultRow
	{
		char	name[32];
		int		score;
	};

	struct ResultsState
	{
		HWND		window;
		HFONT		textFont;
		HFONT		titleFont;
		ResultRow	rows[16];
		int			rowCount;
		RECT		continueRect;
		Logical		dismissed;
		Logical		closed;
	};

	ResultsState *gRS = NULL;

	const char *PlaceName(int place)
	{
		static const char *kPlaces[] = { "1ST", "2ND", "3RD", "4TH" };
		static char other[8];
		if (place < 4)
		{
			return kPlaces[place];
		}
		sprintf(other, "%dTH", place + 1);
		return other;
	}

	void PaintResults(ResultsState *rs)
	{
		PAINTSTRUCT ps;
		HDC hdc = BeginPaint(rs->window, &ps);

		RECT client;
		GetClientRect(rs->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, rs->titleFont);
		SetTextColor(mem, kGreenBright);
		RECT title = client;
		title.top = client.bottom / 28;
		title.bottom = title.top + client.bottom / 10;
		DrawTextA(mem, "RACE RESULTS", -1, &title,
			DT_CENTER | DT_TOP | DT_SINGLELINE);

		SelectObject(mem, rs->textFont);
		int row_h = client.bottom / 16;
		int top = client.bottom / 4;
		int left = client.right / 3;
		int right = 2 * client.right / 3;
		char text[48];
		for (int i = 0; i < rs->rowCount; ++i)
		{
			RECT row;
			row.left = left;
			row.right = right;
			row.top = top + i * row_h;
			row.bottom = row.top + row_h;

			SetTextColor(mem, (i == 0) ? kGreenBright : kGreenDim);
			DrawTextA(mem, PlaceName(i), -1, &row,
				DT_LEFT | DT_VCENTER | DT_SINGLELINE);

			RECT name = row;
			name.left += (right - left) / 5;
			DrawTextA(mem, rs->rows[i].name, -1, &name,
				DT_LEFT | DT_VCENTER | DT_SINGLELINE);

			sprintf(text, "%d", rs->rows[i].score);
			DrawTextA(mem, text, -1, &row,
				DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
		}

		HBRUSH frame_brush = CreateSolidBrush(kGreenBright);
		FrameRect(mem, &rs->continueRect, frame_brush);
		DeleteObject(frame_brush);
		SetTextColor(mem, kGreenBright);
		DrawTextA(mem, "C O N T I N U E", -1, &rs->continueRect,
			DT_CENTER | DT_VCENTER | DT_SINGLELINE);

		BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY);

		SelectObject(mem, old_surface);
		DeleteObject(surface);
		DeleteDC(mem);
		EndPaint(rs->window, &ps);
	}

	LRESULT CALLBACK
		ResultsWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
	{
		ResultsState *rs = gRS;

		switch (message)
		{
		case WM_PAINT:
			if (rs != NULL)
			{
				PaintResults(rs);
				return 0;
			}
			break;

		case WM_LBUTTONDOWN:
			if (rs != NULL)
			{
				POINT point = { (int)(short) LOWORD(lParam), (int)(short) HIWORD(lParam) };
				if (PtInRect(&rs->continueRect, point))
				{
					rs->dismissed = True;
					PostMessageA(rs->window, WM_NULL, 0, 0);
				}
				return 0;
			}
			break;

		case WM_ERASEBKGND:
			return 1;
		}
		return DefWindowProcA(hwnd, message, wParam, lParam);
	}
}

Logical
	RPL4FrontEnd_ShowResults(
		HINSTANCE instance,
		HWND main_window
	)
{
	int result_count = RPL4LocalConsole_ResultCount();
	if (result_count <= 0)
	{
		return True;		// nothing to show (first boot)
	}
	if (!IsWindow(main_window))
	{
		return False;
	}

	ResultsState rs;
	memset(&rs, 0, sizeof(rs));

	//---------------------------------------------------------------
	// Intake, then sort descending by score. Single-player results
	// carry the pilot's own name; with more pods the roster will map
	// host IDs to Steam personas (until then: pod number).
	//---------------------------------------------------------------
	for (int i = 0; i < result_count && rs.rowCount < 16; ++i)
	{
		int host_ID, score;
		if (!RPL4LocalConsole_GetResult(i, &host_ID, &score))
		{
			continue;
		}
		ResultRow *row = &rs.rows[rs.rowCount++];
		const char *pilot_name = RPL4LocalConsole_GetResultName(host_ID);
		if (pilot_name != NULL)
		{
			sprintf(row->name, "%.30s", pilot_name);
		}
		else if (result_count == 1)
		{
			sprintf(row->name, "%s", gLastPilotName);
		}
		else
		{
			sprintf(row->name, "POD %d", host_ID);
		}
		row->score = score;
	}
	for (int i = 1; i < rs.rowCount; ++i)
	{
		ResultRow key = rs.rows[i];
		int j = i - 1;
		while (j >= 0 && rs.rows[j].score < key.score)
		{
			rs.rows[j + 1] = rs.rows[j];
			--j;
		}
		rs.rows[j + 1] = key;
	}

	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 = ResultsWndProc;
		window_class.hInstance = instance;
		window_class.hCursor = LoadCursor(NULL, IDC_ARROW);
		window_class.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
		window_class.lpszClassName = "RPResults";
		RegisterClassA(&window_class);
	}

	RECT client;
	GetClientRect(main_window, &client);

	rs.window = CreateWindowExA(
		0, "RPResults", "",
		WS_CHILD | WS_VISIBLE,
		0, 0, client.right, client.bottom,
		main_window, NULL, instance, NULL);
	if (rs.window == NULL)
	{
		return False;
	}

	int text_h = client.bottom / 40;
	if (text_h < 16)	text_h = 16;
	if (text_h > 24)	text_h = 24;
	rs.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");
	rs.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 / 5;
	rs.continueRect.left = (client.right - col_w) / 2;
	rs.continueRect.right = rs.continueRect.left + col_w;
	rs.continueRect.top = client.bottom - 3 * row_h;
	rs.continueRect.bottom = client.bottom - row_h;

	gRS = &rs;
	SetFocus(rs.window);
	InvalidateRect(rs.window, NULL, TRUE);

	//---------------------------------------------------------------
	// Modal until CONTINUE (or any key) - same shape as the menu loop
	//---------------------------------------------------------------
	MSG msg;
	while (!rs.dismissed && !rs.closed)
	{
		BOOL result = GetMessageA(&msg, NULL, 0, 0);
		if (result <= 0)
		{
			rs.closed = True;
			break;
		}
		if (msg.message == WM_QUIT ||
			(msg.message == WM_SYSCOMMAND && msg.wParam == SC_CLOSE))
		{
			rs.closed = True;
		}
		if (msg.message == WM_CLOSE && msg.hwnd == main_window)
		{
			rs.closed = True;
		}
		if (msg.message == WM_KEYDOWN &&
			(msg.wParam == VK_RETURN || msg.wParam == VK_SPACE ||
			 msg.wParam == VK_ESCAPE))
		{
			rs.dismissed = True;
		}
		TranslateMessage(&msg);
		DispatchMessageA(&msg);
		if (!IsWindow(main_window))
		{
			rs.closed = True;
		}
	}

	DestroyWindow(rs.window);
	DeleteObject(rs.textFont);
	DeleteObject(rs.titleFont);
	gRS = NULL;

	return !rs.closed;
}

//########################################################################
// Ordinal plasma graphics (verbatim from the console / TEST.EGG)
//########################################################################

namespace
{
	const char *kOrdinalsBlock =
		"[ordinals]\n"
		"bitmap=Ordinal::BitMap::1\n"
		"bitmap=Ordinal::BitMap::2\n"
		"bitmap=Ordinal::BitMap::3\n"
		"bitmap=Ordinal::BitMap::4\n"
		"[Ordinal::BitMap::1]\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=000038000003C000001FF00000000F00\n"
		"bitmap=0000F8000003C000003FF80000000F00\n"
		"bitmap=0001F8000003C00000707C0000000F00\n"
		"bitmap=0001F8000003C00000603C0000000F00\n"
		"bitmap=00007801FC0FF00000003C3DF807FF00\n"
		"bitmap=00007803FE0FF00000003C3FFC0FFF00\n"
		"bitmap=00007803C703C00000003C3E3C1F0F00\n"
		"bitmap=000078078303C0000000783C1E1E0F00\n"
		"bitmap=000078078003C0000000783C1E1E0F00\n"
		"bitmap=00007807C003C0000000F03C1E1E0F00\n"
		"bitmap=00007807F003C0000001E03C1E1E0F00\n"
		"bitmap=00007803FC03C0000003C03C1E1E0F00\n"
		"bitmap=00007801FE03C0000007803C1E1E0F00\n"
		"bitmap=000078007F03C000000F003C1E1E0F00\n"
		"bitmap=000078001F03C000001E003C1E1E0F00\n"
		"bitmap=000078000F03C000003C003C1E1E0F00\n"
		"bitmap=000078060F03C0000078003C1E1E0F00\n"
		"bitmap=000078071E03E0000078003C1E1F1F00\n"
		"bitmap=00007803FE01F000007FFC3C1E0FFF00\n"
		"bitmap=00007801FC00F000007FFC3C1E07EF00\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"x=128\n"
		"y=32\n"
		"width=8\n"
		"[Ordinal::BitMap::2]\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=001FFF0000003C0000003C03C0780000\n"
		"bitmap=001FFF0000003C0000007C03C0780000\n"
		"bitmap=00000E0000003C000000FC03C0780000\n"
		"bitmap=00003C0000003C000001BC03C0780000\n"
		"bitmap=0000700F3E1FFC000003BC0FF07BF000\n"
		"bitmap=0001E00F7E3FFC0000073C0FF07FF800\n"
		"bitmap=0003800FFE7C3C0000063C03C07C7800\n"
		"bitmap=0007F80FFE783C00000C3C03C0783C00\n"
		"bitmap=0007FE0F80783C0000183C03C0783C00\n"
		"bitmap=00001E0F00783C0000383C03C0783C00\n"
		"bitmap=00000F0F00783C0000703C03C0783C00\n"
		"bitmap=00000F0F00783C00007FFF03C0783C00\n"
		"bitmap=00000F0F00783C00007FFF03C0783C00\n"
		"bitmap=00000F0F00783C0000003C03C0783C00\n"
		"bitmap=00000F0F00783C0000003C03C0783C00\n"
		"bitmap=00000F0F00783C0000003C03C0783C00\n"
		"bitmap=00180F0F00783C0000003C03C0783C00\n"
		"bitmap=001C1F0F007C7C0000003C03E0783C00\n"
		"bitmap=000FFE0F003FFC0000003C01F0783C00\n"
		"bitmap=0007FC0F001FBC0000003C00F0783C00\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"x=128\n"
		"y=32\n"
		"width=8\n"
		"[Ordinal::BitMap::3]\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=001FFF03C07800000000FC03C0780000\n"
		"bitmap=001FFF03C07800000003FC03C0780000\n"
		"bitmap=001E0003C07800000007C003C0780000\n"
		"bitmap=001E0003C0780000000F0003C0780000\n"
		"bitmap=001E000FF07BF000000F000FF07BF000\n"
		"bitmap=001E000FF07FF800001E000FF07FF800\n"
		"bitmap=001E0003C07C7800001E0003C07C7800\n"
		"bitmap=001FFC03C0783C00001EFC03C0783C00\n"
		"bitmap=001FFE03C0783C00001FFE03C0783C00\n"
		"bitmap=00001F03C0783C00001F1F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00000F03C0783C00001E0F03C0783C00\n"
		"bitmap=00180F03C0783C00001E0F03C0783C00\n"
		"bitmap=001C1F03E0783C00001F1F03E0783C00\n"
		"bitmap=000FFE01F0783C00000FFE01F0783C00\n"
		"bitmap=0007FC00F0783C000007FC00F0783C00\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"x=128\n"
		"y=32\n"
		"width=8\n"
		"[Ordinal::BitMap::4]\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=001FFF03C07800000007FC03C0780000\n"
		"bitmap=001FFF03C0780000000FFE03C0780000\n"
		"bitmap=00000F03C0780000001F1F03C0780000\n"
		"bitmap=00000F03C0780000001E0F03C0780000\n"
		"bitmap=00000F0FF07BF000001E0F0FF07BF000\n"
		"bitmap=00001F0FF07FF800001E0F0FF07FF800\n"
		"bitmap=00001E03C07C7800001E0F03C07C7800\n"
		"bitmap=00003E03C0783C00001E0F03C0783C00\n"
		"bitmap=00003C03C0783C00000F1E03C0783C00\n"
		"bitmap=00003C03C0783C000007FC03C0783C00\n"
		"bitmap=00007803C0783C000007FC03C0783C00\n"
		"bitmap=00007803C0783C00000F1E03C0783C00\n"
		"bitmap=00007803C0783C00001E0F03C0783C00\n"
		"bitmap=0000F003C0783C00001E0F03C0783C00\n"
		"bitmap=0000F003C0783C00001E0F03C0783C00\n"
		"bitmap=0000F003C0783C00001E0F03C0783C00\n"
		"bitmap=0000F003C0783C00001E0F03C0783C00\n"
		"bitmap=0000F003E0783C00000F1E03E0783C00\n"
		"bitmap=0000F001F0783C00000FFE01F0783C00\n"
		"bitmap=0000F000F0783C000007FC00F0783C00\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"bitmap=00000000000000000000000000000000\n"
		"x=128\n"
		"y=32\n"
		"width=8\n";
}
