board: lane L1 - hive creation draws in BeginProcessTurn; CDiff edges played forward; SnLv verified; VM140 free

This commit is contained in:
alex 2026-09-08 18:26:51 -04:00
parent 940aecad0e
commit 79245837be
22 changed files with 463 additions and 3 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,306 @@
# Multiplayer combat, run on the real game: two humans, one battle, lockstep
- **Type:** subsystem (live verification)
- **Status:** **verified** — a player-versus-player battle was fought inside a two-player LAN game
with no GameSpy service of any kind, and the whole run was captured outside the guest
- **Confidence:** high on everything below; every claim is a screenshot, a socket table, or a packet
- **Owner / date:** lane L2 · 2026-09-08 · guest **VM141** (`sots-re-win10-b`)
- **Predictions, committed before the run:** `sots-engine/docs/L2-predictions.md`
- **Builds on:** `multiplayer-tier0-verified.md` (lane W2) and `multiplayer-gamespy.md` (lane G2)
---
## 0. The headline
**Combat multiplayer works, and it needs no GameSpy either.** Two human players in one LAN-mode
game fought a battle at a contested colony: both got the encounter query, both chose *Fight
Manually*, both loaded the tactical combat screen, and both ran the same battle off **one shared
lockstep clock**. Over the whole session — lobby, twelve strategic turns, the encounter query and
the battle — the capture on the host side of the guest's NIC shows **zero** packets to UDP 27900,
UDP 27901, TCP 28910, TCP 28900, TCP 6667 or TCP 6500, and **zero** DNS lookups for any
`*.gamespy.com` name.
W2 closed Tier 0 with the words "the most likely place for the direct path to still be broken".
It is not broken. **The multiplayer revival is complete end to end: create, join, launch, play,
and fight.**
**And the one prediction that failed is the most interesting result.** `CombatHostPort` **3370 is
never bound — not at lobby time, not at turn time, and not during the battle.** G2 read the default
out of `Game_LoadNetworkConfig` (`0x005a0610`) correctly, and W2 correctly reported it unbound at
lobby time and inferred "it must be opened later, when a combat starts". That inference is wrong on
this arm: the combat ran to a shared clock over the **already-open strategy connection**, with the
socket table unchanged from the moment the lobby opened. See §4 — this matters for anyone writing
port-forwarding instructions, and it corrects the community advice.
---
## 1. What was run
One Windows guest (VM141), GOG 1.8.1, two instances of the shipped executable, `hooks=off` — **the
instrument was removed** (method rule 19). The guest was cloned from VM140 with lane W3's
`hooks=trace` shim config in place; that was replaced with `hooks=off` before the game was launched
(§8).
- **Instance A**, pid 2256 — host. Launched by the `SOTS` scheduled task with no arguments.
- **Instance B**, pid 6620 — joiner. `"C:\SOTS\Sword of the Stars.exe" /concurrent /join 127.0.0.1:3369`
via the `SOTSB` scheduled task. **W2's P5 reconfirmed on a second guest**: two instances, two
windows, two main loops.
Game: *Host Multi-Player* → **Custom** + **LAN**, 28 stars, **Distance 5 ly** (the minimum), 2
players, **Initial Colonies 10**, **Random Encounters 100%**, Strategic Turn Length *Unlimited*,
Combat Turn Length *240 s*. Slot 2 was switched from *Computer* to **Open** before B joined — a
clone of VM140 comes up with slot 2 set to AI, and a joiner cannot take an AI slot.
Three instruments, all outside the game process:
1. `tcpdump -i tap141i0` on **spicy**, rotating into `/root/l2cap/` — the host side of the guest's
tap device, outside the guest entirely.
2. `Get-NetUDPEndpoint` / `Get-NetTCPConnection` filtered to the two SOTS pids, polled over SSH from
a separate process (`C:\SOTS\l2\socks.ps1`).
3. `qm monitor 141 screendump` for the screen, via `tools/vmshot.py` — reads the QEMU framebuffer,
nothing inside the guest.
**Blind spot, stated up front, same as W2's:** both instances talk over 127.0.0.1, so the game's own
3369 traffic never reaches the tap. §5 turns that into a *control* rather than a hole.
## 2. Provoking a battle — what it actually took
This is worth recording because it cost most of the lane and the next lane should not re-derive it.
**Random encounters at 100% do not fire on a static empire.** Eight strategic turns were played with
Random Encounters at maximum, 20 colonies between the two players, and **no encounter at all**.
The reason is visible in the system banner: `DE 00 CR 00 DN 00`. With *Initial Colonies 10* and
*Initial Technologies 0*, **neither player starts with a single ship** — so "Manage Fleets", "Move"
and "Special" are greyed out on every system, and there is nothing for an encounter to happen to.
So a battle needs, in order:
1. **Build warships.** *Build* → Destroyer → `Armor` → *Add To Queue*. At 16 M treasury and 17 k
industrial output a batch of four destroyers completes in **2 turns**.
2. **Order a fleet somewhere hostile.** Select the fleet row in *Fleets at <system>*, press
*Move*, then click the destination **star** (not its banner, and not where the tooltip appears).
3. Because both players were Human, node travel applies: targeting a system off the node network
raises *"Alpha Fleet cannot reach <X> using nodes. Do you want to plot a sublight course?"*
Accepting gives a **sublight** course — here 28 turns to Nu Ophiuchi at Speed 4.00 / 0.2 sublight.
4. **The sublight leg is what produced the battle**, and faster than the nominal ETA: on turn 12 the
fleet drifted into **Avalon**, a colony belonging to the *other* player, and the encounter fired.
**Three UI traps, each of which cost a run here:**
- **The starmap needs real mouse motion.** `SetCursorPos` alone does not move the game's cursor —
the move-mode arrow and the destination tooltip never update. The driver must follow the warp with
an actual `mouse_event(MOUSEEVENTF_MOVE, …)` relative jiggle. W2's `ui.ps1` has no such jiggle,
which is why its recipe covers only fixed-position buttons and never the map.
- **The banner is not the star.** System banners sit up and to the left of their star with a short
leader stem, and a neighbouring banner will cover the star underneath it. Clicking the banner does
nothing; clicking where the hover tooltip is drawn does nothing. Zoom in (mouse wheel) until the
banners separate, then click the star itself.
- **A modal move-warning dialog silently stops the turn loop.** An unattended "end turn on both
instances" loop ran four full cycles against a *"plot a sublight course?"* dialog and the turn
number never moved. Any automated turn driver must screenshot and check before clicking.
`tools/vmshot.py --one 141` plus a PIL crop is the whole read-back loop; the game's own text is
legible at 3–7× upscale and that is how every reading below was taken.
## 3. The battle — P2, P5, and the lockstep result
**P5 — the encounter query reaches both players — confirmed.** Both instances raised the C1
*Encounter at Avalon* screen (`SNMDoEncounterQuery`), each showing **both** player cards: the
attacker `re(2)` with *Ships: 4 / DE: 4 / 4 DE Armor*, and the defender `re` with *No ships
available* (host's own view) / *No ships detected* (attacker's view). The action strip offered the
full `ENCACTION_*` set — *Fight Manually*, auto-resolve, fight-if, auto-resolve-peacefully, and on
the attacker's side the two surrender variants. **Both players independently chose *Fight
Manually*** and pressed *Done*.
**The combat launched.** Both instances showed the C2 loading screen — the `CL_*` versus backdrop
titled *Encounter at Avalon*, `re(2)` **VS** `re` — and then the C3 tactical HUD:
| | Instance A (pid 2256, host, defender) | Instance B (pid 6620, joiner, attacker) |
|---|---|---|
| Title | `Avalon` | `Avalon` |
| Own forces | none — planet only | `DE Armor (1)` selected, weapons panel, 3 gun groups |
| Player list | `re(2)` in the enemy colour | `re(2)` |
| Fleet Commands strip | present | present |
| Combat clock | running | running |
**P2 — combat resolves in lockstep — confirmed, and the clock is the proof.** The two instances
were sampled alternately, and the combat clock is a *single shared value*, not two independent
countdowns:
| Wall clock | Instance sampled | Combat clock |
|---|---|---|
| 18:00:39 | B (attacker) | 3:51 |
| 18:01:48 | A (defender) | 3:31 |
| 18:04:33 | B (attacker) | 2:42 |
| 18:05:59 | B (attacker) | 2:18 |
Those four readings lie on one line at **≈ 0.29 × real time** regardless of which instance is
sampled — 240 s of combat took roughly 14 minutes of wall clock on a guest running two copies of a
D3D9 game through DXVK on llvmpipe. A defender sampled at 18:01:48 sitting exactly on the line drawn
by attacker samples at 18:00:39 and 18:04:33 is what lockstep looks like from the outside: **one
simulation, two views, neither free-running.** No `@SYNC ERROR` appeared on either instance.
**The sim does not stall when a window loses focus.** Instance B advanced 69 s of combat time during
a stretch when instance A held the foreground continuously. This matters for anyone automating the
lab: the two-instance configuration is not a focus-ping-pong problem.
**The battle ended cleanly and both players came out of it together.** The 240 s combat timer ran to
`0:00` and the round expired (`CombatExpiredFunctor`); both instances raised the C4 end dialog
**"Round draw."** — the `COMBAT_*` string for an expired round. Dismissing it returned **both**
instances to the strategy screen on **Turn 13** (the encounter fired on turn 12), each with its own
empire intact: the attacker on Nunki with its fleet detached at Avalon, the defender on Regulus with
its own two-destroyer Alpha Fleet. `SNMAllCombatDone` → `SNMResumePlaying` did what
`ui-screen-map.md` §2.2 step 6 says it does.
Neither instance hung at `COMBAT_WAITINGFORPLAYER` or `COMBAT_WAITINGFORSERVER`, neither crashed,
neither showed a sync error, and the two turn counters never diverged. **P2 confirmed in full.**
## 4. P1 — `CombatHostPort` 3370 — **falsified**
This was the prediction with a number attached, and it failed cleanly.
The UDP socket table for the two SOTS pids was polled continuously — every 700 ms inside the guest
from lobby creation to turn 12, then every 2 s from outside across the whole battle (18:01:24 to
18:16, covering combat load, the full 240 s of tactical combat, the round-draw dialog and the return
to the strategy screen). **Both logs contain exactly one line each.** The socket table never changed:
```
UDP pid=2256 0.0.0.0:3369 <- host, HostPort
UDP pid=2256 0.0.0.0:50020 <- host, ephemeral
UDP pid=6620 0.0.0.0:50021 <- joiner, ephemeral
```
There is **no TCP at all** for either process at any point, confirming G2's import-table reading
(`bind`/`sendto`/`recvfrom` but no `listen`, no `accept`) on a second workload.
**3370 was never bound**: not when the lobby opened, not across twelve strategic turns, not at the
encounter query, not during combat loading, and not while the tactical simulation was running with
both players in it. W2 observed it unbound at lobby time and wrote "it must be opened later, when a
combat starts". On the direct-join arm, with the strategy host also hosting the battle, **it is not**.
The straightforward reading is that the combat lockstep is carried over the **already-established
strategy connection** — the host's 3369 socket and the two ephemeral sockets that carried
`SNMEndTurn` and `SNMDoEncounterQuery` also carry the `CNM*` combat messages. `CombatHostPort` is
presumably for the case the ini key exists to serve: a **separate** combat host, i.e. the
`SNMHostCombat` → `SNMHostCombatReply` handshake electing a *different* player (or a dedicated
server) to host the battle, which cannot happen in a two-player game where the strategy host is
already a participant.
**Consequences.**
- **Port-forwarding advice should say 3369 and only 3369.** PCGamingWiki and the community threads
G2 quotes say "LAN mode + forward port 3369", and that is exactly right; nothing here supports
adding 3370, at least for two players over a direct join.
- **Not generalised.** This is one battle, two players, one guest, with the strategy host as a
combatant. Three or more players, a battle between two *clients* with the host merely relaying, or
`sots_server.exe` as a dedicated host, could each elect a different combat host and bind 3370.
That is the obvious next experiment and it is **not run** here.
- `LanScanPortRange` still covers **3369..3370** for the LAN browse broadcast, so 3370 is not dead
in the binary — it is just not bound on this path.
## 5. P3 and P4 — GameSpy at combat time, and the control
**P3 — no GameSpy traffic at combat time — confirmed.** Across the whole run, sampled twice (once
mid-game and once with the battle in progress):
| Sample | Total packets on `tap141i0` | GameSpy ports | `*.gamespy.com` DNS |
|---|---|---|---|
| Through turn 9 | 42,381 | **0** | **0** |
| With the battle in progress | 82,470 | **0** | **0** |
| **Whole run, both capture files** | **101,835** | **0** | **0** |
Ports counted: UDP 27900, UDP 27901, TCP 28910, TCP 28900, TCP 6667, TCP 6500. This closes the
question G2 left open from the other direction: `GameSpy_NNBeginNegotiationWithSocket`
(`0x00412590`) has two call sites, the browser-mediated join and the host reporting path, and **the
combat host/join path is a third thing that reaches neither**. No `natneg1`/`natneg2` lookup ever
happened, at lobby time or at combat time.
The only names resolved in the combat window were Windows telemetry (`fd.api.iris.microsoft.com`,
`v10.events.data.microsoft.com`, `wpad.home.arpa`, …) plus exactly one
`www.kerberos-productions.com` — the MOTD fetch W2 documented, once per launch.
**P4 — combat traffic stays on loopback — confirmed, and it is the control.** Zero packets to or
from UDP 3369 or 3370 appeared on the tap, while 101,835 other packets did. The instrument is
demonstrably live and demonstrably blind to the game's own traffic, which is exactly the shape of
evidence that makes the zero in P3 mean something: **the capture cannot see SOTS's own packets, and
it still sees every other packet the guest emits — so a GameSpy packet would have been caught.**
It also settles a smaller question: **the combat host does not announce itself on the LAN.** Nothing
was broadcast to 3369 or 3370 when the battle started, so a combat session is not discoverable the
way a lobby is.
## 6. Adopting VM141 — what a ZFS clone of VM140 actually needed
Recorded because four more guests exist and this took under fifteen minutes.
The clone (`zfs clone rpool/data/vm-140-disk-0@clone-base`, fresh MAC `BC:24:11:8A:11:F0`) had **no
IPv4 address in any ARP table on spicy** and `qm agent` reported *"QEMU guest agent is not running"*
(none is installed on these guests). A 40-second `tcpdump -i tap141i0 -e "ether src <mac>"` caught
**one** packet — an IPv6 neighbour solicitation — and no DHCP at all, which looks exactly like a
clone whose inherited static IPv4 has been disabled for a duplicate-address conflict with its parent.
**It was none of those things. The guest was simply idle.** Driving the console with
`qm sendkey 141 …` into an elevated `cmd` and reading `ipconfig /all` off a `screendump` showed a
perfectly healthy **DHCP** lease, `192.168.10.143/16`, obtained at 16:36:40 — half an hour before
anyone looked. SSH with Alex's key worked on the first try.
**The lesson for the remaining guests: do not diagnose a cloned guest from ARP or from a short
packet capture.** An idle Windows box on a quiet LAN emits nothing for minutes at a time, and its
absence from the neighbour table only means nobody has talked to it. Take a `screendump` first.
Console driving via `qm sendkey` works and is worth knowing:
- `qm sendkey 141 meta_l` opens the Start menu; `meta_l-r` opens *Run* — but **key delivery lags by
tens of seconds**, so a screendump taken 2 s after a keystroke will show the *previous* state.
Send, wait, then shoot; a "nothing happened" screenshot is usually just early.
- Letters/digits map to their own names, and `shift-<key>` gives the shifted form; there is no
string primitive, so a typing helper that expands a string into one `qm sendkey` per character
(batched into a single SSH call) is the practical unit.
- The `re` account is a local administrator with UAC on: `cmd` + `ctrl-shift-ret` in *Run* raises the
consent dialog, which `alt-y` accepts.
What the clone carried over correctly and did **not** need fixing: the game at `C:\SOTS`, DXVK, the
`SOTS`/`SOTSUI`/`SOTSUI2` scheduled tasks, the OpenSSH host keys and authorized keys, and — usefully
— **the Windows Defender Firewall rule for the game**, so the "has blocked some features of this
app" prompt W2 had to click through on VM140 never appeared here.
What it carried over that **had** to be fixed:
- **`shim.cfg` was lane W3's `hooks=trace`.** Any lane that wants the shipped game must overwrite it;
`C:\SOTS\shim.cfg.l2backup` holds what was there.
- **The duplicate hostname.** Both guests answer to `SOTS-RE` and share a SID. It was left alone —
SSH by IP is unaffected and nothing in this lane needed NetBIOS — but two machines with one name on
one LAN is a latent trap for anything that resolves by name.
- **`C:\SOTS\launch-b.cmd` does not exist**, although `C:\SOTS\w2\sotsb.xml` invokes it and
`C:\SOTS\w2\runB.txt` proves it ran on VM140. It has to be recreated (it reads the argument line
from `C:\SOTS\w2\argsB.txt`); a copy is at `C:\SOTS\l2\` on VM141.
- **Slot 2 defaults to *Computer*.** A `/join` client cannot take an AI slot; click the small red
cross at the slot card's top-right corner to set it *Open* before launching the joiner.
## 7. Files
- Capture: `verify/results/multiplayer/l2-combat.pcap` (on spicy at `/root/l2cap/`)
- Screenshots: `verify/results/multiplayer/l2-*.png`
- Socket-table log: `verify/results/multiplayer/l2-socks.log`
- Predictions, committed before the run: `sots-engine/docs/L2-predictions.md`
- Helper left on the guest: `C:\SOTS\l2\` (`ui.ps1` with wheel/jiggle support, `socks.ps1`,
`poller.ps1`, `shim.cfg`, `l2ui.xml`); scheduled task **L2UI**; task **SOTSB** now registered.
## 8. What this run did NOT cover (rule 15)
- **One guest, loopback only.** No two-machine combat, no real network latency, no NAT, no packet
loss. `CombatLatency` defaults to 1000 ms and was never stressed.
- **One battle, two players, and the strategy host was a combatant.** The `SNMHostCombat` /
`SNMHostCombatReply` election never had a choice to make. Three-plus players, or a battle between
two clients, is where 3370 might yet appear (§4).
- **Very lopsided.** Four destroyers against an undefended colony. No reinforcement waves, no
reserves, no bombardment, no ceasefire negotiation, no surrender, no combat between fleets.
- **One combat round.** Multi-round battles, `SNMCombatTimeRemaining` extension, and the
querying/combat/resume loop over several successive turns are untested.
- **No sync log.** `SyncCheckCombat` is on by default but `SyncLogCombat` is off, and it was left
off; a desync would have shown as a symptom, not as a log.
- **`sots_server.exe` still not run.** The community's "the dedicated server does not seem to work"
bug remains unreproduced, and it is now the cheapest remaining multiplayer item — and, per §4, the
one most likely to be where `CombatHostPort` earns its name.
- **`hooks=off` throughout**, so nothing internal was observed: no `CNM*` message types, no wire
format, no sync-check payload. Everything here is UI, sockets and packets.

View file

@ -206,7 +206,7 @@ writer for either — **[I]** they look like the SDK's `gsiSetAvailableCheckHost
| Key | Default |
|---|---|
| `HostPort` | **3369** |
| `CombatHostPort` | **3370** |
| `CombatHostPort` | **3370** — but see the note below: never actually bound on the two-player direct-join arm |
| `LanScanPort` | **3369** |
| `LanScanPortRange` | **1** |
| `HeartbeatPeriod` | 15000 ms |
@ -218,7 +218,15 @@ writer for either — **[I]** they look like the SDK's `gsiSetAvailableCheckHost
**[V]** The import table has `connect`, `send`, `recv`, `sendto`, `recvfrom`, `bind` but **no
`listen` and no `accept`** — the game's own transport is **UDP only**. TCP appears only as client
connects (peerchat, the SB list, ghttp).
connects (peerchat, the SB list, ghttp). *Confirmed live twice: lanes W2 and L2 both saw zero TCP
sockets on either SOTS process at any point, including during combat.*
> **Live result on `CombatHostPort` (lane L2, `multiplayer-combat.md` §4).** The default is read
> correctly, but on the two-player direct-join arm **3370 is never bound** — the combat lockstep runs
> over the strategy connection the host already has open on 3369. `CombatHostPort` presumably serves
> the case where `SNMHostCombat` / `SNMHostCombatReply` elects a *different* player, or a dedicated
> server, to host the battle; that cannot happen when the strategy host is itself a combatant.
> Untested for three-plus players, for a battle between two clients, and for `sots_server.exe`.
---

View file

@ -104,6 +104,12 @@ compiled-in default in this install). **No TCP anywhere**, confirming G2's impor
`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.
> **Corrected by lane L2 (`multiplayer-combat.md` §4).** The observation is right; the inference is
> wrong. L2 fought a two-human battle with the socket table polled throughout and **3370 is never
> bound at all** — not at lobby time, not during combat loading, not while the tactical simulation
> was running. The combat lockstep rides the already-open strategy connection. Port-forwarding
> advice should say **3369 and only 3369** for a two-player direct join.
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.
@ -203,6 +209,9 @@ Loudly, because the headline is a strong one and it was established on a narrow
- **No combat.** Both turns resolved without a battle, so `CombatHostPort` 3370 was never bound and
the combat lockstep — the part with a 1000 ms `CombatLatency` and its own sync checking — is
entirely untested. This is the most likely place for the direct path to still be broken.
**Closed by lane L2 on VM141 (`multiplayer-combat.md`): it is not broken.** Two humans fought a
player-versus-player battle at a contested colony off one shared lockstep clock, both reached the
combat report, and both resumed on the same turn — with zero GameSpy packets and 3370 never bound.
- **Two turns.** Long-run desync, the `SyncCheckStrategy` machinery, and reconnection are untested.
- **Two players, both Human, both on defaults.** No password, no teams, no alliances, no scenario,
no more than two slots.

View file

@ -0,0 +1,41 @@
# L2 UI harness — driving the SOTS strategy map from outside the guest
Written for lane L2's multiplayer-combat run on VM141. It is lane W2's `C:\SOTS\w2\ui.ps1` plus the
two things that run needed and W2's version could not do.
## What is different from W2's `ui.ps1`
1. **Real mouse motion.** `SetCursorPos` alone does **not** move the game's cursor as far as the
starmap is concerned — the move-mode arrow and the destination tooltip never update, and clicks
land on stale hit-test state. Every `move` and `click` here warps with `SetCursorPos` and then
emits an actual `mouse_event(MOUSEEVENTF_MOVE, …)` relative jiggle. Without this you can drive
fixed-position buttons (which is all W2 needed) but you cannot drive the map.
2. **`wheel <x> <y> <notches>`** — starmap zoom, via `mouse_event(MOUSEEVENTF_WHEEL)`. System banners
overlap and hide each other's stars at default zoom; you have to zoom in before you can click a
star.
3. `rclick` and `dbl`.
Commands, one per line, read from `C:\SOTS\l2\cmd.txt`:
`fg | move X Y | click X Y | rclick X Y | dbl X Y | wheel X Y N | key <SendKeys> | type <text> | sleep ms`
The target process id is read from `C:\SOTS\l2\pid.txt` (or `-1` for "do not foreground anything",
which is how a firewall or UAC dialog gets clicked). Run it from an **interactive** scheduled task
with `LogonType=InteractiveToken`; a process started straight from an SSH session lands in a
different window station and cannot click anything.
## Gotchas that cost this lane a run each
- **Move mode does not survive between invocations.** "Select fleet → press Move → click the
destination" has to be one `cmd.txt`, not three.
- **The banner is not the star.** Banners sit up-left of their star with a short leader stem, and a
neighbouring banner will cover the star. Clicking the banner does nothing; so does clicking where
the hover tooltip is drawn. Zoom in until they separate, then click the star.
- **A modal dialog silently eats an unattended turn loop.** Screenshot and check before each blind
click; a "plot a sublight course?" dialog swallowed four end-turn cycles without the turn moving.
## Files
- `ui.ps1` — the driver. Deploy to `C:\SOTS\l2\ui.ps1` and register a task pointing at it.
- `socks.ps1` — non-perturbing socket-table probe: which SOTS pid holds which UDP/TCP port, now.
- `launch-b.cmd` — second-instance launcher; reads its argument line from `C:\SOTS\w2\argsB.txt`.
`C:\SOTS\w2\sotsb.xml` invokes this path but the file itself is **not** on the VM140 image.

View file

@ -0,0 +1,11 @@
@echo off
cd /d C:\SOTS
set DXVK_LOG_LEVEL=info
set DXVK_LOG_PATH=C:\SOTS\dxvklog
set VK_DRIVER_FILES=C:\SOTS\lvp\lvp_icd.x86.json
set VK_ICD_FILENAMES=C:\SOTS\lvp\lvp_icd.x86.json
set ARGSB=
set /p ARGSB=<C:\SOTS\w2\argsB.txt
echo launchB start %date% %time% args=[%ARGSB%] > C:\SOTS\w2\runB.txt
"C:\SOTS\Sword of the Stars.exe" %ARGSB% >> C:\SOTS\w2\runB.txt 2>&1
echo exit code %errorlevel% %time% >> C:\SOTS\w2\runB.txt

View file

@ -0,0 +1,16 @@
# Non-perturbing socket-table probe: which SOTS process holds which UDP port, right now.
$ts = Get-Date -Format "HH:mm:ss.fff"
$procs = @{}
Get-Process -Name 'Sword of the Stars' -ErrorAction SilentlyContinue | ForEach-Object { $procs[$_.Id] = $_.Id }
$rows = @()
foreach ($e in (Get-NetUDPEndpoint -ErrorAction SilentlyContinue)) {
if ($procs.ContainsKey([int]$e.OwningProcess)) {
$rows += "$ts UDP pid=$($e.OwningProcess) $($e.LocalAddress):$($e.LocalPort)"
}
}
foreach ($e in (Get-NetTCPConnection -ErrorAction SilentlyContinue)) {
if ($procs.ContainsKey([int]$e.OwningProcess)) {
$rows += "$ts TCP pid=$($e.OwningProcess) $($e.LocalAddress):$($e.LocalPort) -> $($e.RemoteAddress):$($e.RemotePort) $($e.State)"
}
}
if ($rows.Count -eq 0) { "$ts (no SOTS process / no sockets)" } else { $rows | Sort-Object }

View file

@ -0,0 +1,62 @@
param([int]$TargetPid = 0, [string]$Cmd = 'C:\SOTS\l2\cmd.txt')
if ($TargetPid -eq 0 -and (Test-Path C:\SOTS\l2\pid.txt)) { $TargetPid = [int]((Get-Content C:\SOTS\l2\pid.txt -Raw).Trim()) }
Start-Transcript -Path C:\SOTS\l2\uilog.txt -Force | Out-Null
Add-Type -AssemblyName System.Windows.Forms
Add-Type @"
using System; using System.Runtime.InteropServices;
public struct RECT { public int L,T,R,B; }
public class L2 {
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x,int y);
[DllImport("user32.dll")] public static extern void mouse_event(uint f,uint x,uint y,uint d,UIntPtr e);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern int GetWindowTextW(IntPtr h, System.Text.StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
[DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);
}
"@
$out = @()
$hw = [IntPtr]::Zero
if ($TargetPid -lt 0) { $hw = [IntPtr]::Zero } elseif ($TargetPid -gt 0) {
$p = Get-Process -Id $TargetPid -ErrorAction SilentlyContinue
if ($p) { $hw = $p.MainWindowHandle }
}
if ($hw -eq [IntPtr]::Zero) { $out += "NO WINDOW for pid $TargetPid" }
else {
$r = New-Object RECT; [L2]::GetWindowRect($hw,[ref]$r) | Out-Null
$out += "pid=$TargetPid hwnd=$hw rect=$($r.L),$($r.T)-$($r.R),$($r.B)"
[L2]::ShowWindow($hw,9)|Out-Null; [L2]::BringWindowToTop($hw)|Out-Null; [L2]::SetForegroundWindow($hw)|Out-Null
Start-Sleep -m 900
}
foreach ($line in Get-Content $Cmd) {
if ($line.Trim() -eq '' -or $line.StartsWith('#')) { continue }
$a = $line -split ' ',4
switch ($a[0]) {
'fg' { [L2]::ShowWindow($hw,9)|Out-Null; [L2]::BringWindowToTop($hw)|Out-Null; [L2]::SetForegroundWindow($hw)|Out-Null; Start-Sleep -m 900 }
'move' { $mx=[int]$a[1]; $my=[int]$a[2]
[L2]::SetCursorPos($mx-3,$my-3); Start-Sleep -m 150
# real relative motion so DirectInput-style polling sees a move
[L2]::mouse_event(1,[uint32]1,[uint32]1,0,[UIntPtr]::Zero); Start-Sleep -m 120
[L2]::SetCursorPos($mx,$my); Start-Sleep -m 150
[L2]::mouse_event(1,[uint32]1,[uint32]0,0,[UIntPtr]::Zero); Start-Sleep -m 120
[L2]::mouse_event(1,[uint32]4294967295,[uint32]0,0,[UIntPtr]::Zero); Start-Sleep -m 400 }
'click' { [L2]::SetCursorPos([int]$a[1]-2,[int]$a[2]); Start-Sleep -m 200;
[L2]::mouse_event(1,[uint32]2,[uint32]0,0,[UIntPtr]::Zero); Start-Sleep -m 200;
[L2]::SetCursorPos([int]$a[1],[int]$a[2]); Start-Sleep -m 500; [L2]::mouse_event(2,0,0,0,[UIntPtr]::Zero); Start-Sleep -m 300; [L2]::mouse_event(4,0,0,0,[UIntPtr]::Zero); Start-Sleep -m 900 }
'rclick' { [L2]::SetCursorPos([int]$a[1],[int]$a[2]); Start-Sleep -m 700; [L2]::mouse_event(8,0,0,0,[UIntPtr]::Zero); Start-Sleep -m 300; [L2]::mouse_event(16,0,0,0,[UIntPtr]::Zero); Start-Sleep -m 900 }
'dbl' { [L2]::SetCursorPos([int]$a[1],[int]$a[2]); Start-Sleep -m 700; for($i=0;$i -lt 2;$i++){[L2]::mouse_event(2,0,0,0,[UIntPtr]::Zero);Start-Sleep -m 60;[L2]::mouse_event(4,0,0,0,[UIntPtr]::Zero);Start-Sleep -m 60}; Start-Sleep -m 900 }
# wheel <x> <y> <notches, +in/-out>
'wheel' { [L2]::SetCursorPos([int]$a[1],[int]$a[2]); Start-Sleep -m 400
$n=[int]$a[3]; $d = if($n -ge 0){120}else{-120}; $c=[Math]::Abs($n)
for($i=0;$i -lt $c;$i++){ [L2]::mouse_event(0x0800,0,0,[uint32]$d,[UIntPtr]::Zero); Start-Sleep -m 120 }
Start-Sleep -m 700 }
'key' { [System.Windows.Forms.SendKeys]::SendWait($a[1]); Start-Sleep -m 500 }
'type' { [System.Windows.Forms.SendKeys]::SendWait(($line.Substring(5))); Start-Sleep -m 500 }
'sleep' { Start-Sleep -m ([int]$a[1]) }
}
}
$sb = New-Object System.Text.StringBuilder 256; [L2]::GetWindowTextW([L2]::GetForegroundWindow(),$sb,256)|Out-Null
$out += "done $(Get-Date -Format HH:mm:ss) fg='$($sb.ToString())'"
$out
Stop-Transcript | Out-Null

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View file

@ -0,0 +1,2 @@
18:01:24 UDP pid=2256 0.0.0.0:3369 |UDP pid=2256 0.0.0.0:50020 |UDP pid=6620 0.0.0.0:50021 |
poll ended

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB