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.