15 KiB
Multiplayer Tier 0, run on the real game: two players, one guest, no server
- Type: subsystem (live verification)
- Status: verified — a two-player multiplayer game was started, joined and played on VM140 with no GameSpy service of any kind, and the whole run was captured outside the guest
- Confidence: high. Every claim below is a screenshot or a packet.
- Owner / date: lane W2 · 2026-09-08
- Tests:
findings/subsystems/multiplayer-gamespy.md§7 (lane G2's five predictions, written before any VM run) andsots-engine/docs/W2-predictions.md(this lane's restatement, committed before the run)
0. The headline
SOTS1 multiplayer is not dead, and restoring it needs no server. Two clients on one Windows guest, joined by typed IP, played two full turns in lockstep. Over the entire session the capture on the host side of the guest's NIC shows 44,319 packets and exactly zero to UDP 27900, UDP 27901, TCP 28910, TCP 28900, TCP 6667 or TCP 6500 — every GameSpy port the SDK in this binary knows about.
Lane G2's Tier 0 was the cheapest item on the board and it landed exactly as G2 read it out of the instruction stream. Four of its five predictions are confirmed; the fifth (Tier 3, the self-hosted master server) was deliberately not attempted and is reported as not-run.
And the game says so itself. The Message of the Day panel on the main menu is fetched live, on
every launch, from www.kerberos-productions.com/motd/motd_EN.txt — the server still answers today,
Last-Modified: Fri, 16 Jun 2017, served by LiteSpeed at 106.0.62.78. Its text, in full, is
Kerberos's own 2012 notice:
Attention SolForce personnel;
It is with wide, astonished eyes that we have to report that Gamespy was recently purchased by a third-party. This party then began shutting off game servers and informing developers and publishers that they would have to pay tens of thousdands of dollars a month in upkeep, in order to keep the servers on. This despite the fact that SotS1 used Gamespy thanks to a contracted and not-cheap agreement between Lighthouse Interactive and Gamespy.
We are looking into our options. In the meantime, you will not be able to play via Internet. However, you can still play the game via direct connect with your friends. Game hosts please note they have to host the game in LAN mode, not Internet mode, or direct connect will not work.
That is the this+0x2c == 1 && this+0x30 != 0 predicate G2 recovered from 0x007c2dd0, written by
the developer, and it is shipped inside the client we are testing. The fetch uses
User-Agent: GameSpyHTTP/1.0 — the ghttp SDK component — which is the one piece of GameSpy code on
any live path, and it talks to Kerberos, not to GameSpy.
1. What was run
VM140, GOG 1.8.1, single Win10 guest, hooks=off — the instrument was removed (method rule 19),
because the question is what the shipped game does, and because two processes sharing one trace file
is not a measurement. Capture: tcpdump -i tap140i0 on spicy, i.e. on the host side of the
guest's tap device. That is outside the guest, so it cannot perturb it, and it sees everything the
VM emits.
The blind spot, stated up front: loopback traffic between the two instances never reaches the tap, so this capture cannot show the game's own UDP 3369 exchange. That is the right trade — the predictions that matter are all about traffic that leaves, and the join working is evidence enough that the loopback path carried it. The socket table is the corroboration (§3).
Established before the run, and it is what makes §4 a real test: from the guest,
swordots.available.gamespy.com returns NXDOMAIN while www.google.com resolves. There is no
stale wildcard record at gamespy.com; the SOA in the negative answers is
ns-889.awsdns-47.net / awsdns-hostmaster.amazon.….
2. P5 — /concurrent — confirmed
Instance A launched normally. Instance B launched as
"C:\SOTS\Sword of the Stars.exe" /concurrent /join 127.0.0.1:3369
Both reached their own window and their own main loop: Get-Process showed two PIDs (4728 and
8780), and B's own stdout log records the full startup — VFS mount, D3D adapter, ver.1.8.1 Wed Dec 13 03:38:31 2017, profile switch — rather than the foreground-and-exit path. The
CreateMutexA / ERROR_ALREADY_EXISTS / argv-compare chain G2 read at 0x0089ddaf behaves exactly
as read. A second instance on one guest is a supported, shipped configuration, and the
two-client test needs one VM, not two.
3. P1 — direct join with zero GameSpy traffic — confirmed, both halves
Instance A: Host Multi-Player → Custom + LAN → OK → Custom Game Setup (2 players, 28
stars, defaults) → Create Game → game name MyGame → lobby.
At that moment Windows Defender Firewall raised its "has blocked some features of this app" prompt
for C:\sots\sword of the stars.exe — the host had just opened its listening socket. Allowed for
private networks. This is worth knowing for anyone reproducing the setup: the host, not the
joiner, triggers the firewall prompt, and it appears at Create Game, not at launch.
Socket table with the host in the lobby (Get-NetUDPEndpoint, filtered by PID):
| PID | role | UDP bindings |
|---|---|---|
| 4728 | host | 0.0.0.0:3369, 0.0.0.0:60924 |
| 8780 | joiner | 0.0.0.0:60925 |
Three facts fall out. The host binds HostPort 3369 exactly as Game_LoadNetworkConfig
defaults it (there is no [Network] section in sots.ini at all, so every value in G2 §3 is a
compiled-in default in this install). No TCP anywhere, confirming G2's import-table reading (no
listen, no accept). And CombatHostPort 3370 is not bound at lobby time — it must be opened
later, when a combat starts, which no lane has yet observed.
The joiner appeared in slot 2 of the host's lobby within ~50 s of launch. Both instances then showed two player cards, the client's view carrying Ready / Leave Game and the host's Launch / Cancel Game. Client pressed Ready; host pressed Launch.
The game started. Turn 1, two distinct human players — the host is Venkman, the client is
Hitomi, different portraits, different homeworlds, different treasuries. Each pressed End Turn;
the turn resolved and both advanced to Turn 2 (host Imperial Savings 11,200,596; client
11,299,146 — different players, same turn). A second round took both to Turn 3.
Capture over the whole session: zero packets to any GameSpy port; zero DNS queries for any
*.gamespy.com name during the hosting and joining sequence. The only names resolved in that window
were Windows telemetry.
This is the multiplayer revival, demonstrated end to end. Not "the code path exists" — a game was created, joined, launched and played.
4. P2 — the availability check fails open — confirmed
Fresh single instance, DNS cache flushed, Join Multi-Player → Internet → OK.
The Game Browser opened on the Internet tab with an empty Available Games list, the MOTD panel
below it, and no MATCHINGSERVICE_UNSUPPORTED dialog — no "Online support … is no longer
available", nothing. G2's fail-open reading of GSIStartAvailableCheck / GSIAvailableCheckThink
is correct on the running game.
Exactly one A query each, all answered NXDOMAIN, and no retry:
14:39:35.670 A? swordots.available.gamespy.com. -> NXDOMAIN
14:39:35.940 A? swordots.ms5.gamespy.com. -> NXDOMAIN
14:39:36.108 A? peerchat.gamespy.com. -> NXDOMAIN
Three refinements to G2 §7's P2. (a) One query, not one-or-two: the two-attempt/2000 ms retry at
0x0040a2bb is a socket retry, and it never runs because the socket was never created. (b)
The browser resolves the server-list and peerchat hostnames on the same screen entry, not
just the availability host — so a hosts-file redirect for Tier 1 must cover ms5 and peerchat
as well or those two will still fail. (c) swordots.master.gamespy.com and the two natneg
names were not resolved: they belong to the host/reporting path, not the join path.
Consequence for the revival plan: Tier 1 is confirmed unnecessary. G2 hoped finding it a no-op would be a good outcome; it is a no-op. Nothing has to answer the availability check for a client to reach the browser, the manual-join dialog, or a game.
5. G2 §7 caveat (a) — falsified
G2 flagged, honestly, that it had not read the Join Manually button's enable predicate and that
the button might be disabled without a browser object. It is not. On the Internet page with an empty
list and every GameSpy name dead, Join Manually is enabled, and it opens a dialog with Enter
Host's IP Address, Enter Password, a Favorites list, and Join/Cancel. The GameSpy-free arm G2
traced from Game_ManualJoin_OnAccept is reachable in the shipped UI in the state the world is
actually in.
6. P3 — LAN discovery with no server — confirmed, and my own prediction was wrong
I rated this lower than G2 did, and wrote down why: both instances are on one host, so a broadcast
to 255.255.255.255:3369 and the host's QR2 socket might contend for one UDP port. Written before
the run, and wrong. The socket table in §3 shows why: the client never binds 3369 — it
broadcasts from an ephemeral port (60925) — so there is no contention to have.
With A hosting in LAN mode, B's Join Multi-Player → LAN page listed the game on first open, with no manual Refresh:
| Game Name | Players | Ping | Password | Status | Version |
|---|---|---|---|---|---|
re : MyGame |
1/2 | 32 | No | Staging | 1.8.1 |
That row is the QR2 custom-key set of G2 §4.2 rendered by the client, answered by the host's own socket, with no master server in existence. Selecting the row and pressing Join also worked: B went straight into the lobby. So both join paths — manual address and browser-mediated — work GameSpy-free.
One thing G2 could not settle and this run does: the browser-mediated LAN join makes no NAT
negotiation attempt. No natneg1/natneg2 name was ever resolved and nothing was sent to UDP
27901. G2's reading that NatNeg hangs off the browser join is right about the code, but on the LAN
path (queryVersion = 1, lanBrowse = 1) it is not reached.
7. P4 — the self-hosted master server — not run
Deliberately. Tier 3 is a container deployment plus a DNS redirect; it is a lane's work on its own, it is not on the path to playing, and running it before Tier 0 was proved would have been the wrong order. Nothing here confirms or disconfirms the OpenSpy / UniSpyServer plan, and §4's finding that Tier 1 is unnecessary does not generalise to Tier 3 — the Internet list stays empty because nothing answers, and only a real SB v2 + QR2 backend can change that.
Reported as not-run rather than unknown: the experiment is fully specified in
multiplayer-gamespy.md §5 and §7, and the two extra hostnames in §4(b) above are a correction to
its hosts-file list.
8. What this run did NOT cover (rule 15)
Loudly, because the headline is a strong one and it was established on a narrow workload.
- One guest, loopback only. No two-machine LAN test, no VPN, no NAT, no port forwarding. The claim "two boxes on a LAN work" remains G2's inference plus Kerberos's MOTD; what is measured is two processes on one box over 127.0.0.1.
- No combat. Both turns resolved without a battle, so
CombatHostPort3370 was never bound and the combat lockstep — the part with a 1000 msCombatLatencyand its own sync checking — is entirely untested. This is the most likely place for the direct path to still be broken. - Two turns. Long-run desync, the
SyncCheckStrategymachinery, and reconnection are untested. - Two players, both Human, both on defaults. No password, no teams, no alliances, no scenario, no more than two slots.
sots_server.exewas not run. It is present inC:\SOTSalongsideDedicated Server Launchpad.exe. The community bug G2 quotes ("the dedicated server does not seem to work") is still unreproduced and unexplained, and it is now the cheapest remaining multiplayer item.- The shim was
hooks=off, so nothing internal was observed — no message types, no wire formats, no sync-check payloads. Everything above is UI, sockets and packets.
9. Files
- Capture (filtered to DNS, every GameSpy port, the game's own ports and the MOTD host):
verify/results/multiplayer/w2-tier0-filtered.pcap - Screenshots:
verify/results/multiplayer/w2-lobby-client.png,w2-mp-turn1.png,w2-mp-turn2-host.png,w2-mp-turn2-client.png,w2-internet-browser-no-error.png,w2-join-manually-dialog.png,w2-lan-browse-listing.png - Predictions, committed before the run:
sots-engine/docs/W2-predictions.md
10. Recipe, so nobody has to re-derive it
At 1024x768, with the game at the main menu:
- Host Multi-Player (512,478) → Custom (446,287) → LAN (446,428) → OK (551,522)
- Custom Game Setup: Create Game (835,707) → name dialog OK (600,392)
- Firewall prompt: Allow access (623,550) — appears once, on the first host
- Host lobby: Launch (931,663). Client lobby: Ready (931,663), Leave Game (931,692)
- Join Multi-Player (512,506) → LAN (446,357) / Internet (577,357) → OK (551,451)
- Game Browser: Join Manually (781,245), Refresh (874,245), Join (965,443), first row (200,159), Exit (989,716)
- Second instance:
schtaskstask running"C:\SOTS\Sword of the Stars.exe" /concurrent /join 127.0.0.1:3369. It must be a scheduled task withLogonType=InteractiveToken; a process started straight from the SSH session lands in a different window station and cannot be seen or clicked. - Clicking a specific instance needs a helper that resolves the window by PID
(
Get-Process -Id), notSelect -First 1— the existingC:\SOTS\ui\click_helper.ps1picks whichever instance it finds first. Lane W2 leftC:\SOTS\w2\ui.ps1(takes a PID, or-1for "do not foreground anything", which is how the firewall dialog gets clicked); it reads the PID fromC:\SOTS\w2\pid.txtand the commands fromC:\SOTS\w2\cmd.txt, and is run by the SOTSUI2 scheduled task, which is registered and left in place. - The second-instance launcher is staged but NOT registered.
C:\SOTS\w2\sotsb.xmlandC:\SOTS\w2\argsB.txtare on disk; re-register withschtasks /Create /TN SOTSB /XML C:\SOTS\w2\sotsb.xml /Fand put the arguments inargsB.txt. It was deliberately left unregistered so a stray run cannot start a second game under a later lane. - PowerShell over SSH needs
-ExecutionPolicy Bypasseven for-EncodedCommand, or any& … .ps1inside it dies withrunning scripts is disabled on this system— and, because the failure is inside the encoded block, it looks like the script ran and did nothing. This cost one wasted launch here.
11. Corrections filed
- Against
multiplayer-gamespy.md§7: caveat (a) is falsified (§5); P2's "exactly one DNS query" is right but for a different reason, and the browser resolves three names not one (§4); P3's mechanism is confirmed but the single-guest port-contention worry (this lane's, not G2's) was unfounded (§6). - The MOTD is live, not canned: this corrects any assumption that a 2026 launch is offline. One
outbound HTTP GET to
www.kerberos-productions.comhappens on every launch and on every entry to the Game Browser.