//===========================================================================// // btl4fe.cpp -- BT412 in-game front end: local mission-egg builder. // // See btl4fe.hpp. This module is self-contained (no engine types) so it can // build the egg text before the mission machinery exists; it only writes a // NotationFile the standard -egg path then loads. //===========================================================================// // WIN32_LEAN_AND_MEAN keeps from pulling the legacy winsock.h; // the engine chain (via l4app.hpp) brings winsock2.h, and the two collide. // GDI (used below for the plasma name bitmaps) is unaffected. Same preamble // as game/btl4main.cpp. #define WIN32_LEAN_AND_MEAN #define _WIN32_WINNT 0x0500 #define WINVER 0x0500 #include #include #include #include "btl4fe.hpp" #include "btl4lobby.hpp" // BTLobby_Host/Join + LobbyOutcome #include "l4app.hpp" // L4Application::SetEggNotationFileName // Mission length (seconds) chosen on the last LAUNCH; WinMain arms the marshal. static int gLastMissionSeconds = 300; static char gLastPilotName[24] = "Pilot"; static char gLastVehicle[16] = "bhk1"; static char gLastColor[16] = "White"; static char gLastBadge[8] = "VGL"; // Lobby-fed hosted pilots (the owner's egg gains one [pilots] entry each). static FEHostedPilot gHostedPilots[8]; static int gHostedPilotCount = 0; static char gHostedOwnerAddress[64] = ""; // Launch mode from the last LAUNCH: 0 single, 1 host (lobby), 2 member (lobby). static int gLastLaunchMode = 0; void BTFrontEnd_GetLoadout(char *vehicle, char *color, char *badge) { strcpy(vehicle, gLastVehicle); strcpy(color, gLastColor); strcpy(badge, gLastBadge); } void BTFrontEnd_SetHostedPilots(const char *owner_address, const FEHostedPilot *pilots, int count) { if (owner_address == 0 || count <= 0) { gHostedPilotCount = 0; gHostedOwnerAddress[0] = '\0'; return; } strncpy(gHostedOwnerAddress, owner_address, sizeof(gHostedOwnerAddress) - 1); if (count > 8) count = 8; for (int i = 0; i < count; ++i) gHostedPilots[i] = pilots[i]; gHostedPilotCount = count; } int BTFrontEnd_LastLaunchMode() { return gLastLaunchMode; } //---------------------------------------------------------------------------// // Catalogs -- the BattleTech content the console's RPConfig.xml named. // Maps: the 8 in BTL4.RES. Mechs: the base 8 + the known variants. //---------------------------------------------------------------------------// const char *const kBTMaps[] = { "cavern", "grass", "rav", "polar3", "polar4", "arena1", "arena2", "dbase", 0 }; const char *const kBTMechs[] = { "avatar", "bhk1", "loki", "madcat", "owens", "sunder", "thor", "vulture", "ava1", "lok1", "lok2", "mad1", "mad2", "own1", "snd1", "thr1", "vul1", "blkhawk", 0 }; const char *const kBTColors[] = { "White", "Red", "Blue", "Green", "Yellow", "Orange", "Purple", "Cyan", 0 }; int BTCatalogCount(const char *const *list) { int n = 0; while (list[n] != 0) ++n; return n; } //---------------------------------------------------------------------------// // The four rank-ordinal plasma bitmaps (1st..4th place), 128x32, MSB-first // hex, 4 pixels per nibble. These are static content -- the same graphics // the console shipped in every egg's [ordinals] block (verified against // content/MP.EGG). The pilot NAME bitmaps are GDI-rendered per mission. //---------------------------------------------------------------------------// namespace { const char *const kOrdinal[4][32] = { { // 1st "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "000038000003C000001FF00000000F00","0000F8000003C000003FF80000000F00", "0001F8000003C00000707C0000000F00","0001F8000003C00000603C0000000F00", "00007801FC0FF00000003C3DF807FF00","00007803FE0FF00000003C3FFC0FFF00", "00007803C703C00000003C3E3C1F0F00","000078078303C0000000783C1E1E0F00", "000078078003C0000000783C1E1E0F00","00007807C003C0000000F03C1E1E0F00", "00007807F003C0000001E03C1E1E0F00","00007803FC03C0000003C03C1E1E0F00", "00007801FE03C0000007803C1E1E0F00","000078007F03C000000F003C1E1E0F00", "000078001F03C000001E003C1E1E0F00","000078000F03C000003C003C1E1E0F00", "000078060F03C0000078003C1E1E0F00","000078071E03E0000078003C1E1F1F00", "00007803FE01F000007FFC3C1E0FFF00","00007801FC00F000007FFC3C1E07EF00", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", }, { // 2nd "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "001FFF0000003C0000003C03C0780000","001FFF0000003C0000007C03C0780000", "00000E0000003C000000FC03C0780000","00003C0000003C000001BC03C0780000", "0000700F3E1FFC000003BC0FF07BF000","0001E00F7E3FFC0000073C0FF07FF800", "0003800FFE7C3C0000063C03C07C7800","0007F80FFE783C00000C3C03C0783C00", "0007FE0F80783C0000183C03C0783C00","00001E0F00783C0000383C03C0783C00", "00000F0F00783C0000703C03C0783C00","00000F0F00783C00007FFF03C0783C00", "00000F0F00783C00007FFF03C0783C00","00000F0F00783C0000003C03C0783C00", "00000F0F00783C0000003C03C0783C00","00000F0F00783C0000003C03C0783C00", "00180F0F00783C0000003C03C0783C00","001C1F0F007C7C0000003C03E0783C00", "000FFE0F003FFC0000003C01F0783C00","0007FC0F001FBC0000003C00F0783C00", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", }, { // 3rd "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "001FFF03C07800000000FC03C0780000","001FFF03C07800000003FC03C0780000", "001E0003C07800000007C003C0780000","001E0003C0780000000F0003C0780000", "001E000FF07BF000000F000FF07BF000","001E000FF07FF800001E000FF07FF800", "001E0003C07C7800001E0003C07C7800","001FFC03C0783C00001EFC03C0783C00", "001FFE03C0783C00001FFE03C0783C00","00001F03C0783C00001F1F03C0783C00", "00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00", "00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00", "00000F03C0783C00001E0F03C0783C00","00000F03C0783C00001E0F03C0783C00", "00180F03C0783C00001E0F03C0783C00","001C1F03E0783C00001F1F03E0783C00", "000FFE01F0783C00000FFE01F0783C00","0007FC00F0783C000007FC00F0783C00", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", }, { // 4th "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "001FFF03C07800000007FC03C0780000","001FFF03C0780000000FFE03C0780000", "00000F03C0780000001F1F03C0780000","00000F03C0780000001E0F03C0780000", "00000F0FF07BF000001E0F0FF07BF000","00001F0FF07FF800001E0F0FF07FF800", "00001E03C07C7800001E0F03C07C7800","00003E03C0783C00001E0F03C0783C00", "00003C03C0783C00000F1E03C0783C00","00003C03C0783C000007FC03C0783C00", "00007803C0783C000007FC03C0783C00","00007803C0783C00000F1E03C0783C00", "00007803C0783C00001E0F03C0783C00","0000F003C0783C00001E0F03C0783C00", "0000F003C0783C00001E0F03C0783C00","0000F003C0783C00001E0F03C0783C00", "0000F003C0783C00001E0F03C0783C00","0000F003E0783C00000F1E03E0783C00", "0000F001F0783C00000FFE01F0783C00","0000F000F0783C000007FC00F0783C00", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", "00000000000000000000000000000000","00000000000000000000000000000000", }, }; //-------------------------------------------------------------------// // GDI-render `text` centered into a monochrome plasma bitmap and emit // it as `rows` lines of `width/4` hex chars, MSB-first (the exact // [BitMap] format). width/height are the plasma glyph box (128x32 for // the large name, 64x16 for the small). A top-down 1bpp DIB packs bit // 7 of byte 0 as the leftmost pixel -- the same bit order as the egg // hex -- so the readback is a direct nibble-to-hex conversion. //-------------------------------------------------------------------// void RenderNameBitmap(const char *text, int width, int height, FILE *out) { HDC screen = GetDC(NULL); HDC dc = CreateCompatibleDC(screen); ReleaseDC(NULL, screen); // Top-down monochrome DIB: negative height, biBitCount 1. struct { BITMAPINFOHEADER h; RGBQUAD c[2]; } bi; memset(&bi, 0, sizeof(bi)); bi.h.biSize = sizeof(BITMAPINFOHEADER); bi.h.biWidth = width; bi.h.biHeight = -height; // top-down bi.h.biPlanes = 1; bi.h.biBitCount = 1; bi.h.biCompression = BI_RGB; bi.c[0].rgbRed = bi.c[0].rgbGreen = bi.c[0].rgbBlue = 0; // index 0 = off bi.c[1].rgbRed = bi.c[1].rgbGreen = bi.c[1].rgbBlue = 255; // index 1 = lit void *bits = 0; HBITMAP dib = CreateDIBSection(dc, (BITMAPINFO *)&bi, DIB_RGB_COLORS, &bits, NULL, 0); HGDIOBJ oldBmp = SelectObject(dc, dib); RECT rect = { 0, 0, width, height }; SetBkColor(dc, RGB(0, 0, 0)); SetTextColor(dc, RGB(255, 255, 255)); ExtTextOutA(dc, 0, 0, ETO_OPAQUE, &rect, "", 0, NULL); // clear to off // A small readable font that fits the glyph box height. HFONT font = CreateFontA( height >= 32 ? 24 : 12, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, NONANTIALIASED_QUALITY, FF_DONTCARE | DEFAULT_PITCH, "Arial"); HGDIOBJ oldFont = SelectObject(dc, font); SetBkMode(dc, TRANSPARENT); SIZE sz; GetTextExtentPoint32A(dc, text, (int)strlen(text), &sz); int tx = (width - sz.cx) / 2; if (tx < 0) tx = 0; int ty = (height - sz.cy) / 2; if (ty < 0) ty = 0; TextOutA(dc, tx, ty, text, (int)strlen(text)); GdiFlush(); // Emit: each DIB row is width bits, padded to a 4-byte (32-bit) // boundary. The egg wants width/4 hex chars per row (no padding), // so read the first width bits and repack. const int strideBytes = ((width + 31) / 32) * 4; const unsigned char *base = (const unsigned char *)bits; for (int y = 0; y < height; ++y) { const unsigned char *row = base + (size_t)y * strideBytes; fprintf(out, "bitmap="); for (int nib = 0; nib < width / 4; ++nib) { int value = 0; for (int b = 0; b < 4; ++b) { int px = nib * 4 + b; int byte = px >> 3; int bit = 7 - (px & 7); // MSB-first if (row[byte] & (1 << bit)) value |= (1 << (3 - b)); } fputc("0123456789ABCDEF"[value], out); } fputc('\n', out); } SelectObject(dc, oldFont); DeleteObject(font); SelectObject(dc, oldBmp); DeleteObject(dib); DeleteDC(dc); } void WriteOrdinalBlock(FILE *out, int index /*1..4*/) { fprintf(out, "[Ordinal::BitMap::%d]\n", index); for (int r = 0; r < 32; ++r) fprintf(out, "bitmap=%s\n", kOrdinal[index - 1][r]); fprintf(out, "x=128\ny=32\nwidth=8\n"); } } //---------------------------------------------------------------------------// // Defaults: a solo race on grass, one local pilot in a Black Hawk. //---------------------------------------------------------------------------// void BTFeMission_Default(BTFeMission *mission) { memset(mission, 0, sizeof(*mission)); strcpy(mission->map, "grass"); strcpy(mission->scenario, "freeforall"); strcpy(mission->timeOfDay, "day"); strcpy(mission->weather, "clear"); mission->temperature = 27; mission->lengthSeconds = 600; mission->pilotCount = 1; BTFePilot *p = &mission->pilots[0]; strcpy(p->address, "200.0.0.96"); // solo local (matches DEV.EGG) strcpy(p->name, "Pilot"); strcpy(p->vehicle, "bhk1"); strcpy(p->color, "White"); strcpy(p->badge, "VGL"); strcpy(p->patch, "Yellow"); strcpy(p->dropzone, "one"); p->bitmapindex = 1; p->advancedDamage = 1; p->loadzones = 1; } //---------------------------------------------------------------------------// // Write the egg. //---------------------------------------------------------------------------// int BTFeMission_WriteEgg(const BTFeMission *mission, const char *path) { FILE *out = 0; if (fopen_s(&out, path, "wb") != 0 || out == 0) return 0; // [mission] fprintf(out, "[mission]\n"); fprintf(out, "adventure=BattleTech\n"); fprintf(out, "map=%s\n", mission->map); fprintf(out, "scenario=%s\n", mission->scenario); fprintf(out, "time=%s\n", mission->timeOfDay); fprintf(out, "weather=%s\n", mission->weather); fprintf(out, "temperature=%d\n", mission->temperature); fprintf(out, "length=%d\n", mission->lengthSeconds); // [ordinals] -- the four rank-place plasma graphics. fprintf(out, "[ordinals]\n"); for (int i = 1; i <= 4; ++i) fprintf(out, "bitmap=Ordinal::BitMap::%d\n", i); for (int i = 1; i <= 4; ++i) WriteOrdinalBlock(out, i); // [pilots] -- the ordered mesh roster. fprintf(out, "[pilots]\n"); for (int i = 0; i < mission->pilotCount; ++i) fprintf(out, "pilot=%s\n", mission->pilots[i].address); // Per-pilot loadout sections. for (int i = 0; i < mission->pilotCount; ++i) { const BTFePilot *p = &mission->pilots[i]; fprintf(out, "[%s]\n", p->address); fprintf(out, "hostType=0\n"); fprintf(out, "advancedDamage=%d\n", p->advancedDamage); fprintf(out, "loadzones=%d\n", p->loadzones); fprintf(out, "name=%s\n", p->name); fprintf(out, "bitmapindex=%d\n", p->bitmapindex); fprintf(out, "experience=expert\n"); fprintf(out, "badge=%s\n", p->badge); fprintf(out, "patch=%s\n", p->patch); fprintf(out, "role=Role::Default\n"); fprintf(out, "dropzone=%s\n", p->dropzone); fprintf(out, "vehicle=%s\n", p->vehicle); fprintf(out, "vehicleValue=1000\n"); fprintf(out, "color=%s\n", p->color); } // [largebitmap] / [smallbitmap] -- the plasma name readouts, one per // pilot, GDI-rendered from the pilot name. fprintf(out, "[largebitmap]\n"); for (int i = 0; i < mission->pilotCount; ++i) fprintf(out, "bitmap=BitMap::Large::%s\n", mission->pilots[i].name); for (int i = 0; i < mission->pilotCount; ++i) { fprintf(out, "[BitMap::Large::%s]\n", mission->pilots[i].name); RenderNameBitmap(mission->pilots[i].name, 128, 32, out); fprintf(out, "x=128\ny=32\nwidth=8\n"); } fprintf(out, "[smallbitmap]\n"); for (int i = 0; i < mission->pilotCount; ++i) fprintf(out, "bitmap=BitMap::Small::%s\n", mission->pilots[i].name); for (int i = 0; i < mission->pilotCount; ++i) { fprintf(out, "[BitMap::Small::%s]\n", mission->pilots[i].name); RenderNameBitmap(mission->pilots[i].name, 64, 16, out); fprintf(out, "x=64\ny=16\nwidth=4\n"); } // Role models (referenced by each pilot's role= field). fprintf(out, "[Role::Default]\n"); fprintf(out, "model=dfltrole\n"); fprintf(out, "[Role::NoReturn]\n"); fprintf(out, "model=noretun\n"); fclose(out); return 1; } //===========================================================================// // THE MINICONSOLE MENU -- the in-game race setup that replaces the operator // console. A GDI-painted green-on-black terminal (RP412 RPL4FE pattern): the // catalogs render as clickable columns, a pilot-name edit box, and a LAUNCH // button; clicking LAUNCH fills a BTFeMission from the selections and writes // the egg. Runs in its own top-level window before the mission (front-end // mode), so no D3D is up yet. //===========================================================================// namespace { struct MenuEntry { const char *key; const char *name; }; const MenuEntry kMenuMaps[] = { { "grass", "Grasslands" }, { "cavern", "Cavern" }, { "rav", "Ravine" }, { "polar3", "Polar III" }, { "polar4", "Polar IV" }, { "arena1", "Arena I" }, { "arena2", "Arena II" }, { "dbase", "Database" }, }; const MenuEntry kMenuMechs[] = { { "bhk1", "Black Hawk" }, { "avatar", "Avatar" }, { "loki", "Loki" }, { "madcat", "Mad Cat" }, { "owens", "Owens" }, { "sunder", "Sunder" }, { "thor", "Thor" }, { "vulture","Vulture" }, }; const MenuEntry kMenuColors[] = { { "White","White" }, { "Red","Red" }, { "Blue","Blue" }, { "Green","Green" }, { "Yellow","Yellow" },{ "Orange","Orange" },{ "Purple","Purple" },{ "Cyan","Cyan" }, }; const MenuEntry kMenuTimes[] = { { "day","Day" }, { "night","Night" } }; const MenuEntry kMenuWeather[] = { { "clear","Clear" }, { "fog","Light Fog" }, { "soup","Heavy Fog" } }; struct LenEntry { int seconds; const char *name; }; const LenEntry kMenuLengths[] = { { 180,"3:00" }, { 300,"5:00" }, { 600,"10:00" }, { 900,"15:00" }, { 0,"Endless" } }; #define MENU_COUNT(t) ((int)(sizeof(t)/sizeof((t)[0]))) enum MenuGroup { GMap=0, GMech, GColor, GTime, GWeather, GLength, GLaunch, GHost, GJoin, GCount }; const char *GroupTitle(int g) { switch (g) { case GMap: return "MAP"; case GMech: return "MECH"; case GColor: return "COLOR"; case GTime: return "TIME"; case GWeather: return "WEATHER"; case GLength: return "LENGTH"; } return ""; } int GroupSize(int g) { switch (g) { case GMap: return MENU_COUNT(kMenuMaps); case GMech: return MENU_COUNT(kMenuMechs); case GColor: return MENU_COUNT(kMenuColors); case GTime: return MENU_COUNT(kMenuTimes); case GWeather: return MENU_COUNT(kMenuWeather); case GLength: return MENU_COUNT(kMenuLengths); } return 0; } const char *ItemName(int g, int i) { switch (g) { case GMap: return kMenuMaps[i].name; case GMech: return kMenuMechs[i].name; case GColor: return kMenuColors[i].name; case GTime: return kMenuTimes[i].name; case GWeather: return kMenuWeather[i].name; case GLength: return kMenuLengths[i].name; case GLaunch: return "L A U N C H"; case GHost: return "HOST STEAM RACE"; case GJoin: return "JOIN STEAM RACE"; } return ""; } struct MenuItem { int group, index; RECT rect; }; struct MenuState { int selection[GCount]; MenuItem items[128]; int itemCount; HWND window, nameEdit; HFONT textFont, titleFont; HBRUSH editBrush; int launched, closed; int steamAction; // 0 none, 1 host, 2 join }; MenuState *gMenu = NULL; const COLORREF kGreenBright = RGB(64, 255, 64); const COLORREF kGreenDim = RGB(24, 140, 24); const COLORREF kBlack = RGB(0, 0, 0); void AddGroupItems(MenuState *m, int group, int x, int *y, int row_h, int col_w) { int n = GroupSize(group); for (int i = 0; i < n; ++i) { MenuItem *it = &m->items[m->itemCount++]; it->group = group; it->index = i; it->rect.left = x; it->rect.top = *y; it->rect.right = x + col_w; it->rect.bottom = *y + row_h; *y += row_h; } *y += row_h; // gap + header room for the next group } void LayoutMenu(MenuState *m, int cw, int ch) { m->itemCount = 0; int row_h = ch / 26; if (row_h < 20) row_h = 20; if (row_h > 34) row_h = 34; int col_w = cw / 5; int col1 = cw / 12; int col2 = col1 + col_w + cw / 24; int col3 = col2 + col_w + cw / 24; int top = ch / 7; int y = top; AddGroupItems(m, GMap, col1, &y, row_h, col_w); AddGroupItems(m, GTime, col1, &y, row_h, col_w); AddGroupItems(m, GWeather, col1, &y, row_h, col_w); AddGroupItems(m, GLength, col1, &y, row_h, col_w); y = top; AddGroupItems(m, GMech, col2, &y, row_h, col_w); y = top + 2 * row_h; AddGroupItems(m, GColor, col3, &y, row_h, col_w); MenuItem *launch = &m->items[m->itemCount++]; launch->group = GLaunch; launch->index = 0; launch->rect.left = col3; launch->rect.top = ch - 3 * row_h; launch->rect.right = col3 + col_w; launch->rect.bottom = ch - row_h; // Steam HOST / JOIN buttons (only when the Steam transport is up). // (BTLobby_Available is declared globally in btl4lobby.hpp -- a block-scope // `extern` HERE would bind it to this anonymous namespace instead.) if (BTLobby_Available()) { MenuItem *host = &m->items[m->itemCount++]; host->group = GHost; host->index = 0; host->rect.left = col3; host->rect.top = ch - 6 * row_h; host->rect.right = col3 + col_w; host->rect.bottom = ch - 5 * row_h; MenuItem *join = &m->items[m->itemCount++]; join->group = GJoin; join->index = 0; join->rect.left = col3; join->rect.top = ch - (9 * row_h) / 2; join->rect.right = col3 + col_w; join->rect.bottom = ch - (7 * row_h) / 2; } if (m->nameEdit != NULL) MoveWindow(m->nameEdit, col3, top - row_h / 4, col_w, row_h, TRUE); } void PaintMenu(MenuState *m) { PAINTSTRUCT ps; HDC hdc = BeginPaint(m->window, &ps); RECT client; GetClientRect(m->window, &client); HDC mem = CreateCompatibleDC(hdc); HBITMAP surf = CreateCompatibleBitmap(hdc, client.right, client.bottom); HBITMAP oldSurf = (HBITMAP) SelectObject(mem, surf); FillRect(mem, &client, (HBRUSH) GetStockObject(BLACK_BRUSH)); SetBkMode(mem, TRANSPARENT); SelectObject(mem, m->titleFont); SetTextColor(mem, kGreenBright); RECT title = client; title.top = client.bottom / 26; title.bottom = title.top + client.bottom / 9; DrawTextA(mem, "B A T T L E T E C H - MISSION SETUP", -1, &title, DT_CENTER | DT_TOP | DT_SINGLELINE); SelectObject(mem, m->textFont); int prevGroup = -1; for (int i = 0; i < m->itemCount; ++i) { const MenuItem *it = &m->items[i]; if (it->group != prevGroup && it->group < GLaunch) { prevGroup = it->group; RECT h = it->rect; h.top -= (it->rect.bottom - it->rect.top); h.bottom = it->rect.top; SetTextColor(mem, kGreenBright); DrawTextA(mem, GroupTitle(it->group), -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } RECT row = it->rect; if (it->group >= GLaunch) { // LAUNCH / HOST / JOIN -- framed buttons HBRUSH b = CreateSolidBrush(kGreenBright); FrameRect(mem, &row, b); DeleteObject(b); SetTextColor(mem, kGreenBright); DrawTextA(mem, ItemName(it->group, it->index), -1, &row, DT_CENTER | DT_VCENTER | DT_SINGLELINE); continue; } int selected = (m->selection[it->group] == it->index); if (selected) { HBRUSH b = CreateSolidBrush(kGreenDim); FillRect(mem, &row, b); DeleteObject(b); SetTextColor(mem, kGreenBright); } else SetTextColor(mem, kGreenDim); RECT t = row; t.left += 8; DrawTextA(mem, ItemName(it->group, it->index), -1, &t, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } // pilot-name header if (m->nameEdit != NULL) { RECT er; GetWindowRect(m->nameEdit, &er); POINT c = { er.left, er.top }; ScreenToClient(m->window, &c); RECT h; h.left = c.x; h.right = c.x + 300; h.bottom = c.y; h.top = c.y - 26; SetTextColor(mem, kGreenBright); DrawTextA(mem, "PILOT NAME", -1, &h, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } BitBlt(hdc, 0, 0, client.right, client.bottom, mem, 0, 0, SRCCOPY); SelectObject(mem, oldSurf); DeleteObject(surf); DeleteDC(mem); EndPaint(m->window, &ps); } void MenuClick(MenuState *m, int x, int y) { for (int i = 0; i < m->itemCount; ++i) { const MenuItem *it = &m->items[i]; if (x >= it->rect.left && x < it->rect.right && y >= it->rect.top && y < it->rect.bottom) { if (it->group == GLaunch) { m->launched = 1; PostMessageA(m->window, WM_NULL, 0, 0); } else if (it->group == GHost || it->group == GJoin) { m->steamAction = (it->group == GHost) ? 1 : 2; PostMessageA(m->window, WM_NULL, 0, 0); } else { m->selection[it->group] = it->index; InvalidateRect(m->window, NULL, FALSE); } return; } } } LRESULT CALLBACK MenuWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { MenuState *m = gMenu; switch (msg) { case WM_PAINT: if (m) { PaintMenu(m); return 0; } break; case WM_LBUTTONDOWN: if (m) { MenuClick(m, (int)(short)LOWORD(lp), (int)(short)HIWORD(lp)); return 0; } break; case WM_CTLCOLOREDIT: if (m) { SetTextColor((HDC)wp, kGreenBright); SetBkColor((HDC)wp, kBlack); return (LRESULT)m->editBrush; } break; case WM_CLOSE: if (m) m->closed = 1; return 0; case WM_DESTROY: return 0; } return DefWindowProcA(hwnd, msg, wp, lp); } } // namespace //---------------------------------------------------------------------------// // Run the menu; on LAUNCH build the egg + point the engine at it. Returns 1 // if a mission was launched, 0 if the player closed the menu (quit). //---------------------------------------------------------------------------// int BTFrontEnd_Run() { // BT_FE_AUTOLAUNCH: skip the interactive menu and launch the default // loadout straight away (quick-launch / deterministic marshal testing). if (getenv("BT_FE_AUTOLAUNCH") != NULL) { BTFeMission mission; BTFeMission_Default(&mission); const char *egg = "frontend.egg"; if (!BTFeMission_WriteEgg(&mission, egg)) return 0; L4Application::SetEggNotationFileName(egg); gLastMissionSeconds = mission.lengthSeconds; return 1; } HINSTANCE instance = GetModuleHandleA(NULL); MenuState m; memset(&m, 0, sizeof(m)); m.selection[GLength] = 1; // default 5:00 gMenu = &m; static int classReg = 0; if (!classReg) { classReg = 1; WNDCLASSA wc; memset(&wc, 0, sizeof(wc)); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = MenuWndProc; wc.hInstance = instance; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); wc.lpszClassName = "BTFrontEnd"; RegisterClassA(&wc); } int W = 1000, H = 720; int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2; int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2; m.window = CreateWindowExA(0, "BTFrontEnd", "BattleTech", WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx < 0 ? 0 : sx, sy < 0 ? 0 : sy, W, H, NULL, NULL, instance, NULL); if (m.window == NULL) { gMenu = NULL; return 0; } RECT client; GetClientRect(m.window, &client); int text_h = client.bottom / 32; if (text_h < 16) text_h = 16; if (text_h > 26) text_h = 26; m.textFont = CreateFontA(-text_h, 0,0,0, FW_NORMAL, 0,0,0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas"); m.titleFont = CreateFontA(-(text_h * 2), 0,0,0, FW_BOLD, 0,0,0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH | FF_MODERN, "Consolas"); m.editBrush = CreateSolidBrush(kBlack); m.nameEdit = CreateWindowExA(0, "EDIT", "Pilot", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, 0,0,10,10, m.window, NULL, instance, NULL); SendMessageA(m.nameEdit, WM_SETFONT, (WPARAM) m.textFont, TRUE); SendMessageA(m.nameEdit, EM_SETLIMITTEXT, 22, 0); LayoutMenu(&m, client.right, client.bottom); InvalidateRect(m.window, NULL, TRUE); SetForegroundWindow(m.window); MSG msg; while (!m.launched && !m.closed && !m.steamAction) { BOOL r = GetMessageA(&msg, NULL, 0, 0); if (r <= 0) { m.closed = 1; break; } // Enter = LAUNCH, Escape = quit -- intercepted at the loop level so // they fire whichever child (menu or name box) has focus. if (msg.message == WM_KEYDOWN) { if (msg.wParam == VK_RETURN) { m.launched = 1; continue; } if (msg.wParam == VK_ESCAPE) { m.closed = 1; continue; } } if (msg.message == WM_QUIT) m.closed = 1; TranslateMessage(&msg); DispatchMessageA(&msg); } // harvest the loadout char pilotName[24] = "Pilot"; if (!m.closed) { GetWindowTextA(m.nameEdit, pilotName, sizeof(pilotName) - 1); if (pilotName[0] == '\0') strcpy(pilotName, "Pilot"); } int launched = m.launched, closed = m.closed, steamAction = m.steamAction; int selMap = m.selection[GMap], selMech = m.selection[GMech], selColor = m.selection[GColor]; int selTime = m.selection[GTime], selWx = m.selection[GWeather], selLen = m.selection[GLength]; // Remember the loadout so the lobby can publish it as member data. strncpy(gLastPilotName, pilotName, sizeof(gLastPilotName) - 1); strncpy(gLastVehicle, kMenuMechs[selMech].key, sizeof(gLastVehicle) - 1); strncpy(gLastColor, kMenuColors[selColor].key, sizeof(gLastColor) - 1); DestroyWindow(m.window); DeleteObject(m.textFont); DeleteObject(m.titleFont); DeleteObject(m.editBrush); gMenu = NULL; if (closed) return 0; // Steam HOST / JOIN: run the lobby room; act on its outcome. if (steamAction) { // BTLobby_Host/Join are declared in btl4lobby.hpp (global scope). int outcome = (steamAction == 1) ? BTLobby_Host(instance, 0) : BTLobby_Join(instance, 0); if (outcome == LobbyRoomClosed) return 0; if (outcome == LobbyLaunchMember) { gLastLaunchMode = 2; return 1; } // -net pod (no egg) if (outcome == LobbyLaunchHost) { gLastLaunchMode = 1; } // build the egg below else return BTFrontEnd_Run(); // left the room -> menu again } else { if (!launched) return 0; gLastLaunchMode = 0; } // Build the mission from the selections. BTFeMission mission; BTFeMission_Default(&mission); strcpy(mission.map, kMenuMaps[selMap].key); strcpy(mission.timeOfDay, kMenuTimes[selTime].key); strcpy(mission.weather, kMenuWeather[selWx].key); mission.lengthSeconds = kMenuLengths[selLen].seconds; strcpy(mission.pilots[0].name, pilotName); // Host (lobby): append every lobby member as a [pilots] entry so the // owner's egg carries the whole mesh (drop zones one..eight round-robin). if (gHostedPilotCount > 0) { static const char *const dz[8] = { "one","two","three","four","five","six","seven","eight" }; for (int i = 0; i < gHostedPilotCount && mission.pilotCount < 8; ++i) { BTFePilot *p = &mission.pilots[mission.pilotCount]; memset(p, 0, sizeof(*p)); strncpy(p->address, gHostedPilots[i].address, sizeof(p->address)-1); strncpy(p->name, gHostedPilots[i].name, sizeof(p->name)-1); strncpy(p->vehicle, gHostedPilots[i].vehicle[0]?gHostedPilots[i].vehicle:"bhk1", sizeof(p->vehicle)-1); strncpy(p->color, gHostedPilots[i].color[0]?gHostedPilots[i].color:"Red", sizeof(p->color)-1); strcpy(p->badge, "VGL"); strcpy(p->patch, "Yellow"); strncpy(p->dropzone, dz[mission.pilotCount % 8], sizeof(p->dropzone)-1); p->bitmapindex = mission.pilotCount + 1; p->advancedDamage = 1; p->loadzones = 1; mission.pilotCount++; } // the owner is pilot 0; its mesh address is the FakeIP:1502 if (gHostedOwnerAddress[0]) sprintf(mission.pilots[0].address, "%s:1502", gHostedOwnerAddress); } strcpy(mission.pilots[0].vehicle, kMenuMechs[selMech].key); strcpy(mission.pilots[0].color, kMenuColors[selColor].key); const char *egg_path = "frontend.egg"; if (!BTFeMission_WriteEgg(&mission, egg_path)) return 0; L4Application::SetEggNotationFileName(egg_path); gLastMissionSeconds = mission.lengthSeconds; return 1; } int BTFrontEnd_LastMissionSeconds() { return gLastMissionSeconds; } //===========================================================================// // THE RESULTS SCREEN -- shown after a marshaled mission, before the relaunch. // A GDI green-on-black scoreboard reading the marshal's snapshotted scores // (kills/deaths per roster slot; slot 0 = the local pilot, named from the // menu). Dismissed by any key/click or after a timeout. //===========================================================================// namespace { HWND gResultsWin = NULL; int gResultsDone = 0; } extern int BTLocalConsole_ResultCount(); extern int BTLocalConsole_GetResult(int index, int *kills, int *deaths); extern const char *BTLocalConsole_GetResultName(int index); namespace { LRESULT CALLBACK ResultsWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hwnd, &ps); RECT cl; GetClientRect(hwnd, &cl); HDC mem = CreateCompatibleDC(hdc); HBITMAP surf = CreateCompatibleBitmap(hdc, cl.right, cl.bottom); HBITMAP old = (HBITMAP) SelectObject(mem, surf); FillRect(mem, &cl, (HBRUSH) GetStockObject(BLACK_BRUSH)); SetBkMode(mem, TRANSPARENT); int th = cl.bottom / 22; if (th < 16) th = 16; if (th > 28) th = 28; HFONT big = CreateFontA(-(th*2),0,0,0,FW_BOLD,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas"); HFONT reg = CreateFontA(-th,0,0,0,FW_NORMAL,0,0,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,ANTIALIASED_QUALITY,DEFAULT_PITCH|FF_MODERN,"Consolas"); const COLORREF gB = RGB(64,255,64), gD = RGB(24,140,24); SelectObject(mem, big); SetTextColor(mem, gB); RECT t = cl; t.top = cl.bottom/12; t.bottom = t.top + th*3; DrawTextA(mem, "M I S S I O N C O M P L E T E", -1, &t, DT_CENTER|DT_TOP|DT_SINGLELINE); SelectObject(mem, reg); int y = cl.bottom/4; int cxN = cl.right/6, cxK = cl.right/2, cxD = (cl.right*2)/3; SetTextColor(mem, gB); RECT h; h.top=y; h.bottom=y+th*2; h.left=cxN; h.right=cxK; DrawTextA(mem,"PILOT",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE); h.left=cxK; h.right=cxD; DrawTextA(mem,"KILLS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE); h.left=cxD; h.right=cl.right; DrawTextA(mem,"DEATHS",-1,&h,DT_LEFT|DT_VCENTER|DT_SINGLELINE); y += th*2; int n = BTLocalConsole_ResultCount(); if (n <= 0) n = 1; // always show the local pilot line for (int i = 0; i < n; ++i) { int k = 0, d = 0; BTLocalConsole_GetResult(i, &k, &d); char name[24]; const char *rn = BTLocalConsole_GetResultName(i); // set when the lobby collated the sheet if (rn && rn[0]) { strncpy(name, rn, sizeof(name)-1); name[sizeof(name)-1]=0; } else if (i == 0) { strncpy(name, gLastPilotName, sizeof(name)-1); name[sizeof(name)-1]=0; } else sprintf(name, "Pilot %d", i + 1); char kb[8], db[8]; sprintf(kb, "%d", k); sprintf(db, "%d", d); SetTextColor(mem, i == 0 ? gB : gD); RECT r; r.top=y; r.bottom=y+th*2; r.left=cxN; r.right=cxK; DrawTextA(mem,name,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE); r.left=cxK; r.right=cxD; DrawTextA(mem,kb,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE); r.left=cxD; r.right=cl.right; DrawTextA(mem,db,-1,&r,DT_LEFT|DT_VCENTER|DT_SINGLELINE); y += th*2; } SetTextColor(mem, gD); RECT f = cl; f.top = cl.bottom - th*3; f.bottom = cl.bottom - th; DrawTextA(mem, "press any key", -1, &f, DT_CENTER|DT_TOP|DT_SINGLELINE); BitBlt(hdc, 0,0, cl.right, cl.bottom, mem, 0,0, SRCCOPY); SelectObject(mem, old); DeleteObject(surf); DeleteDC(mem); DeleteObject(big); DeleteObject(reg); EndPaint(hwnd, &ps); return 0; } case WM_KEYDOWN: case WM_LBUTTONDOWN: gResultsDone = 1; return 0; case WM_CLOSE: gResultsDone = 1; return 0; } return DefWindowProcA(hwnd, msg, wp, lp); } } // namespace // Show the scoreboard until dismissed (or ~12s timeout). Returns always. void BTFrontEnd_ShowResults() { HINSTANCE instance = GetModuleHandleA(NULL); static int reg = 0; if (!reg) { reg = 1; WNDCLASSA wc; memset(&wc,0,sizeof(wc)); wc.lpfnWndProc = ResultsWndProc; wc.hInstance = instance; wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH); wc.lpszClassName = "BTResults"; RegisterClassA(&wc); } int W = 800, H = 560; int sx = (GetSystemMetrics(SM_CXSCREEN) - W) / 2; int sy = (GetSystemMetrics(SM_CYSCREEN) - H) / 2; gResultsDone = 0; gResultsWin = CreateWindowExA(0, "BTResults", "BattleTech", WS_OVERLAPPEDWINDOW | WS_VISIBLE, sx<0?0:sx, sy<0?0:sy, W, H, NULL, NULL, instance, NULL); if (gResultsWin == NULL) return; SetForegroundWindow(gResultsWin); DWORD deadline = GetTickCount() + 12000; MSG msg; while (!gResultsDone && GetTickCount() < deadline) { while (PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_KEYDOWN || msg.message == WM_QUIT) gResultsDone = 1; TranslateMessage(&msg); DispatchMessageA(&msg); } Sleep(15); } DestroyWindow(gResultsWin); gResultsWin = NULL; }