# Two-pod LAN mesh test: two -net pods on loopback, marshaled by a
# minimal console feeder speaking the Munga protocol (vendored
# TeslaSuite Munga Net.dll). Proves the deterministic TCP mesh works
# through the NetTransport seam end to end:
#   pod A listens on game port 1502, pod B connects to it from 1602,
#   both ACK the egg only after "All connections completed!", race the
#   selected 60s, stop on the console's StopMission(0), and report
#   final scores.
param(
  # a full game install: rpl4opt.exe + RPL4.RES + *.INI + AUDIO/GAUGE/VIDEO
  [string]$GameDir = "$PSScriptRoot\..\dist",
  # the vendored console-protocol library from TeslaSuite
  [string]$MungaNetDll = 'C:\VWE\TeslaSuite\Console\lib\Munga Net.dll',
  # ordinal plasma art is lifted from the reference egg
  [string]$ReferenceEgg = "$PSScriptRoot\..\assets\RP411\TEST.EGG",
  [string]$WorkDir = "$env:TEMP\rp412-twopod"
)
$ErrorActionPreference = 'Stop'
$GameDir = (Resolve-Path $GameDir).Path
$ReferenceEgg = (Resolve-Path $ReferenceEgg).Path
if (-not (Test-Path $WorkDir)) { New-Item -ItemType Directory $WorkDir | Out-Null }
$scratch = $WorkDir
$smoke = $GameDir

Add-Type -Path $MungaNetDll
Add-Type -AssemblyName System.Drawing
Add-Type -AssemblyName System.Windows.Forms
$src = @'
using System;
using System.Runtime.InteropServices;
public class PodWin {
  [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
  [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
  [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }
}
'@
Add-Type -TypeDefinition $src

#-----------------------------------------------------------------
# Pod working dirs: junction the big asset dirs, copy the roots
#-----------------------------------------------------------------
foreach ($pod in 'podA', 'podB') {
  $dir = "$scratch\$pod"
  if (-not (Test-Path $dir)) { New-Item -ItemType Directory $dir | Out-Null }
  foreach ($assets in 'AUDIO', 'GAUGE', 'VIDEO') {
    if (-not (Test-Path "$dir\$assets")) {
      New-Item -ItemType Junction -Path "$dir\$assets" -Target "$smoke\$assets" | Out-Null
    }
  }
  foreach ($file in 'environ.ini', 'JOYSTICK.INI', 'RPDPL.INI', 'RPL4.RES', 'libsndfile-1.dll', 'OpenAL32.dll', 'rpl4opt.exe') {
    if (Test-Path "$smoke\$file") { Copy-Item "$smoke\$file" $dir -Force }
  }
  Remove-Item "$dir\rpl4.log", "$dir\last.egg" -Force -Confirm:$false -ErrorAction SilentlyContinue
}

#-----------------------------------------------------------------
# The two-pilot egg (RPMission.ToEggString layout; blank plasma
# name bitmaps - the glass is hidden in RP412 anyway; ordinals
# lifted verbatim from TEST.EGG)
#-----------------------------------------------------------------
function Add-BlankBitmap([Text.StringBuilder]$sb, [int]$w, [int]$h) {
  $row = 'bitmap=' + ('0' * ($w / 4))
  for ($i = 0; $i -lt $h; $i++) { [void]$sb.AppendLine($row) }
  [void]$sb.AppendLine("x=$w"); [void]$sb.AppendLine("y=$h")
}

$test = [IO.File]::ReadAllText($ReferenceEgg)
$ordinals = $test.Substring($test.IndexOf('[ordinals]'))

$pilots = @(
  @{ addr = '127.0.0.1:1502'; name = 'PODA'; vehicle = 'speck'; color = 'Red' },
  @{ addr = '127.0.0.1:1602'; name = 'PODB'; vehicle = 'roach'; color = 'Blue' }
)

$sb = New-Object Text.StringBuilder
[void]$sb.AppendLine('[mission]')
[void]$sb.AppendLine('adventure=Red Planet')
[void]$sb.AppendLine('map=wise')
[void]$sb.AppendLine('scenario=race')
[void]$sb.AppendLine('time=day')
[void]$sb.AppendLine('weather=clear')
[void]$sb.AppendLine('temperature=0')
[void]$sb.AppendLine('compression=0')
[void]$sb.AppendLine('length=60')
[void]$sb.AppendLine('[pilots]')
foreach ($p in $pilots) { [void]$sb.AppendLine("pilot=$($p.addr)") }
$index = 1
foreach ($p in $pilots) {
  [void]$sb.AppendLine("[$($p.addr)]")
  [void]$sb.AppendLine('hostType=0')
  [void]$sb.AppendLine('dropzone=one')
  [void]$sb.AppendLine("name=$($p.name)")
  [void]$sb.AppendLine("bitmapindex=$index")
  [void]$sb.AppendLine('loadzones=1')
  [void]$sb.AppendLine("vehicle=$($p.vehicle)")
  [void]$sb.AppendLine("color=$($p.color)")
  [void]$sb.AppendLine('badge=None')
  $index++
}
[void]$sb.AppendLine('[largebitmap]')
foreach ($p in $pilots) { [void]$sb.AppendLine("bitmap=BitMap::Large::$($p.name)") }
[void]$sb.AppendLine('[smallbitmap]')
foreach ($p in $pilots) { [void]$sb.AppendLine("bitmap=BitMap::Small::$($p.name)") }
foreach ($p in $pilots) {
  [void]$sb.AppendLine("[BitMap::Large::$($p.name)]")
  Add-BlankBitmap $sb 128 32
  [void]$sb.AppendLine('width=8')
  [void]$sb.AppendLine("[BitMap::Small::$($p.name)]")
  Add-BlankBitmap $sb 64 16
  [void]$sb.AppendLine('width=4')
}
[void]$sb.Append($ordinals)
$eggText = $sb.ToString()
[IO.File]::WriteAllText("$scratch\twopod.egg", $eggText)
Write-Output "egg built: $($eggText.Length) chars"

# wire form: newlines -> NUL, chunked into EggFileMessages (RPMission.ToEggFileMessages)
$wire = $eggText.Replace("`r`n", "`0").Replace("`n", "`0")
$bytes = [Text.Encoding]::ASCII.GetBytes($wire)
$chunks = @()
for ($off = 0; $off -lt $bytes.Length; $off += 1000) {
  $n = [Math]::Min(1000, $bytes.Length - $off)
  $buf = New-Object byte[] 1000
  [Buffer]::BlockCopy($bytes, $off, $buf, 0, $n)
  $chunks += New-Object Munga.Net.EggFileMessage([int]($off / 1000), $bytes.Length, $n, $buf)
}
Write-Output "egg chunks: $($chunks.Count)"

#-----------------------------------------------------------------
# Launch the pods
#-----------------------------------------------------------------
Get-Process rpl4opt -ErrorAction SilentlyContinue | Stop-Process -Force -Confirm:$false -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
$procs = @{}
$procs['A'] = Start-Process -FilePath "$scratch\podA\rpl4opt.exe" -ArgumentList '-windowed','-res','1920','1080','-net','1501' -WorkingDirectory "$scratch\podA" -PassThru
Start-Sleep -Seconds 2
$procs['B'] = Start-Process -FilePath "$scratch\podB\rpl4opt.exe" -ArgumentList '-windowed','-res','1920','1080','-net','1601' -WorkingDirectory "$scratch\podB" -PassThru
Start-Sleep -Seconds 8

# offset pod B's window so a desktop screenshot shows both
if ($procs['B'].MainWindowHandle -ne 0) {
  [PodWin]::SetWindowPos($procs['B'].MainWindowHandle, [IntPtr]::Zero, 700, 250, 0, 0, 0x0015) | Out-Null
}

#-----------------------------------------------------------------
# The feeder: connect, poll state, feed eggs, launch, time, stop
#-----------------------------------------------------------------
$pods = @{}
foreach ($k in @('A','B')) {
  $port = if ($k -eq 'A') { 1501 } else { 1601 }
  $sock = New-Object Munga.Net.MungaSocket
  $sock.Connect([Net.IPAddress]::Loopback, [uint16]$port)
  $pods[$k] = @{
    sock = $sock; state = $null; ack = $false
    eggSent = [DateTime]::MinValue; lastQuery = [DateTime]::MinValue
    score = $null; runSent = $false; stopSent = $false
  }
  Write-Output "console connected to pod $k (port $port)"
}

$stateQuery = New-Object Munga.Net.StateQueryMessage(1)
$runStart = $null
$deadline = (Get-Date).AddMinutes(6)
$raceShotTaken = $false
$log = New-Object System.Collections.Generic.List[string]

while ((Get-Date) -lt $deadline) {
  foreach ($k in @('A','B')) {
    $pod = $pods[$k]
    $now = Get-Date
    if (($now - $pod.lastQuery).TotalSeconds -ge 1) {
      $pod.sock.Send(0, 1, $stateQuery)
      $pod.lastQuery = $now
    }
    for ($m = $pod.sock.Receive(); $m -ne $null; $m = $pod.sock.Receive()) {
      $msg = $m.Message
      if ($msg -eq $null) { continue }
      $type = $msg.GetType().Name
      switch ($type) {
        'StateResponseMessage' {
          if ("$($pod.state)" -ne "$($msg.ApplicationState)") {
            $log.Add("pod ${k}: state -> $($msg.ApplicationState)")
            Write-Output "pod ${k}: state -> $($msg.ApplicationState)"
          }
          $pod.state = $msg.ApplicationState
        }
        'AcknowledgeEggFileMessage' {
          if (-not $pod.ack) { $log.Add("pod ${k}: EGG ACK (mesh complete)"); Write-Output "pod ${k}: EGG ACK (mesh complete)" }
          $pod.ack = $true
        }
        'EndMissionMessage' {
          $pod.score = $msg.FinalScore
          $log.Add("pod ${k}: FINAL SCORE host=$($msg.PlayerHostID) score=$($msg.FinalScore)")
          Write-Output "pod ${k}: FINAL SCORE host=$($msg.PlayerHostID) score=$($msg.FinalScore)"
        }
        default {
          $log.Add("pod ${k}: $type")
        }
      }
    }
    if ("$($pod.state)" -eq 'WaitingForEgg' -and -not $pod.ack -and
        ($now - $pod.eggSent).TotalSeconds -ge 5) {
      Write-Output "pod ${k}: sending egg ($($chunks.Count) chunks)"
      foreach ($chunk in $chunks) { $pod.sock.Send(0, 1, $chunk) }
      $pod.eggSent = $now
    }
  }

  $bothWaiting = ("$($pods['A'].state)" -eq 'WaitingForLaunch') -and ("$($pods['B'].state)" -eq 'WaitingForLaunch')
  if ($bothWaiting -and -not $pods['A'].runSent) {
    Write-Output 'both pods WaitingForLaunch: sending RunMission'
    foreach ($k in @('A','B')) {
      $pods[$k].sock.Send(0, 1, (New-Object Munga.Net.RunMissionMessage))
      $pods[$k].runSent = $true
    }
  }

  $bothRunning = ("$($pods['A'].state)" -eq 'RunningMission') -and ("$($pods['B'].state)" -eq 'RunningMission')
  if ($bothRunning -and $runStart -eq $null) {
    $runStart = Get-Date
    Write-Output "both pods RUNNING at $runStart - 60s race underway"
  }

  if ($runStart -ne $null -and -not $raceShotTaken -and ((Get-Date) - $runStart).TotalSeconds -ge 20) {
    $raceShotTaken = $true
    $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
    $bmp = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
    $g = [System.Drawing.Graphics]::FromImage($bmp)
    $g.CopyFromScreen(0, 0, 0, 0, $bmp.Size)
    $g.Dispose()
    $bmp.Save("$scratch\twopod_race.png", [System.Drawing.Imaging.ImageFormat]::Png)
    $bmp.Dispose()
    Write-Output 'race screenshot saved'
  }

  if ($runStart -ne $null -and -not $pods['A'].stopSent -and
      ((Get-Date) - $runStart).TotalSeconds -ge 60) {
    Write-Output 'race time expired: sending StopMission(0) to both pods'
    foreach ($k in @('A','B')) {
      $pods[$k].sock.Send(0, 1, (New-Object Munga.Net.StopMissionMessage(0)))
      $pods[$k].stopSent = $true
    }
  }

  if ($pods['A'].stopSent -and $pods['A'].score -ne $null -and $pods['B'].score -ne $null) {
    Write-Output 'SUCCESS: both pods raced the mesh, stopped on command, scores collected'
    break
  }
  Start-Sleep -Milliseconds 250
}

Start-Sleep -Seconds 5
Write-Output '=== summary ==='
Write-Output "pod A: ack=$($pods['A'].ack) finalState=$($pods['A'].state) score=$($pods['A'].score)"
Write-Output "pod B: ack=$($pods['B'].ack) finalState=$($pods['B'].state) score=$($pods['B'].score)"
foreach ($k in @('A','B')) { try { $pods[$k].sock.Shutdown() } catch {} }
Get-Process rpl4opt -ErrorAction SilentlyContinue | Stop-Process -Force -Confirm:$false -ErrorAction SilentlyContinue
Write-Output '=== pod A netlog ==='
Get-Content "$scratch\podA\rpl4.log" -ErrorAction SilentlyContinue | Select-String 'isten|onnect|ccept|egg|Egg|host|Host' | Select-Object -First 14 | ForEach-Object Line
Write-Output '=== pod B netlog ==='
Get-Content "$scratch\podB\rpl4.log" -ErrorAction SilentlyContinue | Select-String 'isten|onnect|ccept|egg|Egg|host|Host' | Select-Object -First 14 | ForEach-Object Line