feedback: plasma box, explicit fonts, and per-position text coalescing

Three endpoint gaps, found building the Descent 3 score overlay - the boxed
place|score field the original games drew over the callsign:

plasma box <x> <y> <w> <h> draws an outlined box with a blanked interior, the
overlay chrome, as one ESC P graphics write. The wire addresses whole bytes
horizontally, so the write covers the byte-aligned span containing the box and
clears span pixels outside it; documented, with 8-px alignment the advice.
Boxes queue FIFO with rows.

plasma text gains an explicit font: a third numeric token after the position,
with text still following, so `plasma text 2 2 7` still displays "7". Auto-fit
picks the font by LENGTH - short text always rendered large, and a "1" that
must fit a 12-px box simply could not be sent before. ResolvePosText
generalizes the legacy Score-font special case: 0 = auto, nonzero honored.

Text coalescing is now per position. The global rule - any queued text
superseded every other queued text - meant the documented two-field layout
(callsign top, score bottom) could not survive its own send burst: the second
field silently ate the first whenever both were queued. A newer text now
replaces only a queued text at the same (x,y).

15 new tests; 472 pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-08-02 11:45:59 -05:00
co-authored by Claude Opus 5
parent 23453667d2
commit 46e108de89
9 changed files with 315 additions and 26 deletions
+25 -10
View File
@@ -18,12 +18,13 @@ namespace RioJoy.Core.Feedback;
/// profile-owned lamps for the same reason.
///
/// Plasma writes are single-flight over a small bounded queue whose rules
/// match what each command means: <c>text</c> coalesces (only the newest
/// queued text survives — a flooding score updater shows the latest value),
/// match what each command means: <c>text</c> coalesces per position (only the
/// newest queued text at the same (x,y) survives — a flooding score updater
/// shows the latest value, while other fields on the glass keep theirs),
/// <c>clear</c> flushes everything queued before it, and bitmap <c>row</c>s
/// are FIFO in arrival order (a frame is many rows; coalescing would tear
/// it). The bound caps what a flooding client can queue against the
/// 9600-baud display port.
/// and <c>box</c>es are FIFO in arrival order (a frame is many rows;
/// coalescing would tear it). The bound caps what a flooding client can queue
/// against the 9600-baud display port.
/// </summary>
public sealed class FeedbackRouter
{
@@ -111,6 +112,7 @@ public sealed class FeedbackRouter
case FeedbackCommandKind.PlasmaText:
case FeedbackCommandKind.PlasmaClear:
case FeedbackCommandKind.PlasmaRow:
case FeedbackCommandKind.PlasmaBox:
DispatchPlasma(target, command);
break;
}
@@ -172,20 +174,30 @@ public sealed class FeedbackRouter
break;
case FeedbackCommandKind.PlasmaText:
// Only the newest text survives (a score updater shows the
// latest value); queued rows keep their place.
// Only the newest text FOR THE SAME POSITION survives (a
// score updater shows the latest value). Texts at other
// positions are other fields - the documented multi-field
// layout (callsign top, score bottom) sends several in a
// burst, and the original global coalescing ate all but
// the last of them. Queued rows/boxes keep their place.
for (int i = _plasmaQueue.Count - 1; i >= 0; i--)
{
if (_plasmaQueue[i].Kind == FeedbackCommandKind.PlasmaText)
if (_plasmaQueue[i].Kind == FeedbackCommandKind.PlasmaText &&
_plasmaQueue[i].X == command.X && _plasmaQueue[i].Y == command.Y)
{
_plasmaQueue.RemoveAt(i);
Interlocked.Increment(ref _dropped); // superseded before it ran
}
}
if (_plasmaQueue.Count >= MaxPlasmaQueue)
{
Interlocked.Increment(ref _dropped);
return;
}
_plasmaQueue.Add(command);
break;
default: // PlasmaRow: strict FIFO — a bitmap frame is many rows
default: // PlasmaRow/PlasmaBox: strict FIFO — a bitmap frame is many rows
if (_plasmaQueue.Count >= MaxPlasmaQueue)
{
Interlocked.Increment(ref _dropped); // client outran the display
@@ -219,7 +231,10 @@ public sealed class FeedbackRouter
{
FeedbackCommandKind.PlasmaClear => target.Plasma!.ClearAsync(),
FeedbackCommandKind.PlasmaRow => target.Plasma!.RowAsync(command.Y, command.Data!),
_ => target.Plasma!.PosTextAsync(command.Text ?? string.Empty, command.X, command.Y),
FeedbackCommandKind.PlasmaBox =>
target.Plasma!.BoxAsync(command.X, command.Y, command.Width, command.Height),
_ => target.Plasma!.PosTextAsync(command.Text ?? string.Empty, command.X, command.Y,
0, command.Font),
};
write.ContinueWith(w =>