Implement ADV History data navigation
This commit is contained in:
@@ -78,6 +78,7 @@ public sealed class AdvTextHistory
|
||||
private readonly List<AdvTextHistoryEntry> _entries = new();
|
||||
private readonly Dictionary<int, LayoutState> _layouts = new();
|
||||
private readonly HashSet<int> _pendingGroupStarts = new();
|
||||
private int _navigationAnchorIndex = -1;
|
||||
|
||||
public IReadOnlyList<AdvTextHistoryRecord> Records => _records;
|
||||
public IReadOnlyList<AdvTextHistoryEntry> Entries => _entries;
|
||||
@@ -114,10 +115,11 @@ public sealed class AdvTextHistory
|
||||
layout.CursorY = y;
|
||||
}
|
||||
|
||||
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style)
|
||||
public void AppendText(int requestedSlot, int sourceOffset, string text, AdvTextStyle style,
|
||||
AdvTextHistoryRecordFlags flags = AdvTextHistoryRecordFlags.None)
|
||||
{
|
||||
int slot = ResolveLayout(requestedSlot);
|
||||
AppendRecord(slot, AdvTextHistoryRecordKind.Text, AdvTextHistoryRecordFlags.None,
|
||||
AppendRecord(slot, AdvTextHistoryRecordKind.Text, flags,
|
||||
style, text, 0, 0, sourceOffset);
|
||||
}
|
||||
|
||||
@@ -135,6 +137,81 @@ public sealed class AdvTextHistory
|
||||
_records.Clear();
|
||||
_entries.Clear();
|
||||
_pendingGroupStarts.Clear();
|
||||
_navigationAnchorIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve a logical entry relative to AGE's latest-boundary navigation anchor. Repeated calls do not
|
||||
/// mutate the anchor; HISTORY.BIN supplies cumulative deltas while counting and paging backward.
|
||||
/// </summary>
|
||||
public bool TryStepGroup(int delta, out AdvTextHistoryEntry entry)
|
||||
{
|
||||
entry = new AdvTextHistoryEntry(-1, -1);
|
||||
if ((uint)_navigationAnchorIndex >= (uint)_entries.Count) return false;
|
||||
|
||||
int index = _navigationAnchorIndex;
|
||||
if (delta < 0)
|
||||
{
|
||||
for (int remaining = -delta; remaining > 0; remaining--)
|
||||
{
|
||||
int firstRecord = _entries[index].FirstRecordIndex;
|
||||
do
|
||||
{
|
||||
if (firstRecord == 0 || index < 1) return false;
|
||||
index--;
|
||||
}
|
||||
while (IsNavigationFiltered(_entries[index])
|
||||
|| _entries[index].FirstRecordIndex == firstRecord);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int remaining = delta; remaining > 0; remaining--)
|
||||
{
|
||||
int firstRecord = _entries[index].FirstRecordIndex;
|
||||
do
|
||||
{
|
||||
index++;
|
||||
if (index >= _entries.Count) return false;
|
||||
// Native treats the last entry's record offset as the forward sentinel.
|
||||
if (_entries[index].FirstRecordIndex == _entries[^1].FirstRecordIndex) return false;
|
||||
}
|
||||
while (IsNavigationFiltered(_entries[index])
|
||||
|| _entries[index].FirstRecordIndex == firstRecord);
|
||||
}
|
||||
}
|
||||
|
||||
entry = _entries[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryFindMetadata(int firstRecordIndex, long metadataType, out long value)
|
||||
{
|
||||
value = 0;
|
||||
bool found = false;
|
||||
foreach (var record in EnumerateGroup(firstRecordIndex))
|
||||
{
|
||||
if (!record.Flags.HasFlag(AdvTextHistoryRecordFlags.TypedMetadata)
|
||||
|| record.AuxValue != metadataType) continue;
|
||||
value = record.Value;
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public bool TryFindVoicePair(int firstRecordIndex, out long voiceId, out long voiceArgument)
|
||||
{
|
||||
voiceId = -1;
|
||||
voiceArgument = -1;
|
||||
bool found = false;
|
||||
foreach (var record in EnumerateGroup(firstRecordIndex))
|
||||
{
|
||||
if (!record.Flags.HasFlag(AdvTextHistoryRecordFlags.VoicePair)) continue;
|
||||
voiceId = record.Value;
|
||||
voiceArgument = record.AuxValue;
|
||||
found = true;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
private int SelectLayout(int requestedSlot)
|
||||
@@ -161,6 +238,22 @@ public sealed class AdvTextHistory
|
||||
if (RecordingSuppressed) return;
|
||||
_entries.Add(new AdvTextHistoryEntry(slot, _records.Count));
|
||||
_pendingGroupStarts.Add(slot);
|
||||
_navigationAnchorIndex = _entries.Count - 1;
|
||||
}
|
||||
|
||||
private bool IsNavigationFiltered(AdvTextHistoryEntry entry)
|
||||
=> (uint)entry.FirstRecordIndex < (uint)_records.Count
|
||||
&& _records[entry.FirstRecordIndex].Flags.HasFlag(AdvTextHistoryRecordFlags.NavigationFiltered);
|
||||
|
||||
private IEnumerable<AdvTextHistoryRecord> EnumerateGroup(int firstRecordIndex)
|
||||
{
|
||||
if ((uint)firstRecordIndex >= (uint)_records.Count) yield break;
|
||||
for (int i = firstRecordIndex; i < _records.Count; i++)
|
||||
{
|
||||
if (i > firstRecordIndex
|
||||
&& _records[i].Flags.HasFlag(AdvTextHistoryRecordFlags.GroupStart)) yield break;
|
||||
yield return _records[i];
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendRecord(int slot, AdvTextHistoryRecordKind kind, AdvTextHistoryRecordFlags flags,
|
||||
|
||||
@@ -6,5 +6,7 @@ public sealed class Script
|
||||
public required IReadOnlyList<Instruction> Instructions { get; init; }
|
||||
public required IReadOnlyDictionary<int, int> IndexByOffset { get; init; }
|
||||
public required IReadOnlyDictionary<int, string> Strings { get; init; }
|
||||
/// <summary>Unmodified SYS4 body dwords, retained for inline data operands such as opcode 0x64.</summary>
|
||||
public IReadOnlyList<uint> BodyDwords { get; init; } = Array.Empty<uint>();
|
||||
public string GetString(int offset) => Strings.TryGetValue(offset, out var s) ? s : "";
|
||||
}
|
||||
|
||||
@@ -22,7 +22,15 @@ public static class Sys4Loader
|
||||
|
||||
var header = new ScriptHeader(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]);
|
||||
var (instrs, idxByOff, strings) = DecodeCode(dw, fields, nbody, table);
|
||||
return new Script { Name = name, Header = header, Instructions = instrs, IndexByOffset = idxByOff, Strings = strings };
|
||||
return new Script
|
||||
{
|
||||
Name = name,
|
||||
Header = header,
|
||||
Instructions = instrs,
|
||||
IndexByOffset = idxByOff,
|
||||
Strings = strings,
|
||||
BodyDwords = dw,
|
||||
};
|
||||
}
|
||||
|
||||
private static (List<Instruction>, Dictionary<int, int>, Dictionary<int, string>)
|
||||
|
||||
@@ -35,6 +35,7 @@ public sealed class VirtualMachine
|
||||
private bool _autoVoicePending;
|
||||
private volatile bool _messageSkipEnabled;
|
||||
private AdvTextStyle _advTextStyle = AdvTextStyle.Default;
|
||||
private readonly Dictionary<string, int> _valueSwitchTargets = new(StringComparer.Ordinal);
|
||||
public long CallScriptDispatches { get; private set; }
|
||||
|
||||
public Dictionary<int, long> Globals { get; } = new();
|
||||
@@ -217,6 +218,24 @@ public sealed class VirtualMachine
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteConsecutive(Operand destination, int index, long value)
|
||||
{
|
||||
int address = checked((int)destination.Value + index);
|
||||
switch (destination.Type)
|
||||
{
|
||||
case T_GINT: case T_GFLOAT: Globals[address] = value; break;
|
||||
case T_LINT: _cur.Locals.I[address] = value; break;
|
||||
case T_LFLOAT: _cur.Locals.F[address] = value; break;
|
||||
case T_GPTR: Globals[checked((int)Gi(Globals, (int)destination.Value) + index)] = value; break;
|
||||
case T_LPTR: Globals[checked((int)Gi(_cur.Locals.P, (int)destination.Value) + index)] = value; break;
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatSwitchValue(Operand operand)
|
||||
=> IsStr(operand)
|
||||
? ReadStr(operand)
|
||||
: unchecked((int)Read(operand)).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
|
||||
private enum FrameOutcome { Returned, Halted, RanOff }
|
||||
|
||||
public void Run(int entryOffset = 0)
|
||||
@@ -333,6 +352,26 @@ public sealed class VirtualMachine
|
||||
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1;
|
||||
case "lookup-array-2d":
|
||||
LookupStore(a[0], BaseAddr(a[1]) + Read(a[2]) * Read(a[3]) + Read(a[4])); return pc + 1;
|
||||
case "copy-inline-int-array": // 0x64: count dword followed by plain file values
|
||||
{
|
||||
int offset = checked((int)Read(a[1]));
|
||||
if ((uint)offset >= (uint)_cur.Script.BodyDwords.Count)
|
||||
{
|
||||
HaltReason ??= $"inline-array-offset@0x{offset:x}";
|
||||
return HALT;
|
||||
}
|
||||
uint rawCount = _cur.Script.BodyDwords[offset];
|
||||
int available = _cur.Script.BodyDwords.Count - offset - 1;
|
||||
if (rawCount > (uint)available)
|
||||
{
|
||||
HaltReason ??= $"inline-array-length@0x{offset:x}:{rawCount}";
|
||||
return HALT;
|
||||
}
|
||||
int count = (int)rawCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i]));
|
||||
return pc + 1;
|
||||
}
|
||||
case "bit-set":
|
||||
{
|
||||
long bit = Read(a[1]);
|
||||
@@ -357,6 +396,16 @@ public sealed class VirtualMachine
|
||||
long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value;
|
||||
return tgt == NoJump ? pc + 1 : _cur.Script.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1);
|
||||
}
|
||||
case "begin-value-switch":
|
||||
_valueSwitchTargets.Clear(); return pc + 1;
|
||||
case "add-value-switch-case":
|
||||
_valueSwitchTargets[FormatSwitchValue(a[0])] = checked((int)Read(a[1])); return pc + 1;
|
||||
case "value-switch-jump":
|
||||
{
|
||||
int target = _valueSwitchTargets.TryGetValue(FormatSwitchValue(a[0]), out int matched)
|
||||
? matched : checked((int)Read(a[1]));
|
||||
return _cur.Script.IndexByOffset.GetValueOrDefault(target, pc + 1);
|
||||
}
|
||||
case "u0041ADB0":
|
||||
case "coroutine-save-yield-handlers": // 0x7b: retain native handler metadata
|
||||
_cur.CoroutineYieldHandlerA = (int)Read(a[0]);
|
||||
@@ -619,6 +668,32 @@ public sealed class VirtualMachine
|
||||
return pc + 1;
|
||||
case "append-text-history-metadata": // 0x1d2: typed value attached to the current group
|
||||
TextHistory.AppendMetadata(Read(a[0]), Read(a[1]), _advTextStyle); return pc + 1;
|
||||
case "step-text-history": // 0x1d0: cumulative delta from the latest retained boundary
|
||||
if (TextHistory.TryStepGroup((int)Read(a[2]), out var historyEntry))
|
||||
{
|
||||
Write(a[0], historyEntry.LayoutSlot);
|
||||
Write(a[1], historyEntry.FirstRecordIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
Write(a[0], -1);
|
||||
Write(a[1], -1);
|
||||
}
|
||||
return pc + 1;
|
||||
case "u0041BB90":
|
||||
case "find-text-history-value": // 0x1d3: operand 3 is accepted but ignored natively
|
||||
{
|
||||
bool found = TextHistory.TryFindMetadata((int)Read(a[3]), Read(a[4]), out long value);
|
||||
Write(a[0], found ? 1 : 0);
|
||||
Write(a[1], value);
|
||||
return pc + 1;
|
||||
}
|
||||
case "u0041BC00":
|
||||
case "find-text-history-pair": // 0x1d4: operand 3 is accepted but ignored natively
|
||||
TextHistory.TryFindVoicePair((int)Read(a[3]), out long voiceId, out long voiceArgument);
|
||||
Write(a[0], voiceId);
|
||||
Write(a[1], voiceArgument);
|
||||
return pc + 1;
|
||||
case "clear-text-history": // 0x85: bound the backlog to the current ordinary ADV block
|
||||
TextHistory.Clear(); return pc + 1;
|
||||
case "set-font-size":
|
||||
|
||||
Reference in New Issue
Block a user