<# plasma-bench.ps1 -- Bench-test driver for the MW4/FireStorm cockpit plasma score display. Replays the exact byte sequences MW4.exe sends to the plasma display on COM2 (class CPlasma over C232Comm; sources: mw4\Code\MW4\CRIOMAIN.CPP + c232comm.cpp). Port settings (c232comm.cpp Open()): 9600 baud, 8 data bits, no parity, 1 stop bit, no CTS/RTS/XON flow control; DTR is pulsed high at open then dropped ~10 ms later (DTR_CONTROL_ENABLE -> DTR_CONTROL_DISABLE + Sleep(10)). Bytes go out raw, unframed. Command set (ESC = 0x1B), CRIOMAIN.CPP lines 2998-3139: ESC @ clear screen (PlasmaClear) ESC G n cursor mode (PlasmaCursor: 0=off 1=on 2=flash) ESC R n cursor X (PlasmaCursorX) ESC Q n cursor Y (PlasmaCursorY) ESC H n font attribute (PlasmaFontAttr: 0=normal 1=half 2=line 4=reverse 8=flash) ESC K n font select (PlasmaFont: 0-3,6,7 = 5x7 4,5 = 10x14) ESC X l t r b draw box outline (PlasmaBoxDraw) ESC x 0 l t r b fill/blank box (PlasmaBoxFill) text at current cursor (PlasmaText) Game sequence replayed here (PlasmaDisplay, CRIOMAIN.CPP:3281): startup clear + cursor off (CPlasma ctor) state 1 ready centered 9-space wipe, then centered callsign (<=9 chars -> font 5 [10x14]; 10-20 chars -> font 2 [5x7]) state 2 start score panel: fill(27,19,93,30), boxes (27,19,42,30)+(42,19,93,30); rank in left box (x=33 one digit / x=29 two), score right-aligned ending at x=85-5*len; both font 2, y=20. Later updates redraw only the field that changed (no boxes) -- use -CountUp to exercise that. state 0 end clear (-ClearAtEnd) Examples: .\plasma-bench.ps1 -Port COM5 -Callsign JEFF -Rank 3 -Score 4200 .\plasma-bench.ps1 -Port COM5 -Callsign JEFF -CountUp 5 -CountUpInterval 1.5 .\plasma-bench.ps1 -DryRun # print the byte stream, touch no port #> param( [string]$Port = 'COM2', [int]$Baud = 9600, [string]$Callsign = 'FStest6789012', [ValidateRange(1, 99)][int]$Rank = 1, [ValidateRange(0, 99999999)][int]$Score = 1250, [double]$DelaySeconds = 5, # callsign hold time before the score panel appears [int]$CountUp = 0, # extra score updates after the first draw (simulates in-game scoring) [double]$CountUpInterval = 2, # seconds between count-up updates [int]$CountUpStep = 150, # score increment per update [switch]$ClearAtEnd, # cursor-off + clear before closing (game does this at mission end) [switch]$DtrPulse, # replicate the game's DTR pulse at open (some USB CDC devices choke on it) [switch]$HexDump, # print every packet as hex as it is sent [switch]$DryRun # no serial port; just print the packets (implies -HexDump) ) $ErrorActionPreference = 'Stop' $ESC = [byte]0x1B if ($DryRun) { $HexDump = $true } # --- serial plumbing (C232Comm) --------------------------------------------- $script:SerialPort = $null function New-PlasmaPort { $sp = New-Object System.IO.Ports.SerialPort $Port, $Baud, ([System.IO.Ports.Parity]::None), 8, ([System.IO.Ports.StopBits]::One) $sp.Handshake = [System.IO.Ports.Handshake]::None $sp.WriteTimeout = 1000 return $sp } function Open-Plasma { if ($DryRun) { return } # DEFAULT: plain open, modem lines untouched. The plasma protocol is data bytes # only; the game's DTR pulse (DTR_CONTROL_ENABLE at open -> DISABLE after # Sleep(10), c232comm.cpp) is generic port-open code, not display protocol, and # a failed modem-line request on a USB CDC device (usbser.sys) faults the .NET # serial stream and can knock the device off the bus. -DtrPulse opts back in # for a real RS-232 wiring that expects it. $sp = New-PlasmaPort if ($DtrPulse) { try { $sp.RtsEnable = $false # fRtsControl = 0 $sp.DtrEnable = $true # DTR_CONTROL_ENABLE at open... $sp.Open() Start-Sleep -Milliseconds 10 $sp.DtrEnable = $false # ...then DTR_CONTROL_DISABLE after Sleep(10) } catch { Write-Warning "DTR pulse failed ($($_.Exception.Message.Trim())); reopening without touching modem lines." try { $sp.Dispose() } catch { } # wait for the device to settle / re-enumerate if the failed request upset it $deadline = (Get-Date).AddSeconds(10) while (((Get-Date) -lt $deadline) -and ([System.IO.Ports.SerialPort]::GetPortNames() -notcontains $Port)) { Start-Sleep -Milliseconds 250 } Start-Sleep -Milliseconds 500 $sp = New-PlasmaPort $sp.Open() } } else { $sp.Open() # .NET defaults: DTR/RTS not asserted } Start-Sleep -Milliseconds 200 # let USB CDC settle before the first write $script:SerialPort = $sp } function Send-Packet { # CPlasma::SendPacket -> txComLoop: one raw write per command, no framing param([byte[]]$Bytes) if ($HexDump) { $hex = ($Bytes | ForEach-Object { '{0:X2}' -f $_ }) -join ' ' $txt = -join ($Bytes | ForEach-Object { if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { '.' } }) Write-Host (' TX {0,-42} {1}' -f $hex, $txt) } if ($script:SerialPort) { $script:SerialPort.Write($Bytes, 0, $Bytes.Length) } } # --- CPlasma command primitives (CRIOMAIN.CPP:2998-3097) -------------------- function Plasma-Clear { Send-Packet ([byte[]]($ESC, 0x40)) } # ESC @ function Plasma-Cursor { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x47, $n)) } # ESC G function Plasma-CursorX { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x52, $n)) } # ESC R function Plasma-CursorY { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x51, $n)) } # ESC Q function Plasma-FontAttr { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x48, $n)) } # ESC H function Plasma-Font { param([byte]$n) Send-Packet ([byte[]]($ESC, 0x4B, $n)) } # ESC K function Plasma-BoxDraw { param([int]$l, [int]$t, [int]$r, [int]$b) Send-Packet ([byte[]]($ESC, 0x58, $l, $t, $r, $b)) # ESC X } function Plasma-BoxFill { param([int]$l, [int]$t, [int]$r, [int]$b) Send-Packet ([byte[]]($ESC, 0x78, 0, $l, $t, $r, $b)) # ESC x 0 } function Get-FontSize { # CPlasma::GetFontSize: fonts 0-3,6,7 = 5x7; fonts 4,5 = 10x14 param([int]$Font) if ($Font -eq 4 -or $Font -eq 5) { return @{ W = 10; H = 14 } } return @{ W = 5; H = 7 } } function Plasma-PosText { # CPlasma::PlasmaPosText, including its auto font selection + centering math param([string]$Text, [int]$X = 0, [int]$Y = 0, [int]$Attr = 0, [int]$Font = 5) if (-not $Text) { return } $len = $Text.Length if ($len -le 0) { return } if ($Font -ne 2) { # auto-pick: big font up to 9 chars, else small, cap 20 if ($len -le 9) { $Font = 5 } elseif ($len -le 20) { $Font = 2 } else { $Font = 2; $len = 20 } } elseif ($len -gt 20) { $len = 20 } $size = Get-FontSize $Font if ($X -eq 0 -and $Y -eq 0) { # center on the 112-wide panel (C++ integer division) $X = 56 - [math]::Floor(($len * $size.W) / 2) $Y = 15 - [math]::Floor($size.H / 2) } Plasma-CursorX $X Plasma-CursorY $Y Plasma-FontAttr $Attr Plasma-Font $Font Send-Packet ([System.Text.Encoding]::ASCII.GetBytes($Text.Substring(0, $len))) } # --- game-level draws (CRIOMAIN.CPP:3141-3308) ------------------------------- $script:OldRank = $null # m_nOldRank / m_nOldScore (INT_MIN after "ready") $script:OldScore = $null function Plasma-ScoreDraw { # CPlasma::PlasmaScoreDraw: boxes on first draw, then changed-fields-only updates param([string]$RankStr, [string]$ScoreStr, [bool]$BoxFlag) if ($BoxFlag) { Plasma-BoxFill 27 19 93 30 Plasma-BoxDraw 27 19 42 30 Plasma-BoxDraw 42 19 93 30 } if ($RankStr.Length -ge 1 -and $RankStr.Length -le 2 -and $ScoreStr.Length -ge 1 -and $ScoreStr.Length -le 8) { if ($script:OldRank -ne [int]$RankStr) { Plasma-PosText ' ' 29 20 0 2 if ($RankStr.Length -eq 1) { Plasma-PosText $RankStr 33 20 0 2 } else { Plasma-PosText $RankStr 29 20 0 2 } } if ($script:OldScore -ne [int]$ScoreStr) { Plasma-PosText ' ' 44 20 0 2 Plasma-PosText $ScoreStr (85 - ($ScoreStr.Length * 5)) 20 0 2 } } else { Write-Warning "Rank must be 1-2 chars and score 1-8 chars; skipping draw (game does the same)." } $script:OldRank = [int]$RankStr $script:OldScore = [int]$ScoreStr } # --- main: replay the game's display sequence -------------------------------- if ($Callsign.Length -gt 20) { $Callsign = $Callsign.Substring(0, 20) } try { if (-not $DryRun) { Write-Host "Opening $Port @ $Baud 8N1 (no flow control)..." } Open-Plasma # CPlasma ctor: clear + cursor off Write-Host '[startup] clear + cursor off' Plasma-Clear Plasma-Cursor 0 # state 1 "ready": 9-space wipe then the callsign, both auto-centered Write-Host "[ready] callsign '$Callsign'" Plasma-PosText ' ' Plasma-PosText $Callsign Write-Host ("[wait] {0}s" -f $DelaySeconds) if (-not $DryRun) { Start-Sleep -Seconds $DelaySeconds } # state 2 "start": first score draw includes the panel boxes Write-Host "[score] rank $Rank, score $Score (panel + fields)" Plasma-ScoreDraw "$Rank" "$Score" $true for ($i = 1; $i -le $CountUp; $i++) { if (-not $DryRun) { Start-Sleep -Seconds $CountUpInterval } $Score += $CountUpStep Write-Host "[score] update ${i}: score $Score (changed fields only)" Plasma-ScoreDraw "$Rank" "$Score" $false } if ($ClearAtEnd) { # CPlasma dtor: cursor off + clear Write-Host '[end] cursor off + clear' Plasma-Cursor 0 Plasma-Clear } Write-Host 'Done.' } finally { if ($script:SerialPort) { $script:SerialPort.Close() $script:SerialPort.Dispose() } }