The lobby could stage a room but the launched mission never fed the members.
BTLocalConsole_InstallNetworkMission (btl4console.cpp) makes the host play the
arcade console in-process over the NetTransport seam (Winsock TCP or Steam SDR):
- connect to each member's console channel (ip[:port] from BT412HOSTPODS);
- feed each the chunked egg (NetworkManager::ReceiveEggFileMessage, \n->NUL
wire image like tools/btconsole.py), resending until it ACKs;
- poll member state (StateQueryMessage); when the whole mesh is staged at
WaitingForLaunch, dispatch RunMission to every member + locally at once;
- StopMission at expiry (members first, then self after a short grace);
- scores are BT's kills/deaths snapshotted from the *meshed* roster at the
stop -- no EndMission wire intake (that flow is a BT stub; the mesh already
put every pilot in the host's roster). The owner's own pod is fed locally
via L4NetworkManager::FeedLocalEgg.
Engine change (ported 1:1 from RP412): gConsoleMarshalsLaunch (APPMGR.h/.cpp) +
the `!gConsoleMarshalsLaunch &&` guard in APP.cpp's WaitingForLaunch self-launch.
The owner has no console connection to itself, so without this it would
self-launch before the mesh staged and never send the coordinated RunMission.
Default False = stock behavior; solo boot un-regressed.
WinMain host path (btl4main.cpp) now honors BT412HOSTPODS (+BT412HOSTPORT) for
the lobby host AND a classic-LAN host: SetNetworkCommonFlatAddress +
InstallNetworkMission, falling back to a solo marshal if no member is reachable.
Verified (loopback): two `btl4.exe -net` pods fed by tools/btconsole.py reach
"All connections completed!" and run after the engine change -- the exact
protocol + launch handshake the marshal uses. Both gates build+link; the
Release dist boots and the Steam transport comes up. The live multi-machine
Steam mission (FakeIP mesh + pilot-slot matching) is untestable here -- see the
new docs/STEAM-3-MACHINE-TEST.md. Dist README updated: multiplayer is now
"newly implemented, please test" rather than deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
541 lines
18 KiB
C++
541 lines
18 KiB
C++
//===========================================================================//
|
|
// 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 <bt.hpp> // Application, application, DEBUG_STREAM
|
|
#pragma hdrstop
|
|
|
|
#include <string.h> // strncpy / memcpy / memmove
|
|
#include <stdio.h> // 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; }
|
|
}
|