From f2687a3146f1d566a07ba3332a30c0403d91ad42 Mon Sep 17 00:00:00 2001 From: Cyd Date: Wed, 29 Jul 2026 18:09:22 -0500 Subject: [PATCH] BT410 5.3.72: the fault caught in the act; the inside view gets its cockpit VPX_PF_WATCH (fork, cpu/paging.cpp): on a guest page fault at the watched linear address, dump guest registers, the last 64 serial RX deliveries with guest cs:eip at each, the code bytes at the faulting EIP and the stack top. serialnamedpipe's doReceive feeds the ring. Armed in podrun.sh and launch_pod.ps1. The first catch decoded the residual fault completely: CS:EIP 00FF:000066D4 in the DPMI host, EBX = F000CA60 -- an IVT entry read as a dword, segment F000 offset CA60, a BIOS default interrupt handler -- and the faulting access is [EBX+0x3004], whose 0x80000000 segment-base wrap gives exactly cr2 7000FA64. The host probes a word 0x3004 bytes past a real-mode vector value treated as a flat pointer: harmless for its own low-memory handlers, a fault for BIOS F000:xxxx defaults. The serial ring shows a steady 1-byte/1-3ms vRIO stream with nothing special at the fault -- the stream determines which vectors get walked, not the crash itself. The 0x3004 appears nowhere in DPMI32VM.OVL or 32RTM.EXE as an immediate, so the probe now also dumps code bytes at EIP; faulthunt.sh loops runs until the next catch. Shipped baseline streak: 4/4 clean -- consistent with exposure, not yet discriminating. The inside view now loads the COCKPIT skeleton: the fleet-wide X-variant naming convention (MAD->MAX etc., all 64 skeletons present) selects the same 25-joint chain with a single object -- max_cop.bgf, the MAX_COP canopy shell with the PUNCH-texel windows from the capture forensics. The donor names the same mechanism from the decomp side (inside = SkeletonType_A with '_cop' selection). Fallback to the body skeleton when no X file exists. Co-Authored-By: Claude Fable 5 --- emulator/render-bridge/dosboxlog.txt | 27 - emulator/render-bridge/faulthunt.sh | 16 + emulator/render-bridge/launch_pod.ps1 | 1 + emulator/render-bridge/logs/059-dosbox.txt | 83 ++ emulator/render-bridge/logs/059-prev.txt | 586 +++++++++ emulator/render-bridge/logs/061-CRASH.txt | 236 ++++ emulator/render-bridge/logs/061-dosbox.txt | 13 + emulator/render-bridge/podlog.txt | 584 --------- emulator/render-bridge/podlog_shipped.txt | 38 +- emulator/render-bridge/podrun.sh | 6 + emulator/render-bridge/shpstreak.sh | 13 + emulator/vpx-device/README.md | 77 ++ emulator/vpx-device/ethernet_pcap.cpp | 206 +++- emulator/vpx-device/ethernet_steam.cpp | 477 ++++++++ emulator/vpx-device/ethernet_steam.h | 57 + emulator/vpx-device/serialnamedpipe.cpp | 6 + emulator/vpx-device/steamlink.cpp | 1056 +++++++++++++++++ emulator/vpx-device/steamlink.h | 216 ++++ restoration/source410/BT_L4/BTL4VID.CPP | 52 +- restoration/source410/RENDER-ROADMAP.NOTES.md | 45 + 20 files changed, 3153 insertions(+), 642 deletions(-) create mode 100644 emulator/render-bridge/faulthunt.sh create mode 100644 emulator/render-bridge/logs/059-dosbox.txt create mode 100644 emulator/render-bridge/logs/059-prev.txt create mode 100644 emulator/render-bridge/logs/061-CRASH.txt create mode 100644 emulator/render-bridge/logs/061-dosbox.txt create mode 100644 emulator/render-bridge/shpstreak.sh create mode 100644 emulator/vpx-device/ethernet_steam.cpp create mode 100644 emulator/vpx-device/ethernet_steam.h create mode 100644 emulator/vpx-device/steamlink.cpp create mode 100644 emulator/vpx-device/steamlink.h diff --git a/emulator/render-bridge/dosboxlog.txt b/emulator/render-bridge/dosboxlog.txt index 0e0b0b38..4c60fd41 100644 --- a/emulator/render-bridge/dosboxlog.txt +++ b/emulator/render-bridge/dosboxlog.txt @@ -54,30 +54,3 @@ LOG: ISA Plug & Play BIOS enabled LOG: VGA ROM BIOS init callback LOG: pixratio 1.000, dw false, dh false LOG: Aspect ratio: 640 x 480 xToY=1.333 yToX=0.750 -LOG: WARNING: No translation support (to host) for code page 0 -LOG: DOS keyboard layout loaded with main language code US for layout us -LOG: XMS: 50 handles allocated for use by the DOS environment -LOG: EMS page frame at 0xe000-0xefff -LOG: COMMAND.COM env size: 720 bytes -LOG: COMMAND.COM environment block: 0x0701 sz=0x002d -LOG: COMMAND.COM main body (PSP): 0x072f sz=0x009a -LOG: COMMAND.COM stack: 0x0749 -LOG: pixratio 1.350, dw false, dh false -LOG: Aspect ratio: 720 x 540 xToY=1.333 yToX=0.750 -LOG: SHELL:Redirect output to COM3 -LOG: VPXLOG: fifosock client connected -VPX lights@f1: type6=0 typeE=0 decoded=0 dir=0 amb(0.00,0.00,0.00) -LOG: 420998823 ERROR MOUSE:Unhandled videomode 79 on reset -LOG: pixratio 1.000, dw false, dh false -LOG: Aspect ratio: 640 x 480 xToY=1.333 yToX=0.750 -VPX lightnode t=6 nb=52: [8]00000000/0 [12]00000000/0 [16]00000001/1.4013e-45 [20]3f800000/1 [24]3f800000/1 [28]3f800000/1 [32]64078678/1e+22 [36]64078678/1e+22 [40]00000000/0 [44]3e25a5b9/0.161765 [48]ffffffff/nan -VPX lights@f121: type6=2 typeE=6 decoded=2 dir=1 amb(0.55,0.50,0.65) - L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) -VPX lights@f241: type6=2 typeE=6 decoded=2 dir=1 amb(0.55,0.50,0.65) - L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) -VPX lights@f361: type6=2 typeE=9 decoded=2 dir=1 amb(0.55,0.50,0.65) - L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) -VPX lights@f481: type6=2 typeE=10 decoded=2 dir=1 amb(0.55,0.50,0.65) - L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) -VPX lights@f601: type6=2 typeE=10 decoded=2 dir=1 amb(0.55,0.50,0.65) - L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) diff --git a/emulator/render-bridge/faulthunt.sh b/emulator/render-bridge/faulthunt.sh new file mode 100644 index 00000000..94d9c04b --- /dev/null +++ b/emulator/render-bridge/faulthunt.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Run RIO-live until the watched fault fires (max 6), keep the probe dump. +cd /c/VWE/TeslaRel410/emulator/render-bridge +for i in 1 2 3 4 5 6; do + bash podrun.sh pod_render_rec hunt$i > /dev/null 2>&1 + until grep -q "Turning Plasma" podlog.txt 2>/dev/null || grep -qE "illegal address|page you don't own" podlog.txt 2>/dev/null; do sleep 10; done + if grep -q "page you don't own" podlog.txt; then + echo "hunt$i: FAULT -- dump follows" + grep -A40 "VPX_PF_WATCH hit" dosboxlog.txt | head -84 + cp dosboxlog.txt logs/faultdump.txt + break + fi + echo "hunt$i: clean" +done +taskkill //F //IM dosbox-x.exe > /dev/null 2>&1 +echo "FAULTHUNT DONE" diff --git a/emulator/render-bridge/launch_pod.ps1 b/emulator/render-bridge/launch_pod.ps1 index 961f35c8..ed69ccdf 100644 --- a/emulator/render-bridge/launch_pod.ps1 +++ b/emulator/render-bridge/launch_pod.ps1 @@ -54,6 +54,7 @@ $env:VPX_EXPLODE = '1' # pentapus: 7 cockpit displays $env:VPX_DUMPDIR = $Work $env:VPX_FIFODUMP = "$Work\live.fifodump" # archival/replay copy $env:VPX_FIFOSOCK = '8621' # live tee the bridge rides +$env:VPX_PF_WATCH = '7000FA64' # residual load-window fault: dump serial-RX ring on hit $env:RIO_TAP = "$Work\riotap.txt" # Dave's bridge is the out-the-window view; the native Division window is a # decode diagnostic (-ShowNative), cleared black so missing geometry doesn't diff --git a/emulator/render-bridge/logs/059-dosbox.txt b/emulator/render-bridge/logs/059-dosbox.txt new file mode 100644 index 00000000..0e0b0b38 --- /dev/null +++ b/emulator/render-bridge/logs/059-dosbox.txt @@ -0,0 +1,83 @@ +LOG: Early LOG Init complete +LOG: CONFIG: Loaded config file: C:/VWE/TeslaRel410/emulator/render-bridge/pod_render_rec.conf +LOG: DOSBox-X's working directory: C:\VWE\TeslaRel410\emulator\src\src +LOG: Logging init: beginning logging proper. This is the end of the early init logging +LOG: Logging: No logfile was given. All further logging will be discarded. +LOG: DOSBox-X version 2026.06.02 Commit d75a57c (MinGW SDL2 64-bit) +LOG: Prevent capture: 0 +LOG: SDL: version 2.32.10, Video windows, Audio directsound +LOG: EXPERIMENTAL: Debugger enabled for MinGW build, DOSBox-X crashes depending on the terminal software you use. Launching from command prompt (cmd.exe) is recommended. +LOG: Windows keyboard layout ID is 0x0409 +LOG: Host keyboard layout is now us (US English) +LOG: Mapper keyboard layout is now us (US English) +LOG: SDL2 reports desktop display mode 3440 x 1440 +LOG: OpenGL Version : 4.6.0 NVIDIA 610.47 +LOG: Configured windowposition: - +LOG: SDL: Current window pixel format: SDL_PIXELFORMAT_RGB888 +LOG: SDL: You are running in 24 bpp mode, this will slow down things! +LOG: Releasing keyboard +LOG: SDL: Windows 11 round corners will be disabled. +LOG: Screen report: Method 'Win98base' (3440.000 x 1440.000 pixels) at (0.000 x 0.000) (910.167 x 381.000 mm) (35.833 x 15.000 in) (96.000 x 96.000 DPI) +LOG: ISA BCLK: 8333333.333Hz (25000000/3) +LOG: monopal: green, +LOG: Active save slot: 1 [Empty] +LOG: USING AVI+ZMBV +LOG: Pentium CMPXCHG8B emulation is enabled +LOG: Max 1073741824 sz 32768 +LOG: Final 32768 +LOG: Final arrangement: Below 4GB = 32768KB, Above 4GB = 0KB +LOG: VPXLOG: fifosock listening on 127.0.0.1:8621 +LOG: VPXLOG: live render backend started +LOG: VPXLOG: base 0x150 respond=1 handshakes=3 log='C:\Users\cyd\AppData\Local/Temp/vwe-pod/vpxresp.txt' +LOG: MIDI:Opened device:win32 +LOG: mic_input: Selected microphone device: Microphone (Steam Streaming Microphone) +LOG: One joystick reported, initializing with 4axis +LOG: Using joystick Xbox One Controller with 6 axes, 16 buttons and 1 hat(s) +LOG: VOODOO LFB now at d0000000 +LOG: Serial1: BASE 3f8h +LOG: Serial1: namedpipe client of \\.\pipe\vrio (poll 0.10ms burst x16) +LOG: Serial1: namedpipe connected to \\.\pipe\vrio +LOG: Serial2: BASE 2f8h +LOG: Serial2: namedpipe client of \\.\pipe\vplasma (poll 1.00ms burst x1) +LOG: Serial2: namedpipe connected to \\.\pipe\vplasma +LOG: Serial3: BASE 3e8h +LOG: Serial: port 3 will write to file C:\VWE\TeslaRel410\emulator\render-bridge\podlog.txt +LOG: Parallel1: BASE 378h +LOG: MPU-401 Registering I/O ports as if IBM PC MPU-401 at base 330h +LOG: Memory I/O complexity optimization enabled aka option 'memory io optimization 1'. If the game or demo is unable to draw to the screen properly, set the option to false. +LOG: The 'scanline render on demand' option is available and may provide a modest boost in video render performance if set to true. +LOG: Allocated APM BIOS pm entry point at f000:ce40 +LOG: Writing code to fce40 +LOG: Writing code to fce60 +LOG: Windows: IID_ITaskbarList3 is available +LOG: ISA Plug & Play BIOS enabled +LOG: VGA ROM BIOS init callback +LOG: pixratio 1.000, dw false, dh false +LOG: Aspect ratio: 640 x 480 xToY=1.333 yToX=0.750 +LOG: WARNING: No translation support (to host) for code page 0 +LOG: DOS keyboard layout loaded with main language code US for layout us +LOG: XMS: 50 handles allocated for use by the DOS environment +LOG: EMS page frame at 0xe000-0xefff +LOG: COMMAND.COM env size: 720 bytes +LOG: COMMAND.COM environment block: 0x0701 sz=0x002d +LOG: COMMAND.COM main body (PSP): 0x072f sz=0x009a +LOG: COMMAND.COM stack: 0x0749 +LOG: pixratio 1.350, dw false, dh false +LOG: Aspect ratio: 720 x 540 xToY=1.333 yToX=0.750 +LOG: SHELL:Redirect output to COM3 +LOG: VPXLOG: fifosock client connected +VPX lights@f1: type6=0 typeE=0 decoded=0 dir=0 amb(0.00,0.00,0.00) +LOG: 420998823 ERROR MOUSE:Unhandled videomode 79 on reset +LOG: pixratio 1.000, dw false, dh false +LOG: Aspect ratio: 640 x 480 xToY=1.333 yToX=0.750 +VPX lightnode t=6 nb=52: [8]00000000/0 [12]00000000/0 [16]00000001/1.4013e-45 [20]3f800000/1 [24]3f800000/1 [28]3f800000/1 [32]64078678/1e+22 [36]64078678/1e+22 [40]00000000/0 [44]3e25a5b9/0.161765 [48]ffffffff/nan +VPX lights@f121: type6=2 typeE=6 decoded=2 dir=1 amb(0.55,0.50,0.65) + L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) +VPX lights@f241: type6=2 typeE=6 decoded=2 dir=1 amb(0.55,0.50,0.65) + L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) +VPX lights@f361: type6=2 typeE=9 decoded=2 dir=1 amb(0.55,0.50,0.65) + L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) +VPX lights@f481: type6=2 typeE=10 decoded=2 dir=1 amb(0.55,0.50,0.65) + L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) +VPX lights@f601: type6=2 typeE=10 decoded=2 dir=1 amb(0.55,0.50,0.65) + L0 dir(0.11,0.77,0.63) col(1.10,0.90,1.00) diff --git a/emulator/render-bridge/logs/059-prev.txt b/emulator/render-bridge/logs/059-prev.txt new file mode 100644 index 00000000..57d6ce47 --- /dev/null +++ b/emulator/render-bridge/logs/059-prev.txt @@ -0,0 +1,586 @@ +BattleTech v4.10 +BTL4Application::BTL4Application +RIO never came back from test mode! +GaugeInterpreter: undefined label 'Initialization' +[exp] master player experience=2 simLive=1 heatModelOn=1 advDamage=1 +[role] 'Role::Default' lives=1000 killBonus=500 dmgMod=1 +[torso] horizJoint 'jointtorso' -> 0x00bf3d58 type=1 enabled=1 +[weap] 'AFC100' seg=13 mount=sitelgunport rear=0 +[proj] 'AFC100' ammoBinIndex=21 -> AmmoBinAFC100 (rounds=20) +[weap] 'ERLLaser' seg=7 mount=siterugunport rear=0 +[weap] 'ERSLaser_1' seg=13 mount=sitelgunport rear=0 +[weap] 'ERSLaser_2' seg=8 mount=siterdgunport rear=0 +[weap] 'LRM15_1' seg=18 mount=sitelmissleport rear=0 +[proj] 'LRM15_1' ammoBinIndex=26 -> AmmoBinLRM15_1 (rounds=16) +[weap] 'LRM15_2' seg=19 mount=sitermissleport rear=0 +[proj] 'LRM15_2' ammoBinIndex=28 -> AmmoBinLRM15_2 (rounds=16) +[mech] segment walk done: subsystemCount=32 weaponCount=6 +[skel] jointSubsystem=0x009f6d84 jointCount=25 +[mech] model params: walkTR=65 runTR=40 maxAcc=20 throttleAdj=0.8 (deg,deg,u/s^2,scale) +[mech] look angles: L=50 R=-50 F=-15 B=0 (deg) +[mech] damage zones streamed: 22 [dz_dtorso,dz_hip,dz_larm,dz_ldleg,dz_ldoor,dz_lfoot,dz_lgun,dz_ltorso,dz_luleg,dz_rarm,dz_rdleg,dz_rdoor,dz_reardtorso,dz_rearltorso,dz_rearrtorso,dz_rearutorso,dz_rfoot,dz_rgun,dz_rtorso,dz_ruleg,dz_searchlight,dz_utorso] +[cyl] table 'madcat' LOADED rows=7 +[valve] 'Condenser1' valve=1 flow=0.166667 +[valve] 'Condenser2' valve=1 flow=0.166667 +[valve] 'Condenser3' valve=1 flow=0.166667 +[valve] 'Condenser4' valve=1 flow=0.166667 +[valve] 'Condenser5' valve=1 flow=0.166667 +[valve] 'Condenser6' valve=1 flow=0.166667 +[mapper] ctor id=0 throttleAttr=0x009efd04 controlModeAttr=0x009efd40 +[map] 109 streamed mappings for 'L4' +[map] dir sub=0 cls=2012 id=3 grp=1 elem=0 +[map] dir sub=0 cls=2012 id=4 grp=0 elem=0 +[map] dir sub=0 cls=2012 id=6 grp=3 elem=63 +[map] dir sub=17 cls=3013 id=14 grp=3 elem=66 +[map] dir sub=0 cls=2012 id=10 grp=3 elem=68 +[map] dir sub=0 cls=2012 id=11 grp=3 elem=67 +[map] dir sub=0 cls=2012 id=12 grp=3 elem=65 +[map] dir sub=22 cls=3021 id=19 grp=3 elem=64 +[map] dir sub=23 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=24 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=25 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=27 cls=3024 id=19 grp=3 elem=71 +[map] dir sub=29 cls=3024 id=19 grp=3 elem=71 +[gau2] cluster slot=14 classID=3011 auxScreen=5 name=Avionics +[gau2] cluster slot=15 classID=3014 auxScreen=6 name=Myomers +[gau2] cluster slot=22 classID=3021 auxScreen=1 name=AFC100 +[gau2] cluster slot=23 classID=3016 auxScreen=10 name=ERLLaser +[gau2] cluster slot=24 classID=3016 auxScreen=3 name=ERSLaser_1 +[gau2] cluster slot=25 classID=3016 auxScreen=12 name=ERSLaser_2 +[gau2] cluster slot=27 classID=3024 auxScreen=2 name=LRM15_1 +[gau2] cluster slot=29 classID=3024 auxScreen=9 name=LRM15_2 +ColorMapperArmor warning: damage zone dz_missle not found +ColorMapperArmor warning: damage zone dz_missle not found +ColorMapperArmor warning: damage zone dz_lmissle not found +ColorMapperArmor warning: damage zone dz_rmissle not found +ColorMapperArmor warning: damage zone dz_ftorso not found +ColorMapperArmor warning: damage zone dz_btorso not found +ColorMapperCritical warning: subsystem Torso does not exist +ColorMapperHeat warning: subsystem Torso does not exist +[link] InitializePlayerLink -> vehicle playerLink=0x00ad513c +[launch] state=11 minPriorityEmpty=0 ticks=15308 +[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 +[posts] id3=416 +[launch] state=11 minPriorityEmpty=0 ticks=16322 +[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 +[posts] id3=502 +[launch] state=11 minPriorityEmpty=0 ticks=17334 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=11 minPriorityEmpty=0 ticks=18344 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=11 minPriorityEmpty=0 ticks=19354 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=20422 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 1 class=3035 view=0 res=0 +[mer] 2 class=42 view=0 res=1 +[mer] 3 class=42 view=0 res=1 +[mer] 4 class=42 view=0 res=1 +[mer] 5 class=42 view=0 res=1 +[mer] 6 class=42 view=0 res=1 +[mer] 7 class=42 view=0 res=1 +[mer] 8 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=21430 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 9 class=42 view=0 res=1 +[mer] 10 class=42 view=0 res=1 +[mer] 11 class=42 view=0 res=1 +[mer] 12 class=42 view=0 res=1 +[mer] 13 class=42 view=0 res=1 +[mer] 14 class=42 view=0 res=1 +[mer] 15 class=42 view=0 res=1 +[mer] 16 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=22437 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 17 class=42 view=0 res=1 +[mer] 18 class=42 view=0 res=1 +[mer] 19 class=42 view=0 res=1 +[mer] 20 class=42 view=0 res=1 +[mer] 21 class=42 view=0 res=1 +[mer] 22 class=42 view=0 res=1 +[mer] 23 class=42 view=0 res=1 +[mer] 24 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=23445 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 25 class=42 view=0 res=1 +[mer] 26 class=42 view=0 res=1 +[mer] 27 class=42 view=0 res=1 +[mer] 28 class=42 view=0 res=1 +[mer] 29 class=42 view=0 res=1 +[mer] 30 class=42 view=0 res=1 +[mer] 31 class=42 view=0 res=1 +[mer] 32 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=24453 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 33 class=42 view=0 res=1 +[mer] 34 class=42 view=0 res=1 +[mer] 35 class=42 view=0 res=1 +[mer] 36 class=42 view=0 res=1 +[mer] 37 class=42 view=0 res=1 +[mer] 38 class=94 view=0 res=1 +[mer] 39 class=94 view=0 res=1 +[mer] 40 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=25461 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 41 class=94 view=0 res=1 +[mer] 42 class=94 view=0 res=1 +[mer] 43 class=94 view=0 res=1 +[mer] 44 class=94 view=0 res=1 +[mer] 45 class=94 view=0 res=1 +[mer] 46 class=94 view=0 res=1 +[mer] 47 class=94 view=0 res=1 +[mer] 48 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=26468 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 49 class=42 view=0 res=1 +[mer] 50 class=42 view=0 res=1 +[mer] 51 class=42 view=0 res=1 +[mer] 52 class=42 view=0 res=1 +[mer] 53 class=42 view=0 res=1 +[mer] 54 class=94 view=0 res=1 +[mer] 55 class=94 view=0 res=1 +[mer] 56 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=27476 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 57 class=94 view=0 res=1 +[mer] 58 class=42 view=0 res=1 +[mer] 59 class=42 view=0 res=1 +[mer] 60 class=42 view=0 res=1 +[mer] 61 class=42 view=0 res=1 +[mer] 62 class=94 view=0 res=1 +[mer] 63 class=94 view=0 res=1 +[mer] 64 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=28486 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 65 class=94 view=0 res=1 +[mer] 66 class=94 view=0 res=1 +[mer] 67 class=94 view=0 res=1 +[stack] frame at 0x12dee7 drift=0 over 1001 frames ticks=28857 pump=347/1530 +[mer] 68 class=42 view=0 res=1 +[mer] 69 class=94 view=0 res=1 +[mer] 70 class=94 view=0 res=1 +[mer] 71 class=94 view=0 res=1 +[mer] 72 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=29498 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 73 class=94 view=0 res=1 +[mer] 74 class=94 view=0 res=1 +[mer] 75 class=94 view=0 res=1 +[mer] 76 class=42 view=0 res=1 +[mer] 77 class=94 view=0 res=1 +[mer] 78 class=42 view=0 res=1 +[mer] 79 class=94 view=0 res=1 +[mer] 80 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=30506 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 81 class=94 view=0 res=1 +[mer] 82 class=94 view=0 res=1 +[mer] 83 class=94 view=0 res=1 +[mer] 84 class=94 view=0 res=1 +[mer] 85 class=42 view=0 res=1 +[mer] 86 class=94 view=0 res=1 +[mer] 87 class=42 view=0 res=1 +[mer] 88 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=31514 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 89 class=42 view=0 res=1 +[mer] 90 class=42 view=0 res=1 +[mer] 91 class=94 view=0 res=1 +[mer] 92 class=94 view=0 res=1 +[mer] 93 class=42 view=0 res=1 +[mer] 94 class=94 view=0 res=1 +[mer] 95 class=94 view=0 res=1 +[mer] 96 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=32522 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 97 class=94 view=0 res=1 +[mer] 98 class=94 view=0 res=1 +[mer] 99 class=42 view=0 res=1 +[mer] 100 class=42 view=0 res=1 +[mer] 101 class=42 view=0 res=1 +[mer] 102 class=94 view=0 res=1 +[mer] 103 class=42 view=0 res=1 +[mer] 104 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=33530 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 105 class=42 view=0 res=1 +[mer] 106 class=42 view=0 res=1 +[mer] 107 class=42 view=0 res=1 +[mer] 108 class=42 view=0 res=1 +[mer] 109 class=42 view=0 res=1 +[mer] 110 class=42 view=0 res=1 +[mer] 111 class=42 view=0 res=1 +[mer] 112 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=34538 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 113 class=42 view=0 res=1 +[mer] 114 class=42 view=0 res=1 +[mer] 115 class=42 view=0 res=1 +[mer] 116 class=42 view=0 res=1 +[mer] 117 class=42 view=0 res=1 +[mer] 118 class=42 view=0 res=1 +[mer] 119 class=42 view=0 res=1 +[mer] 120 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=35552 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 121 class=42 view=0 res=1 +[mer] 122 class=42 view=0 res=1 +[mer] 123 class=42 view=0 res=1 +[mer] 124 class=42 view=0 res=1 +[mer] 125 class=42 view=0 res=1 +[mer] 126 class=42 view=0 res=1 +[mer] 127 class=42 view=0 res=1 +[mer] 128 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=36563 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 129 class=42 view=0 res=1 +[mer] 130 class=42 view=0 res=1 +[mer] 131 class=42 view=0 res=1 +[mer] 132 class=42 view=0 res=1 +[mer] 133 class=42 view=0 res=1 +[mer] 134 class=42 view=0 res=1 +[mer] 135 class=42 view=0 res=1 +[mer] 136 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=37571 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 137 class=42 view=0 res=1 +[mer] 138 class=42 view=0 res=1 +[mer] 139 class=42 view=0 res=1 +[mer] 140 class=42 view=0 res=1 +[mer] 141 class=42 view=0 res=1 +[mer] 142 class=42 view=0 res=1 +[mer] 143 class=94 view=0 res=1 +[mer] 144 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=38579 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 145 class=94 view=0 res=1 +[mer] 146 class=42 view=0 res=1 +[mer] 147 class=94 view=0 res=1 +[mer] 148 class=42 view=0 res=1 +[mer] 149 class=94 view=0 res=1 +[mer] 150 class=94 view=0 res=1 +[mer] 151 class=94 view=0 res=1 +[mer] 152 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=39589 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 153 class=42 view=0 res=1 +[mer] 154 class=42 view=0 res=1 +[mer] 155 class=94 view=0 res=1 +[mer] 156 class=42 view=0 res=1 +[mer] 157 class=42 view=0 res=1 +[mer] 158 class=94 view=0 res=1 +[mer] 159 class=94 view=0 res=1 +[mer] 160 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=40597 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 161 class=42 view=0 res=1 +[mer] 162 class=42 view=0 res=1 +[mer] 163 class=42 view=0 res=1 +[mer] 164 class=94 view=0 res=1 +[mer] 165 class=94 view=0 res=1 +[mer] 166 class=42 view=0 res=1 +[mer] 167 class=94 view=0 res=1 +[mer] 168 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=41605 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 169 class=94 view=0 res=1 +[mer] 170 class=94 view=0 res=1 +[mer] 171 class=42 view=0 res=1 +[mer] 172 class=42 view=0 res=1 +[mer] 173 class=42 view=0 res=1 +[mer] 174 class=94 view=0 res=1 +[mer] 175 class=42 view=0 res=1 +[mer] 176 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=42613 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 177 class=94 view=0 res=1 +[mer] 178 class=94 view=0 res=1 +[mer] 179 class=94 view=0 res=1 +[mer] 180 class=42 view=0 res=1 +[mer] 181 class=42 view=0 res=1 +[mer] 182 class=42 view=0 res=1 +[mer] 183 class=94 view=0 res=1 +[mer] 184 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=43621 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 185 class=42 view=0 res=1 +[mer] 186 class=94 view=0 res=1 +[mer] 187 class=94 view=0 res=1 +[mer] 188 class=94 view=0 res=1 +[mer] 189 class=42 view=0 res=1 +[mer] 190 class=42 view=0 res=1 +[mer] 191 class=94 view=0 res=1 +[mer] 192 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=44630 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 193 class=42 view=0 res=1 +[mer] 194 class=42 view=0 res=1 +[stack] frame at 0x12dee7 drift=0 over 2002 frames ticks=44890 pump=490/2674 +[mer] 195 class=42 view=0 res=1 +[mer] 196 class=42 view=0 res=1 +[mer] 197 class=94 view=0 res=1 +[mer] 198 class=94 view=0 res=1 +[mer] 199 class=94 view=0 res=1 +[mer] 200 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=45640 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 201 class=94 view=0 res=1 +[mer] 202 class=94 view=0 res=1 +[mer] 203 class=94 view=0 res=1 +[mer] 204 class=42 view=0 res=1 +[mer] 205 class=94 view=0 res=1 +[mer] 206 class=94 view=0 res=1 +[mer] 207 class=48 view=0 res=0 +[mer] 208 class=3001 view=1 res=1 + [vid] type=1 file='mad.skl' + [eye] cockpit eye on 'jointeye' offset=(0,0,0) +[skl] video\mad.skl -> 26 nodes, 19 objects, 1 eye +[launch] state=2 minPriorityEmpty=0 ticks=46648 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=47654 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=48657 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=49769 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=50881 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=51994 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=53106 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=54217 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=55331 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=56443 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=57446 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=58557 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=59670 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=60781 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[stack] frame at 0x12dee7 drift=0 over 3003 frames ticks=60819 pump=633/3818 +[launch] state=2 minPriorityEmpty=0 ticks=61894 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=63007 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=64117 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=65227 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=66339 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=67450 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=68560 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=69672 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=70782 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=2 minPriorityEmpty=0 ticks=71892 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 209 class=48 view=0 res=0 +[mer] 210 class=48 view=0 res=0 +[mer] 211 class=48 view=0 res=0 +[launch] state=2 minPriorityEmpty=0 ticks=73007 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 212 class=48 view=0 res=0 +[mer] 213 class=48 view=0 res=0 +[mer] 214 class=48 view=0 res=0 +[mer] 215 class=48 view=0 res=0 +[launch] state=2 minPriorityEmpty=0 ticks=74011 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 216 class=48 view=0 res=0 +[mer] 217 class=94 view=0 res=1 +[mer] 218 class=94 view=0 res=1 +[mer] 219 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=75020 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 220 class=94 view=0 res=1 +[mer] 221 class=94 view=0 res=1 +[mer] 222 class=94 view=0 res=1 +[mer] 223 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=76030 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 224 class=94 view=0 res=1 +[mer] 225 class=94 view=0 res=1 +[mer] 226 class=94 view=0 res=1 +[stack] frame at 0x12dee7 drift=0 over 4004 frames ticks=76734 pump=776/4962 +[mer] 227 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=77038 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 228 class=94 view=0 res=1 +[mer] 229 class=94 view=0 res=1 +[mer] 230 class=94 view=0 res=1 +[mer] 231 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=78046 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 232 class=94 view=0 res=1 +[mer] 233 class=94 view=0 res=1 +[mer] 234 class=94 view=0 res=1 +[mer] 235 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=79055 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 236 class=94 view=0 res=1 +[mer] 237 class=94 view=0 res=1 +[mer] 238 class=94 view=0 res=1 +[mer] 239 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=80062 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 240 class=94 view=0 res=1 +[mer] 241 class=94 view=0 res=1 +[mer] 242 class=94 view=0 res=1 +[mer] 243 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=81067 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 244 class=94 view=0 res=1 +[mer] 245 class=94 view=0 res=1 +[mer] 246 class=94 view=0 res=1 +[mer] 247 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=82071 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +MapDisplay: operating=1 static=4 moving=0 +[mer] 248 class=94 view=0 res=1 +[mer] 249 class=94 view=0 res=1 +[mer] 250 class=94 view=0 res=1 +[mer] 251 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=83076 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 252 class=94 view=0 res=1 +[mer] 253 class=94 view=0 res=1 +[mer] 254 class=94 view=0 res=1 +[mer] 255 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=84082 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 256 class=94 view=0 res=1 +[mer] 257 class=94 view=0 res=1 +[mer] 258 class=94 view=0 res=1 +[mer] 259 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=85089 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 260 class=94 view=0 res=1 +[mer] 261 class=94 view=0 res=1 +[mer] 262 class=48 view=0 res=0 +[mer] 263 class=48 view=0 res=0 +[launch] state=2 minPriorityEmpty=0 ticks=86096 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 264 class=48 view=0 res=0 +[mer] 265 class=48 view=0 res=0 +[launch] state=2 minPriorityEmpty=1 ticks=87101 +[queues] p0=- p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +BTL4Application::RunMissionMessageHandler +Turning Plasma Score Display On +BTL4Application::RunMissionMessageHandler +Turning Plasma Score Display On +[tick] roster live (first Sensor frame), radarPercent=1 voltState=4 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 1 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(54.6507,20,-652.982) yaw=-0.0742302 spd=14.4 eyePitch=0 eyeYaw=0 +[fire] 'ERSLaser_1' LOADED (level=7925.73 T=76.8866) +[fire] 'ERSLaser_2' LOADED (level=7921.56 T=76.847) +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 2 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(56.2287,20,-638.527) yaw=-0.142123 spd=14.4 eyePitch=0 eyeYaw=0 +[stack] frame at 0x12dee7 drift=0 over 5005 frames ticks=92822 pump=868/6106 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 3 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(58.787,20,-624.198) yaw=-0.210107 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 4 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(62.301,20,-610.117) yaw=-0.277899 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 5 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(66.7461,20,-596.348) yaw=-0.345495 spd=14.4 eyePitch=0 eyeYaw=0 +[fire] 'ERLLaser' LOADED (level=7925.74 T=76.0949) +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 6 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(72.1168,20,-582.897) yaw=-0.413154 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 7 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(78.3914,20,-569.827) yaw=-0.480884 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 8 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(85.5135,20,-557.248) yaw=-0.548412 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 9 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(93.4932,20,-545.144) yaw=-0.616144 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 10 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(102.265,20,-533.618) yaw=-0.683811 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 +[euler] 11 this=0x12dbbc matrix=0x12db70 stack=0x12db5b +[sim] pos=(111.805,20,-522.701) yaw=-0.751545 spd=14.4 eyePitch=0 eyeYaw=0 +[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDema \ No newline at end of file diff --git a/emulator/render-bridge/logs/061-CRASH.txt b/emulator/render-bridge/logs/061-CRASH.txt new file mode 100644 index 00000000..f5b7511d --- /dev/null +++ b/emulator/render-bridge/logs/061-CRASH.txt @@ -0,0 +1,236 @@ +BattleTech v4.10 +BTL4Application::BTL4Application +GaugeInterpreter: undefined label 'Initialization' +[exp] master player experience=2 simLive=1 heatModelOn=1 advDamage=1 +[role] 'Role::Default' lives=1000 killBonus=500 dmgMod=1 +[torso] horizJoint 'jointtorso' -> 0x00a4fe78 type=1 enabled=1 +[weap] 'AFC100' seg=13 mount=sitelgunport rear=0 +[proj] 'AFC100' ammoBinIndex=21 -> AmmoBinAFC100 (rounds=20) +[weap] 'ERLLaser' seg=7 mount=siterugunport rear=0 +[weap] 'ERSLaser_1' seg=13 mount=sitelgunport rear=0 +[weap] 'ERSLaser_2' seg=8 mount=siterdgunport rear=0 +[weap] 'LRM15_1' seg=18 mount=sitelmissleport rear=0 +[proj] 'LRM15_1' ammoBinIndex=26 -> AmmoBinLRM15_1 (rounds=16) +[weap] 'LRM15_2' seg=19 mount=sitermissleport rear=0 +[proj] 'LRM15_2' ammoBinIndex=28 -> AmmoBinLRM15_2 (rounds=16) +[mech] segment walk done: subsystemCount=32 weaponCount=6 +[skel] jointSubsystem=0x009f6d84 jointCount=25 +[mech] model params: walkTR=65 runTR=40 maxAcc=20 throttleAdj=0.8 (deg,deg,u/s^2,scale) +[mech] look angles: L=50 R=-50 F=-15 B=0 (deg) +[mech] damage zones streamed: 22 [dz_dtorso,dz_hip,dz_larm,dz_ldleg,dz_ldoor,dz_lfoot,dz_lgun,dz_ltorso,dz_luleg,dz_rarm,dz_rdleg,dz_rdoor,dz_reardtorso,dz_rearltorso,dz_rearrtorso,dz_rearutorso,dz_rfoot,dz_rgun,dz_rtorso,dz_ruleg,dz_searchlight,dz_utorso] +[cyl] table 'madcat' LOADED rows=7 +[valve] 'Condenser1' valve=1 flow=0.166667 +[valve] 'Condenser2' valve=1 flow=0.166667 +[valve] 'Condenser3' valve=1 flow=0.166667 +[valve] 'Condenser4' valve=1 flow=0.166667 +[valve] 'Condenser5' valve=1 flow=0.166667 +[valve] 'Condenser6' valve=1 flow=0.166667 +[mapper] ctor id=0 throttleAttr=0x002c30f8 controlModeAttr=0x002c3134 +[map] 109 streamed mappings for 'L4' +[map] dir sub=0 cls=2012 id=3 grp=1 elem=0 +[map] dir sub=0 cls=2012 id=4 grp=0 elem=0 +[map] dir sub=0 cls=2012 id=6 grp=3 elem=63 +[map] dir sub=17 cls=3013 id=14 grp=3 elem=66 +[map] dir sub=0 cls=2012 id=10 grp=3 elem=68 +[map] dir sub=0 cls=2012 id=11 grp=3 elem=67 +[map] dir sub=0 cls=2012 id=12 grp=3 elem=65 +[map] dir sub=22 cls=3021 id=19 grp=3 elem=64 +[map] dir sub=23 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=24 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=25 cls=3016 id=19 grp=3 elem=64 +[map] dir sub=27 cls=3024 id=19 grp=3 elem=71 +[map] dir sub=29 cls=3024 id=19 grp=3 elem=71 +[gau2] cluster slot=14 classID=3011 auxScreen=5 name=Avionics +[gau2] cluster slot=15 classID=3014 auxScreen=6 name=Myomers +[gau2] cluster slot=22 classID=3021 auxScreen=1 name=AFC100 +[gau2] cluster slot=23 classID=3016 auxScreen=10 name=ERLLaser +[gau2] cluster slot=24 classID=3016 auxScreen=3 name=ERSLaser_1 +[gau2] cluster slot=25 classID=3016 auxScreen=12 name=ERSLaser_2 +[gau2] cluster slot=27 classID=3024 auxScreen=2 name=LRM15_1 +[gau2] cluster slot=29 classID=3024 auxScreen=9 name=LRM15_2 +ColorMapperArmor warning: damage zone dz_missle not found +ColorMapperArmor warning: damage zone dz_missle not found +ColorMapperArmor warning: damage zone dz_lmissle not found +ColorMapperArmor warning: damage zone dz_rmissle not found +ColorMapperArmor warning: damage zone dz_ftorso not found +ColorMapperArmor warning: damage zone dz_btorso not found +ColorMapperCritical warning: subsystem Torso does not exist +ColorMapperHeat warning: subsystem Torso does not exist +[link] InitializePlayerLink -> vehicle playerLink=0x00ad513c +[launch] state=11 minPriorityEmpty=0 ticks=10952 +[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 +[posts] id3=416 +[launch] state=11 minPriorityEmpty=0 ticks=11964 +[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 +[posts] id3=502 +[launch] state=11 minPriorityEmpty=0 ticks=12974 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=11 minPriorityEmpty=0 ticks=13983 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[launch] state=11 minPriorityEmpty=0 ticks=14992 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 1 class=3035 view=0 res=0 +[mer] 2 class=42 view=0 res=1 +[mer] 3 class=42 view=0 res=1 +[mer] 4 class=42 view=0 res=1 +[mer] 5 class=42 view=0 res=1 +[mer] 6 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=16093 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 7 class=42 view=0 res=1 +[mer] 8 class=42 view=0 res=1 +[mer] 9 class=42 view=0 res=1 +[mer] 10 class=42 view=0 res=1 +[mer] 11 class=42 view=0 res=1 +[mer] 12 class=42 view=0 res=1 +[mer] 13 class=42 view=0 res=1 +[mer] 14 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=17143 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 15 class=42 view=0 res=1 +[mer] 16 class=42 view=0 res=1 +[mer] 17 class=42 view=0 res=1 +[mer] 18 class=42 view=0 res=1 +[mer] 19 class=42 view=0 res=1 +[mer] 20 class=42 view=0 res=1 +[mer] 21 class=42 view=0 res=1 +[mer] 22 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=18204 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 23 class=42 view=0 res=1 +[mer] 24 class=42 view=0 res=1 +[mer] 25 class=42 view=0 res=1 +[mer] 26 class=42 view=0 res=1 +[mer] 27 class=42 view=0 res=1 +[mer] 28 class=42 view=0 res=1 +[mer] 29 class=42 view=0 res=1 +[mer] 30 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=19266 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 31 class=42 view=0 res=1 +[mer] 32 class=42 view=0 res=1 +[mer] 33 class=42 view=0 res=1 +[mer] 34 class=42 view=0 res=1 +[mer] 35 class=42 view=0 res=1 +[mer] 36 class=42 view=0 res=1 +[mer] 37 class=42 view=0 res=1 +[mer] 38 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=20328 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 39 class=94 view=0 res=1 +[mer] 40 class=42 view=0 res=1 +[mer] 41 class=94 view=0 res=1 +[mer] 42 class=94 view=0 res=1 +[mer] 43 class=94 view=0 res=1 +[mer] 44 class=94 view=0 res=1 +[mer] 45 class=94 view=0 res=1 +[mer] 46 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=21390 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 47 class=94 view=0 res=1 +[mer] 48 class=94 view=0 res=1 +[mer] 49 class=42 view=0 res=1 +[mer] 50 class=42 view=0 res=1 +[mer] 51 class=42 view=0 res=1 +[mer] 52 class=42 view=0 res=1 +[mer] 53 class=42 view=0 res=1 +[mer] 54 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=22451 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 55 class=94 view=0 res=1 +[mer] 56 class=94 view=0 res=1 +[mer] 57 class=94 view=0 res=1 +[mer] 58 class=42 view=0 res=1 +[mer] 59 class=42 view=0 res=1 +[mer] 60 class=42 view=0 res=1 +[mer] 61 class=42 view=0 res=1 +[mer] 62 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=23512 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 63 class=94 view=0 res=1 +[mer] 64 class=94 view=0 res=1 +[mer] 65 class=94 view=0 res=1 +[mer] 66 class=94 view=0 res=1 +[mer] 67 class=94 view=0 res=1 +[mer] 68 class=42 view=0 res=1 +[mer] 69 class=94 view=0 res=1 +[mer] 70 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=24573 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 71 class=94 view=0 res=1 +[mer] 72 class=94 view=0 res=1 +[stack] frame at 0x12dee7 drift=0 over 1001 frames ticks=24846 pump=352/1499 +[mer] 73 class=94 view=0 res=1 +[mer] 74 class=94 view=0 res=1 +[mer] 75 class=94 view=0 res=1 +[mer] 76 class=42 view=0 res=1 +[mer] 77 class=94 view=0 res=1 +[mer] 78 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=25635 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 79 class=94 view=0 res=1 +[mer] 80 class=94 view=0 res=1 +[mer] 81 class=94 view=0 res=1 +[mer] 82 class=94 view=0 res=1 +[mer] 83 class=94 view=0 res=1 +[mer] 84 class=94 view=0 res=1 +[mer] 85 class=42 view=0 res=1 +[mer] 86 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=26697 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 87 class=42 view=0 res=1 +[mer] 88 class=42 view=0 res=1 +[mer] 89 class=42 view=0 res=1 +[mer] 90 class=42 view=0 res=1 +[mer] 91 class=94 view=0 res=1 +[mer] 92 class=94 view=0 res=1 +[mer] 93 class=42 view=0 res=1 +[mer] 94 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=27759 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 95 class=94 view=0 res=1 +[mer] 96 class=94 view=0 res=1 +[mer] 97 class=94 view=0 res=1 +[mer] 98 class=94 view=0 res=1 +[mer] 99 class=42 view=0 res=1 +[mer] 100 class=42 view=0 res=1 +[mer] 101 class=42 view=0 res=1 +[mer] 102 class=94 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=28821 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 103 class=42 view=0 res=1 +[mer] 104 class=42 view=0 res=1 +[mer] 105 class=42 view=0 res=1 +[mer] 106 class=42 view=0 res=1 +[mer] 107 class=42 view=0 res=1 +[mer] 108 class=42 view=0 res=1 +[mer] 109 class=42 view=0 res=1 +[mer] 110 class=42 view=0 res=1 +[launch] state=2 minPriorityEmpty=0 ticks=29883 +[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 +[posts] id3=530 +[mer] 111 class=42 view=0 res=1 +[mer] 112 class=42 view=0 res=1 +[mer] 113 class=42 view=0 res=1 +[mer] 114 class=42 view=0 res=1 +[mer] 115 class=42 view=0 res=1 +[mer] 116 class=42 view=0 res=1 +Reference to a page you don't own +PF cr2= 7000 FA64 at 00FF 0000 66D9 +Unhandled exception 000E at 00FF 66D9 ErrCode 0004 diff --git a/emulator/render-bridge/logs/061-dosbox.txt b/emulator/render-bridge/logs/061-dosbox.txt new file mode 100644 index 00000000..271f7089 --- /dev/null +++ b/emulator/render-bridge/logs/061-dosbox.txt @@ -0,0 +1,13 @@ +LOG: Early LOG Init complete +LOG: CONFIG: Loaded config file: C:/VWE/TeslaRel410/emulator/render-bridge/pod_render_shp.conf +LOG: DOSBox-X's working directory: C:\VWE\TeslaRel410\emulator\src\src +LOG: Logging init: beginning logging proper. This is the end of the early init logging +LOG: Logging: No logfile was given. All further logging will be discarded. +LOG: DOSBox-X version 2026.06.02 Commit d75a57c (MinGW SDL2 64-bit) +LOG: Prevent capture: 0 +LOG: SDL: version 2.32.10, Video windows, Audio directsound +LOG: EXPERIMENTAL: Debugger enabled for MinGW build, DOSBox-X crashes depending on the terminal software you use. Launching from command prompt (cmd.exe) is recommended. +LOG: Windows keyboard layout ID is 0x0409 +LOG: Host keyboard layout is now us (US English) +LOG: Mapper keyboard layout is now us (US English) +LOG: SDL2 reports desktop display mode 3440 x 1440 diff --git a/emulator/render-bridge/podlog.txt b/emulator/render-bridge/podlog.txt index 57d6ce47..c7b130e2 100644 --- a/emulator/render-bridge/podlog.txt +++ b/emulator/render-bridge/podlog.txt @@ -1,586 +1,2 @@ BattleTech v4.10 BTL4Application::BTL4Application -RIO never came back from test mode! -GaugeInterpreter: undefined label 'Initialization' -[exp] master player experience=2 simLive=1 heatModelOn=1 advDamage=1 -[role] 'Role::Default' lives=1000 killBonus=500 dmgMod=1 -[torso] horizJoint 'jointtorso' -> 0x00bf3d58 type=1 enabled=1 -[weap] 'AFC100' seg=13 mount=sitelgunport rear=0 -[proj] 'AFC100' ammoBinIndex=21 -> AmmoBinAFC100 (rounds=20) -[weap] 'ERLLaser' seg=7 mount=siterugunport rear=0 -[weap] 'ERSLaser_1' seg=13 mount=sitelgunport rear=0 -[weap] 'ERSLaser_2' seg=8 mount=siterdgunport rear=0 -[weap] 'LRM15_1' seg=18 mount=sitelmissleport rear=0 -[proj] 'LRM15_1' ammoBinIndex=26 -> AmmoBinLRM15_1 (rounds=16) -[weap] 'LRM15_2' seg=19 mount=sitermissleport rear=0 -[proj] 'LRM15_2' ammoBinIndex=28 -> AmmoBinLRM15_2 (rounds=16) -[mech] segment walk done: subsystemCount=32 weaponCount=6 -[skel] jointSubsystem=0x009f6d84 jointCount=25 -[mech] model params: walkTR=65 runTR=40 maxAcc=20 throttleAdj=0.8 (deg,deg,u/s^2,scale) -[mech] look angles: L=50 R=-50 F=-15 B=0 (deg) -[mech] damage zones streamed: 22 [dz_dtorso,dz_hip,dz_larm,dz_ldleg,dz_ldoor,dz_lfoot,dz_lgun,dz_ltorso,dz_luleg,dz_rarm,dz_rdleg,dz_rdoor,dz_reardtorso,dz_rearltorso,dz_rearrtorso,dz_rearutorso,dz_rfoot,dz_rgun,dz_rtorso,dz_ruleg,dz_searchlight,dz_utorso] -[cyl] table 'madcat' LOADED rows=7 -[valve] 'Condenser1' valve=1 flow=0.166667 -[valve] 'Condenser2' valve=1 flow=0.166667 -[valve] 'Condenser3' valve=1 flow=0.166667 -[valve] 'Condenser4' valve=1 flow=0.166667 -[valve] 'Condenser5' valve=1 flow=0.166667 -[valve] 'Condenser6' valve=1 flow=0.166667 -[mapper] ctor id=0 throttleAttr=0x009efd04 controlModeAttr=0x009efd40 -[map] 109 streamed mappings for 'L4' -[map] dir sub=0 cls=2012 id=3 grp=1 elem=0 -[map] dir sub=0 cls=2012 id=4 grp=0 elem=0 -[map] dir sub=0 cls=2012 id=6 grp=3 elem=63 -[map] dir sub=17 cls=3013 id=14 grp=3 elem=66 -[map] dir sub=0 cls=2012 id=10 grp=3 elem=68 -[map] dir sub=0 cls=2012 id=11 grp=3 elem=67 -[map] dir sub=0 cls=2012 id=12 grp=3 elem=65 -[map] dir sub=22 cls=3021 id=19 grp=3 elem=64 -[map] dir sub=23 cls=3016 id=19 grp=3 elem=64 -[map] dir sub=24 cls=3016 id=19 grp=3 elem=64 -[map] dir sub=25 cls=3016 id=19 grp=3 elem=64 -[map] dir sub=27 cls=3024 id=19 grp=3 elem=71 -[map] dir sub=29 cls=3024 id=19 grp=3 elem=71 -[gau2] cluster slot=14 classID=3011 auxScreen=5 name=Avionics -[gau2] cluster slot=15 classID=3014 auxScreen=6 name=Myomers -[gau2] cluster slot=22 classID=3021 auxScreen=1 name=AFC100 -[gau2] cluster slot=23 classID=3016 auxScreen=10 name=ERLLaser -[gau2] cluster slot=24 classID=3016 auxScreen=3 name=ERSLaser_1 -[gau2] cluster slot=25 classID=3016 auxScreen=12 name=ERSLaser_2 -[gau2] cluster slot=27 classID=3024 auxScreen=2 name=LRM15_1 -[gau2] cluster slot=29 classID=3024 auxScreen=9 name=LRM15_2 -ColorMapperArmor warning: damage zone dz_missle not found -ColorMapperArmor warning: damage zone dz_missle not found -ColorMapperArmor warning: damage zone dz_lmissle not found -ColorMapperArmor warning: damage zone dz_rmissle not found -ColorMapperArmor warning: damage zone dz_ftorso not found -ColorMapperArmor warning: damage zone dz_btorso not found -ColorMapperCritical warning: subsystem Torso does not exist -ColorMapperHeat warning: subsystem Torso does not exist -[link] InitializePlayerLink -> vehicle playerLink=0x00ad513c -[launch] state=11 minPriorityEmpty=0 ticks=15308 -[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 -[posts] id3=416 -[launch] state=11 minPriorityEmpty=0 ticks=16322 -[queues] p0=BUSY p1=BUSY p2=BUSY p3=BUSY p4=- nextReady=1 -[posts] id3=502 -[launch] state=11 minPriorityEmpty=0 ticks=17334 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=11 minPriorityEmpty=0 ticks=18344 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=11 minPriorityEmpty=0 ticks=19354 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=20422 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 1 class=3035 view=0 res=0 -[mer] 2 class=42 view=0 res=1 -[mer] 3 class=42 view=0 res=1 -[mer] 4 class=42 view=0 res=1 -[mer] 5 class=42 view=0 res=1 -[mer] 6 class=42 view=0 res=1 -[mer] 7 class=42 view=0 res=1 -[mer] 8 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=21430 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 9 class=42 view=0 res=1 -[mer] 10 class=42 view=0 res=1 -[mer] 11 class=42 view=0 res=1 -[mer] 12 class=42 view=0 res=1 -[mer] 13 class=42 view=0 res=1 -[mer] 14 class=42 view=0 res=1 -[mer] 15 class=42 view=0 res=1 -[mer] 16 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=22437 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 17 class=42 view=0 res=1 -[mer] 18 class=42 view=0 res=1 -[mer] 19 class=42 view=0 res=1 -[mer] 20 class=42 view=0 res=1 -[mer] 21 class=42 view=0 res=1 -[mer] 22 class=42 view=0 res=1 -[mer] 23 class=42 view=0 res=1 -[mer] 24 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=23445 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 25 class=42 view=0 res=1 -[mer] 26 class=42 view=0 res=1 -[mer] 27 class=42 view=0 res=1 -[mer] 28 class=42 view=0 res=1 -[mer] 29 class=42 view=0 res=1 -[mer] 30 class=42 view=0 res=1 -[mer] 31 class=42 view=0 res=1 -[mer] 32 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=24453 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 33 class=42 view=0 res=1 -[mer] 34 class=42 view=0 res=1 -[mer] 35 class=42 view=0 res=1 -[mer] 36 class=42 view=0 res=1 -[mer] 37 class=42 view=0 res=1 -[mer] 38 class=94 view=0 res=1 -[mer] 39 class=94 view=0 res=1 -[mer] 40 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=25461 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 41 class=94 view=0 res=1 -[mer] 42 class=94 view=0 res=1 -[mer] 43 class=94 view=0 res=1 -[mer] 44 class=94 view=0 res=1 -[mer] 45 class=94 view=0 res=1 -[mer] 46 class=94 view=0 res=1 -[mer] 47 class=94 view=0 res=1 -[mer] 48 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=26468 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 49 class=42 view=0 res=1 -[mer] 50 class=42 view=0 res=1 -[mer] 51 class=42 view=0 res=1 -[mer] 52 class=42 view=0 res=1 -[mer] 53 class=42 view=0 res=1 -[mer] 54 class=94 view=0 res=1 -[mer] 55 class=94 view=0 res=1 -[mer] 56 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=27476 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 57 class=94 view=0 res=1 -[mer] 58 class=42 view=0 res=1 -[mer] 59 class=42 view=0 res=1 -[mer] 60 class=42 view=0 res=1 -[mer] 61 class=42 view=0 res=1 -[mer] 62 class=94 view=0 res=1 -[mer] 63 class=94 view=0 res=1 -[mer] 64 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=28486 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 65 class=94 view=0 res=1 -[mer] 66 class=94 view=0 res=1 -[mer] 67 class=94 view=0 res=1 -[stack] frame at 0x12dee7 drift=0 over 1001 frames ticks=28857 pump=347/1530 -[mer] 68 class=42 view=0 res=1 -[mer] 69 class=94 view=0 res=1 -[mer] 70 class=94 view=0 res=1 -[mer] 71 class=94 view=0 res=1 -[mer] 72 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=29498 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 73 class=94 view=0 res=1 -[mer] 74 class=94 view=0 res=1 -[mer] 75 class=94 view=0 res=1 -[mer] 76 class=42 view=0 res=1 -[mer] 77 class=94 view=0 res=1 -[mer] 78 class=42 view=0 res=1 -[mer] 79 class=94 view=0 res=1 -[mer] 80 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=30506 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 81 class=94 view=0 res=1 -[mer] 82 class=94 view=0 res=1 -[mer] 83 class=94 view=0 res=1 -[mer] 84 class=94 view=0 res=1 -[mer] 85 class=42 view=0 res=1 -[mer] 86 class=94 view=0 res=1 -[mer] 87 class=42 view=0 res=1 -[mer] 88 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=31514 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 89 class=42 view=0 res=1 -[mer] 90 class=42 view=0 res=1 -[mer] 91 class=94 view=0 res=1 -[mer] 92 class=94 view=0 res=1 -[mer] 93 class=42 view=0 res=1 -[mer] 94 class=94 view=0 res=1 -[mer] 95 class=94 view=0 res=1 -[mer] 96 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=32522 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 97 class=94 view=0 res=1 -[mer] 98 class=94 view=0 res=1 -[mer] 99 class=42 view=0 res=1 -[mer] 100 class=42 view=0 res=1 -[mer] 101 class=42 view=0 res=1 -[mer] 102 class=94 view=0 res=1 -[mer] 103 class=42 view=0 res=1 -[mer] 104 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=33530 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 105 class=42 view=0 res=1 -[mer] 106 class=42 view=0 res=1 -[mer] 107 class=42 view=0 res=1 -[mer] 108 class=42 view=0 res=1 -[mer] 109 class=42 view=0 res=1 -[mer] 110 class=42 view=0 res=1 -[mer] 111 class=42 view=0 res=1 -[mer] 112 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=34538 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 113 class=42 view=0 res=1 -[mer] 114 class=42 view=0 res=1 -[mer] 115 class=42 view=0 res=1 -[mer] 116 class=42 view=0 res=1 -[mer] 117 class=42 view=0 res=1 -[mer] 118 class=42 view=0 res=1 -[mer] 119 class=42 view=0 res=1 -[mer] 120 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=35552 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 121 class=42 view=0 res=1 -[mer] 122 class=42 view=0 res=1 -[mer] 123 class=42 view=0 res=1 -[mer] 124 class=42 view=0 res=1 -[mer] 125 class=42 view=0 res=1 -[mer] 126 class=42 view=0 res=1 -[mer] 127 class=42 view=0 res=1 -[mer] 128 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=36563 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 129 class=42 view=0 res=1 -[mer] 130 class=42 view=0 res=1 -[mer] 131 class=42 view=0 res=1 -[mer] 132 class=42 view=0 res=1 -[mer] 133 class=42 view=0 res=1 -[mer] 134 class=42 view=0 res=1 -[mer] 135 class=42 view=0 res=1 -[mer] 136 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=37571 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 137 class=42 view=0 res=1 -[mer] 138 class=42 view=0 res=1 -[mer] 139 class=42 view=0 res=1 -[mer] 140 class=42 view=0 res=1 -[mer] 141 class=42 view=0 res=1 -[mer] 142 class=42 view=0 res=1 -[mer] 143 class=94 view=0 res=1 -[mer] 144 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=38579 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 145 class=94 view=0 res=1 -[mer] 146 class=42 view=0 res=1 -[mer] 147 class=94 view=0 res=1 -[mer] 148 class=42 view=0 res=1 -[mer] 149 class=94 view=0 res=1 -[mer] 150 class=94 view=0 res=1 -[mer] 151 class=94 view=0 res=1 -[mer] 152 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=39589 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 153 class=42 view=0 res=1 -[mer] 154 class=42 view=0 res=1 -[mer] 155 class=94 view=0 res=1 -[mer] 156 class=42 view=0 res=1 -[mer] 157 class=42 view=0 res=1 -[mer] 158 class=94 view=0 res=1 -[mer] 159 class=94 view=0 res=1 -[mer] 160 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=40597 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 161 class=42 view=0 res=1 -[mer] 162 class=42 view=0 res=1 -[mer] 163 class=42 view=0 res=1 -[mer] 164 class=94 view=0 res=1 -[mer] 165 class=94 view=0 res=1 -[mer] 166 class=42 view=0 res=1 -[mer] 167 class=94 view=0 res=1 -[mer] 168 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=41605 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 169 class=94 view=0 res=1 -[mer] 170 class=94 view=0 res=1 -[mer] 171 class=42 view=0 res=1 -[mer] 172 class=42 view=0 res=1 -[mer] 173 class=42 view=0 res=1 -[mer] 174 class=94 view=0 res=1 -[mer] 175 class=42 view=0 res=1 -[mer] 176 class=42 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=42613 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 177 class=94 view=0 res=1 -[mer] 178 class=94 view=0 res=1 -[mer] 179 class=94 view=0 res=1 -[mer] 180 class=42 view=0 res=1 -[mer] 181 class=42 view=0 res=1 -[mer] 182 class=42 view=0 res=1 -[mer] 183 class=94 view=0 res=1 -[mer] 184 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=43621 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 185 class=42 view=0 res=1 -[mer] 186 class=94 view=0 res=1 -[mer] 187 class=94 view=0 res=1 -[mer] 188 class=94 view=0 res=1 -[mer] 189 class=42 view=0 res=1 -[mer] 190 class=42 view=0 res=1 -[mer] 191 class=94 view=0 res=1 -[mer] 192 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=44630 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 193 class=42 view=0 res=1 -[mer] 194 class=42 view=0 res=1 -[stack] frame at 0x12dee7 drift=0 over 2002 frames ticks=44890 pump=490/2674 -[mer] 195 class=42 view=0 res=1 -[mer] 196 class=42 view=0 res=1 -[mer] 197 class=94 view=0 res=1 -[mer] 198 class=94 view=0 res=1 -[mer] 199 class=94 view=0 res=1 -[mer] 200 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=45640 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 201 class=94 view=0 res=1 -[mer] 202 class=94 view=0 res=1 -[mer] 203 class=94 view=0 res=1 -[mer] 204 class=42 view=0 res=1 -[mer] 205 class=94 view=0 res=1 -[mer] 206 class=94 view=0 res=1 -[mer] 207 class=48 view=0 res=0 -[mer] 208 class=3001 view=1 res=1 - [vid] type=1 file='mad.skl' - [eye] cockpit eye on 'jointeye' offset=(0,0,0) -[skl] video\mad.skl -> 26 nodes, 19 objects, 1 eye -[launch] state=2 minPriorityEmpty=0 ticks=46648 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=47654 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=48657 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=49769 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=50881 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=51994 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=53106 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=54217 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=55331 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=56443 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=57446 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=58557 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=59670 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=60781 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[stack] frame at 0x12dee7 drift=0 over 3003 frames ticks=60819 pump=633/3818 -[launch] state=2 minPriorityEmpty=0 ticks=61894 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=63007 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=64117 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=65227 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=66339 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=67450 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=68560 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=69672 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=70782 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[launch] state=2 minPriorityEmpty=0 ticks=71892 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 209 class=48 view=0 res=0 -[mer] 210 class=48 view=0 res=0 -[mer] 211 class=48 view=0 res=0 -[launch] state=2 minPriorityEmpty=0 ticks=73007 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 212 class=48 view=0 res=0 -[mer] 213 class=48 view=0 res=0 -[mer] 214 class=48 view=0 res=0 -[mer] 215 class=48 view=0 res=0 -[launch] state=2 minPriorityEmpty=0 ticks=74011 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 216 class=48 view=0 res=0 -[mer] 217 class=94 view=0 res=1 -[mer] 218 class=94 view=0 res=1 -[mer] 219 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=75020 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 220 class=94 view=0 res=1 -[mer] 221 class=94 view=0 res=1 -[mer] 222 class=94 view=0 res=1 -[mer] 223 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=76030 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 224 class=94 view=0 res=1 -[mer] 225 class=94 view=0 res=1 -[mer] 226 class=94 view=0 res=1 -[stack] frame at 0x12dee7 drift=0 over 4004 frames ticks=76734 pump=776/4962 -[mer] 227 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=77038 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 228 class=94 view=0 res=1 -[mer] 229 class=94 view=0 res=1 -[mer] 230 class=94 view=0 res=1 -[mer] 231 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=78046 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 232 class=94 view=0 res=1 -[mer] 233 class=94 view=0 res=1 -[mer] 234 class=94 view=0 res=1 -[mer] 235 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=79055 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 236 class=94 view=0 res=1 -[mer] 237 class=94 view=0 res=1 -[mer] 238 class=94 view=0 res=1 -[mer] 239 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=80062 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 240 class=94 view=0 res=1 -[mer] 241 class=94 view=0 res=1 -[mer] 242 class=94 view=0 res=1 -[mer] 243 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=81067 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 244 class=94 view=0 res=1 -[mer] 245 class=94 view=0 res=1 -[mer] 246 class=94 view=0 res=1 -[mer] 247 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=82071 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -MapDisplay: operating=1 static=4 moving=0 -[mer] 248 class=94 view=0 res=1 -[mer] 249 class=94 view=0 res=1 -[mer] 250 class=94 view=0 res=1 -[mer] 251 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=83076 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 252 class=94 view=0 res=1 -[mer] 253 class=94 view=0 res=1 -[mer] 254 class=94 view=0 res=1 -[mer] 255 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=84082 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 256 class=94 view=0 res=1 -[mer] 257 class=94 view=0 res=1 -[mer] 258 class=94 view=0 res=1 -[mer] 259 class=94 view=0 res=1 -[launch] state=2 minPriorityEmpty=0 ticks=85089 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 260 class=94 view=0 res=1 -[mer] 261 class=94 view=0 res=1 -[mer] 262 class=48 view=0 res=0 -[mer] 263 class=48 view=0 res=0 -[launch] state=2 minPriorityEmpty=0 ticks=86096 -[queues] p0=BUSY p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -[mer] 264 class=48 view=0 res=0 -[mer] 265 class=48 view=0 res=0 -[launch] state=2 minPriorityEmpty=1 ticks=87101 -[queues] p0=- p1=- p2=BUSY p3=- p4=- nextReady=1 -[posts] id3=530 -BTL4Application::RunMissionMessageHandler -Turning Plasma Score Display On -BTL4Application::RunMissionMessageHandler -Turning Plasma Score Display On -[tick] roster live (first Sensor frame), radarPercent=1 voltState=4 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 1 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(54.6507,20,-652.982) yaw=-0.0742302 spd=14.4 eyePitch=0 eyeYaw=0 -[fire] 'ERSLaser_1' LOADED (level=7925.73 T=76.8866) -[fire] 'ERSLaser_2' LOADED (level=7921.56 T=76.847) -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 2 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(56.2287,20,-638.527) yaw=-0.142123 spd=14.4 eyePitch=0 eyeYaw=0 -[stack] frame at 0x12dee7 drift=0 over 5005 frames ticks=92822 pump=868/6106 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 3 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(58.787,20,-624.198) yaw=-0.210107 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 4 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(62.301,20,-610.117) yaw=-0.277899 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 5 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(66.7461,20,-596.348) yaw=-0.345495 spd=14.4 eyePitch=0 eyeYaw=0 -[fire] 'ERLLaser' LOADED (level=7925.74 T=76.0949) -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 6 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(72.1168,20,-582.897) yaw=-0.413154 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 7 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(78.3914,20,-569.827) yaw=-0.480884 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 8 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(85.5135,20,-557.248) yaw=-0.548412 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 9 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(93.4932,20,-545.144) yaw=-0.616144 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 10 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(102.265,20,-533.618) yaw=-0.683811 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDemand=0.0625 torsoElev=0 -[euler] 11 this=0x12dbbc matrix=0x12db70 stack=0x12db5b -[sim] pos=(111.805,20,-522.701) yaw=-0.751545 spd=14.4 eyePitch=0 eyeYaw=0 -[mppr] thr=0.6 rev=0 stickX=0.25 stickY=0 mode=0 -> speedDemand=14.4 turnDema \ No newline at end of file diff --git a/emulator/render-bridge/podlog_shipped.txt b/emulator/render-bridge/podlog_shipped.txt index 4d749fa5..faf1a9cf 100644 --- a/emulator/render-bridge/podlog_shipped.txt +++ b/emulator/render-bridge/podlog_shipped.txt @@ -1,7 +1,7 @@ BattleTech v4.10 BTL4Application::BTL4Application GaugeInterpreter: undefined label 'Initialization' -MakeVehicleSystems hud=0x01158dcc +MakeVehicleSystems hud=0x01157f6c Warning: damage zone 'dz_missle' not found! Warning: damage zone 'dz_missle' not found! Warning: damage zone 'dz_lmissle' not found! @@ -9,36 +9,26 @@ Warning: damage zone 'dz_rmissle' not found! Warning: damage zone 'dz_ftorso' not found! Warning: damage zone 'dz_btorso' not found! LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =4252 +iThinkIRQIsOn =1, actual state =1, characters sent =373 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =4408 +iThinkIRQIsOn =1, actual state =1, characters sent =668 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =6128 +iThinkIRQIsOn =1, actual state =1, characters sent =743 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =6417 +iThinkIRQIsOn =1, actual state =1, characters sent =747 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =6783 +iThinkIRQIsOn =1, actual state =1, characters sent =1888 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =7185 +iThinkIRQIsOn =1, actual state =1, characters sent =1914 LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =7193 +iThinkIRQIsOn =1, actual state =1, characters sent =2158 +LBE4ControlsManager::Execute, lost RIO analog request +iThinkIRQIsOn =1, actual state =1, characters sent =4085 +LBE4ControlsManager::Execute, lost RIO analog request +iThinkIRQIsOn =1, actual state =1, characters sent =6986 +LBE4ControlsManager::Execute, lost RIO analog request +iThinkIRQIsOn =1, actual state =1, characters sent =8237 BTL4Application::RunMissionMessageHandler Turning Plasma Score Display On BTL4Application::RunMissionMessageHandler Turning Plasma Score Display On -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =9235 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =9240 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =11662 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =11666 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =12328 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =12332 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =13939 -LBE4ControlsManager::Execute, lost RIO analog request -iThinkIRQIsOn =1, actual state =1, characters sent =13943 diff --git a/emulator/render-bridge/podrun.sh b/emulator/render-bridge/podrun.sh index 8a3b09d9..80259a91 100644 --- a/emulator/render-bridge/podrun.sh +++ b/emulator/render-bridge/podrun.sh @@ -89,6 +89,12 @@ export RIO_TAP="$WORK/riotap.txt" # export VPX_FIFODUMP="$WORK/live.fifodump" export VPX_FIFOSOCK=8621 +# +# Arm the page-fault watch on the residual load-window fault (cpu/paging.cpp +# VPX_PFWatch). When cr2 hits this address the emulator dumps guest state + +# the last 64 serial RX deliveries to its stderr (dosboxlog.txt). +# +export VPX_PF_WATCH=7000FA64 rm -f "$BR/podlog.txt" cd /c/VWE/TeslaRel410/emulator/src/src diff --git a/emulator/render-bridge/shpstreak.sh b/emulator/render-bridge/shpstreak.sh new file mode 100644 index 00000000..dc8597ea --- /dev/null +++ b/emulator/render-bridge/shpstreak.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Four consecutive SHIPPED-exe runs with vRIO live: the exposure baseline. +# Shipped's log goes to podlog_shipped.txt (its conf's serial3). +cd /c/VWE/TeslaRel410/emulator/render-bridge +for i in 1 2 3 4; do + rm -f podlog_shipped.txt + bash podrun.sh pod_render_shp shpstreak$i > /dev/null 2>&1 + until grep -q "Turning Plasma" podlog_shipped.txt 2>/dev/null || grep -qE "illegal address|page you don't own" podlog_shipped.txt 2>/dev/null; do sleep 10; done + if grep -q "page you don't own" podlog_shipped.txt; then R=FAULT; else R=CLEAN; fi + echo "shipped run$i: $R" +done +taskkill //F //IM dosbox-x.exe > /dev/null 2>&1 +echo "SHIPPED STREAK DONE" diff --git a/emulator/vpx-device/README.md b/emulator/vpx-device/README.md index e96b09e1..54cffcc0 100644 --- a/emulator/vpx-device/README.md +++ b/emulator/vpx-device/README.md @@ -83,6 +83,33 @@ version control because the DOSBox-X source tree itself vRIO-over-pipe keeps `namedpipe pipe:vrio`. Confs: `net_loop_rio.conf` / `net_rp_rio.conf`; `pod-launch --rio`. B0 (pad) LIVE-CONFIRMED 2026-07-22 from the repacked dist; keyboard-field live test pending. +- **`steamlink.cpp` / `steamlink.h`** — the Steam transport core (2026-07-24): + guest Ethernet **frames** over `ISteamNetworkingMessages`, so a private group + can fly missions over the internet. Shared VERBATIM by two consumers — the + fork's `backend=steam` and the host's standalone `vwe-steam-gw.exe` + steam↔pcap gateway — which is why it is a free-function namespace and not a + class. **L2, not L3, is the whole point:** the 1995 mesh dials peers by raw + IP on one flat subnet and ARPs for them, and NAT/hole-punching has nowhere to + put an ARP. Steamworks is bound at RUNTIME (`LoadLibrary`/`GetProcAddress` on + `steam_api64.dll`), exactly as the pcap backend binds `WPCAP.DLL`, so no SDK + is needed to build or link. Load-bearing: everything ships **Reliable + + NoNagle** (WATTCP's DOS-era retransmit timers are multi-second, so an outer + retransmit costs one RTT where an inner one costs seconds — the long + rationale is in the header, read it before "optimizing" to unreliable), and + the API is **not thread-safe by design** (one thread, pumped from the NE2000 + tick). Wire format, session file and env handoff are frozen in + `emulator/steam/SESSION-CONTRACT.md`. +- **`ethernet_steam.cpp` / `ethernet_steam.h`** — the `[ne2000] backend=steam` + `EthernetConnection` that plugs `steamlink` into the emulated NIC. `Pump()` + runs at the top of `GetPackets()`, which `NE2000_Poller` calls off a + `TIMER_AddTickHandler` — i.e. once per emulated millisecond — so that tick + **is** the Steam callback pump and there is no new thread or timer anywhere. + RX policy deliberately mirrors the pcap backend's BPF (accept `dst==self` or + broadcast, drop other multicast) so `backend=steam` and `backend=pcap` are + indistinguishable from the guest's side, and `macaddr=` must be a stable + literal — never `random` — because the MAC is what peers route to. Same + power-of-2-throttled `STEAM TX/RX #n` log lines as pcap. Operator runbook: + `emulator/STEAM-NOTES.md`. ## Applying to a DOSBox-X source checkout @@ -171,6 +198,56 @@ Tested against DOSBox-X `v2026.06.02`, MSYS2 mingw64. - `src/src/dosbox.cpp`: add `"plasma"` to the `serials[]` allowed-values list. (The plasma WINDOW is drawn by `vpxlog.cpp` via the `PLASMA_GetFrame` extern — already in this dir's `vpxlog.cpp`, so it lands with step 1.) +3e. The Steam ethernet backend (`[ne2000] backend=steam` — internet play; see + `../STEAM-NOTES.md` and `../steam/SESSION-CONTRACT.md`). These are `misc/` + files, not `hardware/`: they sit beside the pcap backend they are modelled + on. + ``` + cp emulator/vpx-device/steamlink.cpp emulator/src/src/misc/ + cp emulator/vpx-device/steamlink.h emulator/src/src/misc/ + cp emulator/vpx-device/ethernet_steam.cpp emulator/src/src/misc/ + cp emulator/vpx-device/ethernet_steam.h emulator/src/src/misc/ + ``` + plus three stock edits (grep for how `ethernet_pcap` appears and copy the + pattern): + - `src/src/misc/Makefile.am`: append `steamlink.cpp ethernet_steam.cpp` to + `libmisc_a_SOURCES` (we put them after `ethernet_nothing.cpp`). The + hand-mirrored generated-Makefile caveat from step 2 does **not** apply + here: `build-mingw-sdl2` re-runs `./autogen.sh` before configuring, so + `Makefile.in`/`Makefile` are regenerated from the `.am`. + - `src/src/misc/ethernet.cpp`: `#include "ethernet_steam.h"`; add the + `backend == "steam"` case to `OpenEthernetConnection()` next to the pcap + one — `new SteamEthernetConnection`, settings from + `control->GetSection("ethernet, steam")` — inside `#ifdef WIN32` (the + transport's DLL loader is Windows-only; keep the guard or a Linux build + breaks at link). Deliberately NOT in the `auto` ladder: steam is only ever + selected explicitly, since an internet session is never how a pod boots by + default. Add the `else if (backend == "steam")` arm to the failure + `LOG_MSG` chain too, otherwise a steam backend that fails to initialize + falls out as "Unknown ethernet backend". + - `src/src/dosbox.cpp`: add `"steam"` to the `backendopts[]` allowed-values + list (without this the config parser refuses the value and silently falls + back to `auto` — same trap as the `serials[]` list in 3b), and register + the settings section next to + `"ethernet, pcap"`: + `secprop = control->AddSection_prop("ethernet, steam", &Null_Init, true);` + with string props `peers`, `lobby`, `session` (all defaulting to `env`) + and int `channel` (0). Those names and defaults are frozen in + SESSION-CONTRACT.md §2 — the fork and the gateway both parse them. + No new `.conf.tmpl` is needed: the backend is selected per session with + `-set "ne2000 backend=steam"` on the DOSBox-X command line (`pod-launch + --steam` appends it), so the existing `net_loop_rio.conf` / `net_rp_rio.conf` + are reused as-is. + **The Steamworks SDK is OPTIONAL.** `steamlink.cpp` compiles by default + against hand-declared ABI structs and binds `steam_api64.dll` at runtime, so + there is nothing to install to build and nothing to link against. To + validate those structs against the real headers, drop the SDK at + `emulator/steamworks/` (git-ignored, same pattern as `emulator/src/`) and + build with `-DVWE_STEAM_USE_SDK_HEADERS` plus the SDK's `public/` dir on the + include path — worth doing once after an SDK bump, then go back to the plain + build. A build made without the SDK still **runs**: `backend=steam` just + fails Init with a readable message when `steam_api64.dll` is missing or + Steam isn't running, exactly the way a missing Npcap fails pcap. 4. Build: ``` cd emulator/src diff --git a/emulator/vpx-device/ethernet_pcap.cpp b/emulator/vpx-device/ethernet_pcap.cpp index af860a8b..2caf58bf 100644 --- a/emulator/vpx-device/ethernet_pcap.cpp +++ b/emulator/vpx-device/ethernet_pcap.cpp @@ -26,6 +26,9 @@ #include "support.h" /* strcasecmp */ #include "control.h" /* [ne2000] macaddr for the RX filter */ #include +#include /* getenv/strtoul -- the latency shim's knobs */ +#include +#include extern std::string niclist; @@ -143,6 +146,149 @@ bool LoadPcapLibrary() { #endif +/* ---- Phase 0: the latency-tolerance gate -------------------------------- + * + * The riskiest assumption in the whole internet-play plan is that the 1995 + * stack -- WATTCP + netnub + L4NetworkManager, every one of them tuned for a + * same-switch LAN with sub-millisecond RTT -- still functions at 30-150ms RTT + * with jitter. They are unpatchable 30-year-old binaries, so the answer has to + * be MEASURED on the existing LAN rigs, with zero Steam code, before any + * transport work is committed to. External shapers (clumsy, WinDivert) hook + * the host IP stack and never see Npcap-injected frames at all, so the delay + * has to live inside the backend. Three env knobs, read once at Initialize(): + * + * VWE_NET_DELAY_MS fixed one-way delay applied to BOTH TX and RX, so one + * shimmed pod adds 2x delay to every RTT through it. + * VWE_NET_JITTER_MS uniform 0..N ms extra per frame, with release times + * clamped monotonic per queue -- frames NEVER reorder. + * The real transport is Steam's reliable ORDERED channel; + * a reordering shim would be measuring a transport we are + * not going to build. + * VWE_NET_LOSS_PCT drop percentage, default 0. Reliable delivery surfaces + * loss as a delay spike rather than a drop, so this is + * for characterization only and is not part of the gate. + * + * All unset = the shim does not exist: SendPacket/GetPackets run byte-for-byte + * the code they ran before it was added, and not a single extra log line is + * emitted (same convention as VPXLOG). If any of the three IS set, exactly one + * NETSHIM line is always logged -- ACTIVE or INERT -- because a silent run is + * ambiguous with a binary that was built without this file at all. + * + * Timebase is WALL clock, not emulated time -- network latency is wall clock + * and the guest's own timers must stay free to disagree with it. GetPackets() + * is called from NE2000_Poller, a TIMER_AddTickHandler tick, i.e. once per + * emulated millisecond, and that is the drain quantum: no thread and no timer + * is added anywhere. steady_clock rather than GetTickCount64 because the + * latter advances in ~15.6ms steps, which would quantize the 15ms knobs into + * all-or-nothing jumps. + * + * State is file-static rather than class members because ethernet_pcap.h is + * stock DOSBox-X we would rather not fork, and there is only ever one NE2000. + * Test matrix, two-rig topology and the pass/fail gate: emulator/NET-NOTES.md. + */ +enum { SHIM_FRAMEBUF = 1600 }; /* larger than any frame the NE2000 can make */ + +struct DelayedFrame { + uint64_t release_ms; + int len; + uint8_t data[SHIM_FRAMEBUF]; +}; + +static bool shim_on = false; +static unsigned shim_delay_ms = 0, shim_jitter_ms = 0, shim_loss_pct = 0; +static std::deque shim_txq, shim_rxq; +static uint64_t shim_tx_release = 0, shim_rx_release = 0; /* monotonic clamp */ +static unsigned long shim_lost = 0; +static uint32_t shim_rng = 0; +static bool shim_oversize_warned = false; + +static uint64_t shim_now_ms() { + using namespace std::chrono; + return (uint64_t)duration_cast( + steady_clock::now().time_since_epoch()).count(); +} + +/* Own xorshift instead of rand(): the shim must not perturb the global rand() + * sequence the rest of the emulator draws from. */ +static uint32_t shim_rand() { + shim_rng ^= shim_rng << 13; + shim_rng ^= shim_rng >> 17; + shim_rng ^= shim_rng << 5; + return shim_rng; +} + +/* Park a frame on `q`. True = the caller must NOT act on the frame now (it was + * queued, or the loss knob ate it). A frame too big for the buffer is passed + * through undelayed instead of truncated -- a truncated frame would be a + * corruption bug wearing a latency finding's clothes. */ +static bool shim_hold(std::deque& q, uint64_t& last_release, + const uint8_t* frame, int len) +{ + if (len < 0 || len > (int)SHIM_FRAMEBUF) { + if (!shim_oversize_warned) { + shim_oversize_warned = true; + LOG_MSG("NETSHIM: len=%d exceeds the %d-byte queue buffer -- oversize frames pass through undelayed", + len, (int)SHIM_FRAMEBUF); + } + return false; + } + if (shim_loss_pct && (shim_rand() % 100u) < shim_loss_pct) { + shim_lost++; + return true; + } + uint64_t rel = shim_now_ms() + shim_delay_ms; + if (shim_jitter_ms) rel += shim_rand() % (shim_jitter_ms + 1u); + if (rel < last_release) rel = last_release; /* ordered, never reordered */ + last_release = rel; + q.push_back(DelayedFrame()); + DelayedFrame& f = q.back(); + f.release_ms = rel; + f.len = len; + memcpy(f.data, frame, (size_t)len); + return true; +} + +/* Read one knob. `present` is set if the variable EXISTS, whatever its value, + * so that (a) an all-zero control run still announces itself and (b) a typo'd + * or non-numeric value is reported instead of quietly measuring nothing -- + * strtoul("abc") is 0, which is indistinguishable from "shim not built in". + * Trailing whitespace is tolerated: `set VWE_NET_DELAY_MS=50 ` in a .bat keeps + * the space, and rejecting that would be a trap rather than a check. */ +static unsigned shim_knob(const char* name, bool& present) { + const char* e = getenv(name); + if (!e) return 0; + present = true; + const char* p = e; + while (*p == ' ' || *p == '\t') p++; + char* end = NULL; + unsigned long v = strtoul(p, &end, 10); + if (end) while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n') end++; + if (end == p || (end && *end) || *p == '-') { + LOG_MSG("NETSHIM: %s=\"%s\" is not a plain non-negative number -- IGNORED (treated as 0)", + name, e); + return 0; + } + if (v > 100000ul) { /* a 100s one-way delay is a typo, not a test point */ + LOG_MSG("NETSHIM: %s=%lu is absurd -- clamped to 100000", name, v); + v = 100000ul; + } + return (unsigned)v; +} + +/* Queue-depth tail for the throttled TX/RX log lines; empty when off, so an + * unshimmed run's logs are unchanged. */ +static const char* shim_qtail() { + static char t[64]; + if (!shim_on) return ""; + if (shim_loss_pct) + snprintf(t, sizeof(t), " q=%u/%u lost=%lu", + (unsigned)shim_txq.size(), (unsigned)shim_rxq.size(), shim_lost); + else + snprintf(t, sizeof(t), " q=%u/%u", + (unsigned)shim_txq.size(), (unsigned)shim_rxq.size()); + return t; +} + PcapEthernetConnection::PcapEthernetConnection() : EthernetConnection() { @@ -314,6 +460,36 @@ bool PcapEthernetConnection::Initialize(Section* config) LOG_MSG("PCAP RX filter NOT installed (%s) -- promiscuous, expect LAN noise", have_filter_api ? "no ne2000 macaddr" : "wpcap lacks pcap_compile"); + /* Latency-tolerance gate: read the knobs once, and say out loud what was + * applied so a test run's logs are self-describing months later. */ + shim_txq.clear(); shim_rxq.clear(); + shim_tx_release = shim_rx_release = 0; + shim_lost = 0; + shim_oversize_warned = false; + shim_delay_ms = shim_jitter_ms = shim_loss_pct = 0; + bool shim_env_seen = false; + shim_delay_ms = shim_knob("VWE_NET_DELAY_MS", shim_env_seen); + shim_jitter_ms = shim_knob("VWE_NET_JITTER_MS", shim_env_seen); + shim_loss_pct = shim_knob("VWE_NET_LOSS_PCT", shim_env_seen); + if (shim_loss_pct > 100) shim_loss_pct = 100; + shim_on = (shim_delay_ms || shim_jitter_ms || shim_loss_pct); + if (shim_on) { + shim_rng = (uint32_t)shim_now_ms() | 1u; /* xorshift is stuck at 0 */ + LOG_MSG("NETSHIM ACTIVE: delay=%ums each way (+%ums on every RTT through this pod), " + "jitter=0..%ums monotonic, loss=%u%% -- Phase 0 latency gate, see NET-NOTES.md", + shim_delay_ms, shim_delay_ms * 2, shim_jitter_ms, shim_loss_pct); + } else if (shim_env_seen) { + /* All knobs zero is a legitimate control point in the matrix -- but it + * is ALSO exactly what a dosbox-x.exe built WITHOUT this file looks + * like, and vpx-device/ is only the version-controlled copy (it has to + * be copied into src/src/misc/ before `make`). Announcing the inert + * case is what tells the operator the binary really does contain the + * shim: VWE_NET_* set and NO "NETSHIM" line at all means it does not, + * and the whole matrix would otherwise read as "0ms, no effect". */ + LOG_MSG("NETSHIM INERT: VWE_NET_* present but every knob is 0 " + "-- baseline/control run, no delay applied (shim IS compiled in)"); + } + return true; } @@ -321,8 +497,10 @@ void PcapEthernetConnection::SendPacket(const uint8_t* packet, int len) { static unsigned long txn = 0; if (((++txn) & (txn - 1)) == 0) /* powers of 2, avoid spam */ - LOG_MSG("PCAP TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x", - txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]); + LOG_MSG("PCAP TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x%s", + txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8], + shim_qtail()); + if (shim_on && shim_hold(shim_txq, shim_tx_release, packet, len)) return; int ret = pcap_sendpacket(adhandle, packet, len); if(ret) LOG_MSG("PCAP error: %s", pcap_geterr(adhandle)); } @@ -333,15 +511,35 @@ void PcapEthernetConnection::GetPackets(std::function struct pcap_pkthdr *header; u_char *pkt_data; static unsigned long rxn = 0; + /* Shim TX first: SendPacket parked these, and this handler is the only + * thing that runs often enough to release them on time. */ + if (shim_on) { + uint64_t now = shim_now_ms(); + while (!shim_txq.empty() && shim_txq.front().release_ms <= now) { + const DelayedFrame& f = shim_txq.front(); + if (pcap_sendpacket(adhandle, f.data, f.len)) + LOG_MSG("PCAP error: %s", pcap_geterr(adhandle)); + shim_txq.pop_front(); + } + } //#if 0 while((res = pcap_next_ex( adhandle, &header, (const u_char **)&pkt_data)) > 0) { if (((++rxn) & (rxn - 1)) == 0) - LOG_MSG("PCAP RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x", + LOG_MSG("PCAP RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x%s", rxn, header->len, pkt_data[0],pkt_data[1],pkt_data[2], - pkt_data[6],pkt_data[7],pkt_data[8]); + pkt_data[6],pkt_data[7],pkt_data[8], shim_qtail()); + if (shim_on && shim_hold(shim_rxq, shim_rx_release, pkt_data, (int)header->len)) continue; callback(pkt_data, header->len); } //#endif + if (shim_on) { + uint64_t now = shim_now_ms(); + while (!shim_rxq.empty() && shim_rxq.front().release_ms <= now) { + const DelayedFrame& f = shim_rxq.front(); + callback(f.data, f.len); + shim_rxq.pop_front(); + } + } } #endif diff --git a/emulator/vpx-device/ethernet_steam.cpp b/emulator/vpx-device/ethernet_steam.cpp new file mode 100644 index 00000000..1c7e4294 --- /dev/null +++ b/emulator/vpx-device/ethernet_steam.cpp @@ -0,0 +1,477 @@ +/* + * VWE fork: `backend=steam` -- EthernetConnection over the vwesteam:: tunnel. + * See ethernet_steam.h for why this exists, steamlink.h for the wire format, + * and ../steam/SESSION-CONTRACT.md for the session file and env handoff. + */ + +#include "config.h" + +#ifdef WIN32 + +#include "ethernet_steam.h" +#include "steamlink.h" +#include "dosbox.h" +#include "logging.h" +#include "support.h" /* strcasecmp */ +#include "control.h" /* [ne2000] macaddr -- required here, not optional */ +#include +#include +#include +#include +#include +#include + +/* vwesteam:: logs through a plain function pointer so the gateway can send the + * same diagnostics to stderr; in the fork they belong wherever every other + * device logs. Set before Init() so Init()'s own failures are visible. */ +static void SteamLog(const char* msg) +{ + LOG_MSG("%s", msg); +} + +/* ------------------------------------------------------------------ * + * Session file (SESSION-CONTRACT section 2) + * + * A tolerant hand-rolled reader, deliberately NOT a JSON library: the fork + * has no JSON dependency and this file is machine-written by TeslaLobby in + * one fixed shape, so scanning for the six keys we need is cheaper than + * taking on one. What it will not do is half-read a roster -- a peer that + * silently went missing looks exactly like a peer that never connected, and + * that is the worst thing to be debugging at 9pm. Anything malformed is a + * hard failure naming the file and the key. + * ------------------------------------------------------------------ */ + +struct SessionPeer { + uint64_t steamId = 0; + uint32_t ip = 0; /* host order; 0 = absent */ + uint8_t mac[6] = {}; + bool macKnown = false; + int slot = -1; + bool gateway = false; + std::string name; +}; + +static const char* SkipWs(const char* p, const char* end) +{ + while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++; + return p; +} + +/* Matching '}' / ']' for the bracket at p. Quote-aware, so a bracket inside a + * pilot name cannot unbalance the walk. */ +static const char* MatchBracket(const char* p, const char* end) +{ + const char open = *p, close = (open == '{') ? '}' : ']'; + int depth = 0; + bool instr = false; + for (; p < end; p++) { + if (instr) { + if (*p == '\\') p++; + else if (*p == '"') instr = false; + continue; + } + if (*p == '"') instr = true; + else if (*p == open) depth++; + else if (*p == close && --depth == 0) return p; + } + return NULL; +} + +/* Scalar value of "key" in [p,end), quotes and escapes stripped. Callers bound + * the span to one object, so a flat search cannot wander into a sibling. */ +static bool JsonGet(const char* p, const char* end, const char* key, std::string& out) +{ + char pat[32]; + snprintf(pat, sizeof(pat), "\"%s\"", key); + const size_t klen = strlen(pat); + + for (const char* q = p; q < end; q++) { + q = (const char*)memchr(q, '"', (size_t)(end - q)); + if (!q) break; + if ((size_t)(end - q) < klen || memcmp(q, pat, klen) != 0) continue; + const char* v = SkipWs(q + klen, end); + if (v >= end || *v != ':') continue; + v = SkipWs(v + 1, end); + out.clear(); + if (v < end && *v == '"') { + for (v++; v < end && *v != '"'; v++) { + if (*v == '\\' && v + 1 < end) v++; + out += *v; + } + } else { + while (v < end && *v != ',' && *v != '}' && *v != ']' && + *v != ' ' && *v != '\t' && *v != '\r' && *v != '\n') + out += *v++; + } + return true; + } + return false; +} + +/* Contents of the {object} or [array] value of "key", brackets excluded. */ +static bool JsonSpan(const char* p, const char* end, const char* key, + const char*& bo, const char*& eo) +{ + char pat[32]; + snprintf(pat, sizeof(pat), "\"%s\"", key); + const size_t klen = strlen(pat); + + for (const char* q = p; q < end; q++) { + q = (const char*)memchr(q, '"', (size_t)(end - q)); + if (!q) break; + if ((size_t)(end - q) < klen || memcmp(q, pat, klen) != 0) continue; + const char* v = SkipWs(q + klen, end); + if (v >= end || *v != ':') continue; + v = SkipWs(v + 1, end); + if (v >= end || (*v != '{' && *v != '[')) continue; + const char* close = MatchBracket(v, end); + if (!close) return false; + bo = v + 1; + eo = close; + return true; + } + return false; +} + +/* Next {object} inside an array span, advancing p past it. */ +static bool JsonNextObject(const char*& p, const char* end, + const char*& bo, const char*& eo) +{ + p = SkipWs(p, end); + while (p < end && *p == ',') p = SkipWs(p + 1, end); + if (p >= end || *p != '{') return false; + const char* close = MatchBracket(p, end); + if (!close) return false; + bo = p + 1; + eo = close; + p = close + 1; + return true; +} + +/* The WIDTH-LIMITED %02x is not cosmetic: it is exactly what ne2000.cpp's own + * macaddr parser uses, and the emulated NIC's answer has to be the same six + * bytes this backend filters on and announces in HELLO. Plain %x diverges + * silently on a malformed conf -- measured, not theorised: + * + * macaddr ne2000.cpp (%02x) plain %x + * 02:00:C8:00:00:7100 02:00:c8:00:00:71 02:00:c8:00:00:00 (truncated) + * 02:00:3E7:00:00:70 ac:de:48:88:bb:aa 02:00:e7:00:00:70 (its fallback) + * + * Either row is a pod whose NIC answers to one address while the tunnel routes + * to another: no unicast frame ever arrives and nothing says why. %n + the tail + * check turns the leftover-digits row into a loud refusal instead of a third + * wrong answer. ("02:00:3E7:..." is not hypothetical -- see configure.ps1's own + * warning comment about emitting exactly that.) */ +static bool ParseMac(const char* s, uint8_t mac[6]) +{ + unsigned int m[6]; + int n = -1; + if (!s || sscanf(s, "%02x:%02x:%02x:%02x:%02x:%02x%n", + &m[0],&m[1],&m[2],&m[3],&m[4],&m[5], &n) != 6 || n < 0) + return false; + while (s[n] == ' ' || s[n] == '\t' || s[n] == '\r' || s[n] == '\n') n++; + if (s[n] != '\0') return false; /* trailing junk = a MAC nobody meant */ + for (int i = 0; i < 6; i++) mac[i] = (uint8_t)m[i]; + return true; +} + +static bool ParseIp(const char* s, uint32_t& ip) +{ + unsigned int a,b,c,d; + if (!s || sscanf(s, "%u.%u.%u.%u", &a,&b,&c,&d) != 4 || (a|b|c|d) > 255) + return false; + ip = (a << 24) | (b << 16) | (c << 8) | d; + return true; +} + +static bool ParseId(const std::string& s, uint64_t& id) +{ + char* e = NULL; + id = strtoull(s.c_str(), &e, 10); + return id != 0 && e && *e == '\0'; +} + +/* One peer/self object. `where` names it in errors ("peers[1]", "self"). */ +static bool ParsePeerObject(const char* bo, const char* eo, SessionPeer& out, + const std::string& path, const char* where, + std::string& err) +{ + std::string v; + if (!JsonGet(bo, eo, "steamId", v) || !ParseId(v, out.steamId)) { + err = path + ": " + where + " has no usable steamId"; + return false; + } + if (JsonGet(bo, eo, "ip", v) && !ParseIp(v.c_str(), out.ip)) { + err = path + ": " + where + " has a bad ip \"" + v + "\""; + return false; + } + if (JsonGet(bo, eo, "mac", v)) { + if (!ParseMac(v.c_str(), out.mac)) { + err = path + ": " + where + " has a bad mac \"" + v + "\""; + return false; + } + out.macKnown = true; + } + if (JsonGet(bo, eo, "slot", v)) out.slot = atoi(v.c_str()); + if (JsonGet(bo, eo, "gateway", v)) out.gateway = (strcasecmp(v.c_str(), "true") == 0); + JsonGet(bo, eo, "name", out.name); + return true; +} + +static bool ParseSessionFile(const std::string& path, SessionPeer& self, + std::vector& peers, std::string& err) +{ + FILE* f = fopen(path.c_str(), "rb"); + if (!f) { + err = path + ": cannot open the session file"; + return false; + } + std::string doc; + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) doc.append(buf, n); + fclose(f); + if (doc.size() >= 3 && (unsigned char)doc[0] == 0xef) doc.erase(0, 3); /* stray BOM */ + + const char* p = doc.c_str(); + const char* end = p + doc.size(); + const char *bo, *eo; + + /* self is optional: the MAC that matters comes from [ne2000] anyway, and + * the IP is only diagnostics. peers is not -- without it there is no + * session and no allowlist. */ + if (JsonSpan(p, end, "self", bo, eo) && + !ParsePeerObject(bo, eo, self, path, "self", err)) + return false; + + if (!JsonSpan(p, end, "peers", bo, eo)) { + err = path + ": no \"peers\" array"; + return false; + } + for (const char* q = bo; ; ) { + const char *pb, *pe; + if (!JsonNextObject(q, eo, pb, pe)) break; + SessionPeer peer; + char where[24]; + snprintf(where, sizeof(where), "peers[%u]", (unsigned)peers.size()); + if (!ParsePeerObject(pb, pe, peer, path, where, err)) return false; + peers.push_back(peer); + } + if (peers.empty()) { + err = path + ": \"peers\" is empty -- nobody to fly with"; + return false; + } + return true; +} + +/* A conf key left at the literal "env" (the default) defers to `var`. The + * confs are install-static -- configure.ps1 renders them once -- while the + * roster changes every session, so everything per-session arrives by env or + * session file. SESSION-CONTRACT section 2. */ +static std::string ConfOrEnv(Section_prop* section, const char* key, const char* var) +{ + const char* v = section->Get_string(key); + if (v && *v && strcasecmp(v, "env") != 0) return std::string(v); + const char* e = getenv(var); + return e ? std::string(e) : std::string(); +} + +static void ParseIdCsv(const std::string& csv, std::vector& out) +{ + size_t i = 0; + while (i < csv.size()) { + size_t j = csv.find_first_of(",; \t", i); + if (j == std::string::npos) j = csv.size(); + const std::string tok = csv.substr(i, j - i); + i = j + 1; + if (tok.empty()) continue; + uint64_t id; + if (!ParseId(tok, id)) { + LOG_MSG("STEAM: ignoring unparseable peer id \"%s\"", tok.c_str()); + continue; + } + if (std::find(out.begin(), out.end(), id) == out.end()) out.push_back(id); + } +} + +SteamEthernetConnection::SteamEthernetConnection() + : EthernetConnection() +{ +} + +SteamEthernetConnection::~SteamEthernetConnection() +{ + /* Only if Init() took: Shutdown() is refcounted and ethernet.cpp deletes + * us when Initialize() fails. */ + if (up) vwesteam::Shutdown(); +} + +bool SteamEthernetConnection::Initialize(Section* config) +{ + Section_prop *section = static_cast(config); + + const std::string sessionpath = ConfOrEnv(section, "session", "VWE_STEAM_SESSION"); + const std::string peerscsv = ConfOrEnv(section, "peers", "VWE_STEAM_PEERS"); + const std::string lobbystr = ConfOrEnv(section, "lobby", "VWE_STEAM_LOBBY"); + + // Where pcap only loses its RX filter without a macaddr, the mesh ROUTES + // to this one: peers bind it to our SteamID from the HELLO record and + // address unicast frames by it. "random" would hand every peer a new + // address on every restart -- so refuse to come up rather than half-work. + const char* macstr = NULL; + Section_prop *ne2ksec = control ? static_cast(control->GetSection("ne2000")) : NULL; + if (ne2ksec) macstr = ne2ksec->Get_string("macaddr"); + if (!ParseMac(macstr, mac)) { + LOG_MSG("STEAM: [ne2000] macaddr is \"%s\" -- backend=steam needs a fixed MAC, " + "exactly six two-digit hex octets (02:00: + the four IP octets, " + "see SESSION-CONTRACT section 1)", + (macstr && *macstr) ? macstr : "unset"); + return false; + } + + vwesteam::Config cfg; + uint32_t selfip = 0; + uint64_t sessionid = 0; + std::string name; + + if (!sessionpath.empty()) { + SessionPeer self; + std::vector peers; + std::string err; + if (!ParseSessionFile(sessionpath, self, peers, err)) { + LOG_MSG("STEAM: %s", err.c_str()); + return false; + } + selfip = self.ip; + sessionid = self.steamId; + name = self.name; + if (self.macKnown && memcmp(self.mac, mac, 6) != 0) + LOG_MSG("STEAM: session file says our MAC is %02x:%02x:%02x:%02x:%02x:%02x but " + "[ne2000] macaddr is %02x:%02x:%02x:%02x:%02x:%02x -- the conf wins, " + "peers will route to the conf MAC", + self.mac[0],self.mac[1],self.mac[2],self.mac[3],self.mac[4],self.mac[5], + mac[0],mac[1],mac[2],mac[3],mac[4],mac[5]); + // Peer MACs/IPs are logged but not fed to the transport -- those are + // learned from HELLO, which stays correct when a player relaunches into + // a different slot. The "gateway": true flag IS passed through: it is + // the default route for any unicast we cannot place, which in practice + // means everything bound for TeslaConsole at .10 (a host-LAN NIC MAC + // that can never appear in the peer table). Without it those frames + // would flood, putting the whole egg transfer on every player's uplink. + for (size_t i = 0; i < peers.size(); i++) { + cfg.peers.push_back(peers[i].steamId); + if (peers[i].gateway) { + if (cfg.gateway && cfg.gateway != peers[i].steamId) + LOG_MSG("STEAM: session file marks more than one peer as the " + "gateway; keeping %s", std::to_string(cfg.gateway).c_str()); + else + cfg.gateway = peers[i].steamId; + } + LOG_MSG("STEAM: peer slot %d \"%s\" %s ip %u.%u.%u.%u mac %02x:%02x:%02x:%02x:%02x:%02x%s", + peers[i].slot, peers[i].name.c_str(), + std::to_string(peers[i].steamId).c_str(), + (peers[i].ip >> 24) & 0xff, (peers[i].ip >> 16) & 0xff, + (peers[i].ip >> 8) & 0xff, peers[i].ip & 0xff, + peers[i].mac[0],peers[i].mac[1],peers[i].mac[2], + peers[i].mac[3],peers[i].mac[4],peers[i].mac[5], + peers[i].gateway ? " (gateway)" : ""); + } + } + + // A csv is a union with the session file's roster rather than an override: + // peers is an allowlist, and an operator who names an ID means to admit it. + ParseIdCsv(peerscsv, cfg.peers); + + if (!lobbystr.empty() && !ParseId(lobbystr, cfg.lobby)) + LOG_MSG("STEAM: ignoring unparseable lobby id \"%s\"", lobbystr.c_str()); + + cfg.channel = section->Get_int("channel"); + + const char* envname = getenv("VWE_STEAM_NAME"); + cfg.name = (envname && *envname) ? std::string(envname) : name; + + if (cfg.peers.empty() && cfg.lobby == 0) { + LOG_MSG("STEAM: no peers -- set [ethernet, steam] session/peers or the " + "VWE_STEAM_SESSION / VWE_STEAM_PEERS environment variables"); + return false; + } + + // No session file: recover our IP from the MAC. Every VWE MAC is 02:00: + + // the four IP octets (deploy/configure.ps1's formula), so this is exact + // for a configured pod and simply stays 0 ("unknown") for anything else. + if (!selfip && mac[0] == 0x02 && mac[1] == 0x00) + selfip = ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) | + ((uint32_t)mac[4] << 8) | (uint32_t)mac[5]; + + vwesteam::SetLog(SteamLog); + std::string err; + if (!vwesteam::Init(cfg, mac, selfip, err)) { + LOG_MSG("STEAM: link did not come up: %s", err.c_str()); + return false; + } + up = true; + + // Stale session file, or the machine is signed into a different Steam + // account than the lobby handed the roster to: every peer's allowlist will + // refuse us. Say it here, not at mission start when nobody appears. + if (sessionid && vwesteam::SelfSteamID() && sessionid != vwesteam::SelfSteamID()) + LOG_MSG("STEAM: session file was written for SteamID %s but this machine is signed in as %s " + "-- peers will refuse us", + std::to_string(sessionid).c_str(), std::to_string(vwesteam::SelfSteamID()).c_str()); + + LOG_MSG("STEAM: link up -- self %s mac %02x:%02x:%02x:%02x:%02x:%02x ip %u.%u.%u.%u, " + "%u peer(s), channel %d", + std::to_string(vwesteam::SelfSteamID()).c_str(), + mac[0],mac[1],mac[2],mac[3],mac[4],mac[5], + (selfip >> 24) & 0xff, (selfip >> 16) & 0xff, (selfip >> 8) & 0xff, selfip & 0xff, + (unsigned)cfg.peers.size(), cfg.channel); + return true; +} + +void SteamEthernetConnection::SendPacket(const uint8_t* packet, int len) +{ + static unsigned long txn = 0; + ++txn; /* split off: pcap's one-liner form of this is a sequence-point warning */ + if ((txn & (txn - 1)) == 0 && len >= 14) /* powers of 2, avoid spam */ + LOG_MSG("STEAM TX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x", + txn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]); + vwesteam::SendFrame(packet, len); +} + +void SteamEthernetConnection::GetPackets(std::function callback) +{ + // The one and only pump site. NE2000_Poller is a TIMER tick handler, so + // this runs once per emulated millisecond -- which is why the Steam link + // needs no thread and no timer of its own anywhere. + vwesteam::Pump(); + + static unsigned long rxn = 0; + vwesteam::PollFrames([&](const uint8_t* packet, int len) { + // Acceptance mirrors the pcap backend's BPF clause for clause so the + // guest cannot tell the two backends apart. + if (len < 14 || len > vwesteam::VWE_MAX_FRAME) return; + const bool bcast = (packet[0] & packet[1] & packet[2] & + packet[3] & packet[4] & packet[5]) == 0xff; + if (!bcast) { + if (packet[0] & 1) return; /* multicast: the BPF never matched it */ + if (memcmp(packet, mac, 6) != 0) return; /* ether dst */ + } + // "and not ether src ", the BPF's third clause. steamlink.h + // calls this structurally impossible because a peer never echoes; + // the GATEWAY can, though. Its capture filter is bare + // "ether broadcast" until the first HELLO lands (steam-gw + // BuildFilter()), with no "not ether src" clause to build yet -- so + // in that window it captures its own npcap injection of OUR + // broadcast and tunnels it straight back to us. A real NIC never + // hears its own transmit, and a boot-ARP that returns to its sender + // is duplicate-address bait, so drop it here for one memcmp. + if (memcmp(packet + 6, mac, 6) == 0) return; + ++rxn; + if ((rxn & (rxn - 1)) == 0) + LOG_MSG("STEAM RX #%lu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x", + rxn, len, packet[0],packet[1],packet[2], packet[6],packet[7],packet[8]); + callback(packet, len); + }); +} + +#endif diff --git a/emulator/vpx-device/ethernet_steam.h b/emulator/vpx-device/ethernet_steam.h new file mode 100644 index 00000000..3dd4e781 --- /dev/null +++ b/emulator/vpx-device/ethernet_steam.h @@ -0,0 +1,57 @@ +/* + * VWE fork: NE2000 backend that tunnels guest Ethernet frames over Steam + * (`backend=steam`), so a private group can fly missions over the internet. + * + * Why L2 and not an ordinary L3 tunnel: the 1995 mesh (WATTCP + netnub + + * L4NetworkManager) dials peers by raw IP on one flat subnet and ARPs for + * them, so whole FRAMES have to cross the internet for the DOS binaries to + * see the same-switch world they were built for. + * + * All of the transport lives in vwesteam:: (steamlink.h) and is shared + * VERBATIM with the host's steam<->pcap gateway; this class is only the + * EthernetConnection adapter -- config resolution, the receive-side + * acceptance policy, and the log lines. Steamworks is bound at runtime in + * there, exactly as the pcap backend binds WPCAP.DLL, so a build on a machine + * with no SDK still runs and just fails Initialize() with a readable reason. + * + * Only REMOTE players run this. The session host's pod stays on + * backend=pcap and reaches the tunnel through the gateway, which is what + * keeps exactly one process per machine on ISteamNetworkingMessages. + * + * Conf: [ne2000] backend=steam, macaddr= + * [ethernet, steam] peers= lobby= session= channel= + * Contracts: ../steam/SESSION-CONTRACT.md (session file, env, addressing). + */ + +#ifndef DOSBOX_ETHERNET_STEAM_H +#define DOSBOX_ETHERNET_STEAM_H + +#include "config.h" + +#ifdef WIN32 + +#include "ethernet.h" + +#include + +/** A Steam-P2P Ethernet connection + * Carries raw Ethernet frames to the other pods in the session over + * SteamNetworkingMessages instead of over a host NIC. Needs Steam running, + * a stable [ne2000] macaddr, and a peer roster (session file, csv or lobby). + */ +class SteamEthernetConnection : public EthernetConnection { + public: + SteamEthernetConnection(); + ~SteamEthernetConnection(); + bool Initialize(Section* config) override; + void SendPacket(const uint8_t* packet, int len) override; + void GetPackets(std::function callback) override; + + private: + uint8_t mac[6] = {}; /*!< [ne2000] macaddr; the RX filter's "ether dst self" */ + bool up = false; /*!< Init() took -- gates the refcounted Shutdown() */ +}; + +#endif + +#endif diff --git a/emulator/vpx-device/serialnamedpipe.cpp b/emulator/vpx-device/serialnamedpipe.cpp index d3b3f42d..4f1084bc 100644 --- a/emulator/vpx-device/serialnamedpipe.cpp +++ b/emulator/vpx-device/serialnamedpipe.cpp @@ -229,11 +229,17 @@ void CSerialNamedPipe::pumpPipe() { inbuf.erase(inbuf.begin(), inbuf.begin() + pos); } +/* VPX page-fault watch (cpu/paging.cpp): records each delivered RX byte with + * the guest cs:eip at delivery, so a watched fault can say how close it was + * to a serial delivery and where the guest was interrupted. */ +extern void VPX_SerialTraceByte(double now, uint8_t b); + bool CSerialNamedPipe::doReceive() { if (rxq.empty()) pumpPipe(); if (rxq.empty()) return false; uint8_t b = rxq.front(); rxq.pop_front(); + VPX_SerialTraceByte(PIC_FullIndex(), b); receiveByteEx(b, 0); return true; } diff --git a/emulator/vpx-device/steamlink.cpp b/emulator/vpx-device/steamlink.cpp new file mode 100644 index 00000000..8d9c7439 --- /dev/null +++ b/emulator/vpx-device/steamlink.cpp @@ -0,0 +1,1056 @@ +/* + * steamlink.cpp -- VWE Steam transport core. Implements steamlink.h; that + * header carries the API contract, the wire format and the + * why-reliable-not-unreliable rationale. Addressing, the session file and the + * gateway's forwarding rules live in ../steam/SESSION-CONTRACT.md. + * + * This file is compiled VERBATIM into two binaries -- the fork's + * `backend=steam` NE2000 backend and the host's steam<->pcap gateway -- so it + * pulls in nothing from DOSBox-X (no dosbox.h, no logging.h, no config.h) and + * never printf()s. Every diagnostic goes out through the LogFn the caller + * installs; that is the only reason the two consumers can route logs + * differently without a fork of this file. + * + * Steamworks is bound at RUNTIME, the same way ethernet_pcap.cpp binds + * WPCAP.DLL: a build made on a machine with no Steam SDK still compiles and + * runs, and `backend=steam` just fails Init() with a readable message the way + * a missing Npcap fails the pcap backend. + */ + +#include "steamlink.h" + +#include +#include +#include +#include +#include +#include + +namespace vwesteam { + +static LogFn g_log = 0; + +void SetLog(LogFn fn) { g_log = fn; } + +static void Log(const char* fmt, ...) +{ + if (!g_log) return; + char buf[512]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + buf[sizeof(buf) - 1] = 0; + g_log(buf); +} + +/* Powers of 2 only, so a whole mission's frames cost ~20 log lines. Same + * throttle the pcap backend's "PCAP TX/RX #n" lines use. */ +static inline bool LogNth(unsigned long long n) { return (n & (n - 1)) == 0; } + +} // namespace vwesteam + +#if defined(WIN32) || defined(_WIN32) + +#include + +#ifdef VWE_STEAM_USE_SDK_HEADERS +/* Validation build: -DVWE_STEAM_USE_SDK_HEADERS -I/public. The struct + * declarations below are then the SDK's own and the static_asserts check + * Valve's layout instead of our transcription of it. */ +#include +#include +#include +#include +#include +#endif + +namespace vwesteam { + +/* ------------------------------------------------------------------ * + * Protocol constants (SDK name in the comment; we spell them locally so + * the SDK-headers build cannot collide with Valve's enums). + * ------------------------------------------------------------------ */ +enum { + IDENTITY_TYPE_STEAMID = 16, /* k_ESteamNetworkingIdentityType_SteamID */ + SEND_NONAGLE = 1, /* k_nSteamNetworkingSend_NoNagle */ + SEND_RELIABLE = 8, /* k_nSteamNetworkingSend_Reliable */ + SEND_FLAGS = SEND_RELIABLE | SEND_NONAGLE, + RESULT_OK = 1, /* k_EResultOK */ + CB_SESSION_REQUEST = 1251, /* SteamNetworkingMessagesSessionRequest_t */ + CB_SESSION_FAILED = 1252, /* SteamNetworkingMessagesSessionFailed_t */ + CB_LOBBY_CHAT_UPDATE = 506, /* LobbyChatUpdate_t */ + CHAT_MEMBER_ENTERED = 0x0001/* k_EChatMemberStateChangeEntered */ +}; + +namespace sdkabi { + +#ifdef VWE_STEAM_USE_SDK_HEADERS + +typedef ::SteamNetworkingIdentity SteamNetworkingIdentity; +typedef ::SteamNetworkingMessage_t SteamNetworkingMessage_t; +typedef ::CallbackMsg_t CallbackMsg_t; + +#else + +/* Hand-declared, ABI-compatible with Steamworks SDK 1.62 on x86-64. Only the + * three structs that cross the DLL boundary are here; everything else the + * flat API takes is a scalar. */ +struct SteamNetworkingIdentity { + int32_t m_eType; /* ESteamNetworkingIdentityType */ + int32_t m_cbSize; /* 8 when carrying a SteamID64 */ + union { + uint64_t m_steamID64; + uint8_t m_genericBytes[32]; + char m_szUnknownRawString[128]; + uint32_t m_reserved[32]; /* this is what pins the struct at 136 bytes */ + }; +}; + +struct SteamNetworkingMessage_t { + void* m_pData; + int32_t m_cbSize; + uint32_t m_conn; /* HSteamNetConnection */ + SteamNetworkingIdentity m_identityPeer; + int64_t m_nConnUserData; + int64_t m_usecTimeReceived; + int64_t m_nMessageNumber; + void (*m_pfnFreeData)(SteamNetworkingMessage_t*); + void (*m_pfnRelease)(SteamNetworkingMessage_t*); + int32_t m_nChannel; + int32_t m_nFlags; + int64_t m_nUserData; + uint16_t m_idxLane; + uint16_t _pad1__; +}; + +struct CallbackMsg_t { + int32_t m_hSteamUser; /* HSteamUser */ + int32_t m_iCallback; + uint8_t* m_pubParam; + int32_t m_cubParam; +}; + +#endif /* VWE_STEAM_USE_SDK_HEADERS */ + +/* m_eType is an int here and a Valve enum in the SDK build; the casts keep one + * body compiling both ways. */ +static inline void IdentitySetSteamID(SteamNetworkingIdentity& id, uint64_t steamId) +{ + memset(&id, 0, sizeof(id)); + id.m_eType = static_cast(IDENTITY_TYPE_STEAMID); + id.m_cbSize = static_cast(sizeof(uint64_t)); + id.m_steamID64 = steamId; +} + +static inline uint64_t IdentityGetSteamID(const SteamNetworkingIdentity& id) +{ + if ((int)id.m_eType != IDENTITY_TYPE_STEAMID) return 0; + return id.m_steamID64; +} + +} // namespace sdkabi + +/* ================================================================== * + * LAYOUT IS LOAD-BEARING -- READ BEFORE TOUCHING THE STRUCTS ABOVE. + * + * steam_api64.dll allocates SteamNetworkingMessage_t and the callback + * payloads; we only index into them. A wrong offset is not a compile + * error and not a crash, it is silent memory corruption inside a + * process that runs all evening. + * + * Layout transcribed from Steamworks SDK 1.62 (steamnetworkingtypes.h, + * steamclientpublic.h, isteamclient.h), x86-64. To CHECK it against the + * real headers rather than trust the transcription, build this file with + * -DVWE_STEAM_USE_SDK_HEADERS -I/public -- the asserts below then + * run against Valve's own declarations and any drift fails the build. + * ================================================================== */ +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Winvalid-offsetof" /* SDK structs carry methods */ +#endif +static_assert(sizeof(void*) == 8, + "steam_api64.dll is x86-64 -- build steamlink.cpp 64-bit"); +static_assert(sizeof(sdkabi::SteamNetworkingIdentity) == 136, + "SteamNetworkingIdentity must be 136 bytes"); +static_assert(offsetof(sdkabi::SteamNetworkingMessage_t, m_identityPeer) == 16, + "SteamNetworkingMessage_t::m_identityPeer must sit at offset 16"); +static_assert(offsetof(sdkabi::SteamNetworkingMessage_t, m_pfnRelease) == 184, + "SteamNetworkingMessage_t tail drifted -- re-check against the SDK"); +static_assert(sizeof(sdkabi::SteamNetworkingMessage_t) == 216, + "SteamNetworkingMessage_t must be 216 bytes"); +static_assert(sizeof(sdkabi::CallbackMsg_t) == 24, + "CallbackMsg_t must be 24 bytes"); +#ifdef VWE_STEAM_USE_SDK_HEADERS +static_assert((int)CB_SESSION_REQUEST == (int)SteamNetworkingMessagesSessionRequest_t::k_iCallback, ""); +static_assert((int)CB_SESSION_FAILED == (int)SteamNetworkingMessagesSessionFailed_t::k_iCallback, ""); +static_assert((int)CB_LOBBY_CHAT_UPDATE == (int)LobbyChatUpdate_t::k_iCallback, ""); +static_assert((int)IDENTITY_TYPE_STEAMID == (int)k_ESteamNetworkingIdentityType_SteamID, ""); +static_assert((int)SEND_FLAGS == (int)(k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_NoNagle), ""); +#endif +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +using sdkabi::SteamNetworkingIdentity; +using sdkabi::SteamNetworkingMessage_t; +using sdkabi::CallbackMsg_t; + +/* ------------------------------------------------------------------ * + * Runtime-bound entry points (flat C exports of steam_api64.dll -- the + * same surface every non-C++ language binding consumes, and the only + * one that is stable enough to GetProcAddress). + * ------------------------------------------------------------------ */ +static int (__cdecl *SteamInitFlat)(char* errmsg) = 0; /* 0 == OK */ +static bool (__cdecl *SteamInitOld)(void) = 0; /* pre-1.59 fallback */ +static void (__cdecl *SteamShutdown)(void) = 0; +static int32_t (__cdecl *SteamGetPipe)(void) = 0; +static void (__cdecl *DispatchInit)(void) = 0; +static void (__cdecl *DispatchRunFrame)(int32_t pipe) = 0; +static bool (__cdecl *DispatchGetNext)(int32_t pipe, CallbackMsg_t* out) = 0; +static void (__cdecl *DispatchFreeLast)(int32_t pipe) = 0; +static void* (__cdecl *GetMessagesIface)(void) = 0; +static int (__cdecl *MsgSendToUser)(void* self, const SteamNetworkingIdentity* to, + const void* data, uint32_t len, + int flags, int channel) = 0; +static int (__cdecl *MsgReceiveOnChannel)(void* self, int channel, + SteamNetworkingMessage_t** out, int max) = 0; +static bool (__cdecl *MsgAcceptSession)(void* self, const SteamNetworkingIdentity* who) = 0; +static bool (__cdecl *MsgCloseSession)(void* self, const SteamNetworkingIdentity* who) = 0; +static void (__cdecl *MsgRelease)(SteamNetworkingMessage_t* msg) = 0; +static void* (__cdecl *GetUserIface)(void) = 0; +static uint64_t (__cdecl *UserGetSteamID)(void* self) = 0; +/* optional -- absence degrades to "no lobby roster", never a failed Init */ +static void* (__cdecl *GetMatchmakingIface)(void) = 0; +static uint64_t (__cdecl *LobbyJoin)(void* self, uint64_t lobby) = 0; +static int (__cdecl *LobbyNumMembers)(void* self, uint64_t lobby) = 0; +static uint64_t (__cdecl *LobbyMemberByIndex)(void* self, uint64_t lobby, int i) = 0; +/* optional -- route diagnostics only (SetRouteDiagnostics) */ +typedef void (__cdecl *SteamDebugOutputFn)(int level, const char* msg); +static void* (__cdecl *GetUtilsIface)(void) = 0; +static void (__cdecl *UtilsSetDebugOutput)(void* self, int level, SteamDebugOutputFn fn) = 0; + +/* Version-suffixed accessors change between SDK releases, so try an ordered + * list and log which one bound -- that log line is the first thing to look at + * when a new Steam client ships and Init suddenly fails. */ +static const char* const kUserNames[] = { + "SteamAPI_SteamUser_v023", "SteamAPI_SteamUser_v022", "SteamAPI_SteamUser_v021" +}; +static const char* const kMessagesNames[] = { + "SteamAPI_SteamNetworkingMessages_SteamAPI_v002" +}; +static const char* const kMatchmakingNames[] = { + "SteamAPI_SteamMatchmaking_v009" +}; +/* Only used by SetRouteDiagnostics(). Optional everywhere -- absence costs a + * diagnostic, not the link. */ +static const char* const kUtilsNames[] = { + "SteamAPI_SteamNetworkingUtils_SteamAPI_v004", + "SteamAPI_SteamNetworkingUtils_SteamAPI_v003" +}; + +static FARPROC BindFirst(HMODULE dll, const char* const* names, size_t count, + const char** bound) +{ + for (size_t i = 0; i < count; i++) { + FARPROC p = GetProcAddress(dll, names[i]); + if (p) { *bound = names[i]; return p; } + } + *bound = 0; + return 0; +} + +static bool g_bound = false; +static std::string g_bindErr; + +/* Same idiom as LoadPcapLibrary(); one macro deep because there are twenty of + * these and a hand-spelled cast per entry point is where a signature typo + * hides. decltype keeps the cast welded to the declaration above. */ +#define BIND_ONE(ptr, name) \ + do { ptr = (decltype(ptr))GetProcAddress(dll, (name)); } while (0) +#define BIND_ANY(ptr, names, what) \ + do { const char* _w = 0; \ + ptr = (decltype(ptr))BindFirst(dll, names, sizeof(names)/sizeof(names[0]), &_w); \ + if (_w) Log("STEAM bound %s", _w); \ + else Log("STEAM no %s accessor in steam_api64.dll", what); } while (0) + +static bool BindSteamApi(std::string& err) +{ + /* Cache the handle forever, like LoadPcapLibrary: -1 = never tried, + * NULL = tried and failed (don't thrash LoadLibrary on every retry). */ + static HMODULE dll = (HMODULE)-1; + if (dll != (HMODULE)-1) { err = g_bindErr; return g_bound; } + + dll = LoadLibraryA("steam_api64.dll"); + if (!dll) { + g_bindErr = "steam_api64.dll not found -- copy the Steamworks redistributable " + "(redistributable_bin/win64) next to the executable"; + err = g_bindErr; + return false; + } + +#ifdef __MINGW32__ + // C++ defines function and data pointers as separate types to reflect + // Harvard architecture machines (like the Arduino). As such, casting + // between them isn't portable and GCC will helpfully warn us about it. + // We're only running this code on Windows which explicitly allows this + // behaviour, so silence the warning to avoid confusion. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + + BIND_ONE(SteamInitFlat, "SteamAPI_InitFlat"); + BIND_ONE(SteamInitOld, "SteamAPI_Init"); + BIND_ONE(SteamShutdown, "SteamAPI_Shutdown"); + BIND_ONE(SteamGetPipe, "SteamAPI_GetHSteamPipe"); + BIND_ONE(DispatchInit, "SteamAPI_ManualDispatch_Init"); + BIND_ONE(DispatchRunFrame, "SteamAPI_ManualDispatch_RunFrame"); + BIND_ONE(DispatchGetNext, "SteamAPI_ManualDispatch_GetNextCallback"); + BIND_ONE(DispatchFreeLast, "SteamAPI_ManualDispatch_FreeLastCallback"); + BIND_ONE(MsgSendToUser, "SteamAPI_ISteamNetworkingMessages_SendMessageToUser"); + BIND_ONE(MsgReceiveOnChannel, "SteamAPI_ISteamNetworkingMessages_ReceiveMessagesOnChannel"); + BIND_ONE(MsgAcceptSession, "SteamAPI_ISteamNetworkingMessages_AcceptSessionWithUser"); + BIND_ONE(MsgCloseSession, "SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser"); + BIND_ONE(MsgRelease, "SteamAPI_SteamNetworkingMessage_t_Release"); + BIND_ONE(UserGetSteamID, "SteamAPI_ISteamUser_GetSteamID"); + BIND_ONE(LobbyJoin, "SteamAPI_ISteamMatchmaking_JoinLobby"); + BIND_ONE(LobbyNumMembers, "SteamAPI_ISteamMatchmaking_GetNumLobbyMembers"); + BIND_ONE(LobbyMemberByIndex, "SteamAPI_ISteamMatchmaking_GetLobbyMemberByIndex"); + + BIND_ANY(GetMessagesIface, kMessagesNames, "ISteamNetworkingMessages"); + BIND_ANY(GetUserIface, kUserNames, "ISteamUser"); + BIND_ANY(GetMatchmakingIface, kMatchmakingNames, "ISteamMatchmaking"); + BIND_ANY(GetUtilsIface, kUtilsNames, "ISteamNetworkingUtils"); + BIND_ONE(UtilsSetDebugOutput, "SteamAPI_ISteamNetworkingUtils_SetDebugOutputFunction"); + +#ifdef __MINGW32__ +#pragma GCC diagnostic pop +#endif + + /* Matchmaking is deliberately NOT in this list: no lobby is a degraded + * mode (peers= still works), a missing transport export is not. */ + struct { const void* p; const char* what; } required[] = { + { (const void*)SteamShutdown, "SteamAPI_Shutdown" }, + { (const void*)SteamGetPipe, "SteamAPI_GetHSteamPipe" }, + { (const void*)DispatchInit, "SteamAPI_ManualDispatch_Init" }, + { (const void*)DispatchRunFrame, "SteamAPI_ManualDispatch_RunFrame" }, + { (const void*)DispatchGetNext, "SteamAPI_ManualDispatch_GetNextCallback" }, + { (const void*)DispatchFreeLast, "SteamAPI_ManualDispatch_FreeLastCallback" }, + { (const void*)GetMessagesIface, "SteamAPI_SteamNetworkingMessages_SteamAPI_v002" }, + { (const void*)MsgSendToUser, "SendMessageToUser" }, + { (const void*)MsgReceiveOnChannel, "ReceiveMessagesOnChannel" }, + { (const void*)MsgAcceptSession, "AcceptSessionWithUser" }, + { (const void*)MsgCloseSession, "CloseSessionWithUser" }, + { (const void*)MsgRelease, "SteamNetworkingMessage_t_Release" }, + { (const void*)GetUserIface, "ISteamUser accessor" }, + { (const void*)UserGetSteamID, "ISteamUser_GetSteamID" } + }; + for (size_t i = 0; i < sizeof(required)/sizeof(required[0]); i++) { + if (!required[i].p) { + g_bindErr = std::string("steam_api64.dll is missing ") + required[i].what + + " -- SDK too old for the flat NetworkingMessages API (need 1.46+)"; + err = g_bindErr; + return false; + } + } + if (!SteamInitFlat && !SteamInitOld) { + g_bindErr = "steam_api64.dll exports neither SteamAPI_InitFlat nor SteamAPI_Init"; + err = g_bindErr; + return false; + } + + g_bound = true; + return true; +} + +/* ------------------------------------------------------------------ * + * Link state. Single-threaded by contract (see steamlink.h), so no locks + * and no atomics anywhere below. + * ------------------------------------------------------------------ */ +struct Peer { + uint64_t steamId; + uint8_t mac[6]; + bool macKnown; + uint32_t ip; /* host order */ + char name[16]; + bool sessionUp; + bool isGateway; /* the session host: default route for unknown unicast */ + uint64_t framesTx, framesRx; + uint32_t helloSentMs; /* 0 = never sent */ + uint32_t helloHeardMs; /* 0 = never answered -- keeps it on the fast ladder */ + bool fromLobby; /* lobby-learned: a lobby leave may retire it */ +}; + +enum { + HELLO_LEN = 28, /* 'H' ver mac[6] ip[4] name[16] */ + HELLO_FAST_MS = 2000, /* until the peer has answered */ + HELLO_SLOW_MS = 30000, /* after */ + HELLO_REPLY_MIN_MS = 1000, /* floor on answering a peer's HELLO */ + LOBBY_POLL_MS = 2000, + RX_BATCH = 16 +}; + +static bool g_up = false; +static int g_refs = 0; +static int32_t g_pipe = 0; +static void* g_iMessages = 0; +static void* g_iUser = 0; +static void* g_iMatchmaking = 0; +static uint64_t g_selfId = 0; +static uint8_t g_selfMac[6] = { 0, 0, 0, 0, 0, 0 }; +static uint32_t g_selfIp = 0; +static char g_selfName[16] = { 0 }; +static int g_channel = 0; +static uint64_t g_lobby = 0; +static uint32_t g_lobbyPolledMs = 0; +static uint32_t g_epoch = 0; +static uint64_t g_tx = 0, g_rx = 0, g_txDropped = 0; + +/* std::map, not unordered_map: the peer set is <=7 entries and the gateway + * builds a BPF string out of Peers(), which is easier to eyeball and diff when + * the order is stable. */ +static std::map g_peers; +/* Refused identities, so a stranger hammering us costs one log line total. */ +static std::set g_refused; + +/* 32-bit like the rest of the fork (vpxlog.cpp): every elapsed test below is + * an unsigned 32-bit subtraction, which is exact across the 49.7-day wrap. + * GetTickCount64 is deliberately NOT used -- it disappears when a build pins + * _WIN32_WINNT below 0x0600, which parts of this tree do. */ +static inline uint32_t NowMs() { return (uint32_t)GetTickCount(); } + +static Peer* Find(uint64_t steamId) +{ + std::map::iterator it = g_peers.find(steamId); + return (it == g_peers.end()) ? 0 : &it->second; +} + +static Peer* FindByMac(const uint8_t mac[6]) +{ + for (std::map::iterator it = g_peers.begin(); it != g_peers.end(); ++it) + if (it->second.macKnown && memcmp(it->second.mac, mac, 6) == 0) return &it->second; + return 0; +} + +/* The session host, i.e. the default route for unicast we can't place. Null in + * a gateway-less session (two players and no console), which is why every + * caller falls back to flooding rather than dropping. */ +static Peer* FindGateway(void) +{ + for (std::map::iterator it = g_peers.begin(); it != g_peers.end(); ++it) + if (it->second.isGateway) return &it->second; + return 0; +} + +static const char* PeerName(const Peer& p) +{ + return p.name[0] ? p.name : "?"; +} + +/* ------------------------------------------------------------------ * + * Wire + * ------------------------------------------------------------------ */ +static bool SendRecord(Peer& p, const uint8_t* rec, int len) +{ + SteamNetworkingIdentity id; + sdkabi::IdentitySetSteamID(id, p.steamId); + /* A send to a peer we have no session with is what OPENS the session -- + * the peer gets a 1251 and accepts if we are in its allowlist. */ + const int r = MsgSendToUser(g_iMessages, &id, rec, (uint32_t)len, SEND_FLAGS, g_channel); + if (r != RESULT_OK) { + static unsigned long long errn = 0; + if (LogNth(++errn)) + Log("STEAM send error #%llu EResult=%d to %s (%llu)", + (unsigned long long)errn, r, PeerName(p), (unsigned long long)p.steamId); + return false; + } + return true; +} + +static void SendHello(Peer& p, uint32_t now) +{ + uint8_t rec[HELLO_LEN]; + memset(rec, 0, sizeof(rec)); + rec[0] = REC_HELLO; + rec[1] = 1; /* version */ + memcpy(rec + 2, g_selfMac, 6); + rec[8] = (uint8_t)(g_selfIp >> 24); /* BIG-endian on the wire */ + rec[9] = (uint8_t)(g_selfIp >> 16); + rec[10] = (uint8_t)(g_selfIp >> 8); + rec[11] = (uint8_t)(g_selfIp); + memcpy(rec + 12, g_selfName, 15); /* rec is zeroed => NUL-terminated */ + p.helloSentMs = now; + SendRecord(p, rec, (int)sizeof(rec)); +} + +/* ------------------------------------------------------------------ * + * Peer table + * ------------------------------------------------------------------ */ +static void AddPeerInternal(uint64_t steamId, bool fromLobby) +{ + if (!g_up || !steamId || steamId == g_selfId) return; + if (g_peers.find(steamId) != g_peers.end()) return; + + Peer p; + memset(&p, 0, sizeof(p)); + p.steamId = steamId; + p.fromLobby = fromLobby; + g_peers[steamId] = p; + g_refused.erase(steamId); /* allowlisted now; let it be logged again if dropped */ + Log("STEAM peer %llu added%s", (unsigned long long)steamId, fromLobby ? " (lobby)" : ""); + /* HELLO immediately: it opens the session AND carries our MAC binding. */ + SendHello(g_peers[steamId], NowMs()); +} + +void AddPeer(uint64_t steamId) { AddPeerInternal(steamId, false); } + +void RemovePeer(uint64_t steamId) +{ + std::map::iterator it = g_peers.find(steamId); + if (it == g_peers.end()) return; + const bool hadMac = it->second.macKnown; + if (g_iMessages && MsgCloseSession) { + SteamNetworkingIdentity id; + sdkabi::IdentitySetSteamID(id, steamId); + MsgCloseSession(g_iMessages, &id); + } + Log("STEAM peer %s (%llu) removed", PeerName(it->second), (unsigned long long)steamId); + g_peers.erase(it); + if (hadMac) g_epoch++; +} + +/* ------------------------------------------------------------------ * + * Callbacks (manual dispatch) + * ------------------------------------------------------------------ */ +/* Lift the leading SteamNetworkingIdentity out of a callback payload. + * memcpy, not a cast: the payload is a uint8_t* into steam_api64.dll's own + * callback buffer, so it carries no alignment promise (same reasoning as + * OnLobbyChatUpdate below). It also must not be handed straight back to the + * SDK -- the flat API takes the identity BY VALUE, which on the Win64 ABI is + * a pointer to a caller-owned temporary the callee is free to write to, and + * that buffer is Steam's, not ours. Copy first, pass the copy. */ +static bool IdentityFromCallback(const CallbackMsg_t& cb, SteamNetworkingIdentity& out) +{ + if (!cb.m_pubParam || cb.m_cubParam < (int)sizeof(SteamNetworkingIdentity)) return false; + memcpy(&out, cb.m_pubParam, sizeof(out)); + return true; +} + +static void OnSessionRequest(const CallbackMsg_t& cb) +{ + SteamNetworkingIdentity ident; + if (!IdentityFromCallback(cb, ident)) return; + const SteamNetworkingIdentity* id = &ident; + const uint64_t who = sdkabi::IdentityGetSteamID(*id); + + Peer* p = Find(who); + if (!p) { + /* App 480 is public, so anyone who learns our SteamID can ring the + * bell; refusing everything outside the session's peer list IS the + * security story for a private group. Nothing else gates inbound. */ + if (g_refused.insert(who).second) + Log("STEAM session request from %llu REFUSED (not in peer list)", + (unsigned long long)who); + return; + } + if (!MsgAcceptSession(g_iMessages, id)) { + Log("STEAM AcceptSessionWithUser failed for %llu", (unsigned long long)who); + return; + } + p->sessionUp = true; + Log("STEAM session accepted from %s (%llu)", PeerName(*p), (unsigned long long)who); + /* A fresh inbound session means that peer just (re)started: HELLO now so it + * binds our MAC without waiting out its own ladder. */ + SendHello(*p, NowMs()); +} + +static void OnSessionFailed(const CallbackMsg_t& cb) +{ + /* Payload is a SteamNetConnectionInfo_t whose FIRST member is the peer + * identity. We read exactly that and nothing past it -- the tail of that + * struct has grown between SDK releases. */ + SteamNetworkingIdentity ident; + if (!IdentityFromCallback(cb, ident)) return; + const SteamNetworkingIdentity* id = &ident; + const uint64_t who = sdkabi::IdentityGetSteamID(*id); + + Peer* p = Find(who); + if (!p) return; + /* Close it explicitly: a failed session stays failed, and every later + * SendMessageToUser fails instantly until the session is torn down. This + * call is what makes the re-announce below an actual retry. */ + if (MsgCloseSession) MsgCloseSession(g_iMessages, id); + p->sessionUp = false; + p->helloHeardMs = 0; /* back onto the 2s ladder... */ + p->helloSentMs = 0; /* ...starting with the next Pump */ + Log("STEAM session FAILED with %s (%llu) -- re-announcing", + PeerName(*p), (unsigned long long)who); +} + +static void OnLobbyChatUpdate(const CallbackMsg_t& cb) +{ + /* LobbyChatUpdate_t: u64 lobby, u64 userChanged, u64 userMakingChange, + * u32 stateChange. memcpy because the callback buffer is only byte-aligned. */ + if (!g_lobby || !cb.m_pubParam || cb.m_cubParam < 28) return; + uint64_t lobby = 0, user = 0; + uint32_t state = 0; + memcpy(&lobby, cb.m_pubParam + 0, 8); + memcpy(&user, cb.m_pubParam + 8, 8); + memcpy(&state, cb.m_pubParam + 24, 4); + if (lobby != g_lobby || !user || user == g_selfId) return; + + if (state & CHAT_MEMBER_ENTERED) { + AddPeerInternal(user, true); + } else { + /* Only lobby-learned peers leave with the lobby: the session file's + * peer list is the allowlist and stays put for the whole session. */ + Peer* p = Find(user); + if (p && p->fromLobby) RemovePeer(user); + } +} + +static void ServiceHellos(uint32_t now) +{ + for (std::map::iterator it = g_peers.begin(); it != g_peers.end(); ++it) { + Peer& p = it->second; + const uint32_t due = p.helloHeardMs ? (uint32_t)HELLO_SLOW_MS : (uint32_t)HELLO_FAST_MS; + if (!p.helloSentMs || (uint32_t)(now - p.helloSentMs) >= due) SendHello(p, now); + } +} + +static void ServiceLobby(uint32_t now) +{ + if (!g_lobby || !g_iMatchmaking || !LobbyNumMembers || !LobbyMemberByIndex) return; + if (g_lobbyPolledMs && (uint32_t)(now - g_lobbyPolledMs) < (uint32_t)LOBBY_POLL_MS) return; + g_lobbyPolledMs = now; + /* JoinLobby is asynchronous and we intentionally await no call result (see + * Pump), so the initial roster arrives by polling; 506 handles the deltas + * after that. Polling a 2-8 entry list every 2s costs nothing. */ + const int n = LobbyNumMembers(g_iMatchmaking, g_lobby); + for (int i = 0; i < n; i++) { + const uint64_t id = LobbyMemberByIndex(g_iMatchmaking, g_lobby, i); + if (id && id != g_selfId && !Find(id)) AddPeerInternal(id, true); + } +} + +/* ------------------------------------------------------------------ * + * API + * ------------------------------------------------------------------ */ +bool Init(const Config& cfg, const uint8_t self_mac[6], uint32_t self_ip, std::string& err) +{ + err.clear(); + + if (g_up) { + /* Refcounted per the header. A second Init only widens the peer set -- + * the MAC/IP/name of the first caller win, because they are the guest's + * and there is only one guest per process. */ + g_refs++; + for (size_t i = 0; i < cfg.peers.size(); i++) AddPeerInternal(cfg.peers[i], false); + return true; + } + if (!self_mac) { err = "steam: self MAC is required"; return false; } + if (!BindSteamApi(err)) return false; + + if (SteamInitFlat) { + char msg[1024]; + msg[0] = 0; + const int r = SteamInitFlat(msg); + if (r != 0) { + err = std::string("SteamAPI_Init failed (") + std::to_string(r) + "): " + + (msg[0] ? msg : "is Steam running and logged in? SteamAppId must be set (480)"); + return false; + } + } else if (!SteamInitOld()) { + err = "SteamAPI_Init failed -- is Steam running and logged in? " + "SteamAppId must be set (480)"; + return false; + } + + DispatchInit(); /* we own the pump; see Pump() */ + g_pipe = SteamGetPipe(); + if (!g_pipe) { err = "SteamAPI_GetHSteamPipe returned 0"; SteamShutdown(); return false; } + + g_iMessages = GetMessagesIface(); + if (!g_iMessages) { err = "ISteamNetworkingMessages unavailable"; SteamShutdown(); return false; } + + g_iUser = GetUserIface(); + g_selfId = g_iUser ? UserGetSteamID(g_iUser) : 0; + if (!g_selfId) { + err = "Steam user is not logged in (no SteamID)"; + SteamShutdown(); + return false; + } + g_iMatchmaking = GetMatchmakingIface ? GetMatchmakingIface() : 0; + + memcpy(g_selfMac, self_mac, 6); + g_selfIp = self_ip; + g_channel = cfg.channel; + g_lobby = cfg.lobby; + memset(g_selfName, 0, sizeof(g_selfName)); + if (!cfg.name.empty()) memcpy(g_selfName, cfg.name.c_str(), + cfg.name.size() < 15 ? cfg.name.size() : 15); + g_lobbyPolledMs = 0; + g_tx = g_rx = g_txDropped = 0; + g_peers.clear(); + g_refused.clear(); + g_up = true; + g_refs = 1; + + Log("STEAM link up: self %llu %02x:%02x:%02x:%02x:%02x:%02x %u.%u.%u.%u \"%s\" channel %d", + (unsigned long long)g_selfId, + g_selfMac[0], g_selfMac[1], g_selfMac[2], g_selfMac[3], g_selfMac[4], g_selfMac[5], + (g_selfIp >> 24) & 0xff, (g_selfIp >> 16) & 0xff, (g_selfIp >> 8) & 0xff, g_selfIp & 0xff, + g_selfName, g_channel); + + /* The pod MAC is derived from the slot IP by configure.ps1's formula; a + * mismatch means a hand-edited conf and shows up later as a peer nobody can + * route to, so say it once at Init instead. */ + if (g_selfIp && + (g_selfMac[0] != 0x02 || g_selfMac[1] != 0x00 || + g_selfMac[2] != (uint8_t)(g_selfIp >> 24) || g_selfMac[3] != (uint8_t)(g_selfIp >> 16) || + g_selfMac[4] != (uint8_t)(g_selfIp >> 8) || g_selfMac[5] != (uint8_t)(g_selfIp))) + Log("STEAM WARNING: macaddr is not 02:00:+IP octets -- check [ne2000] macaddr against the slot IP"); + + for (size_t i = 0; i < cfg.peers.size(); i++) AddPeerInternal(cfg.peers[i], false); + + /* The gateway is a peer like any other -- it just also serves as the + * default route. Seed it even if it was left out of cfg.peers, otherwise a + * session file listing it only as the gateway would have nowhere to send + * console traffic. */ + if (cfg.gateway) { + AddPeerInternal(cfg.gateway, false); + Peer* gw = Find(cfg.gateway); + if (gw) { + gw->isGateway = true; + Log("STEAM gateway (session host) = %llu -- default route for non-peer MACs", + (unsigned long long)cfg.gateway); + } + } else { + Log("STEAM no gateway configured -- unicast to an unknown MAC will flood every peer"); + } + + if (g_lobby) { + if (g_iMatchmaking && LobbyJoin) { + LobbyJoin(g_iMatchmaking, g_lobby); + Log("STEAM joining lobby %llu for the roster", (unsigned long long)g_lobby); + } else { + Log("STEAM lobby %llu ignored -- no ISteamMatchmaking in this steam_api64.dll", + (unsigned long long)g_lobby); + } + } + if (g_peers.empty() && !g_lobby) + Log("STEAM WARNING: no peers configured -- nothing will be sent"); + + return true; +} + +void Shutdown() +{ + if (!g_up) return; + if (--g_refs > 0) return; + + bool hadMacs = false; + for (std::map::iterator it = g_peers.begin(); it != g_peers.end(); ++it) { + hadMacs = hadMacs || it->second.macKnown; + if (MsgCloseSession) { + SteamNetworkingIdentity id; + sdkabi::IdentitySetSteamID(id, it->first); + MsgCloseSession(g_iMessages, &id); + } + } + g_peers.clear(); + g_refused.clear(); + if (hadMacs) g_epoch++; + + Log("STEAM link down (tx %llu rx %llu dropped %llu)", + (unsigned long long)g_tx, (unsigned long long)g_rx, (unsigned long long)g_txDropped); + + g_up = false; + g_iMessages = g_iUser = g_iMatchmaking = 0; + g_selfId = 0; + SteamShutdown(); + /* The DLL handle stays loaded on purpose: Steam does not survive a + * LoadLibrary/FreeLibrary cycle, and the pcap backend caches its handle the + * same way. */ +} + +void Pump() +{ + if (!g_up) return; + + /* The documented manual-dispatch loop. Manual dispatch (rather than + * SteamAPI_RunCallbacks) is what lets the fork service Steam from the + * emulator thread inside NE2000_Poller -- no callback registration, no + * second thread, no timer. */ + DispatchRunFrame(g_pipe); + CallbackMsg_t cb; + while (DispatchGetNext(g_pipe, &cb)) { + switch (cb.m_iCallback) { + case CB_SESSION_REQUEST: OnSessionRequest(cb); break; + case CB_SESSION_FAILED: OnSessionFailed(cb); break; + case CB_LOBBY_CHAT_UPDATE: OnLobbyChatUpdate(cb); break; + default: break; /* incl. SteamAPICallCompleted_t: nothing here awaits + * a call result, so there is no GetAPICallResult arm */ + } + DispatchFreeLast(g_pipe); /* every GetNextCallback that returned true */ + } + + const uint32_t now = NowMs(); + ServiceHellos(now); + ServiceLobby(now); +} + +void SendFrame(const uint8_t* frame, int len) +{ + if (!g_up) return; + + if (!frame || len < 14 || len > (int)VWE_MAX_FRAME) { + g_txDropped++; + if (LogNth(g_txDropped)) + Log("STEAM TX DROP #%llu len=%d (not 14..%d)", + (unsigned long long)g_txDropped, len, (int)VWE_MAX_FRAME); + return; + } + + /* One record = ['F'][frame]. Single-threaded by contract, so one static + * staging buffer beats a per-frame allocation on a 1 kHz path. */ + static uint8_t rec[1 + VWE_MAX_FRAME]; + rec[0] = REC_FRAME; + memcpy(rec + 1, frame, (size_t)len); + const int reclen = len + 1; + + int tried = 0, sent = 0; + Peer* dest = 0; + if ((frame[0] & 1) == 0) { /* unicast */ + dest = FindByMac(frame); + /* Not a peer MAC: this is almost certainly console-bound (200.0.0.10 + * lives on the host LAN behind the gateway, so its NIC MAC can never + * appear in the peer table). Route it to the gateway rather than + * flooding -- flooding would put the whole console<->pod stream, the + * egg transfer included, on every other player's uplink. */ + if (!dest) dest = FindGateway(); + } + if (dest) { + tried = 1; + if (SendRecord(*dest, rec, reclen)) { sent = 1; dest->framesTx++; } + } else { + /* Broadcast/multicast (ARP must reach everyone), or unicast with no + * gateway configured -- a gateway-less session, or the narrow race + * before a peer's HELLO has bound its MAC. */ + for (std::map::iterator it = g_peers.begin(); it != g_peers.end(); ++it) { + tried++; + if (SendRecord(it->second, rec, reclen)) { sent++; it->second.framesTx++; } + } + } + + if (sent) { + g_tx++; + if (LogNth(g_tx)) + Log("STEAM TX #%llu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x peers=%d", + (unsigned long long)g_tx, len, frame[0], frame[1], frame[2], + frame[6], frame[7], frame[8], sent); + } else { + g_txDropped++; + if (LogNth(g_txDropped)) + Log("STEAM TX DROP #%llu len=%d dst=%02x:%02x:%02x (%s)", + (unsigned long long)g_txDropped, len, frame[0], frame[1], frame[2], + tried ? "no peer took it" : "no peers"); + } +} + +/* Returns 1 if a frame reached the callback. ALWAYS releases the message -- + * every early-out below leaks a Steam allocation otherwise, and this runs a + * thousand times a second for a whole evening. */ +static int HandleMessage(SteamNetworkingMessage_t* m, + const std::function& cb) +{ + if (!m) return 0; + int delivered = 0; + const uint8_t* d = (const uint8_t*)m->m_pData; + const int len = m->m_cbSize; + const uint64_t who = sdkabi::IdentityGetSteamID(m->m_identityPeer); + Peer* p = Find(who); + + if (!p) { + /* Only allowlisted identities get a session, so this is a peer we + * dropped mid-flight -- or an SDK identity type we do not speak. */ + static unsigned long long strayn = 0; + if (LogNth(++strayn)) + Log("STEAM RX from unknown identity %llu discarded (#%llu)", + (unsigned long long)who, (unsigned long long)strayn); + } else if (!d || len < 1) { + /* Throttled like every other junk path: these are driven by the PEER's + * send rate, and a version-skewed pod re-HELLOs every 2s for the whole + * mission. Powers of 2 still log the first one loudly. */ + static unsigned long long emptyn = 0; + if (LogNth(++emptyn)) + Log("STEAM RX empty record from %s (#%llu)", + PeerName(*p), (unsigned long long)emptyn); + } else if (d[0] == REC_HELLO) { + if (len < HELLO_LEN || d[1] != 1) { + static unsigned long long badhn = 0; + if (LogNth(++badhn)) + Log("STEAM RX bad HELLO from %llu (len=%d ver=%d) (#%llu)", + (unsigned long long)who, len, len >= 2 ? d[1] : -1, + (unsigned long long)badhn); + } else { + const uint32_t now = NowMs(); + p->sessionUp = true; + p->helloHeardMs = now; + p->ip = ((uint32_t)d[8] << 24) | ((uint32_t)d[9] << 16) | + ((uint32_t)d[10] << 8) | (uint32_t)d[11]; + memcpy(p->name, d + 12, 16); + p->name[15] = 0; + if (!p->macKnown || memcmp(p->mac, d + 2, 6) != 0) { + memcpy(p->mac, d + 2, 6); + p->macKnown = true; + g_epoch++; /* the gateway recompiles its BPF off this */ + Log("STEAM peer %s (%llu) is %02x:%02x:%02x:%02x:%02x:%02x %u.%u.%u.%u", + PeerName(*p), (unsigned long long)who, + p->mac[0], p->mac[1], p->mac[2], p->mac[3], p->mac[4], p->mac[5], + (p->ip >> 24) & 0xff, (p->ip >> 16) & 0xff, (p->ip >> 8) & 0xff, p->ip & 0xff); + } + /* Answer a HELLO we were not going to send for another 30s: a peer + * that just restarted has an empty table and needs our binding now. + * The 1s floor is what keeps two peers from ping-ponging forever. */ + if ((uint32_t)(now - p->helloSentMs) >= (uint32_t)HELLO_REPLY_MIN_MS) + SendHello(*p, now); + } + } else if (d[0] == REC_FRAME) { + const int flen = len - 1; + if (flen < 14) { + static unsigned long long runtn = 0; + if (LogNth(++runtn)) + Log("STEAM RX runt frame from %s len=%d (#%llu)", + PeerName(*p), flen, (unsigned long long)runtn); + } else { + p->sessionUp = true; + if (!p->macKnown) { + /* Fallback binding for a HELLO that raced past us. HELLO stays + * authoritative -- it can correct this, this never overwrites it + * (a relayed frame's src MAC is not necessarily the peer's). */ + memcpy(p->mac, d + 1 + 6, 6); + p->macKnown = true; + g_epoch++; + Log("STEAM peer %llu src-MAC learned %02x:%02x:%02x:%02x:%02x:%02x (no HELLO yet)", + (unsigned long long)who, + p->mac[0], p->mac[1], p->mac[2], p->mac[3], p->mac[4], p->mac[5]); + } + p->framesRx++; + g_rx++; + if (LogNth(g_rx)) + Log("STEAM RX #%llu len=%d dst=%02x:%02x:%02x src=%02x:%02x:%02x from=%s", + (unsigned long long)g_rx, flen, d[1], d[2], d[3], d[7], d[8], d[9], + PeerName(*p)); + cb(d + 1, flen); + delivered = 1; + } + } else { + static unsigned long long unkn = 0; + if (LogNth(++unkn)) + Log("STEAM RX unknown record type 0x%02x from %s (#%llu)", + d[0], PeerName(*p), (unsigned long long)unkn); + } + + MsgRelease(m); + return delivered; +} + +int PollFrames(const std::function& cb) +{ + if (!g_up) return 0; + int delivered = 0; + SteamNetworkingMessage_t* msgs[RX_BATCH]; + for (;;) { + const int n = MsgReceiveOnChannel(g_iMessages, g_channel, msgs, RX_BATCH); + if (n <= 0) break; + for (int i = 0; i < n; i++) delivered += HandleMessage(msgs[i], cb); + if (n < RX_BATCH) break; /* short batch == queue drained */ + } + return delivered; +} + +uint64_t SelfSteamID() { return g_selfId; } + +/* Steam calls this from ITS OWN service thread, not ours. Log() must therefore + * stay a plain sink write -- do not touch the peer table or any other state + * from in here. */ +static void __cdecl SteamDebugSink(int level, const char* msg) +{ + Log("STEAM/net[%d] %s", level, msg ? msg : ""); +} + +void SetRouteDiagnostics(int level) +{ + if (!g_up) { Log("STEAM route diagnostics ignored -- link is not up"); return; } + void* utils = GetUtilsIface ? GetUtilsIface() : 0; + if (!utils || !UtilsSetDebugOutput) { + Log("STEAM route diagnostics unavailable -- this steam_api64.dll has no " + "ISteamNetworkingUtils::SetDebugOutputFunction"); + return; + } + UtilsSetDebugOutput(utils, level, level > 0 ? SteamDebugSink : 0); + Log("STEAM route diagnostics ON at level %d -- watch for the relay/direct path", level); +} + +uint32_t PeerEpoch() { return g_epoch; } + +size_t Peers(std::vector& out) +{ + out.clear(); + out.reserve(g_peers.size()); + for (std::map::const_iterator it = g_peers.begin(); it != g_peers.end(); ++it) { + const Peer& p = it->second; + PeerInfo pi; + memset(&pi, 0, sizeof(pi)); + pi.steamId = p.steamId; + memcpy(pi.mac, p.mac, 6); + pi.macKnown = p.macKnown; + pi.ip = p.ip; + memcpy(pi.name, p.name, sizeof(pi.name)); + pi.name[sizeof(pi.name) - 1] = 0; + pi.sessionUp = p.sessionUp; + pi.isGateway = p.isGateway; + pi.framesTx = p.framesTx; + pi.framesRx = p.framesRx; + out.push_back(pi); + } + return out.size(); +} + +void Counters(uint64_t& tx, uint64_t& rx, uint64_t& txDropped) +{ + tx = g_tx; + rx = g_rx; + txDropped = g_txDropped; +} + +} // namespace vwesteam + +#else /* !WIN32 -- the whole VWE deployment is Windows; this keeps the file + * harmless in a cross-platform build instead of breaking it. */ + +namespace vwesteam { + +bool Init(const Config&, const uint8_t*, uint32_t, std::string& err) +{ + err = "steam transport is Windows-only in this build"; + Log("%s", err.c_str()); + return false; +} + +void Shutdown() {} +void Pump() {} +void SendFrame(const uint8_t*, int) {} +int PollFrames(const std::function&) { return 0; } +uint64_t SelfSteamID() { return 0; } +void SetRouteDiagnostics(int) {} +uint32_t PeerEpoch() { return 0; } +size_t Peers(std::vector& out) { out.clear(); return 0; } +void AddPeer(uint64_t) {} +void RemovePeer(uint64_t) {} +void Counters(uint64_t& tx, uint64_t& rx, uint64_t& txDropped) { tx = rx = txDropped = 0; } + +} // namespace vwesteam + +#endif /* WIN32 */ diff --git a/emulator/vpx-device/steamlink.h b/emulator/vpx-device/steamlink.h new file mode 100644 index 00000000..dd703c1f --- /dev/null +++ b/emulator/vpx-device/steamlink.h @@ -0,0 +1,216 @@ +/* + * steamlink.h -- VWE Steam transport core (Ethernet frames over Steam P2P). + * + * Shared VERBATIM by two consumers, which is why it is a free-function + * namespace and not a class: + * * the DOSBox-X fork's `backend=steam` NE2000 backend (ethernet_steam.cpp) + * -- run by every REMOTE player; no Npcap on those machines at all; + * * the session host's standalone steam<->pcap gateway (steam-gw/main.cpp), + * which bridges the tunnel onto the Network Bridge so TeslaConsole + * (200.0.0.10) and the host's own pcap pod can reach the remote pods. + * + * WHY L2: the 1995 mesh (WATTCP + netnub + L4NetworkManager) dials peers by + * raw IP on one flat subnet and ARPs for them. NAT/UDP hole-punching at L3 + * cannot carry that; tunnelling whole Ethernet FRAMES preserves the + * same-switch semantics the DOS binaries were built for. See + * ../STEAM-NOTES.md and ../steam/SESSION-CONTRACT.md. + * + * Threading: NOT thread-safe by design. Every entry point is called from a + * single thread -- in the fork that is the emulator thread (GetPackets runs + * off NE2000_Poller, a TIMER tick handler, i.e. once per emulated + * millisecond), in the gateway it is the one service loop. Do not call from + * two threads. + * + * Steamworks is bound at RUNTIME (LoadLibrary/GetProcAddress on + * steam_api64.dll), exactly like the pcap backend binds WPCAP.DLL. A build + * made without the SDK DLL present still runs -- `backend=steam` just fails + * Init() with a readable message, the same way a missing Npcap fails pcap. + * + * Windows-only for now (the whole VWE deployment is), but nothing here is + * structurally Windows-bound apart from the DLL loader. + */ + +#ifndef VWE_STEAMLINK_H +#define VWE_STEAMLINK_H + +#include +#include +#include +#include +#include + +namespace vwesteam { + +/* ------------------------------------------------------------------ * + * Wire format (channel `Config::channel`, default 0) + * + * One Steam message == one record. First byte is the record type. + * + * 'F' frame : ['F'][raw Ethernet frame, 14..1514 bytes] + * 'H' hello : ['H'][u8 ver=1][u8 mac[6]][u32 ip, BIG-endian][char name[16]] + * + * HELLO is the authoritative SteamID64 <-> MAC binding. It is sent when a + * session comes up and then periodically (fast until the peer has answered, + * slow after) so a peer that restarts its pod is re-learned without any + * lobby round trip. Learning the src MAC off an 'F' frame is the fallback + * for a HELLO that raced past us. + * + * Everything is sent RELIABLE + NoNagle. Rationale (do not "optimize" this + * to unreliable without re-reading it): the payload is guest TCP driven by + * WATTCP's DOS-era retransmit timers, which are multi-second. One dropped + * segment on an unreliable tunnel therefore costs seconds of visible stall, + * whereas the reliable channel's RTT-based retransmit costs one RTT. The + * usual TCP-over-TCP meltdown argument needs an AGGRESSIVE inner + * retransmitter; WATTCP is the opposite. Reliable is also ORDERED (matches + * the no-reorder model the Phase 0 latency gate validated) and fragments + * transparently far past 1514 bytes, which retires MTU as an issue. + * ------------------------------------------------------------------ */ + +enum : uint8_t { + REC_FRAME = 'F', + REC_HELLO = 'H' +}; + +/* Largest Ethernet frame we relay (no jumbo; the NE2000 can't make one). */ +enum { VWE_MAX_FRAME = 1514 }; + +struct Config { + /* Allowlist AND initial peer set: SteamID64s of the other pods in this + * session. Steam session requests from an identity that is not in here + * are refused -- with a private lobby that is the whole security story, + * which is adequate for a friends group on app 480. */ + std::vector peers; + + /* Optional: a Steam lobby to pull the roster from instead of / in + * addition to `peers` (Phase 3). 0 = don't touch matchmaking. */ + uint64_t lobby = 0; + + /* The session host, i.e. the peer running the steam<->pcap gateway + * (SESSION-CONTRACT.md section 2: exactly one peer carries + * "gateway": true). 0 = no gateway in this session. + * + * This is the DEFAULT ROUTE. A remote pod's frames for TeslaConsole at + * 200.0.0.10 carry the console's real NIC MAC, which is a host-LAN + * address and will never appear in the peer table, so without a default + * route the only way to deliver them is to flood every peer -- which + * duplicates the whole console<->pod stream (the egg transfer above all) + * to all 7 other players and pays for it on everyone's uplink. */ + uint64_t gateway = 0; + + /* Steam networking channel. Keep 0 unless two links share a process. */ + int channel = 0; + + /* Short label announced in HELLO and printed in logs (pilot/slot name). + * Truncated to 15 chars + NUL on the wire. */ + std::string name; +}; + +/* Log sink. The fork points this at LOG_MSG, the gateway at stderr. Set it + * BEFORE Init() if you want to see Init()'s own diagnostics. */ +typedef void (*LogFn)(const char* msg); +void SetLog(LogFn fn); + +/* Bring the link up. + * self_mac the guest's 6-byte MAC ([ne2000] macaddr). REQUIRED and must be + * stable -- it is what peers route to. A random MAC breaks the + * mesh on every restart. + * self_ip the guest's IPv4 in HOST byte order (diagnostics + the + * gateway's a-priori 02:00:xx:xx:xx:xx sanity check). 0 = unknown. + * err human-readable reason on failure. + * Returns false if steam_api64.dll is missing, Steam isn't running, the user + * isn't logged in, or the SDK rejected init. Refcounted with Shutdown(). */ +bool Init(const Config& cfg, const uint8_t self_mac[6], uint32_t self_ip, + std::string& err); + +/* Tear down: closes every peer session and releases the SDK. */ +void Shutdown(); + +/* Service Steam: runs the manual-dispatch callback pump (session requests -> + * accept-if-allowlisted, session failures -> log + mark for re-HELLO, lobby + * roster deltas when Config::lobby is set) and re-sends HELLOs when due. + * + * MUST be called regularly -- the fork calls it at the top of GetPackets(), + * i.e. once per emulated ms, which is also why no separate thread or timer is + * needed anywhere. */ +void Pump(); + +/* Queue one Ethernet frame to the peer(s) it is addressed to. Frames shorter + * than 14 or longer than VWE_MAX_FRAME are dropped. + * + * Routing: + * broadcast / multicast -> every peer (ARP has to reach everyone) + * unicast to a learned MAC -> that one peer + * unicast to an unknown MAC -> Config::gateway if set, else every peer + * + * The unknown-unicast case is almost always console-bound traffic, which is + * why the gateway is the default route rather than a flood. Falling back to + * flooding when no gateway is configured keeps a gateway-less session (two + * players, no console) working, and also covers the narrow race where a peer's + * MAC is not yet learned -- narrow because ARP is broadcast, so a peer is + * normally bound before anyone unicasts to it. */ +void SendFrame(const uint8_t* frame, int len); + +/* Drain received frames, invoking `cb` for each. Returns how many were + * delivered. NOTE: this hands over EVERY frame a peer sent us -- the caller + * applies its own acceptance policy (the fork mimics the NE2000's + * dst==self||broadcast rule so behaviour matches the pcap backend's BPF; the + * gateway injects everything onto the bridge unmodified). The pointer is only + * valid inside the callback. */ +int PollFrames(const std::function& cb); + +/* Our own SteamID64 (0 before a successful Init). */ +uint64_t SelfSteamID(); + +/* Route diagnostics: relayed (SDR/TURN) vs direct P2P, and the switch between + * them. Call AFTER Init; safe to skip entirely. + * + * Why it is a log firehose and not a clean bool: the flag that answers this, + * k_nSteamNetworkConnectionInfoFlags_Relayed, lives in SteamNetConnectionInfo_t + * and needs an HSteamNetConnection to read. ISteamNetworkingMessages never + * gives one out -- the SDK says so at SteamNetworkingMessage_t::m_conn: "Not + * used when using the ISteamNetworkingMessages interface". So the only route + * left is Steam's own debug output, which names the path it chose and logs the + * relay->direct upgrade when it happens. + * + * `level` is Valve's ESteamNetworkingSocketsDebugOutputType: 4 = Warning, + * 5 = Msg (route changes, the useful setting), 6 = Verbose, 7 = Debug, + * 8 = Everything (a wall of per-packet text). Output goes to the same LogFn + * sink as everything else. Degrades to a logged notice if this steam_api64.dll + * has no ISteamNetworkingUtils. */ +void SetRouteDiagnostics(int level); + +/* --- introspection, for the gateway's BPF and for status lines ---------- */ + +/* Bumps every time the known set of peer MACs changes (HELLO from a new peer, + * a peer's MAC changing after a pod restart, a session dropping). The gateway + * watches this to know when to recompile + reinstall its pcap filter; polling + * a counter keeps that policy in the gateway instead of in a callback. */ +uint32_t PeerEpoch(); + +struct PeerInfo { + uint64_t steamId; + uint8_t mac[6]; + bool macKnown; /* false until HELLO or a learned src MAC */ + uint32_t ip; /* host order; 0 if unknown */ + char name[16]; + bool sessionUp; + bool isGateway; /* the session host; the default route (see Config) */ + uint64_t framesTx, framesRx; +}; + +/* Snapshot of the peer table (for logs, status panels and the gateway's + * filter build). Returns the number written. */ +size_t Peers(std::vector& out); + +/* Add/remove a peer at runtime (lobby deltas, or a launcher that learns a + * late joiner). Adding an already-known peer is a no-op. */ +void AddPeer(uint64_t steamId); +void RemovePeer(uint64_t steamId); + +/* Aggregate counters, for the throttled "STEAM TX/RX #n" log lines that mirror + * the pcap backend's. */ +void Counters(uint64_t& tx, uint64_t& rx, uint64_t& txDropped); + +} // namespace vwesteam + +#endif // VWE_STEAMLINK_H diff --git a/restoration/source410/BT_L4/BTL4VID.CPP b/restoration/source410/BT_L4/BTL4VID.CPP index 76eace0c..482668e3 100644 --- a/restoration/source410/BT_L4/BTL4VID.CPP +++ b/restoration/source410/BT_L4/BTL4VID.CPP @@ -611,11 +611,57 @@ void // joint DCS -- so the eye sits in the canopy and // rides torso twist through chain composition). // + // THE INSIDE VIEW LOADS THE X-VARIANT SKELETON. The + // resource names one skeleton (mad.skl) for both views; + // the cockpit build is derived from it by the fleet-wide + // naming convention -- third letter X: MAD->MAX, + // AVA->AVX, BAT->BAX, BLH->BLX, FIR->FIX, JAK->JAX, + // LOK->LOX, with the numbered chassis following + // (MAD1->MAX1, LOK1->LOX1; all present in VIDEO/). The + // X skeleton carries the SAME 25-joint chain -- so the + // canopy eye and torso twist compose identically -- but + // its only geometry is the cockpit shell (MAX.SKL -> + // max_cop.bgf, the MAX_COP canopy with PUNCH-texel + // windows from the real-pod capture forensics). The + // donor names the same mechanism from the decomp side: + // inside = SkeletonType_A with '_cop' selection + // (btl4vid.hpp:678). Without this, the pilot sits + // inside the OUTSIDE model staring at torso panels. + // int eye_count = 0; - ReadSKLFile(entity, root_DCS, - video_object->GetObjectFilename(), view_type, - &eye_count); + const char + *skeleton_name = video_object->GetObjectFilename(); + char + inside_name[64]; + dpl_DCS + *skl_result = NULL; + + if ( + view_type == insideEntity && + strlen(skeleton_name) >= 3 && + strlen(skeleton_name) < sizeof(inside_name) + ) + { + strcpy(inside_name, skeleton_name); + inside_name[2] = + (inside_name[2] >= 'a' && inside_name[2] <= 'z') + ? 'x' : 'X'; + skl_result = ReadSKLFile(entity, root_DCS, + inside_name, view_type, &eye_count); + if (skl_result == NULL) + { + DEBUG_STREAM << "[skl] no cockpit variant '" + << inside_name + << "' -- falling back to the body skeleton\n" + << flush; + } + } + if (skl_result == NULL) + { + ReadSKLFile(entity, root_DCS, + skeleton_name, view_type, &eye_count); + } // // 3. FALLBACK eyepoint only. A skeleton without a diff --git a/restoration/source410/RENDER-ROADMAP.NOTES.md b/restoration/source410/RENDER-ROADMAP.NOTES.md index 49070704..eff689dd 100644 --- a/restoration/source410/RENDER-ROADMAP.NOTES.md +++ b/restoration/source410/RENDER-ROADMAP.NOTES.md @@ -1559,3 +1559,48 @@ overwrites the ramp with 1.0f -- authored dead weight, preserved as the binary's shape). recenterActive carved from dynamicsState (class size unchanged). With 5.3.68's table fix, the RIO/TM twist buttons now land in these members and move the torso -- and the canopy eye rides the twist chain. + +-------------------------------------------------------------------------------- +THE RESIDUAL FAULT, CAUGHT IN THE ACT -- AND THE INSIDE VIEW GETS ITS COCKPIT +-------------------------------------------------------------------------------- +TWO instruments landed together this session: + +1. VPX_PF_WATCH (emulator fork, cpu/paging.cpp): on any guest page fault at + the watched linear address, dump guest registers, the last 64 serial RX + deliveries (time + guest cs:eip at delivery + byte), the code bytes at the + faulting EIP and the stack top. serialnamedpipe's doReceive() feeds the + ring (VPX_SerialTraceByte). Armed via VPX_PF_WATCH=7000FA64 in podrun.sh + and launch_pod.ps1; the dump lands in dosboxlog.txt / pod_err.txt. + + FIRST CATCH decoded the fault completely: + CS:EIP = 00FF:000066D4 (the DPMI host, flat selector) + EBX = F000CA60 <- an IVT ENTRY read as a dword: seg F000, off CA60 + = a BIOS DEFAULT interrupt handler + access = [EBX+0x3004], and the segment's 0x80000000 base wrap gives + exactly cr2 = 0x7000FA64 + So the host reads a probe word 0x3004 bytes past a REAL-MODE VECTOR VALUE + treated as a flat pointer. For the host's own handlers (low memory) that + read lands in mapped conventional memory and is harmless; for a BIOS + F000:xxxx default it lands in unmapped high linear space and faults. The + serial ring shows a steady 1-byte/1-3ms vRIO stream with NOTHING special + at the fault -- the stream only determines WHICH vectors the host walks + and when. The +0x3004 constant appears nowhere in DPMI32VM.OVL or + 32RTM.EXE as an immediate, so it is composed at runtime; the probe now + also dumps code bytes at EIP + stack to close that gap on the next catch + (faulthunt.sh loops runs until it fires). + +2. SHIPPED BASELINE: 4/4 clean. Consistent with the exposure theory + (expected ~0.6 faults if the hazard is proportional to the load window) + -- not yet discriminating. The probe data is the discriminator now. + +THE INSIDE VIEW LOADS THE COCKPIT SKELETON. MAX.SKL solved the own-mech +question the authentic way: the X-variant naming convention (third letter X: +MAD->MAX, AVA->AVX, BAT->BAX, BLH->BLX, FIR->FIX, JAK->JAX, LOK->LOX, plus +numbered MAX1/MAX2 -- all 64 present in VIDEO/) selects a skeleton with the +SAME 25-joint chain but a single object: max_cop.bgf, the MAX_COP canopy +shell with the PUNCH-texel windows from the real-pod capture forensics. The +donor names the same mechanism from the decomp side (inside = SkeletonType_A +with '_cop' selection, btl4vid.hpp:678). Our mech case now derives the +inside name from the resource's skeleton (fallback to the body skeleton if +the X file is missing), so the pilot sits in the authentic cockpit instead +of inside the body model's torso panels.