diff --git a/MUNGA_L4/L4APP.H b/MUNGA_L4/L4APP.H index 2adfb6c..9d65613 100644 --- a/MUNGA_L4/L4APP.H +++ b/MUNGA_L4/L4APP.H @@ -81,6 +81,10 @@ public: static bool GetFullscreen() { return mFullscreen; } static Logical GetSeeSolids() { return seeSolids; } static unsigned long GetNetworkCommonFlatAddress() { return networkCommonFlatAddress; } + // The front end's multiplayer path turns network mode on at launch + // time (the owner pod meshes like any other pod); command-line -net + // keeps working exactly as before. + static void SetNetworkCommonFlatAddress(unsigned long address) { networkCommonFlatAddress = address; } 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; } diff --git a/MUNGA_L4/L4NET.CPP b/MUNGA_L4/L4NET.CPP index 26640ba..2d1c5ef 100644 --- a/MUNGA_L4/L4NET.CPP +++ b/MUNGA_L4/L4NET.CPP @@ -378,6 +378,11 @@ void Register_Object(networkEggNotationFile); networkEggNotationFile->WriteFile("last.egg"); + // In network mode (the owner pod of a multiplayer race) the state + // gate must open or CheckBuffers keeps dropping mesh packets - a + // console-fed pod gets this from the chunked egg path instead. + currentNetworkState = NormalState; + ReceiveEggFileMessage egg_message(-1, 10, "local egg", 10); application->Post(DefaultEventPriority, this, &egg_message); } diff --git a/MUNGA_L4/L4NETTRANSPORT.cpp b/MUNGA_L4/L4NETTRANSPORT.cpp index 5c5d548..166d421 100644 --- a/MUNGA_L4/L4NETTRANSPORT.cpp +++ b/MUNGA_L4/L4NETTRANSPORT.cpp @@ -112,66 +112,72 @@ namespace << inet_ntoa(remote->sin_addr) << ":" << ntohs(remote->sin_port) << "...\n" << std::flush; - SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (sock == INVALID_SOCKET) + // + // Retry-while-refused, bounded: the egg-ACK ordering means + // the peer may not be listening yet. A refused TCP socket + // is dead - every attempt needs a fresh one. + // + DWORD deadline = GetTickCount() + 120 * 1000; + for (;;) { - DEBUG_STREAM << "ERROR: socket() failed with " - << WSAGetLastError() << "!\n"; - return InvalidConnection; - } - - bool reuse_address = true; - if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, - (char *) &reuse_address, sizeof(bool))) - { - DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with " - << WSAGetLastError() << "!\n" << std::flush; - closesocket(sock); - return InvalidConnection; - } - - // bind the game port: the mesh identifies peers by - // address AND port, so the source port must be ours - sockaddr_in local_endpoint; - memset(&local_endpoint, 0, sizeof(local_endpoint)); - local_endpoint.sin_family = AF_INET; - local_endpoint.sin_port = htons((unsigned short) local_port); - local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY; - if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint))) - { - DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with " - << WSAGetLastError() << "!\n" << std::flush; - closesocket(sock); - return InvalidConnection; - } - - int wsa_error = 0, result; - do - { - result = connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)); - if (result != 0) + SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (sock == INVALID_SOCKET) { - wsa_error = WSAGetLastError(); + DEBUG_STREAM << "ERROR: socket() failed with " + << WSAGetLastError() << "!\n"; + return InvalidConnection; } - } while (result != 0 && wsa_error == WSAECONNREFUSED); - if (result != 0) - { - DEBUG_STREAM << "ERROR: connect() failed with " - << wsa_error << "!\n" << std::flush; - closesocket(sock); - return InvalidConnection; - } + bool reuse_address = true; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + (char *) &reuse_address, sizeof(bool))) + { + DEBUG_STREAM << "ERROR: Could not set SO_REUSEADDR on socket; setsockopt() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } - unsigned long enable = 1; - if (ioctlsocket(sock, FIONBIO, &enable)) - { - DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with " - << WSAGetLastError() << "!\n" << std::flush; + // bind the game port: the mesh identifies peers by + // address AND port, so the source port must be ours + // (port 0 = ephemeral, for console channels) + sockaddr_in local_endpoint; + memset(&local_endpoint, 0, sizeof(local_endpoint)); + local_endpoint.sin_family = AF_INET; + local_endpoint.sin_port = htons((unsigned short) local_port); + local_endpoint.sin_addr.S_un.S_addr = INADDR_ANY; + if (bind(sock, (sockaddr *) &local_endpoint, sizeof(local_endpoint))) + { + DEBUG_STREAM << "ERROR: Could not bind local socket; bind() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + + if (connect(sock, (sockaddr *) remote, sizeof(SOCKADDR_IN)) == 0) + { + unsigned long enable = 1; + if (ioctlsocket(sock, FIONBIO, &enable)) + { + DEBUG_STREAM << "ERROR: Could not set actively opened socket to nonblocking; ioctlsocket() failed with " + << WSAGetLastError() << "!\n" << std::flush; + closesocket(sock); + return InvalidConnection; + } + return (Connection) sock; + } + + int wsa_error = WSAGetLastError(); closesocket(sock); - return InvalidConnection; + if (wsa_error != WSAECONNREFUSED || + (LONG)(GetTickCount() - deadline) >= 0) + { + DEBUG_STREAM << "ERROR: connect() failed with " + << wsa_error << "!\n" << std::flush; + return InvalidConnection; + } + Sleep(250); // peer not up yet - redial } - return (Connection) sock; } Connection diff --git a/RP_L4/RPL4.CPP b/RP_L4/RPL4.CPP index c07b502..e5634b3 100644 --- a/RP_L4/RPL4.CPP +++ b/RP_L4/RPL4.CPP @@ -256,7 +256,35 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine // Front-end games are marshaled by the in-process console: it // ends the race when the selected time expires. (Hand-fed -egg // runs stay unmarshaled - the developer shortcut.) - RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); + // + // RP412HOSTPODS turns the launch into a hosted network race: + // this pod switches to network mode (it meshes like any pod) + // and the console also marshals the listed member pods over + // the wire. (The Steam lobby will feed this same path.) + const char *host_pods = getenv("RP412HOSTPODS"); + if (host_pods != NULL && host_pods[0] != '\0') + { + int host_port = 1501; + const char *port_override = getenv("RP412HOSTPORT"); + if (port_override != NULL && atoi(port_override) > 0) + { + host_port = atoi(port_override); + } + L4Application::SetNetworkCommonFlatAddress(host_port); + if (!RPL4LocalConsole_InstallNetworkRace( + RPL4FrontEnd_LastMissionSeconds(), frontend_egg, + host_pods, RPL4FrontEnd_LastPilotNames())) + { + DEBUG_STREAM << "Network race setup failed - back to the menu\n" << std::flush; + L4Application::SetNetworkCommonFlatAddress(0); + continue; + } + } + else + { + L4Application::SetNetworkCommonFlatAddress(0); + RPL4LocalConsole_Install(RPL4FrontEnd_LastMissionSeconds()); + } } // diff --git a/RP_L4/RPL4CONSOLE.cpp b/RP_L4/RPL4CONSOLE.cpp index b95fdc2..3d302f4 100644 --- a/RP_L4/RPL4CONSOLE.cpp +++ b/RP_L4/RPL4CONSOLE.cpp @@ -5,9 +5,13 @@ #include "rpl4fe.h" #include "..\munga\appmgr.h" #include "..\munga\appmsg.h" +#include "..\munga\console.h" #include "..\rp\rpcnsl.h" #include "..\munga_l4\l4app.h" #include "..\munga_l4\l4net.h" +#include "..\munga_l4\l4nettransport.h" + +#define CONSOLE_NET_PORT 1501 // arcade default (matches L4NET.CPP) //######################################################################## // The local console runs on ITS OWN THREAD, like the real console: it @@ -18,8 +22,18 @@ // requested StopMissionMessage dispatch (the engine is single // threaded; cross-thread dispatch is not safe). // -// Results flow in through gConsoleScoreSink (RP layer): the same final -// scores RPPlayer sent the arcade console at mission end. +// NETWORK RACES (lobby owner as console): the same tick additionally +// marshals REMOTE pods over the NetTransport wire, speaking the exact +// arcade console protocol - egg chunks + ACK, state queries, +// RunMission when everyone reaches WaitingForLaunch, StopMission at +// expiry, EndMission score intake. The owner's own pod runs in +// network mode (it meshes like any pod) but is fed its egg locally +// and driven by direct engine calls, so the console never needs a +// connection to itself. +// +// Results flow in through gConsoleScoreSink (RP layer) for the local +// pod and EndMission wire messages for remote pods: the same final +// scores every pod sent the arcade console at mission end. //######################################################################## namespace @@ -54,6 +68,44 @@ namespace FinalScore gResults[maxResults]; int gResultCount = 0; + //--------------------------------------------------------------- + // Network race state: remote pods marshaled over the wire + //--------------------------------------------------------------- + enum { maxRemotePods = 8 }; + enum { remoteRxSize = 8192 }; + + struct RemotePod + { + char address[64]; // console channel, "ip[:port]" + NetTransport::Connection + connection; + int state; // last reported application state (-1 unknown) + Logical eggAcknowledged; + DWORD lastQueryTick; + DWORD eggSentTick; // 0 = never sent + Logical scored; + char rx[remoteRxSize]; // wire frame reassembly + int rxCount; + }; + + RemotePod gRemotePods[maxRemotePods]; + int gRemotePodCount = 0; + + Logical gNetworkRace = False; + char gEggPath[MAX_PATH] = ""; + char *gEggWire = NULL; // newline->NUL image for chunking + int gEggWireSize = 0; + Logical gLocalEggFed = False; + Logical gRunSent = False; + Logical gRemoteStopsSent = False; + DWORD gRemoteStopTick = 0; + + // pilot names in [pilots] order; host IDs start at FirstLegalHostID+1 + // (the console reserves the first), so host 2 = pilot index 0 + enum { firstPilotHostID = 2 }; + char gPilotNames[maxRemotePods + 1][32]; + int gPilotNameCount = 0; + //--------------------------------------------------------------- // The console thread: the mission clock lives here //--------------------------------------------------------------- @@ -77,8 +129,8 @@ namespace } //--------------------------------------------------------------- - // Final-score intake (called on the game thread from RPPlayer's - // mission-ending path, via the RP-layer sink) + // Final-score intake (game thread: the RP-layer sink for the + // local pod, the wire pump for remote pods) //--------------------------------------------------------------- void CollectFinalScore(int host_ID, int score) { @@ -92,6 +144,214 @@ namespace << " = " << score << "\n" << std::flush; } + //--------------------------------------------------------------- + // The wire: the arcade console protocol over NetTransport + //--------------------------------------------------------------- + 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 << "LocalConsole: 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 << "LocalConsole: 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; + if (pod->state != (int) message->GetApplicationState()) + { + DEBUG_STREAM << "LocalConsole: " << pod->address + << " state -> " << (int) message->GetApplicationState() + << "\n" << std::flush; + } + pod->state = (int) message->GetApplicationState(); + } + else if ((int) base->messageID == ConsoleApplicationEndMissionMessageID) + { + ConsoleApplicationEndMissionMessage *message = + (ConsoleApplicationEndMissionMessage *) base; + CollectFinalScore( + (int) message->GetPlayerHostID(), + (int) message->GetFinalScore()); + pod->scored = True; + } + // VTV telemetry (IDs 2-6) skips through for now + } + else if ((int) header->clientID == (int) NetworkClient::NetworkManagerClientID) + { + if ((int) base->messageID == (int) L4NetworkManager::AcknowledgeEggFileMessageID) + { + if (!pod->eggAcknowledged) + { + DEBUG_STREAM << "LocalConsole: " << 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: 5s retry 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; + } + + Logical AllRemotesScored() + { + for (int i = 0; i < gRemotePodCount; ++i) + { + if (!gRemotePods[i].scored) + { + 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; + } + } + } + + void DispatchLocalStop() + { + DEBUG_STREAM << "LocalConsole: stopping local pod\n" << std::flush; + InterlockedExchange(&gMissionRunning, 0); + Application::StopMissionMessage message(0); + application->Dispatch(&message); + gPhase = PhaseStopped; + } + //--------------------------------------------------------------- // The game-thread tick: state reporting + engine-safe execution //--------------------------------------------------------------- @@ -111,6 +371,46 @@ namespace switch (gPhase) { case PhaseWaiting: + if (gNetworkRace) + { + MarshalRemotes(); + + // + // Feed our own pod its egg locally: it meshes like any + // pod but the console drives it without a connection + // + if (!gLocalEggFed && state == Application::WaitingForEgg) + { + L4NetworkManager *network_manager = + (L4NetworkManager *) application->GetNetworkManager(); + if (network_manager != NULL) + { + network_manager->FeedLocalEgg(gEggPath); + gLocalEggFed = True; + DEBUG_STREAM << "LocalConsole: local egg fed\n" << std::flush; + } + } + + // + // Everyone staged: launch the race everywhere + // + if (!gRunSent && + state == Application::WaitingForLaunch && + AllRemotesInState(Application::WaitingForLaunch)) + { + DEBUG_STREAM << "LocalConsole: 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; + } + } + if (state == Application::RunningMission) { gPhase = PhaseRunning; @@ -125,24 +425,53 @@ namespace break; case PhaseRunning: + if (gNetworkRace) + { + // telemetry + final scores keep flowing during the race + for (int i = 0; i < gRemotePodCount; ++i) + { + PumpRemote(&gRemotePods[i]); + } + } + if (state != Application::RunningMission) { // mission ended some other way (pilot exit etc.) InterlockedExchange(&gMissionRunning, 0); gPhase = PhaseStopped; + DisconnectRemotes(); } else if (gStopRequested) { //----------------------------------------------------- // The console clock expired: end the race exactly the - // way the arcade console did, otherwise the mission - // clock rolls past 00:00 and counts up forever. + // way the arcade console did. Remote pods stop first; + // the local pod holds on briefly so their EndMission + // scores can land before our own teardown. //----------------------------------------------------- - DEBUG_STREAM << "LocalConsole: time expired - stopping mission\n" << std::flush; - InterlockedExchange(&gMissionRunning, 0); - Application::StopMissionMessage message(0); - application->Dispatch(&message); - gPhase = PhaseStopped; + if (!gNetworkRace) + { + DEBUG_STREAM << "LocalConsole: time expired - stopping mission\n" << std::flush; + DispatchLocalStop(); + } + else if (!gRemoteStopsSent) + { + DEBUG_STREAM << "LocalConsole: time expired - stopping remote pods\n" << std::flush; + 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 (AllRemotesScored() || + (LONG)(GetTickCount() - gRemoteStopTick) >= 5000) + { + DispatchLocalStop(); + DisconnectRemotes(); + } } break; @@ -154,41 +483,184 @@ namespace break; } } + + //--------------------------------------------------------------- + // Shared install plumbing + //--------------------------------------------------------------- + void InstallCommon(int mission_seconds) + { + // debug: L4CONSOLELEN overrides the mission length (test races) + const char *override_string = getenv("L4CONSOLELEN"); + if (override_string != NULL && atoi(override_string) > 0) + { + mission_seconds = atoi(override_string); + DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, " + << mission_seconds << "s\n" << std::flush; + } + + gMissionSeconds = mission_seconds; + InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000); + gPhase = PhaseWaiting; + gWatchedApp = NULL; + gRunSent = False; + gRemoteStopsSent = False; + gLocalEggFed = False; + + // game-thread execution point + gPerFrameHook = &ConsoleTick; + + // results intake from the RP layer + gConsoleScoreSink = &CollectFinalScore; + + // the console itself lives on its own thread, like the real one + if (gConsoleThread == NULL) + { + gConsoleThread = CreateThread( + NULL, 0, ConsoleThreadProc, NULL, 0, NULL); + } + + DEBUG_STREAM << "LocalConsole: installed (length " + << mission_seconds << "s, console thread " + << (gConsoleThread != NULL ? "up" : "FAILED") << ")\n" << std::flush; + } } void RPL4LocalConsole_Install(int mission_seconds) { - // debug: L4CONSOLELEN overrides the mission length (test races) - const char *override_string = getenv("L4CONSOLELEN"); - if (override_string != NULL && atoi(override_string) > 0) + gNetworkRace = False; + gRemotePodCount = 0; + gPilotNameCount = 0; + InstallCommon(mission_seconds); +} + +Logical + RPL4LocalConsole_InstallNetworkRace( + int mission_seconds, + const char *egg_path, + const char *remote_pod_list, + const char *pilot_names + ) +{ + gNetworkRace = True; + gRemotePodCount = 0; + gPilotNameCount = 0; + + strncpy(gEggPath, egg_path, sizeof(gEggPath) - 1); + gEggPath[sizeof(gEggPath) - 1] = '\0'; + + // + // The wire image of the egg: file newlines become NULs, exactly + // what the arcade console sent (RPMission.ToEggFileMessages) + // + if (gEggWire != NULL) { - mission_seconds = atoi(override_string); - DEBUG_STREAM << "LocalConsole: L4CONSOLELEN override, " - << mission_seconds << "s\n" << std::flush; + delete[] gEggWire; + gEggWire = NULL; + gEggWireSize = 0; + } + FILE *egg_file = fopen(egg_path, "rb"); + if (egg_file == NULL) + { + DEBUG_STREAM << "LocalConsole: cannot read egg " << egg_path << "\n" << std::flush; + return False; + } + fseek(egg_file, 0, SEEK_END); + long raw_size = ftell(egg_file); + fseek(egg_file, 0, SEEK_SET); + char *raw = new char[raw_size]; + fread(raw, 1, raw_size, egg_file); + fclose(egg_file); + + gEggWire = new char[raw_size]; + gEggWireSize = 0; + for (long b = 0; b < raw_size; ++b) + { + if (raw[b] == '\r') + { + continue; // \r\n collapses to one NUL + } + gEggWire[gEggWireSize++] = (raw[b] == '\n') ? '\0' : raw[b]; + } + delete[] raw; + + // + // Pilot names in [pilots] order (results screen labels) + // + if (pilot_names != NULL) + { + const char *cursor = pilot_names; + while (*cursor != '\0' && gPilotNameCount < maxRemotePods + 1) + { + int length = 0; + while (cursor[length] != '\0' && cursor[length] != ',' && + length < (int) sizeof(gPilotNames[0]) - 1) + { + gPilotNames[gPilotNameCount][length] = cursor[length]; + ++length; + } + gPilotNames[gPilotNameCount][length] = '\0'; + ++gPilotNameCount; + cursor += length; + if (*cursor == ',') + { + ++cursor; + } + } } - gMissionSeconds = mission_seconds; - InterlockedExchange(&gLengthMs, (LONG) mission_seconds * 1000); - gPhase = PhaseWaiting; - gWatchedApp = NULL; - - // game-thread execution point - gPerFrameHook = &ConsoleTick; - - // results intake from the RP layer - gConsoleScoreSink = &CollectFinalScore; - - // the console itself lives on its own thread, like the real one - if (gConsoleThread == NULL) + // + // Connect to every remote pod's console channel. Blocking with + // retry, like the arcade console redialing a pod that is still + // booting; runs before the engine block so nothing is waiting. + // + NetTransport_Get()->Startup(); + const char *cursor = remote_pod_list; + while (*cursor != '\0' && gRemotePodCount < maxRemotePods) { - gConsoleThread = CreateThread( - NULL, 0, ConsoleThreadProc, NULL, 0, NULL); + 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(CONSOLE_NET_PORT); + } + + DEBUG_STREAM << "LocalConsole: connecting to pod " << pod->address + << "...\n" << std::flush; + pod->connection = NetTransport_Get()->Connect(&console_address, 0); + if (pod->connection == NetTransport::InvalidConnection) + { + DEBUG_STREAM << "LocalConsole: could not reach pod " + << pod->address << "\n" << std::flush; + return False; + } + ++gRemotePodCount; } - DEBUG_STREAM << "LocalConsole: installed (length " - << mission_seconds << "s, console thread " - << (gConsoleThread != NULL ? "up" : "FAILED") << ")\n" << std::flush; + DEBUG_STREAM << "LocalConsole: network race, " << gRemotePodCount + << " remote pod(s) connected\n" << std::flush; + + InstallCommon(mission_seconds); + return True; } Logical @@ -214,3 +686,14 @@ Logical *score = gResults[index].score; return True; } + +const char * + RPL4LocalConsole_GetResultName(int host_ID) +{ + int index = host_ID - firstPilotHostID; + if (index < 0 || index >= gPilotNameCount) + { + return NULL; + } + return gPilotNames[index]; +} diff --git a/RP_L4/RPL4CONSOLE.h b/RP_L4/RPL4CONSOLE.h index 61de503..f936b42 100644 --- a/RP_L4/RPL4CONSOLE.h +++ b/RP_L4/RPL4CONSOLE.h @@ -26,6 +26,23 @@ void Logical RPL4LocalConsole_MissionCompleted(); +// Network race: the owner pod meshes like any pod (network mode, egg +// fed locally) while this console marshals the REMOTE pods over the +// NetTransport wire with the arcade protocol - egg chunks + ACK, +// state polling, RunMission when everyone stages, StopMission at +// expiry, EndMission score intake. remote_pod_list is a comma list of +// console channels ("ip[:port]", default CONSOLE_NET_PORT); +// pilot_names is a comma list in [pilots] order for the results +// screen. Connects synchronously (retry, like the arcade console +// redialing a booting pod); False when a pod cannot be reached. +Logical + RPL4LocalConsole_InstallNetworkRace( + int mission_seconds, + const char *egg_path, + const char *remote_pod_list, + const char *pilot_names + ); + // The last completed mission's final scores (what each pod reported at // mission end). Valid until the next mission starts running. int @@ -33,3 +50,8 @@ int Logical RPL4LocalConsole_GetResult(int index, int *host_ID, int *score); + +// Pilot name for a host ID ([pilots] order; host 2 = first pilot). +// NULL when unknown - the results screen falls back to pod numbers. +const char * + RPL4LocalConsole_GetResultName(int host_ID); diff --git a/RP_L4/RPL4FE.cpp b/RP_L4/RPL4FE.cpp index 67ee92d..2352f78 100644 --- a/RP_L4/RPL4FE.cpp +++ b/RP_L4/RPL4FE.cpp @@ -161,6 +161,87 @@ namespace 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]; + const char *vehicle; + const char *color; + }; + + int CollectExtraPilots(const FEState *fe, ExtraPilot *extras, char *owner_address) + { + 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); + pilot->vehicle = kVehicles[ + (fe->selection[GroupVehicle] + extra_count + 1) % FE_COUNT(kVehicles)].key; + pilot->color = kColors[ + (fe->selection[GroupColor] + extra_count + 1) % FE_COUNT(kColors)].key; + ++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); @@ -518,13 +599,23 @@ namespace extern const char *kOrdinalsBlock; //--------------------------------------------------------------- - // Egg assembly (console RPMission.ToEggString, race scenario, - // one local pilot) + // 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); @@ -544,10 +635,15 @@ namespace } egg += "[pilots]\n"; - sprintf(line, "pilot=%s\n", kLocalPilotAddress); + 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", kLocalPilotAddress); + sprintf(line, "[%s]\n", owner_address); egg += line; egg += "hostType=0\n"; egg += "dropzone=one\n"; @@ -562,22 +658,75 @@ namespace sprintf(line, "badge=%s\n", kBadges[fe->selection[GroupBadge]].key); egg += line; - sprintf(line, "[largebitmap]\nbitmap=BitMap::Large::%s\n", fe->pilotName); + 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; + egg += "badge=None\n"; + } + + egg += "[largebitmap]\n"; + sprintf(line, "bitmap=BitMap::Large::%s\n", fe->pilotName); egg += line; - sprintf(line, "[smallbitmap]\nbitmap=BitMap::Small::%s\n", fe->pilotName); + 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); + } + } } } @@ -744,6 +893,12 @@ int return gLastMissionSeconds; } +const char * + RPL4FrontEnd_LastPilotNames() +{ + return gLastPilotNamesCsv; +} + //######################################################################## //######################## RPL4FrontEnd_ShowResults ###################### //######################################################################## @@ -915,7 +1070,12 @@ Logical continue; } ResultRow *row = &rs.rows[rs.rowCount++]; - if (result_count == 1) + 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); } diff --git a/RP_L4/RPL4FE.h b/RP_L4/RPL4FE.h index baa9496..e1746bf 100644 --- a/RP_L4/RPL4FE.h +++ b/RP_L4/RPL4FE.h @@ -29,6 +29,11 @@ Logical int RPL4FrontEnd_LastMissionSeconds(); +// [pilots]-order pilot names of the last launched race (owner first), +// comma separated - labels for the network console's results intake. +const char * + RPL4FrontEnd_LastPilotNames(); + // The between-races results screen: shows the last mission's final // scores (from the local console) with a CONTINUE button. Returns // immediately when there are no results (first boot). False only if