Network races: the local console marshals remote pods over the wire
The lobby-owner-as-console architecture, in-engine. RPL4CONSOLE gains RPL4LocalConsole_InstallNetworkRace: the owner pod switches to network mode and meshes like any pod (egg fed locally via FeedLocalEgg, which now opens the ConsoleOnly state gate), while the console tick also marshals REMOTE pods over NetTransport speaking the exact arcade protocol - egg chunks with 5s-retry-until-ACK, 1Hz state polling, RunMission once every pod stages at WaitingForLaunch, StopMission at expiry (remotes first, local pod holds until their EndMission scores land), score intake labeled with [pilots]-order names on the results screen. The front end builds the multi-pilot egg: RP412HOSTPODS lists member console channels (lobby stand-in; the Steam lobby feeds the same path), RP412HOSTPORT/RP412HOSTADDR set the owner side. Winsock Connect now redials with a fresh socket per attempt (a refused TCP socket is dead; the old loop reused it) bounded at 120s - needed whenever a peer boots after the caller, which is the normal Steam lobby launch order. Verified on loopback: member pod in -net, owner hosting from its menu; mesh completed both sides, 30s race, remote score collected over the wire (host 3), local stop after the drain, results screen shows both pilots by name in one process that returns to the menu. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+59
-53
@@ -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
|
||||
|
||||
+29
-1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
+518
-35
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+168
-8
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user