|
- # Shared helpers for Nova128 build and x128 verification scripts.
- # Import-Module (Join-Path $PSScriptRoot "lib\Nova128Test.psm1") -Force
-
- $script:ViceBin = "C:\Program Files\GTK3VICE-3.10-win64\bin"
-
- function Get-ViceTool([string]$Name) {
- $path = Join-Path $script:ViceBin "$Name.exe"
- if (-not (Test-Path -LiteralPath $path)) { throw "$Name.exe not found at $path" }
- return $path
- }
-
- # Assemble each source and link them with an ld65 config.
- # Returns @{ Program; Map; Labels }.
- function Invoke-Nova128Link {
- param(
- [Parameter(Mandatory)][string]$Name,
- [Parameter(Mandatory)][string[]]$Sources,
- [Parameter(Mandatory)][string]$Config,
- [Parameter(Mandatory)][string]$BuildDir,
- [string[]]$Defines = @()
- )
- $ca65 = (Get-Command ca65 -ErrorAction Stop).Source
- $ld65 = (Get-Command ld65 -ErrorAction Stop).Source
- New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
- $out = @{
- Program = Join-Path $BuildDir "$Name.prg"
- Map = Join-Path $BuildDir "$Name.map"
- Labels = Join-Path $BuildDir "$Name.lbl"
- }
- foreach ($path in $out.Values) {
- if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force }
- }
- $defineArgs = @()
- foreach ($d in $Defines) { $defineArgs += @("-D", $d) }
- $objects = @()
- foreach ($source in $Sources) {
- $object = Join-Path $BuildDir ("$Name-" + [IO.Path]::GetFileNameWithoutExtension($source) + ".o")
- & $ca65 --cpu 6502 @defineArgs -o $object $source
- if ($LASTEXITCODE -ne 0) { throw "ca65 failed on $source" }
- $objects += $object
- }
- $linkOutput = & $ld65 -C $Config -o $out.Program -m $out.Map -Ln $out.Labels @objects 2>&1
- if ($LASTEXITCODE -ne 0) { throw ("ld65 failed:`n" + ($linkOutput | Out-String)) }
- return $out
- }
-
- function Get-Nova128Label {
- param([Parameter(Mandatory)][string]$Labels, [Parameter(Mandatory)][string]$Name)
- $text = Get-Content -Raw -LiteralPath $Labels
- $m = [regex]::Match($text, "(?im)^al\s+([0-9a-f]+)\s+\." + [regex]::Escape($Name) + "\s*$")
- if (-not $m.Success) { throw "label $Name not found in $Labels" }
- return [Convert]::ToInt32($m.Groups[1].Value, 16)
- }
-
- # ASCII text -> PETSCII SEQ bytes, using the same rules as Vim and
- # prepare-disk.ps1: a-z -> $41-$5A, A-Z -> $C1-$DA, CRLF/LF -> $0D.
- function ConvertTo-Nova128Petscii {
- param([Parameter(Mandatory)][AllowEmptyString()][string]$Text)
- $bytes = [Text.Encoding]::ASCII.GetBytes($Text)
- $out = New-Object System.Collections.Generic.List[byte]
- for ($i = 0; $i -lt $bytes.Length; $i++) {
- $v = $bytes[$i]
- if ($v -eq 13) {
- if ($i + 1 -lt $bytes.Length -and $bytes[$i + 1] -eq 10) { continue }
- $out.Add(13); continue
- }
- if ($v -eq 10) { $out.Add(13); continue }
- if ($v -ge 0x61 -and $v -le 0x7A) { $out.Add([byte]($v - 0x20)); continue }
- if ($v -ge 0x41 -and $v -le 0x5A) { $out.Add([byte]($v + 0x80)); continue }
- $out.Add($v)
- }
- return ,$out.ToArray()
- }
-
- # Create a fresh disk image and write files to it.
- # $Files: ordered list of @{ Name = "decls.n8"; Path = host file; Type = "seq"|"prg" }
- function New-Nova128Disk {
- param(
- [Parameter(Mandatory)][string]$Path,
- [Parameter(Mandatory)][object[]]$Files,
- [ValidateSet("d64", "d81")][string]$Type = "d81",
- [string]$Label = "nova128,n8"
- )
- $c1541 = Get-ViceTool "c1541"
- if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Force }
- $c1541Args = @("-format", $Label, $Type, $Path)
- foreach ($f in $Files) {
- $suffix = if ($f.Type -eq "seq") { ",s" } else { "" }
- $c1541Args += @("-write", $f.Path, ($f.Name + $suffix))
- }
- $output = & $c1541 @c1541Args 2>&1
- if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $Path)) {
- throw ("c1541 failed:`n" + ($output | Out-String))
- }
- $listing = (& $c1541 -attach $Path -list 2>&1) | Out-String
- foreach ($f in $Files) {
- if ($listing -notmatch ('"' + [regex]::Escape($f.Name) + '"\s+' + $f.Type)) {
- throw "$($f.Name) ($($f.Type)) missing from $Path"
- }
- }
- }
-
- # Start x128 with a remote monitor. Returns a session object.
- function Start-Nova128X128 {
- param(
- [Parameter(Mandatory)][string]$Autostart,
- [int]$Port = 6530,
- [string]$Drive8Image,
- [string]$Drive8Type = "1581",
- [string]$Screenshot
- )
- $x128 = Get-ViceTool "x128"
- $argList = @()
- if ($Drive8Image) {
- $argList += @("-drive8type", $Drive8Type, "-drive8truedrive", "-8", ('"' + $Drive8Image + '"'))
- }
- $argList += @("-autostart", ('"' + $Autostart + '"'), "-autostart-warp", "-warp",
- "-sounddev", "dummy", "-remotemonitor",
- "-remotemonitoraddress", "ip4://127.0.0.1:$Port")
- if ($Screenshot) {
- if (Test-Path -LiteralPath $Screenshot) { Remove-Item -LiteralPath $Screenshot -Force }
- $argList += @("-exitscreenshotvicii", ('"' + $Screenshot + '"'))
- }
- $psi = [Diagnostics.ProcessStartInfo]::new()
- $psi.FileName = $x128
- $psi.Arguments = $argList -join ' '
- $psi.UseShellExecute = $true
- $psi.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
- $process = [Diagnostics.Process]::Start($psi)
-
- $client = $null
- $deadline = [DateTime]::UtcNow.AddSeconds(10)
- while ([DateTime]::UtcNow -lt $deadline) {
- if ($process.HasExited) { throw "x128 exited before the remote monitor connected" }
- try {
- $client = [Net.Sockets.TcpClient]::new()
- $client.Connect("127.0.0.1", $Port)
- break
- } catch {
- if ($client) { $client.Dispose(); $client = $null }
- Start-Sleep -Milliseconds 200
- }
- }
- if (-not $client) { $process.Kill(); throw "could not connect to the VICE remote monitor" }
- $stream = $client.GetStream()
- $writer = [IO.StreamWriter]::new($stream, [Text.Encoding]::ASCII, 1024, $true)
- $writer.NewLine = "`n"
- $writer.AutoFlush = $true
- return [pscustomobject]@{ Process = $process; Client = $client; Stream = $stream; Writer = $writer }
- }
-
- function Invoke-Nova128Monitor {
- param([Parameter(Mandatory)]$Session, [Parameter(Mandatory)][string]$Command,
- [int]$TimeoutMs = 5000)
- $Session.Writer.WriteLine($Command)
- $buffer = New-Object byte[] 65536
- $text = ""
- # Every monitor reply ends with a prompt such as "(C:$1c0d) ".
- $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
- while ([DateTime]::UtcNow -lt $deadline) {
- if ($Session.Stream.DataAvailable) {
- $n = $Session.Stream.Read($buffer, 0, $buffer.Length)
- if ($n -le 0) { break }
- $text += [Text.Encoding]::ASCII.GetString($buffer, 0, $n)
- if ($text -match '\(C:\$[0-9a-f]{4}\)\s*$') { break }
- } else {
- Start-Sleep -Milliseconds 50
- }
- }
- return $text
- }
-
- # Read memory through the monitor. Returns byte[] (length = Count).
- # Reads RAM bank 0 by default, not the CPU's current view: after a program
- # returns to BASIC, $4000-$BFFF shows BASIC ROM in the CPU view.
- function Read-Nova128Memory {
- param([Parameter(Mandatory)]$Session, [int]$Address, [int]$Count, [string]$Bank = "ram")
- Invoke-Nova128Monitor $Session "bank $Bank" | Out-Null
- $timeout = 5000 + 2 * $Count # large dumps print slowly
- $text = Invoke-Nova128Monitor $Session ("m `${0:X4} `${1:X4}" -f $Address, ($Address + $Count - 1)) -TimeoutMs $timeout
- $memory = @{}
- foreach ($row in [regex]::Matches($text, "(?i)C:([0-9a-f]{4})((?:[ ]{1,2}[0-9a-f]{2}){1,16})")) {
- $a = [Convert]::ToInt32($row.Groups[1].Value, 16)
- foreach ($hex in ($row.Groups[2].Value.Trim() -split '\s+')) {
- $memory[$a] = [Convert]::ToByte($hex, 16); $a++
- }
- }
- $bytes = New-Object byte[] $Count
- for ($i = 0; $i -lt $Count; $i++) {
- if (-not $memory.ContainsKey($Address + $i)) {
- throw ("monitor did not return `${0:X4}; response: {1}" -f ($Address + $i), $text)
- }
- $bytes[$i] = $memory[$Address + $i]
- }
- return ,$bytes
- }
-
- # Poll the test mailbox ($1BF0) until it reports PASS or FAIL. Returns its
- # 16 bytes. Resumes emulation between polls.
- function Wait-Nova128Mailbox {
- param([Parameter(Mandatory)]$Session, [int]$TimeoutSeconds = 60)
- $deadline = [DateTime]::UtcNow.AddSeconds($TimeoutSeconds)
- while ($true) {
- Start-Sleep -Seconds 1
- $mb = Read-Nova128Memory $Session 0x1BF0 16
- if ($mb[0] -eq 0x4E -and $mb[1] -eq 0x38 -and ($mb[3] -eq 0xA5 -or $mb[3] -eq 0xFA)) {
- return ,$mb
- }
- if ([DateTime]::UtcNow -ge $deadline) {
- $state = if ($mb[0] -eq 0x4E) { "status `$" + $mb[3].ToString("X2") } else { "never initialized" }
- throw "mailbox did not finish within $TimeoutSeconds s ($state)"
- }
- Invoke-Nova128Monitor $Session "x" | Out-Null
- }
- }
-
- # Let the program finish printing, then quit x128 (writes any exit screenshot).
- function Stop-Nova128X128 {
- param([Parameter(Mandatory)]$Session, [int]$SettleSeconds = 2)
- try {
- if ($SettleSeconds -gt 0) {
- Invoke-Nova128Monitor $Session "x" | Out-Null
- Start-Sleep -Seconds $SettleSeconds
- Invoke-Nova128Monitor $Session "m 0000 0000" | Out-Null
- }
- $Session.Writer.WriteLine("quit")
- $Session.Writer.Dispose()
- $Session.Client.Dispose()
- if (-not $Session.Process.WaitForExit(10000)) { throw "x128 did not exit" }
- } finally {
- if (-not $Session.Process.HasExited) { $Session.Process.Kill(); $Session.Process.WaitForExit() }
- }
- }
-
- Export-ModuleMember -Function Get-ViceTool, Invoke-Nova128Link, Get-Nova128Label,
- ConvertTo-Nova128Petscii, New-Nova128Disk, Start-Nova128X128, Invoke-Nova128Monitor,
- Read-Nova128Memory, Wait-Nova128Mailbox, Stop-Nova128X128
|