Implement deployment card numeric rendering

This commit is contained in:
gamer147
2026-07-21 17:31:57 -04:00
parent 56cafeb9f2
commit cc6db721db
7 changed files with 175 additions and 17 deletions

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class FormattedIntegerSurfaceOpsTests
{
[Fact]
public void DrawEnpLevelPatternDrawsHalfWidthValueAtRightAlignedPosition()
{
var host = Run(
(0x75, new[] { I(18) }),
(0x205, new[] { I(0x51), I(0x3f), I(0x3c), I(80), I(3), I(0x10000) }));
Assert.Equal((0x51, 0x48, 0x3c, "80"), Assert.Single(host.SurfaceStrings));
}
[Fact]
public void ZeroPaddingConsumesTheWholeFieldWithoutMovingTheAnchor()
{
var host = Run(
(0x75, new[] { I(18) }),
(0x205, new[] { I(0x51), I(100), I(20), I(7), I(3), I(0x10001) }));
Assert.Equal((0x51, 100, 20, "007"), Assert.Single(host.SurfaceStrings));
}
[Fact]
public void FullWidthLeftAlignedFormattingUsesCp932GlyphForms()
{
var host = Run(
(0x75, new[] { I(22) }),
(0x205, new[] { I(0x2a), I(40), I(22), I(61), I(3), I(0x04) }));
Assert.Equal((0x2a, 40, 22, ""), Assert.Single(host.SurfaceStrings));
}
private static RecordingHost Run(params (int Opcode, Operand[] Args)[] instructions)
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var body = new List<(int, Operand[])>(instructions) { (0x2, Array.Empty<Operand>()) };
var script = ScriptAssembler.Assemble(table, "FORMAT_INTEGER", body, Array.Empty<string>());
var host = new RecordingHost();
new VirtualMachine(script, table, host).Run();
return host;
}
private static Operand I(long value) => new(0, value);
}

View File

@@ -327,6 +327,60 @@ public sealed class VirtualMachine
return _nativeStringEncoding.GetByteCount(nul < 0 ? value : value[..nul]);
}
private string FormatIntegerForSurface(ref int x, int value, int fieldWidth, int flags)
{
int width = Math.Max(0, fieldWidth);
var field = new char[Math.Max(1, width)];
bool signed = value < 0
|| (value > 0 && (flags & 0x08) != 0)
|| (value == 0 && (flags & 0x30) != 0);
char sign = value < 0 || (value == 0 && (flags & 0x10) == 0 && (flags & 0x20) != 0) ? '-' : '+';
int digitSlots = width - (signed ? 1 : 0);
int first = 0;
long magnitude = value < 0 ? -(long)value : value;
for (int pos = digitSlots - 1; pos >= 0; pos--)
{
if (pos == digitSlots - 1 || magnitude != 0 || (flags & 0x01) != 0)
{
field[pos + (signed ? 1 : 0)] = (char)('0' + magnitude % 10);
first = pos;
}
magnitude /= 10;
}
if (signed)
field[first] = digitSlots < 0 ? '#' : sign;
int length = first;
while (length < field.Length && field[length] != '\0') length++;
string text = new(field, first, length - first);
// Native uses the primary LOGFONT cell height as a fixed-width advance, halved for ASCII.
// Odd heights (and the engine's 32/33-pixel special cases) are rounded down on font rebuild.
int fontSize = _advTextStyle.PrimaryFontSize > 0 ? _advTextStyle.PrimaryFontSize : 24;
int cellAdvance = fontSize == 33 ? 31
: fontSize == 32 || (fontSize & 1) != 0 ? fontSize - 1
: fontSize;
if ((flags & 0x04) == 0)
{
int divisor = (flags & 0x10000) != 0 ? 2 : 1;
if ((flags & 0x02) != 0) divisor *= 2;
x += (cellAdvance * first) / divisor;
}
if ((flags & 0x10000) == 0)
text = string.Concat(text.Select(c => c switch
{
>= '0' and <= '9' => (char)('' + c - '0'),
'-' => '',
>= 'A' and <= 'Z' => (char)('' + c - 'A'),
>= 'a' and <= 'z' => (char)('' + c - 'a'),
_ => '',
}));
return text;
}
private static VmAddress Ga(Dictionary<int, VmAddress> d, int k)
=> d.TryGetValue(k, out var value) ? value : VmAddress.Global(0);
@@ -977,6 +1031,15 @@ public sealed class VirtualMachine
_host.DrawStringToSurface((int)Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), ReadStr(a[3]),
_advTextStyle);
return pc + 1;
case "u00420A60": // pre-reference compatibility
case "draw-formatted-integer": // 0x205 (surface slot, x, y, value, field width, flags)
{
int x = (int)Read(a[1]);
string text = FormatIntegerForSurface(
ref x, unchecked((int)Read(a[3])), (int)Read(a[4]), (int)Read(a[5]));
_host.DrawStringToSurface((int)Read(a[0]), x, (int)Read(a[2]), text, _advTextStyle);
return pc + 1;
}
case "wait-for-input":
// Faithful headless: no player => halt here rather than plow past every prompt (see VmOptions).
if (_o.HaltAtWaitForInput) { HaltReason ??= "wait-for-input"; return HALT; }