BridgeTownCrier handles inbound towncrier.add / towncrier.remove, pushing
website-published news into GlobalTownCrierEntryList so every town crier
announces it until it expires. Both run on the Core thread (required: AddEntry
mutates a shared list and the criers send packets). An id maps to the created
TownCrierEntry so a later remove can pull it, and re-adding an id replaces the
prior entry.
Caps are enforced before touching the shared list -- line count, line length,
active-entry count, duration -- as defense in depth on top of the loopback trust
boundary: a buggy or compromised sidecar still cannot flood the criers or pin a
message forever. Config: Bridge.TownCrierMax{Lines,LineLength,Active,DurationSec}.
Verified with a sending stub and a probe that logs the actual crier list. Replies
and game state agree: add n1 -> towncrier.ok and the entry appears with the exact
lines; add n2 (8 lines over the cap of 6) -> towncrier.error and never enters the
list; remove n1 -> towncrier.ok and the entry is gone; remove unknown ->
towncrier.error. Evidence in docs/PLAN.md §16.
Adds BridgeJson.GetStringList for JSON string arrays, tools/stub_sidecar_crier.ps1,
and tools/scaffolding/BridgeCrierProbe.cs. This closes the pure-plugin inbound
work; only the PlayerVendorSale core edit (Phase 7) remains on the ServUO side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
65 lines
2.0 KiB
PowerShell
65 lines
2.0 KiB
PowerShell
param(
|
|
[int] $Port = 7788,
|
|
[string] $Log = "$PSScriptRoot\sc_crier.log"
|
|
)
|
|
|
|
function Say($msg) {
|
|
for ($i = 0; $i -lt 5; $i++) {
|
|
try { "$msg" | Out-File -FilePath $Log -Append -Encoding utf8; return }
|
|
catch { Start-Sleep -Milliseconds 100 }
|
|
}
|
|
}
|
|
|
|
"" | Out-File -FilePath $Log -Encoding utf8
|
|
Say "[crier-sc] starting on 127.0.0.1:$Port"
|
|
|
|
$listener = New-Object System.Net.Sockets.TcpListener([System.Net.IPAddress]::Loopback, $Port)
|
|
$listener.Server.SetSocketOption('Socket', 'ReuseAddress', $true)
|
|
$bound = $false
|
|
for ($i = 0; $i -lt 30 -and -not $bound; $i++) {
|
|
try { $listener.Start(); $bound = $true } catch { Start-Sleep -Seconds 1 }
|
|
}
|
|
if (-not $bound) { Say "[crier-sc] could not bind"; exit 1 }
|
|
Say "[crier-sc] listening"
|
|
|
|
$client = $listener.AcceptTcpClient()
|
|
Say "[crier-sc] === shard connected ==="
|
|
$stream = $client.GetStream()
|
|
$stream.ReadTimeout = 1500
|
|
$reader = New-Object System.IO.StreamReader($stream)
|
|
$writer = New-Object System.IO.StreamWriter($stream)
|
|
$writer.AutoFlush = $true
|
|
Start-Sleep -Milliseconds 500
|
|
|
|
# Blocking read with a timeout, so StreamReader-buffered lines are not missed the way
|
|
# checking $stream.DataAvailable does.
|
|
function Drain($seconds) {
|
|
$deadline = (Get-Date).AddSeconds($seconds)
|
|
while ((Get-Date) -lt $deadline) {
|
|
try {
|
|
$l = $reader.ReadLine()
|
|
if ($null -ne $l) { Say "[crier-sc] <- $l" }
|
|
} catch { Start-Sleep -Milliseconds 50 }
|
|
}
|
|
}
|
|
|
|
function Send($obj) {
|
|
$writer.WriteLine($obj)
|
|
Say "[crier-sc] -> $obj"
|
|
Drain 1.5
|
|
}
|
|
|
|
# 1. valid add
|
|
Send '{"kind":"towncrier.add","id":"n1","lines":["Hear ye, hear ye!","The market tax is now 5 percent."],"durationSec":3600}'
|
|
# 2. add exceeding the line cap (default 6) -> error
|
|
Send '{"kind":"towncrier.add","id":"n2","lines":["1","2","3","4","5","6","7","8"],"durationSec":60}'
|
|
# 3. remove the valid one
|
|
Send '{"kind":"towncrier.remove","id":"n1"}'
|
|
# 4. remove an unknown id -> error
|
|
Send '{"kind":"towncrier.remove","id":"does-not-exist"}'
|
|
|
|
Drain 3
|
|
|
|
Say "[crier-sc] done"
|
|
$client.Close(); $listener.Stop()
|