# 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, [string]$Keys = "", [int]$KeyDelay = 4000 ) $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 ($Keys) { $argList += @("-keybuf", ('"' + $Keys + '"'), "-keybuf-delay", "$KeyDelay") } 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