#include "..\munga_l4\mungal4.h" #pragma hdrstop #include "rpl4fe.h" #include "rpl4console.h" #include "rpl4lobby.h" #include #include //######################################################################## // 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" }, }; 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, GroupLaunch, GroupSteamHost, // buttons, not selections (Steam builds only) GroupSteamJoin, GroupCount }; 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]; }; // lobby-fed override (real personas + loadouts); empty = env parsing FEHostedPilot gHostedPilots[maxExtraPilots]; int gHostedPilotCount = 0; char gHostedOwnerAddress[64] = ""; int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address) { if (gHostedPilotCount > 0) { sprintf(owner_address, "%s:1502", gHostedOwnerAddress); for (int i = 0; i < gHostedPilotCount; ++i) { strcpy(extras[i].address, gHostedPilots[i].address); strcpy(extras[i].name, gHostedPilots[i].name[0] ? gHostedPilots[i].name : "Pilot"); strcpy(extras[i].vehicle, gHostedPilots[i].vehicle[0] ? gHostedPilots[i].vehicle : "speck"); strcpy(extras[i].color, gHostedPilots[i].color[0] ? gHostedPilots[i].color : "Red"); strcpy(extras[i].badge, gHostedPilots[i].badge[0] ? gHostedPilots[i].badge : "None"); } return gHostedPilotCount; } const char *host_pods = getenv("RP412HOSTPODS"); if (host_pods == NULL || host_pods[0] == '\0') { 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]; 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, GroupMap, FE_COUNT(kMaps), col1, &y, row_h, col_w); int y_after_maps = y; 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 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 GroupMap: return "TRACK"; case GroupVehicle: return "VEHICLE"; case GroupColor: return "COLOR"; case GroupBadge: return "BADGE"; case GroupTime: return "TIME OF DAY"; case GroupWeather: return "WEATHER"; case GroupLength: return "RACE LENGTH"; } return ""; } const char *ItemName(int group, int index) { switch (group) { case GroupMap: return kMaps[index].name; case GroupVehicle: return kVehicles[index].name; case GroupColor: return kColors[index].name; case GroupBadge: return kBadges[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 R A C E"; case GroupSteamHost: return "HOST STEAM RACE"; case GroupSteamJoin: return "JOIN STEAM RACE"; } 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 SelectObject(mem, fe->titleFont); SetTextColor(mem, kGreenBright); RECT title = client; title.top = client.bottom / 28; title.bottom = title.top + client.bottom / 10; DrawTextA(mem, "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; 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, race scenario). // Single player uses the standalone address; a hosted network // race lists the owner first plus one pilot per member pod. //--------------------------------------------------------------- void BuildEggText(const FEState *fe, std::string &egg) { char line[256]; 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 } egg += "[mission]\n"; egg += "adventure=Red Planet\n"; sprintf(line, "map=%s\n", kMaps[fe->selection[GroupMap]].key); egg += line; egg += "scenario=race\n"; 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; } egg += "[pilots]\n"; sprintf(line, "pilot=%s\n", owner_address); egg += line; for (int p = 0; p < extra_count; ++p) { sprintf(line, "pilot=%s\n", extras[p].address); egg += line; } sprintf(line, "[%s]\n", owner_address); egg += line; egg += "hostType=0\n"; egg += "dropzone=one\n"; sprintf(line, "name=%s\n", fe->pilotName); egg += line; egg += "bitmapindex=1\n"; egg += "loadzones=1\n"; sprintf(line, "vehicle=%s\n", kVehicles[fe->selection[GroupVehicle]].key); egg += line; sprintf(line, "color=%s\n", kColors[fe->selection[GroupColor]].key); egg += line; sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key); egg += line; for (int p = 0; p < extra_count; ++p) { sprintf(line, "[%s]\n", extras[p].address); egg += line; egg += "hostType=0\n"; egg += "dropzone=one\n"; sprintf(line, "name=%s\n", extras[p].name); egg += line; sprintf(line, "bitmapindex=%d\n", p + 2); egg += line; egg += "loadzones=1\n"; sprintf(line, "vehicle=%s\n", extras[p].vehicle); egg += line; sprintf(line, "color=%s\n", extras[p].color); egg += line; sprintf(line, "badge=%s\n", extras[p].badge); egg += line; } egg += "[largebitmap]\n"; sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName); egg += line; for (int p = 0; p < extra_count; ++p) { sprintf(line, "bitmap=BitMap::Large::%s\n", extras[p].name); egg += line; } egg += "[smallbitmap]\n"; sprintf(line, "bitmap=BitMap::Small::%s\n", fe->pilotName); egg += line; for (int p = 0; p < extra_count; ++p) { sprintf(line, "bitmap=BitMap::Small::%s\n", extras[p].name); egg += line; } sprintf(line, "[BitMap::Large::%s]\n", fe->pilotName); egg += line; AppendNameBitmap(egg, 128, 32, fe->pilotName); egg += "width=8\n"; sprintf(line, "[BitMap::Small::%s]\n", fe->pilotName); egg += line; AppendNameBitmap(egg, 64, 16, fe->pilotName); egg += "width=4\n"; for (int p = 0; p < extra_count; ++p) { sprintf(line, "[BitMap::Large::%s]\n", extras[p].name); egg += line; AppendNameBitmap(egg, 128, 32, extras[p].name); egg += "width=8\n"; sprintf(line, "[BitMap::Small::%s]\n", extras[p].name); egg += line; AppendNameBitmap(egg, 64, 16, extras[p].name); egg += "width=4\n"; } egg += kOrdinalsBlock; // [pilots]-order names for the network console's results strcpy(gLastPilotNamesCsv, fe->pilotName); for (int p = 0; p < extra_count; ++p) { if (strlen(gLastPilotNamesCsv) + strlen(extras[p].name) + 2 < sizeof(gLastPilotNamesCsv)) { strcat(gLastPilotNamesCsv, ","); strcat(gLastPilotNamesCsv, extras[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); } 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"; }