Implements the firmware-disasm-confirmed Firestorm box protocol in the MatrixPortal replica (ESC X outline / ESC x mode-fill via a BOXOP operand collector, plus the ESC Y region op), updates FIRMWARE.md with the findings, and adds FS-plasma-bench.ps1 -- a simulated MW4 serial stream for benching the display without a pod. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
248 lines
10 KiB
PowerShell
248 lines
10 KiB
PowerShell
<#
|
|
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)
|
|
<ascii bytes> 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()
|
|
}
|
|
}
|