diff --git a/MUNGA_L4/L4APP.H b/MUNGA_L4/L4APP.H index 6791b4d..2adfb6c 100644 --- a/MUNGA_L4/L4APP.H +++ b/MUNGA_L4/L4APP.H @@ -82,6 +82,8 @@ public: static Logical GetSeeSolids() { return seeSolids; } static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; } static CString GetEggNotationFileName() { return eggNotationFileName; } + // the in-game front end builds an egg and injects it here + static void SetEggNotationFileName(const char *name) { eggNotationFileName = name; } static CString GetSpoolFileName() { return spoolFileName; } static CString GetRIOPlaybackFileName() { return rioPlaybackFileName; } static CString GetRIORecordingFileName() { return rioRecordingFileName; } diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index 2fadf2c..0e171b6 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -21,6 +21,7 @@ #pragma hdrstop #include "rpl4pb.h" +#include "rpl4fe.h" #include "..\munga_l4\l4splr.h" #include "rpl4ver.h" #include "..\munga\resver.h" @@ -191,6 +192,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine } #endif + // + //------------------------------------------------------------------------- + // Front end: started without an egg, a network, or mission review -> + // run the in-game race setup and build the mission egg locally. + //------------------------------------------------------------------------- + // + if (L4Application::GetMissionReviewMode() == 0 && + L4Application::GetNetworkCommonFlatAddress() == 0 && + !L4Application::GetEggNotationFileName()) // no egg on the command line + { + static char frontend_egg[MAX_PATH]; + if (!RPL4FrontEnd_Run(hInstance, hWnd, frontend_egg, sizeof(frontend_egg))) + { + return 0; // player closed the menu without launching + } + L4Application::SetEggNotationFileName(frontend_egg); + } + // //------------------------------------------------------------------------- // Open the resource file. The brace will help correct the scoping problem diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp new file mode 100644 index 0000000..58a7ef9 --- /dev/null +++ b/RP_L4/RPL4FE.cpp @@ -0,0 +1,877 @@ +#include "..\munga_l4\mungal4.h" +#pragma hdrstop + +#include "rpl4fe.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, + 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; + }; + + FEState *gFE = NULL; + + 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; + + // 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"; + } + 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 + { + 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, + // one local pilot) + //--------------------------------------------------------------- + void BuildEggText(const FEState *fe, std::string &egg) + { + char line[256]; + + 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", kLocalPilotAddress); + egg += line; + + sprintf(line, "[%s]\n", kLocalPilotAddress); + 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; + + sprintf(line, "[largebitmap]\nbitmap=BitMap::Large::%s\n", fe->pilotName); + egg += line; + sprintf(line, "[smallbitmap]\nbitmap=BitMap::Small::%s\n", fe->pilotName); + 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"; + + egg += kOrdinalsBlock; + } +} + +//######################################################################## +//############################ RPL4FrontEnd_Run ########################## +//######################################################################## + +Logical + RPL4FrontEnd_Run( + HINSTANCE instance, + HWND main_window, + char *egg_path_out, + int egg_path_size + ) +{ + FEState fe; + memset(&fe, 0, sizeof(fe)); + strcpy(fe.pilotName, "Pilot"); + 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 the player launches or closes the window + //--------------------------------------------------------------- + MSG msg; + while (!fe.launched && !fe.closed) + { + 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; + } + } + + Logical launched = fe.launched; + if (launched) + { + GetWindowTextA(fe.nameEdit, fe.pilotName, sizeof(fe.pilotName) - 1); + if (fe.pilotName[0] == '\0') + { + strcpy(fe.pilotName, "Pilot"); + } + + std::string egg; + egg.reserve(16384); + BuildEggText(&fe, egg); + + strncpy(egg_path_out, "frontend.egg", egg_path_size - 1); + egg_path_out[egg_path_size - 1] = '\0'; + + FILE *file = fopen(egg_path_out, "wb"); + if (file != NULL) + { + fwrite(egg.data(), 1, egg.size(), file); + fclose(file); + DEBUG_STREAM << "FrontEnd: wrote " << egg_path_out << " (" + << (int) egg.size() << " bytes)\n" << std::flush; + } + else + { + DEBUG_STREAM << "FrontEnd: could not write " << egg_path_out + << "\n" << std::flush; + launched = False; + } + } + + DestroyWindow(fe.menuWindow); + DeleteObject(fe.textFont); + DeleteObject(fe.titleFont); + DeleteObject(fe.editBrush); + gFE = NULL; + + return launched; +} + +//######################################################################## +// 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"; +} diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h new file mode 100644 index 0000000..8c31aa7 --- /dev/null +++ b/RP_L4/RPL4FE.h @@ -0,0 +1,25 @@ +#pragma once + +#include "..\munga\style.h" + +//######################################################################## +//############################ RPL4 Front End ############################ +//######################################################################## +// +// The in-game race setup: shown when the game starts with no egg, no +// network, and no mission-review mode. Presents the Death Race catalog +// (track / vehicle / color / badge / time / weather / length + pilot +// name), builds the mission egg locally - the same NotationFile text +// TeslaConsole generated, including the pre-rendered plasma name +// bitmaps - and hands its path back for the standard egg-load path. +// +// Returns True with egg_path_out filled when the player launches; +// False if they close the window instead. +// +Logical + RPL4FrontEnd_Run( + HINSTANCE instance, + HWND main_window, + char *egg_path_out, + int egg_path_size + ); diff --git a/RP_L4/RP_L4.vcxproj b/RP_L4/RP_L4.vcxproj index f369a14..04687f7 100644 --- a/RP_L4/RP_L4.vcxproj +++ b/RP_L4/RP_L4.vcxproj @@ -106,6 +106,7 @@ + @@ -141,6 +142,7 @@ + diff --git a/RP_L4/RP_L4.vcxproj.filters b/RP_L4/RP_L4.vcxproj.filters index bf6f82e..518fa3b 100644 --- a/RP_L4/RP_L4.vcxproj.filters +++ b/RP_L4/RP_L4.vcxproj.filters @@ -33,6 +33,9 @@ Source Files\RP_L4 + + Source Files\RP_L4 + Source Files\RP_L4 @@ -134,6 +137,9 @@ Header Files\RP_L4 + + Header Files\RP_L4 + Header Files\RP_L4 diff --git a/docs/RP412-ROADMAP.md b/docs/RP412-ROADMAP.md index 8ffd5ef..76b898a 100644 --- a/docs/RP412-ROADMAP.md +++ b/docs/RP412-ROADMAP.md @@ -77,6 +77,18 @@ so press-feedback works like the real button field. ## Workstream B — In-game sessions (from TeslaConsole) +- ✅ **Single-player front end landed** (2026-07-12): starting without + `-egg`/`-net` boots a race-setup menu (`RP_L4/RPL4FE.cpp`) — track / + vehicle / color / badge / time / weather / length + pilot name from the + console's catalog — which builds the mission egg locally (full + `RPMission.ToEggString` port incl. GDI-rendered plasma name bitmaps) + and feeds the standard egg-load path. Verified end to end: menu → + LAUNCH → generated egg → racing in the cockpit. +- See [RP412-FRONTEND-DESIGN.md](RP412-FRONTEND-DESIGN.md) for the + TeslaConsole control-code analysis and the Steam lobby mapping this + builds toward (lobby owner = console, SteamID list = pilot list, + Steam-sockets mesh = the egg's connect/listen ordering). + Consumer players get no operator. The create/configure/launch flow that TeslaConsole drives over TCP 1501 becomes in-game UI: diff --git a/pack-dist.ps1 b/pack-dist.ps1 index 30321ca..fcd3bc5 100644 --- a/pack-dist.ps1 +++ b/pack-dist.ps1 @@ -80,10 +80,11 @@ TARGETFPS=60 Set-Content -Path "$dist\start-windowed.bat" -Encoding ascii -Value @" @echo off -rem Red Planet 4.12 - desktop prototype (windowed, local test mission) -rem The cockpit canvas is 1920x1080; -res matches it for native 3D. +rem Red Planet 4.12 - desktop prototype. Boots into the race-setup +rem front end; the cockpit canvas is 1920x1080 and -res matches it. +rem (Add -egg TEST.EGG to skip the menu and run the canned mission.) cd /d "%~dp0" -start rpl4opt.exe -windowed -res 1920 1080 -egg TEST.EGG +start rpl4opt.exe -windowed -res 1920 1080 "@ Set-Content -Path "$dist\README.txt" -Encoding ascii -Value @"