//===========================================================================// // btl4console.cpp -- BT412 in-process LocalConsole marshal. // // Two roles, one game-thread tick (gPerFrameHook, so every engine call is on // the game thread and safe): // SOLO -- own the mission clock; StopMissionMessage at the chosen length. // NETWORK -- the lobby/host OWNER also plays the arcade console for the // member pods over the NetTransport wire: egg chunks + ACK, state // polls, a COORDINATED RunMission when the whole mesh is staged, // StopMission at expiry. The owner's own pod meshes like any pod // but is fed its egg locally (FeedLocalEgg) and driven by direct // engine calls, so the console never connects to itself. // // Final scores are BT's own kills/deaths, snapshotted from the (meshed) roster // at the stop -- NO EndMission wire intake (BT's mission-end wire flow is a // stub; in a meshed mission the host's roster already holds every pilot's // tally). Modeled on RP412's RPL4CONSOLE (which collected remote scores over // the wire instead; BT's roster snapshot replaces that). //===========================================================================// #include // Application, application, DEBUG_STREAM #pragma hdrstop #include // strncpy / memcpy / memmove #include // fopen / fread (egg wire image) #include "appmsg.hpp" // Application::StopMissionMessage / RunMissionMessage / StateQueryMessage #include "appmgr.hpp" // gPerFrameHook, gConsoleLossEndsMission, gConsoleMarshalsLaunch #include "l4app.hpp" // L4Application::GetNetworkManager #include "l4net.hpp" // L4NetworkManager, FeedLocalEgg, egg / ack messages #include "console.hpp" // ConsoleApplicationStateResponseMessage #include "network.hpp" // NetworkPacketHeader, NetworkClient, Receiver__Message #include "l4nettransport.hpp" // NetTransport / NetTransport_Get #include "btl4console.hpp" // The pilot roster + score bridges (btl4gau3.cpp / mechmppr.cpp / btplayer.cpp): // the pilotList reads them for the Comm MFD, and the marshal snapshots them at // the stop so the results screen has the final scores. extern void *BTResolveRosterPilot(int slot); extern int BTPilotKills(void *pilot); extern int BTPilotDeaths(void *pilot); namespace { enum Phase { PhaseIdle, PhaseWaiting, PhaseRunning, PhaseStopped }; Phase gPhase = PhaseIdle; int gMissionSeconds = 0; unsigned long gStartTick = 0; Application *gWatched = 0; // Final scores snapshotted at the stop (roster order; slot 0 = local). enum { maxResults = 8 }; struct Result { int kills, deaths; char name[24]; }; Result gResults[maxResults]; int gResultCount = 0; void SnapshotResults() { gResultCount = 0; for (int slot = 0; slot < maxResults; ++slot) { void *pilot = BTResolveRosterPilot(slot); if (pilot == 0) break; // past the last occupied roster slot gResults[gResultCount].kills = BTPilotKills(pilot); gResults[gResultCount].deaths = BTPilotDeaths(pilot); gResults[gResultCount].name[0] = 0; // filled by the lobby (owner) if networked ++gResultCount; } DEBUG_STREAM << "[marshal] snapshot " << gResultCount << " pilot score(s)\n" << std::flush; } //-----------------------------------------------------------------------// // NETWORK mission: the owner marshals the member pods over the wire. //-----------------------------------------------------------------------// enum { maxRemotePods = 8, remoteRxSize = 8192 }; const int kConsoleNetPort = 1501; // arcade default (L4NET.CPP) struct RemotePod { char address[64]; // console channel, "ip[:port]" NetTransport::Connection connection; int state; // last reported app state (-1 unknown) Logical eggAcknowledged; DWORD lastQueryTick; DWORD eggSentTick; // 0 = never sent char rx[remoteRxSize]; // wire-frame reassembly int rxCount; }; RemotePod gRemotePods[maxRemotePods]; int gRemotePodCount = 0; Logical gNetworkMission = False; char gEggPath[MAX_PATH] = ""; char *gEggWire = 0; // newline->NUL image for chunking int gEggWireSize = 0; Logical gLocalEggFed = False; Logical gRunSent = False; Logical gRemoteStopsSent = False; DWORD gRemoteStopTick = 0; // The arcade console protocol over the NetTransport seam (Winsock or Steam). void SendWire(RemotePod *pod, int client_ID, const void *message, int size) { char packet[sizeof(NetworkPacketHeader) + 1400]; if (size > (int)sizeof(packet) - (int)sizeof(NetworkPacketHeader)) return; memset(packet, 0, sizeof(NetworkPacketHeader)); NetworkPacketHeader *header = (NetworkPacketHeader *)packet; header->clientID = (NetworkClient::ClientID)client_ID; header->gameID = 0; header->fromHost = 1; // the console's reserved host ID memcpy(packet + sizeof(NetworkPacketHeader), message, size); NetTransport_Get()->Send(pod->connection, packet, (int)sizeof(NetworkPacketHeader) + size); } void SendEggTo(RemotePod *pod) { int chunk_count = (gEggWireSize + 999) / 1000; for (int i = 0; i < chunk_count; ++i) { int offset = i * 1000; int length = gEggWireSize - offset; if (length > 1000) length = 1000; NetworkManager::ReceiveEggFileMessage chunk( i, gEggWireSize, gEggWire + offset, length); SendWire(pod, NetworkClient::NetworkManagerClientID, &chunk, (int)chunk.messageLength); } pod->eggSentTick = GetTickCount(); DEBUG_STREAM << "[marshal] egg sent to " << pod->address << " (" << chunk_count << " chunks)\n" << std::flush; } void PumpRemote(RemotePod *pod) { // Read whatever the wire has pending. for (;;) { int space = remoteRxSize - pod->rxCount; if (space <= 0) break; int received = NetTransport_Get()->Receive( pod->connection, pod->rx + pod->rxCount, space); if (received <= 0) break; // no data / disconnected pod->rxCount += received; if (received < space) break; } // Parse complete frames: NetworkPacketHeader + engine message. const int header_size = (int)sizeof(NetworkPacketHeader); const int base_size = (int)sizeof(Receiver__Message); for (;;) { if (pod->rxCount < header_size + base_size) break; NetworkPacketHeader *header = (NetworkPacketHeader *)pod->rx; Receiver__Message *base = (Receiver__Message *)(pod->rx + header_size); int total = header_size + (int)base->messageLength; if (total < header_size + base_size || total > remoteRxSize) { DEBUG_STREAM << "[marshal] garbage frame from " << pod->address << " -- dropping buffer\n" << std::flush; pod->rxCount = 0; break; } if (pod->rxCount < total) break; if ((int)header->clientID == (int)NetworkClient::ConsoleClientID) { if ((int)base->messageID == ConsoleApplicationStateResponseMessageID) { ConsoleApplicationStateResponseMessage *message = (ConsoleApplicationStateResponseMessage *)base; pod->state = (int)message->GetApplicationState(); } // EndMission (ID 7) is ignored: BT scores come from the host's // own meshed roster snapshot, not the wire (msgID 0x18 stub). } else if ((int)header->clientID == (int)NetworkClient::NetworkManagerClientID) { if ((int)base->messageID == (int)L4NetworkManager::AcknowledgeEggFileMessageID) { if (!pod->eggAcknowledged) DEBUG_STREAM << "[marshal] " << pod->address << " EGG ACK (mesh complete)\n" << std::flush; pod->eggAcknowledged = True; } } memmove(pod->rx, pod->rx + total, pod->rxCount - total); pod->rxCount -= total; } } void MarshalRemotes() { DWORD now = GetTickCount(); for (int i = 0; i < gRemotePodCount; ++i) { RemotePod *pod = &gRemotePods[i]; // state poll, once a second (the arcade console's cadence) if ((LONG)(now - pod->lastQueryTick) >= 1000) { Application::StateQueryMessage query(1); SendWire(pod, NetworkClient::ApplicationClientID, &query, (int)query.messageLength); pod->lastQueryTick = now; } PumpRemote(pod); // egg feed: (re)send every 5s until the pod ACKs (post-mesh) if (pod->state == (int)Application::WaitingForEgg && !pod->eggAcknowledged && (pod->eggSentTick == 0 || (LONG)(now - pod->eggSentTick) >= 5000)) { SendEggTo(pod); } } } Logical AllRemotesInState(int state) { for (int i = 0; i < gRemotePodCount; ++i) if (gRemotePods[i].state != state) return False; return True; } void DisconnectRemotes() { for (int i = 0; i < gRemotePodCount; ++i) if (gRemotePods[i].connection != NetTransport::InvalidConnection) { NetTransport_Get()->Close(gRemotePods[i].connection); gRemotePods[i].connection = NetTransport::InvalidConnection; } } // // The game-thread tick (called every frame while an application is live). // void ConsoleTick() { if (application == 0) return; if (gPhase != PhaseWaiting && application != gWatched) return; // only marshal our own mission int state = application->GetApplicationState(); switch (gPhase) { case PhaseWaiting: if (gNetworkMission) { MarshalRemotes(); // Feed our own pod its egg locally: it meshes like any pod, but // the in-process console drives it without a self-connection. if (!gLocalEggFed && state == Application::WaitingForEgg) { L4NetworkManager *network_manager = (L4NetworkManager *)application->GetNetworkManager(); if (network_manager != 0) { network_manager->FeedLocalEgg(gEggPath); gLocalEggFed = True; DEBUG_STREAM << "[marshal] local egg fed\n" << std::flush; } } // Whole mesh staged -> launch everywhere at once (the owner // holds at WaitingForLaunch via gConsoleMarshalsLaunch until here). if (!gRunSent && state == Application::WaitingForLaunch && AllRemotesInState(Application::WaitingForLaunch)) { DEBUG_STREAM << "[marshal] all pods staged -- RUN\n" << std::flush; for (int i = 0; i < gRemotePodCount; ++i) { Application::RunMissionMessage run; SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID, &run, (int)run.messageLength); } Application::RunMissionMessage local_run; application->Dispatch(&local_run); gRunSent = True; } } // The mission started running -> start the clock. if (state == Application::RunningMission) { gPhase = PhaseRunning; gWatched = application; gStartTick = GetTickCount(); DEBUG_STREAM << "[marshal] mission running -- length " << gMissionSeconds << "s\n" << std::flush; } break; case PhaseRunning: if (gNetworkMission) for (int i = 0; i < gRemotePodCount; ++i) PumpRemote(&gRemotePods[i]); if (state != Application::RunningMission) { // ended some other way (pilot abort, teardown) gPhase = PhaseStopped; if (gNetworkMission) DisconnectRemotes(); DEBUG_STREAM << "[marshal] mission ended (state change)\n" << std::flush; } else if (gMissionSeconds > 0 && (GetTickCount() - gStartTick) >= (unsigned long)(gMissionSeconds * 1000)) { if (!gNetworkMission) { // SOLO: snapshot the final scores (game state still live), // then stop the mission as the arcade console did. DEBUG_STREAM << "[marshal] time expired -- stopping mission\n" << std::flush; SnapshotResults(); Application::StopMissionMessage message(0); application->Dispatch(&message); gPhase = PhaseStopped; } else if (!gRemoteStopsSent) { // NETWORK: snapshot the meshed roster (every pilot's tally) // while still live, stop the members, then hold the local pod // briefly so the stop propagates before our own teardown. DEBUG_STREAM << "[marshal] time expired -- stopping remote pods\n" << std::flush; SnapshotResults(); for (int i = 0; i < gRemotePodCount; ++i) { Application::StopMissionMessage stop(0); SendWire(&gRemotePods[i], NetworkClient::ApplicationClientID, &stop, (int)stop.messageLength); } gRemoteStopsSent = True; gRemoteStopTick = GetTickCount(); } else if ((LONG)(GetTickCount() - gRemoteStopTick) >= 2000) { Application::StopMissionMessage message(0); application->Dispatch(&message); DisconnectRemotes(); gPhase = PhaseStopped; } } break; default: break; } } // Shared arm plumbing for a fresh mission. void ArmClock(int mission_seconds) { gMissionSeconds = mission_seconds; gPhase = PhaseWaiting; gStartTick = 0; gWatched = 0; gPerFrameHook = &ConsoleTick; } } void BTLocalConsole_Install(int mission_seconds) { gNetworkMission = False; gConsoleMarshalsLaunch = False; gRemotePodCount = 0; // Marshaled: the in-process console owns the clock, so a console-loss // (there is none locally) would end the mission -- harmless for solo. gConsoleLossEndsMission = True; ArmClock(mission_seconds); DEBUG_STREAM << "[marshal] installed (length " << mission_seconds << "s)\n" << std::flush; } int BTLocalConsole_InstallNetworkMission(int mission_seconds, const char *egg_path, const char *remote_pod_list) { gNetworkMission = True; gRemotePodCount = 0; gLocalEggFed = False; gRunSent = False; gRemoteStopsSent = False; // The owner IS the console: hold the whole mesh at WaitingForLaunch and // launch it at once; and never treat "console loss" as our own end. gConsoleMarshalsLaunch = True; gConsoleLossEndsMission = False; strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1); gEggPath[sizeof(gEggPath) - 1] = 0; // The wire image of the egg: file newlines -> NULs, exactly what the arcade // console (and tools/btconsole.py) sent -- BT NotationFile::ReadText walks // NUL-separated lines. Sending raw text parses as one line ("no map in egg!"). if (gEggWire) { delete[] gEggWire; gEggWire = 0; gEggWireSize = 0; } FILE *egg_file = fopen(egg_path, "rb"); if (egg_file == 0) { DEBUG_STREAM << "[marshal] cannot read egg " << egg_path << "\n" << std::flush; return 0; } fseek(egg_file, 0, SEEK_END); long raw_size = ftell(egg_file); fseek(egg_file, 0, SEEK_SET); char *raw = new char[raw_size > 0 ? raw_size : 1]; size_t got = fread(raw, 1, raw_size, egg_file); fclose(egg_file); gEggWire = new char[got + 1]; gEggWireSize = 0; for (long b = 0; b < (long)got; ++b) { if (raw[b] == '\r') continue; // \r\n collapses to one NUL gEggWire[gEggWireSize++] = (raw[b] == '\n') ? '\0' : raw[b]; } // btconsole.py appends a trailing NUL per line, including the last; match it // when the file did not end in a newline so the final line terminates. if (gEggWireSize == 0 || gEggWire[gEggWireSize - 1] != '\0') gEggWire[gEggWireSize++] = '\0'; delete[] raw; // Connect to every member's console channel. Blocking with retry inside // the transport (like the arcade console redialing a booting pod); runs // BEFORE the engine inits, so nothing local is waiting on it. NetTransport_Get()->Startup(); const char *cursor = remote_pod_list; while (*cursor != '\0' && gRemotePodCount < maxRemotePods) { RemotePod *pod = &gRemotePods[gRemotePodCount]; memset(pod, 0, sizeof(*pod)); pod->state = -1; pod->connection = NetTransport::InvalidConnection; int length = 0; while (cursor[length] != '\0' && cursor[length] != ',' && length < (int)sizeof(pod->address) - 1) { pod->address[length] = cursor[length]; ++length; } pod->address[length] = '\0'; cursor += length; if (*cursor == ',') ++cursor; SOCKADDR_IN console_address; NetTransport_Get()->Resolve(pod->address, &console_address); if (console_address.sin_port == 0) console_address.sin_port = htons(kConsoleNetPort); DEBUG_STREAM << "[marshal] connecting to pod " << pod->address << "...\n" << std::flush; pod->connection = NetTransport_Get()->Connect(&console_address, 0); if (pod->connection == NetTransport::InvalidConnection) { DEBUG_STREAM << "[marshal] could not reach pod " << pod->address << "\n" << std::flush; return 0; } ++gRemotePodCount; } DEBUG_STREAM << "[marshal] network mission, " << gRemotePodCount << " remote pod(s) connected\n" << std::flush; ArmClock(mission_seconds); DEBUG_STREAM << "[marshal] installed network (length " << mission_seconds << "s)\n" << std::flush; return 1; } int BTLocalConsole_MissionCompleted() { return (gPhase == PhaseStopped) ? 1 : 0; } int BTLocalConsole_ResultCount() { return gResultCount; } int BTLocalConsole_GetResult(int index, int *kills, int *deaths) { if (index < 0 || index >= gResultCount) return 0; if (kills) *kills = gResults[index].kills; if (deaths) *deaths = gResults[index].deaths; return 1; } const char *BTLocalConsole_GetResultName(int index) { if (index < 0 || index >= gResultCount) return ""; return gResults[index].name; } // The lobby rebuilds the sheet from wire data (owner collates every pod's // scores) before showing the shared results, so it needs to clear + refill. void BTLocalConsole_ClearResults() { gResultCount = 0; } void BTLocalConsole_InjectResult(int slot, int kills, int deaths, const char *name) { if (slot < 0 || slot >= maxResults) return; gResults[slot].kills = kills; gResults[slot].deaths = deaths; gResults[slot].name[0] = 0; if (name) { strncpy(gResults[slot].name, name, sizeof(gResults[slot].name) - 1); gResults[slot].name[sizeof(gResults[slot].name) - 1] = 0; } if (slot >= gResultCount) gResultCount = slot + 1; } void BTLocalConsole_Uninstall() { gPerFrameHook = 0; gPhase = PhaseIdle; if (gNetworkMission) DisconnectRemotes(); gNetworkMission = False; gConsoleMarshalsLaunch = False; if (gEggWire) { delete[] gEggWire; gEggWire = 0; gEggWireSize = 0; } }