Net: host->member mission marshal -- a Steam/LAN mission plays end to end
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>
This commit is contained in:
@@ -178,12 +178,31 @@ Verified after merge: clean build; solo front-end mode; loopback MP through the
|
||||
`[pilots]` (mesh `FakeIP:1502` addresses, round-robin drop zones) via
|
||||
`BTFrontEnd_SetHostedPilots`; a member enters as a network pod on `:1501`
|
||||
(`SetNetworkCommonFlatAddress` + `gConsoleLossEndsMission=True`).
|
||||
**Deferred / not built:** the host→member **wire egg-feed marshal** (`InstallNetworkMission` —
|
||||
the owner streaming eggs + RunMission to remote pods, as `tools/btconsole.py` does). Without
|
||||
it a member waits for a console connection nothing supplies, so a live host+member mission
|
||||
is blocked on it. The lobby object does NOT survive the per-mission relaunch (the
|
||||
single-binary limitation — everyone returns to a fresh menu, not the room).
|
||||
`docs/STEAM-3-MACHINE-TEST.md` for BT is not yet authored.
|
||||
**Host→member wire egg-feed marshal — DONE (2026-07-17):**
|
||||
`BTLocalConsole_InstallNetworkMission` (btl4console.cpp) makes the owner play the arcade console
|
||||
in-process over the NetTransport seam (Winsock TCP or Steam SDR): connect to each member's
|
||||
console channel, feed the chunked egg (`NetworkManager::ReceiveEggFileMessage`, \n→NUL wire image
|
||||
like `tools/btconsole.py`) retry-until-ACK, poll state (`StateQueryMessage`), and when the whole
|
||||
mesh is staged at `WaitingForLaunch` dispatch `RunMission` to all + locally at once; `StopMission`
|
||||
at expiry. The owner's own pod is fed via `L4NetworkManager::FeedLocalEgg`. Scores are BT's
|
||||
kills/deaths snapshotted from the (meshed) roster at the stop — NO EndMission wire intake (BT's
|
||||
mission-end flow is a stub; the mesh already put every pilot in the host's roster).
|
||||
- **Engine change (ported from RP412):** `gConsoleMarshalsLaunch` (APPMGR.h/.cpp) + the
|
||||
`!gConsoleMarshalsLaunch &&` guard in APP.cpp's WaitingForLaunch self-launch — holds the owner
|
||||
(which has no console connection to itself) at WaitingForLaunch so the in-process console
|
||||
launches the mesh together instead of the owner self-launching early. Default False = stock
|
||||
behavior; solo boot un-regressed.
|
||||
- **WinMain host path** honors `BT412HOSTPODS` (+`BT412HOSTPORT`) for the lobby host AND a classic
|
||||
LAN host: `SetNetworkCommonFlatAddress` + `InstallNetworkMission(secs, "frontend.egg", pods)`,
|
||||
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 (FakeIP allocated).
|
||||
- **Remaining:** the live multi-machine Steam mission (FakeIP mesh + pilot-slot matching across
|
||||
machines) — untestable here (needs Steam + ≥2 accounts/machines). Procedure:
|
||||
`docs/STEAM-3-MACHINE-TEST.md`. The lobby object does NOT survive the per-mission relaunch
|
||||
(single-binary limitation — everyone returns to a fresh menu, not the room).
|
||||
- Phase 7: packaging (`pack-dist.ps1`), KeyLight, release — remain.
|
||||
|
||||
## The seams (what plugs in where) [T0 unless noted]
|
||||
|
||||
+18
-15
@@ -178,21 +178,24 @@ identical-to-baseline → take RP412's file verbatim, diverged → hand-apply th
|
||||
kills/deaths → stop → results everywhere → rematch. Known MP fidelity gaps (peer
|
||||
warp spheres, missile clusters, PNAME/PLACE billboards) do not block.
|
||||
|
||||
> **Status (2026-07-16): CODE LANDED; live 3-machine test DEFERRED (no Steam + multi-machine
|
||||
> here).** `game/reconstructed/btl4lobby.*` ships (compiled both gates: stubs without
|
||||
> `BT412_STEAM`, full ISteamMatchmaking room under it). Built: the room screen (green-on-black,
|
||||
> like the menu), member data exchange (FakeIP + fake console/game ports + persona +
|
||||
> mech/color/badge), the nonced "go" launch roster → `SteamNetTransport_RegisterPeer` all peers,
|
||||
> and Push/PullMissionResults over lobby data. The menu shows HOST/JOIN when `BTLobby_Available()`
|
||||
> (Steam transport up); a launch routes through `BTFrontEnd_LastLaunchMode()`: **host** builds the
|
||||
> egg with every member as a `[pilots]` entry + owns the marshal clock, **member** enters as a
|
||||
> network pod on `:1501` (`SetNetworkCommonFlatAddress` + `gConsoleLossEndsMission`). WinMain
|
||||
> installs the Steam transport on `BT412STEAM` env. **Remaining / deferred:** the host→member
|
||||
> **wire egg-feed marshal** (`InstallNetworkMission` — the owner streaming eggs to remote pods) is
|
||||
> NOT built; a member currently waits for a console connection that only that marshal provides, so
|
||||
> a live host+member mission needs it. The lobby OBJECT does not survive the per-mission relaunch
|
||||
> (documented single-binary limitation — everyone lands on a fresh menu, not back in the room).
|
||||
> `docs/STEAM-3-MACHINE-TEST.md` for BT is not yet authored. Details in `context/steamification.md`.
|
||||
> **Status (2026-07-17): CODE COMPLETE; live multi-machine test remains (no Steam + multi-machine
|
||||
> here).** `game/reconstructed/btl4lobby.*` ships (both gates: stubs without `BT412_STEAM`, full
|
||||
> ISteamMatchmaking room under it). Built: the room screen, member data exchange (FakeIP + fake
|
||||
> console/game ports + persona + mech/color/badge), the nonced "go" launch roster →
|
||||
> `SteamNetTransport_RegisterPeer` all peers, and Push/PullMissionResults over lobby data. The menu
|
||||
> shows HOST/JOIN when `BTLobby_Available()`; a launch routes through `BTFrontEnd_LastLaunchMode()`.
|
||||
> **The host→member wire egg-feed marshal is now DONE** (`BTLocalConsole_InstallNetworkMission`,
|
||||
> btl4console.cpp): the owner connects to each member's console channel over the NetTransport seam,
|
||||
> feeds each the chunked egg (ACK-tracked), holds the whole mesh at WaitingForLaunch via the ported
|
||||
> engine flag `gConsoleMarshalsLaunch` (APP.cpp/APPMGR), launches everyone together, and stops at
|
||||
> expiry; scores are BT's kills/deaths snapshotted from the meshed roster (no EndMission wire
|
||||
> intake — that flow is a stub). WinMain's host path (lobby OR classic-LAN `BT412HOSTPODS`) calls it;
|
||||
> member enters as a network pod on `:1501`. **Verified:** loopback — two `-net` pods fed by
|
||||
> `tools/btconsole.py` mesh + run after the `gConsoleMarshalsLaunch` change (the exact protocol +
|
||||
> launch handshake the in-process marshal uses); both gates build+link; Release dist boots + Steam
|
||||
> transport comes up. **Remaining:** the live multi-machine Steam mission (FakeIP mesh + pilot-slot
|
||||
> matching across machines) — `docs/STEAM-3-MACHINE-TEST.md` is the procedure. The lobby OBJECT does
|
||||
> not survive the per-mission relaunch (single-binary limitation). Details in `context/steamification.md`.
|
||||
|
||||
## Phase 7 — Polish, packaging, release
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# Steam multiplayer — multi-machine test procedure (BattleTech 4.12)
|
||||
|
||||
The full end-to-end: two or three machines, that many Steam accounts, one
|
||||
lobby, one marshaled mission over Steam Datagram Relay (SDR). Adapted from
|
||||
RP412's procedure for BattleTech's kills/deaths model.
|
||||
|
||||
> **Status:** the host→member mission feed (`BTLocalConsole_InstallNetworkMission`)
|
||||
> is implemented. The console protocol + launch handshake it uses are
|
||||
> **loopback-verified** (two `-net` pods fed by `tools/btconsole.py` mesh and run
|
||||
> after the engine's `gConsoleMarshalsLaunch` launch-hold change). The full
|
||||
> multi-machine Steam mesh has **not** been lab-tested — this procedure IS that
|
||||
> test. If a step diverges, collect the logs (bottom of this doc).
|
||||
|
||||
## Per machine (all of them)
|
||||
|
||||
1. Steam client installed, logged in (a **different account per machine**), and
|
||||
RUNNING.
|
||||
2. Copy the `dist\` folder onto the machine. (It already contains
|
||||
`steam_appid.txt` = `480` — Valve's Spacewar public dev AppID — and an
|
||||
`environ.ini` with `BT412STEAM=1`.)
|
||||
3. Run `start.bat`.
|
||||
|
||||
Sanity check per machine: `btl4.log` (beside the exe) should show
|
||||
`SteamNetTransport: up as 169.254.x.y (fake ports N console, M game)` and
|
||||
`[steam] transport up -- lobby available`. If it says "staying on TCP", Steam
|
||||
isn't running / not logged in / `steam_appid.txt` is missing.
|
||||
|
||||
## The test
|
||||
|
||||
1. **Machine A (host):** on the setup menu pick a map, mech, color, time,
|
||||
weather, length, and pilot name, then click **HOST STEAM MISSION**. The lobby
|
||||
room appears with A listed as `[HOST]`.
|
||||
2. **Machines B (and C):** pick loadouts on the setup menu FIRST (the lobby
|
||||
publishes them), then click **JOIN STEAM MISSION**. It joins the first open
|
||||
BT412 lobby it finds.
|
||||
3. When every member's name shows **bright** in A's room (bright = FakeIP +
|
||||
loadout published), A clicks **L A U N C H M I S S I O N**.
|
||||
4. All pods leave the room and boot the mission. Expect in the logs:
|
||||
- host A: `[marshal] network mission, N remote pod(s) connected`
|
||||
- host A: `[marshal] connecting to pod 169.254...` then `[marshal] egg sent
|
||||
to 169.254... (K chunks)` and `[marshal] <addr> EGG ACK (mesh complete)`
|
||||
per member
|
||||
- host A: `[marshal] all pods staged -- RUN`
|
||||
- every pod: `All connections completed!` then the mission runs
|
||||
5. The mission runs for the length A picked; at expiry A's marshal snapshots the
|
||||
roster, stops every member, then stops itself
|
||||
(`[marshal] time expired -- stopping remote pods`). A gets the MISSION
|
||||
COMPLETE results (kills/deaths); the sheet is published to the lobby so B/C
|
||||
show the same board.
|
||||
6. Everyone lands back on a fresh setup menu (the single-binary relaunch — the
|
||||
lobby ROOM does not persist across the relaunch in this build; re-HOST/JOIN
|
||||
for the next mission).
|
||||
|
||||
## Addressing (how the mesh is wired)
|
||||
|
||||
- Every pod uses engine ports **1501 console / 1502 game** by convention under
|
||||
Steam; the transport maps them to the Steam-assigned fake ports per peer.
|
||||
- The lobby exchanges each member's FakeIP + fake console/game ports live
|
||||
through lobby member data (`ip`/`cp`/`gp`); nothing is configured by hand.
|
||||
- On LAUNCH the host writes the nonced `go` roster into lobby data; every pod
|
||||
calls `SteamNetTransport_RegisterPeer` for all peers before dialing out.
|
||||
- The host bakes every member into the mission egg's `[pilots]` list
|
||||
(`<fakeip>:1502`), so each pod finds its own slot in the shared mission.
|
||||
|
||||
## Controllers: disable Steam Input (once per machine)
|
||||
|
||||
Steam Input intercepts Xbox controllers for AppID 480 the moment the game
|
||||
initializes SteamAPI, hiding them from XInput — the pad works everywhere else
|
||||
and is invisible only in-game. Fix, either scope:
|
||||
|
||||
- Per game: Steam Library → Spacewar → Properties → Controller → Override:
|
||||
**Disable Steam Input**.
|
||||
- Global: Steam → Settings → Controller → uncheck **Enable Steam Input for Xbox
|
||||
controllers**.
|
||||
|
||||
PadRIO re-probes every ~3 s, so the pad hot-connects the moment Steam releases
|
||||
it — no restart needed. (BT412's own AppID will set this server-side; test
|
||||
machines need the client setting.)
|
||||
|
||||
## Notes and knowns
|
||||
|
||||
- Two instances under ONE Steam account cannot mesh (one identity per account) —
|
||||
hence separate machines/accounts.
|
||||
- If a member joins after A already launched once, it answers the NEXT launch
|
||||
only (launch signals are nonced).
|
||||
- Same-LAN machines may get a direct SDR route; different networks relay. Both
|
||||
are fine.
|
||||
- Known MP fidelity gaps carried from single-player (peer warp spheres, missile
|
||||
clusters, PNAME/PLACE billboards) do not block the connectivity test — log
|
||||
observations in `context/multiplayer.md`.
|
||||
|
||||
## No-Steam fallback (proven, for isolating a Steam-specific problem)
|
||||
|
||||
Set `BT412STEAM=0` and drive the pods with the external console emulator over
|
||||
TCP (needs Python 3):
|
||||
```
|
||||
Machine A: btl4.exe -egg MP.EGG -net 1501
|
||||
Machine B: btl4.exe -egg MP.EGG -net 1601
|
||||
Feeder: python tools\btconsole.py MP.EGG <A_ip>:1501 <B_ip>:1601
|
||||
```
|
||||
If this works but the Steam path does not, the problem is in the Steam transport
|
||||
/ lobby layer, not the game mesh.
|
||||
|
||||
## What to collect if something breaks
|
||||
|
||||
`btl4.log` from each machine (each dist folder) — grep `[steam]`, `[frontend]`,
|
||||
`[marshal]`, `net-tx` / `net-rx` — plus which step above diverged.
|
||||
+11
-4
@@ -1386,15 +1386,22 @@ void
|
||||
|
||||
console_host = GetHostManager()->GetConsoleHost();
|
||||
if (
|
||||
(console_host == NULL) ||
|
||||
!gConsoleMarshalsLaunch &&
|
||||
(
|
||||
console_host != NULL &&
|
||||
console_host->GetConnectStatus() != Host::OnLineConnectionStatus
|
||||
(console_host == NULL) ||
|
||||
(
|
||||
console_host != NULL &&
|
||||
console_host->GetConnectStatus() != Host::OnLineConnectionStatus
|
||||
)
|
||||
)
|
||||
)
|
||||
{
|
||||
//
|
||||
// In the absence of the console just post the message to run
|
||||
// In the absence of the console just post the message to run.
|
||||
// An IN-PROCESS console (hosted network mission) has no
|
||||
// connection to this pod but still owns the launch -- the
|
||||
// gConsoleMarshalsLaunch flag holds us at WaitingForLaunch
|
||||
// until every pod in the mesh is staged (BT412).
|
||||
//
|
||||
RunMissionMessage run_mission_message;
|
||||
Post(DefaultEventPriority, this, &run_mission_message);
|
||||
|
||||
@@ -17,6 +17,11 @@ Logical gConsoleLossEndsMission = False;
|
||||
// own the mission clock). NULL when unregistered.
|
||||
void (*gPerFrameHook)() = NULL;
|
||||
|
||||
// BT412: the in-process console owns the launch of a hosted network mission --
|
||||
// when True, a staged pod stays at WaitingForLaunch instead of self-launching,
|
||||
// so the console can launch the whole mesh at once (APP.cpp). Default False.
|
||||
Logical gConsoleMarshalsLaunch = False;
|
||||
|
||||
ApplicationManager* ApplicationManager::CurrentAppManager = NULL;
|
||||
|
||||
ApplicationManager::ApplicationManager(HINSTANCE hInstance, HWND hWnd, Scalar frame_rate) : Node(ApplicationManagerClassID), runningApplications(this)
|
||||
|
||||
@@ -12,6 +12,13 @@ extern Logical gConsoleLossEndsMission;
|
||||
// BT412: per-frame observer hook (the LocalConsole marshal's game-thread tick).
|
||||
extern void (*gPerFrameHook)();
|
||||
|
||||
// BT412: when True, a pod at WaitingForLaunch does NOT self-launch even with no
|
||||
// connected console -- the IN-PROCESS console (hosted network mission) owns the
|
||||
// launch and holds every pod at WaitingForLaunch until the whole mesh is staged,
|
||||
// then dispatches RunMission everywhere at once. See APP.cpp. (Ported from
|
||||
// RP412's engine mod; default False = the stock self-launch behavior.)
|
||||
extern Logical gConsoleMarshalsLaunch;
|
||||
|
||||
class ApplicationManager : public Node
|
||||
{
|
||||
public:
|
||||
|
||||
+35
-7
@@ -548,6 +548,8 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
extern int BTFrontEnd_LastLaunchMode();
|
||||
extern void BTFrontEnd_ShowResults();
|
||||
extern void BTLocalConsole_Install(int mission_seconds);
|
||||
extern int BTLocalConsole_InstallNetworkMission(int mission_seconds,
|
||||
const char *egg_path, const char *remote_pod_list);
|
||||
extern int BTLocalConsole_MissionCompleted();
|
||||
extern void BTLobby_PushMissionResults();
|
||||
extern void BTLobby_PullMissionResults();
|
||||
@@ -580,16 +582,42 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
std::cout << "[frontend] lobby MEMBER -> network pod on :1501"
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
else if (getenv("BT412HOSTPODS") != 0 && getenv("BT412HOSTPODS")[0] != '\0')
|
||||
{
|
||||
// HOST (lobby OR classic LAN): mesh on the session port AND play the
|
||||
// console for the member pods over the wire. BT412HOSTPODS is their
|
||||
// console channels (ip[:port]); the lobby primes it, or a LAN host
|
||||
// sets it in environ.ini. BT412HOSTPORT overrides this pod's port
|
||||
// (default 1501). The marshal connects, feeds each member its egg,
|
||||
// launches the mesh together, and stops at expiry.
|
||||
const char *host_pods = getenv("BT412HOSTPODS");
|
||||
int host_port = 1501;
|
||||
const char *port_override = getenv("BT412HOSTPORT");
|
||||
if (port_override != 0 && atoi(port_override) > 0)
|
||||
host_port = atoi(port_override);
|
||||
L4Application::SetNetworkCommonFlatAddress(host_port);
|
||||
if (BTLocalConsole_InstallNetworkMission(secs, "frontend.egg", host_pods))
|
||||
{
|
||||
std::cout << "[frontend] HOST -> network mission (" << secs
|
||||
<< "s) on :" << host_port << ", pods=" << host_pods
|
||||
<< std::endl << std::flush;
|
||||
}
|
||||
else
|
||||
{
|
||||
// no reachable members: fall back to a solo mission on this pod
|
||||
L4Application::SetNetworkCommonFlatAddress(0);
|
||||
BTLocalConsole_Install(secs);
|
||||
std::cout << "[frontend] HOST setup failed -> solo ("
|
||||
<< secs << "s)" << std::endl << std::flush;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// SOLO or LOBBY HOST: the egg was built locally and the in-process
|
||||
// marshal owns the mission clock (the host is also the console). A
|
||||
// host additionally joins the mesh on the session port.
|
||||
if (launch_mode == 1)
|
||||
L4Application::SetNetworkCommonFlatAddress(1501);
|
||||
// SOLO: the egg was built locally; the in-process marshal owns the
|
||||
// mission clock and stops it at the chosen length.
|
||||
BTLocalConsole_Install(secs);
|
||||
std::cout << "[frontend] menu LAUNCH (" << (launch_mode == 1 ? "host" : "solo")
|
||||
<< ") -> marshal armed (" << secs << "s)" << std::endl << std::flush;
|
||||
std::cout << "[frontend] menu LAUNCH (solo) -> marshal armed ("
|
||||
<< secs << "s)" << std::endl << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
#include "../../engine/MUNGA_L4/L4NETTRANSPORT.h"
|
||||
@@ -1,18 +1,35 @@
|
||||
//===========================================================================//
|
||||
// btl4console.cpp -- BT412 in-process LocalConsole marshal (solo path).
|
||||
// btl4console.cpp -- BT412 in-process LocalConsole marshal.
|
||||
//
|
||||
// See btl4console.hpp. Models RP412's RPL4CONSOLE ConsoleTick, reduced to the
|
||||
// single-player case: no remote pods, no wire -- just own the mission clock and
|
||||
// dispatch StopMissionMessage at expiry. The tick runs on the game thread
|
||||
// (gPerFrameHook), so the engine call (Dispatch) is safe.
|
||||
// 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 (result names from the wire)
|
||||
#include <string.h> // strncpy / memcpy / memmove
|
||||
#include <stdio.h> // fopen / fread (egg wire image)
|
||||
|
||||
#include "appmsg.hpp" // full Application::StopMissionMessage definition
|
||||
#include "appmgr.hpp" // gPerFrameHook
|
||||
#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):
|
||||
@@ -53,6 +70,181 @@ namespace
|
||||
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).
|
||||
//
|
||||
@@ -60,13 +252,52 @@ namespace
|
||||
{
|
||||
if (application == 0)
|
||||
return;
|
||||
if (gPhase != PhaseWaiting && application != gWatched)
|
||||
return; // only marshal our own mission
|
||||
|
||||
int state = application->GetApplicationState();
|
||||
|
||||
switch (gPhase)
|
||||
{
|
||||
case PhaseWaiting:
|
||||
// The mission has staged and started running -> start the clock.
|
||||
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;
|
||||
@@ -78,26 +309,54 @@ namespace
|
||||
break;
|
||||
|
||||
case PhaseRunning:
|
||||
if (application != gWatched)
|
||||
return; // only marshal our own mission
|
||||
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))
|
||||
{
|
||||
// The console clock expired: snapshot the final scores (the
|
||||
// game state is still live here), then stop the mission exactly
|
||||
// as the arcade console did (StopMissionMessage on our pod).
|
||||
DEBUG_STREAM << "[marshal] time expired -- stopping mission\n" << std::flush;
|
||||
SnapshotResults();
|
||||
Application::StopMissionMessage message(0);
|
||||
application->Dispatch(&message);
|
||||
gPhase = PhaseStopped;
|
||||
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;
|
||||
|
||||
@@ -105,21 +364,124 @@ namespace
|
||||
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)
|
||||
{
|
||||
gMissionSeconds = mission_seconds;
|
||||
gPhase = PhaseWaiting;
|
||||
gStartTick = 0;
|
||||
gWatched = 0;
|
||||
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;
|
||||
gPerFrameHook = &ConsoleTick;
|
||||
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;
|
||||
@@ -170,4 +532,9 @@ void BTLocalConsole_Uninstall()
|
||||
{
|
||||
gPerFrameHook = 0;
|
||||
gPhase = PhaseIdle;
|
||||
if (gNetworkMission)
|
||||
DisconnectRemotes();
|
||||
gNetworkMission = False;
|
||||
gConsoleMarshalsLaunch = False;
|
||||
if (gEggWire) { delete[] gEggWire; gEggWire = 0; gEggWireSize = 0; }
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
// per-frame hook.
|
||||
void BTLocalConsole_Install(int mission_seconds);
|
||||
|
||||
// Arm the marshal as the HOST/OWNER of a NETWORKED mission: connect to every
|
||||
// member pod's console channel (comma-separated "ip[:port]" list), then act as
|
||||
// the arcade console -- feed each its egg, hold the mesh at WaitingForLaunch,
|
||||
// launch everyone together, and StopMission at expiry. The owner's own pod is
|
||||
// fed `egg_path` locally. Returns 1 on success, 0 if the egg is unreadable or
|
||||
// a member could not be reached (caller should fall back to a solo mission).
|
||||
int BTLocalConsole_InstallNetworkMission(int mission_seconds,
|
||||
const char *egg_path, const char *remote_pod_list);
|
||||
|
||||
// True once the marshaled mission has stopped (time expired, or ended some
|
||||
// other way while armed) -- the WinMain loop cycles back to the menu.
|
||||
int BTLocalConsole_MissionCompleted();
|
||||
|
||||
+21
-17
@@ -189,25 +189,29 @@ Controls (XInput controller and/or keyboard) - rebindable in bindings.txt:
|
||||
|
||||
MULTIPLAYER - please read
|
||||
-------------------------
|
||||
This build is for testing the STEAM LOBBY. What works vs. what is still
|
||||
in progress:
|
||||
This build is for testing STEAM multiplayer end to end.
|
||||
|
||||
WORKS - Steam lobby room. With Steam running and BT412STEAM=1, the menu
|
||||
shows HOST STEAM MISSION and JOIN STEAM MISSION. HOST creates a room;
|
||||
JOIN finds and enters one. Everyone in the room sees the member list
|
||||
(name, mech, color) and exchanges their FakeIP + loadout. The host
|
||||
presses LAUNCH MISSION to fire the roster. -> Test: two machines, each
|
||||
signed into a DIFFERENT Steam account, both running this build; confirm
|
||||
they find each other, see the member list, and the launch fires.
|
||||
Steam lobby + networked mission. With Steam running and BT412STEAM=1, the
|
||||
menu shows HOST STEAM MISSION and JOIN STEAM MISSION. HOST creates a room;
|
||||
JOIN finds and enters one. Everyone in the room sees the member list (name,
|
||||
mech, color) and exchanges their FakeIP + loadout. The host presses LAUNCH
|
||||
MISSION: it then acts as the in-process console -- it feeds every member the
|
||||
mission over the wire, holds the whole mesh until all are staged, launches
|
||||
them together, and stops the mission at the chosen length. The member pods
|
||||
mesh with the host and each other; kills/deaths are collected and the same
|
||||
results screen shows on every machine.
|
||||
-> Test: two (or three) machines, each signed into a DIFFERENT Steam
|
||||
account, all running this build. Confirm they find each other in the
|
||||
room, the launch fires, everyone drops into the SAME mission and can
|
||||
see/fight each other, and results show everywhere.
|
||||
-> The host->member mission feed is NEWLY IMPLEMENTED. The console
|
||||
protocol + launch handshake are loopback-verified here, but the full
|
||||
multi-machine Steam mesh has not been lab-tested. If a member does not
|
||||
drop in, grab btl4.log from each machine (grep [marshal], [frontend],
|
||||
net-tx / net-rx) so it can be diagnosed.
|
||||
|
||||
NOT FINISHED - the networked mission itself. When the host launches, each
|
||||
member is put into "network pod" mode waiting for the host to stream the
|
||||
mission over the wire. That host-as-console streaming marshal is the one
|
||||
remaining piece, so a host+member mission does NOT fully connect and play
|
||||
yet. The lobby mechanics above are what to exercise in this build.
|
||||
|
||||
CLASSIC MULTIPLAYER (works today, no Steam): the proven path is the external
|
||||
console emulator feeding -net pods over TCP. Needs Python 3.
|
||||
CLASSIC MULTIPLAYER (no Steam, proven): the external console emulator feeding
|
||||
-net pods over TCP. Needs Python 3.
|
||||
Set BT412STEAM=0 in environ.ini, then:
|
||||
Machine A: btl4.exe -egg MP.EGG -net 1501
|
||||
Machine B: btl4.exe -egg MP.EGG -net 1601
|
||||
|
||||
Reference in New Issue
Block a user