Implement INPUTNAME string opcodes

This commit is contained in:
gamer147
2026-07-29 13:21:15 -04:00
parent 862b74ecbc
commit feef18d411
12 changed files with 443 additions and 4 deletions

View File

@@ -2891,6 +2891,13 @@ character cells, copies that text into both `0x144` operands, then calls `0x2c6`
the VM worker blocks while Godot's main thread presents a LineEdit dialog, validates the CP932 byte limit
and full-width-only rule, and signals the worker on accept or cancel. No save/profile format is involved.
**Port implementation (2026-07-29).** `Cp932Text` now owns the byte-level character count, substring,
and AGERC acceptance rules. The VM dispatches all three handlers and exposes the modal as an explicit
accept/cancel `IHost` exchange, so cancellation cannot overwrite operand 1. Godot uses a main-thread
`FullwidthTextEditorDialog` while its VM worker waits; over-16-byte and non-double-byte submissions retain
focus and show AGERC's exact Japanese errors. The helper operates on the C-string prefix and encodes through
the configured native code page, avoiding both UTF-16 indexing and accidental splitting of CP932 pairs.
`0xc2` is BGM rather than SFX: `op_0xc2_bgm_fade@0x4204c0` sets run-state `0x200`, arms the service timer,
and calls `bgm_fade_arm@0x464830`. `bgm_fade_tick@0x464960` linearly interpolates current to target percent;
durations at least 1000 ms take 100 steps, shorter durations take 10, and target zero releases the source.

View File

@@ -423,14 +423,14 @@ This is raw strlen(bytes), not a .NET UTF-16 character count. BUNKI compares all
- **grounding:** source=investigation, confidence=high
- **evidence:** The real /v2 dispatch slot registers op_0x2c6_cp932_character_length@0x42a6d0. The handler sets LC_ALL to `japanese`, resolves operand 2, calls MSVC _mbstrlen, and writes the result to operand 1. INPUTNAME's sole site at 0x1002 uses this count as the loop bound before slicing each character with opcode 0x2c8.
This is character count rather than .NET UTF-16 length or raw CP932 byte length. The implementation should use VmOptions.NativeStringCodePage and preserve valid CP932 multibyte boundaries.
This is character count rather than .NET UTF-16 length or raw CP932 byte length. Port status (2026-07-29): implemented by encoding the C-string prefix through VmOptions.NativeStringCodePage and counting valid CP932 lead/trail pairs as one character; focused tests cover mixed single-byte and double-byte characters.
### 0x2c8 `cp932-substring` (cp932-substring, argc 4)
- **summary:** (out)(string)(start)(count) - copy a CP932 substring selected by multibyte-character index and count without splitting valid lead/trail pairs.
- **grounding:** source=investigation, confidence=high
- **evidence:** The real /v2 dispatch slot registers op_0x2c8_cp932_substring@0x42c420. It copies operand 2 into a 256-byte buffer, sets LC_ALL to `japanese`, obtains _mbstrlen, reads start and count, clamps end=start+count to the character length when end is below 1 or beyond that length, and walks bytes with _mbbtype so CP932 lead/trail pairs are copied together. It writes the selected byte interval back through operand 1. INPUTNAME's sole site at 0x1021 loops substring(name,index,1) into its eight local character cells.
The release call uses nonnegative in-range indices and count 1. A compatible general implementation should reproduce native end clamping (`end = length` when start+count < 1 or > length) and select the half-open character interval [start,end).
The release call uses nonnegative in-range indices and count 1. Port status (2026-07-29): implemented over encoded byte spans, preserving every valid lead/trail pair and reproducing native end clamping (`end = length` when start+count < 1 or > length) before selecting the half-open character interval [start,end).
## control
@@ -1256,6 +1256,11 @@ character palette. The VM can use a synchronous host seam like the existing diag
the Godot host runs script execution on its worker thread while the main thread owns the modal UI.
Compatibility requires the native full-width-only and 16-CP932-byte limits before accepting the result.
Port status (2026-07-29): implemented. The VM issues an explicit accept/cancel request through IHost;
headless hosts preserve the inout value by default. Godot parks the VM worker, opens a main-thread modal
LineEdit, keeps it open on either native validation error, and resumes the worker only after valid accept
or cancel. Accept replaces operand 1 and cancel leaves it untouched; operand 2 is never modified.
### 0x19a `get-message-skip` (u00414E50, argc 1)
- **summary:** (out) - return the current all-message skip state set by op 0x88.

View File

@@ -930,6 +930,23 @@ gaps and 3 of 11 instructions; no persistence or save-format work is involved.
**NEXT:** implement and test `0x144`/`0x2c6`/`0x2c8` together, including CP932 mixed-width helper cases,
the native 16-byte/full-width acceptance rules, accept/cancel behavior, and INPUTNAME's split/rejoin shape.
**INPUTNAME string/input slice implemented (2026-07-29):** the VM now dispatches all three opcodes.
`0x2c6` counts CP932 characters rather than UTF-16 units or bytes, and `0x2c8` slices encoded character
spans with native end clamping. A shared validator applies command 10's 16-byte limit first and then
requires every accepted cell to be a valid CP932 lead/trail pair.
`0x144` uses an explicit synchronous host result, preserving operand 1 on cancel and operand 2 in both
paths. Godot parks the VM worker while a main-thread modal editor owns input; invalid submissions leave
the editor open with AGERC's original error text. Headless hosts cancel safely by default. Focused
regressions cover mixed-width count/slicing, end clamping, valid and invalid names, accept replacement,
cancel preservation, and operand-2 preservation. The slice removes three gaps and three instructions,
leaving 6 effectful opcodes / 8 instructions. Validation passes 493/493 engine tests, opcode and
EngineCtx lint, a zero-warning Godot build, clean diff checking, and the Himegari-targeted threaded
`SELFTEST OK`.
**NEXT:** rerank the remaining six gaps; begin with the two-site `0x24d` and check whether adjacent
`0x248` belongs to the same native subsystem before choosing the next implementation boundary.
## Later Phase B breadth
**INIT data-semantics side track started (2026-07-22).** Before naming more gameplay state, the static

View File

@@ -0,0 +1,139 @@
using System.Text;
using Age.Engine.Diagnostics;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
public class InputNameStringOpcodeTests
{
private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson);
private static Operand I(long value) => new(0, value);
private static Operand S(int index) => new(2, index);
private static Operand G(int address) => new(3, address);
private static Operand GS(int address) => new(5, address);
private static Operand LI(int index) => new(9, index);
private static Operand LS(int index) => new(11, index);
private static Operand LSP(int index) => new(14, index);
private static (int, Operand[]) Exit() => (0x2, []);
static InputNameStringOpcodeTests()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
[Fact]
public void CharacterLengthAndSubstringUseCp932CharacterBoundaries()
{
Assert.Equal("cp932-character-length", Table.Label(0x2c6));
Assert.Equal(4, Cp932Text.CharacterLength("AリアB", Encoding.GetEncoding(932)));
int lookup = Table.ByLabel("lookup-array")!.Value;
int move = Table.ByLabel("mov")!.Value;
Script script = ScriptAssembler.Assemble(Table, "INPUTNAME_HELPERS.BIN",
[
(move, [LS(0), GS(0x700)]),
(0x2c6, [G(0x500), LS(0)]),
(lookup, [LSP(0), GS(0x600), I(0)]),
(0x2c8, [LSP(0), LS(0), I(0), I(1)]),
(lookup, [LSP(1), GS(0x600), I(1)]),
(0x2c8, [LSP(1), LS(0), I(1), I(1)]),
(lookup, [LSP(2), GS(0x600), I(2)]),
(0x2c8, [LSP(2), LS(0), I(2), I(99)]),
Exit(),
], []);
var trace = new RecordingTraceSink { TracingSteps = true };
var vm = new VirtualMachine(script, Table, new RecordingHost(), sink: trace);
vm.GlobalStrings[0x700] = "AリアB";
vm.Run();
Assert.Equal(new[] { move, 0x2c6, lookup, 0x2c8, lookup, 0x2c8, lookup, 0x2c8, 0x2 },
trace.Events.Where(e => e.Kind == TraceEventKind.Step).Select(e => e.Opcode));
Assert.Equal(4, vm.Globals[0x500]);
Assert.Equal("A", vm.GlobalStrings[0x600]);
Assert.Equal("リ", vm.GlobalStrings[0x601]);
Assert.Equal("アB", vm.GlobalStrings[0x602]);
}
[Fact]
public void SubstringReproducesNativeEndClamp()
{
int lookup = Table.ByLabel("lookup-array")!.Value;
int move = Table.ByLabel("mov")!.Value;
Script script = ScriptAssembler.Assemble(Table, "INPUTNAME_CLAMP.BIN",
[
(move, [LS(0), S(0)]),
(lookup, [LSP(0), GS(0x610), I(0)]),
(0x2c8, [LSP(0), LS(0), I(0), I(0)]),
Exit(),
], ["魔王"]);
var vm = new VirtualMachine(script, Table, new RecordingHost());
vm.Run();
Assert.Equal("魔王", vm.GlobalStrings[0x610]);
}
[Fact]
public void ModalEditorAcceptsReplacementAndPreservesInitialOperand()
{
int move = Table.ByLabel("mov")!.Value;
Script script = ScriptAssembler.Assemble(Table, "INPUTNAME_ACCEPT.BIN",
[
(move, [LS(0), S(0)]),
(move, [LS(1), S(1)]),
(0x144, [LS(0), LS(1)]),
(move, [GS(0x620), LS(0)]),
(move, [GS(0x621), LS(1)]),
Exit(),
], ["魔王", "魔王"]);
var host = new RecordingHost
{
OnFullwidthTextEdit = _ => new(true, "リリィ"),
};
var vm = new VirtualMachine(script, Table, host);
vm.Run();
Assert.Equal(new FullwidthTextEditRequest("魔王", "魔王"),
Assert.Single(host.FullwidthTextEdits));
Assert.Equal("リリィ", vm.GlobalStrings[0x620]);
Assert.Equal("魔王", vm.GlobalStrings[0x621]);
}
[Fact]
public void ModalEditorCancelLeavesResultOperandUnchanged()
{
int move = Table.ByLabel("mov")!.Value;
Script script = ScriptAssembler.Assemble(Table, "INPUTNAME_CANCEL.BIN",
[
(move, [LS(0), S(0)]),
(move, [LS(1), S(1)]),
(0x144, [LS(0), LS(1)]),
(move, [GS(0x630), LS(0)]),
Exit(),
], ["魔王", "初期値"]);
var host = new RecordingHost
{
OnFullwidthTextEdit = _ => new(false, "破棄される文字列"),
};
var vm = new VirtualMachine(script, Table, host);
vm.Run();
Assert.Equal("魔王", vm.GlobalStrings[0x630]);
}
[Theory]
[InlineData("", FullwidthTextValidationError.None)]
[InlineData("リリィ", FullwidthTextValidationError.None)]
[InlineData("一二三四五六七八", FullwidthTextValidationError.None)]
[InlineData("一二三四五六七八九", FullwidthTextValidationError.TooLong)]
[InlineData("B", FullwidthTextValidationError.NonDoubleByteCharacter)]
[InlineData("ア", FullwidthTextValidationError.NonDoubleByteCharacter)]
public void FullwidthValidationMatchesAgercAcceptanceRules(
string text, FullwidthTextValidationError expected)
{
Assert.Equal(expected, Cp932Text.ValidateFullwidthName(text));
}
}

View File

@@ -63,6 +63,8 @@ internal class RecordingHost : IHost
public readonly List<string> Warnings = new();
public readonly List<DiagnosticMessage> Diagnostics = new();
public System.Action<DiagnosticMessage>? OnDiagnosticMessage;
public readonly List<FullwidthTextEditRequest> FullwidthTextEdits = new();
public System.Func<FullwidthTextEditRequest, FullwidthTextEditResult>? OnFullwidthTextEdit;
public readonly List<long> CursorResources = new();
public readonly List<bool> AdvPagePresentationSuspended = new();
public readonly List<(AdvLiveTextRun Run, int GlyphDelayMilliseconds)> LiveTextRuns = new();
@@ -76,6 +78,11 @@ internal class RecordingHost : IHost
Diagnostics.Add(message);
OnDiagnosticMessage?.Invoke(message);
}
public FullwidthTextEditResult EditFullwidthString(FullwidthTextEditRequest request)
{
FullwidthTextEdits.Add(request);
return OnFullwidthTextEdit?.Invoke(request) ?? new(false, request.CurrentText);
}
public void ShowText(int offset, string text) => Lines.Add((offset, text));
public void ShowText(AdvLiveTextRun run, int glyphDelayMilliseconds)
{

View File

@@ -31,6 +31,10 @@ public readonly record struct SurfaceRectCopy(
/// <summary>A synchronous AGE-owned diagnostic prompt after native body/context formatting.</summary>
public readonly record struct DiagnosticMessage(string Caption, string Text);
/// <summary>AGERc command 10's synchronous full-width text edit request and result.</summary>
public readonly record struct FullwidthTextEditRequest(string CurrentText, string InitialText);
public readonly record struct FullwidthTextEditResult(bool Accepted, string Text);
public enum SurfaceBlackFadeDirection
{
FromBlack,
@@ -44,6 +48,9 @@ public interface IHost
/// <summary>Present a modal diagnostic and return only after the user dismisses it.</summary>
void ShowDiagnosticMessage(DiagnosticMessage message)
=> System.Console.Error.WriteLine($"{message.Caption}: {message.Text}");
/// <summary>Present AGERc's modal full-width editor. Cancel preserves CurrentText.</summary>
FullwidthTextEditResult EditFullwidthString(FullwidthTextEditRequest request)
=> new(false, request.CurrentText);
// Script context is retained for diagnostics/page location; resource operands are universal packed ids.
void EnterScriptContext(string scriptName) { }
void ExitScriptContext() { }

View File

@@ -0,0 +1,98 @@
using System.Text;
namespace Age.Engine.Sys4;
public enum FullwidthTextValidationError
{
None,
TooLong,
NonDoubleByteCharacter,
}
/// <summary>AGE's Japanese-locale byte-string operations used by INPUTNAME.</summary>
public static class Cp932Text
{
public const int NativeNameByteLimit = 16;
private static readonly Encoding NativeEncoding;
static Cp932Text()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
NativeEncoding = Encoding.GetEncoding(932);
}
private static bool IsLeadByte(byte value)
=> value is >= 0x81 and <= 0x9f or >= 0xe0 and <= 0xfc;
private static bool IsTrailByte(byte value)
=> value is >= 0x40 and <= 0x7e or >= 0x80 and <= 0xfc;
private static byte[] EncodeCString(string value, Encoding encoding)
{
int nul = value.IndexOf('\0');
return encoding.GetBytes(nul < 0 ? value : value[..nul]);
}
public static int CharacterLength(string value, Encoding encoding)
{
byte[] bytes = EncodeCString(value, encoding);
int count = 0;
for (int offset = 0; offset < bytes.Length; count++)
{
if (IsLeadByte(bytes[offset]) && offset + 1 < bytes.Length
&& IsTrailByte(bytes[offset + 1]))
offset += 2;
else
offset++;
}
return count;
}
public static string Substring(
string value, int start, int count, Encoding encoding)
{
byte[] bytes = EncodeCString(value, encoding);
var characters = new List<(int Offset, int Length)>();
for (int offset = 0; offset < bytes.Length;)
{
int length = IsLeadByte(bytes[offset]) && offset + 1 < bytes.Length
&& IsTrailByte(bytes[offset + 1])
? 2
: 1;
characters.Add((offset, length));
offset += length;
}
int end = unchecked(start + count);
if (end < 1 || end > characters.Count) end = characters.Count;
using var selected = new MemoryStream();
for (int index = 0; index < characters.Count; index++)
{
if (index < start || index >= end) continue;
(int offset, int length) = characters[index];
selected.Write(bytes, offset, length);
}
return encoding.GetString(selected.ToArray());
}
public static FullwidthTextValidationError ValidateFullwidthName(
string value, Encoding encoding, int byteLimit = NativeNameByteLimit)
{
byte[] bytes = EncodeCString(value, encoding);
if (bytes.Length > byteLimit) return FullwidthTextValidationError.TooLong;
for (int offset = 0; offset < bytes.Length; offset += 2)
{
if (offset + 1 >= bytes.Length
|| !IsLeadByte(bytes[offset])
|| !IsTrailByte(bytes[offset + 1]))
return FullwidthTextValidationError.NonDoubleByteCharacter;
}
return FullwidthTextValidationError.None;
}
public static FullwidthTextValidationError ValidateFullwidthName(
string value, int byteLimit = NativeNameByteLimit)
=> ValidateFullwidthName(value, NativeEncoding, byteLimit);
}

View File

@@ -2,6 +2,7 @@ using Age.Engine.Diagnostics;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Persistence;
using Age.Engine.Sys4;
using System.Text;
namespace Age.Engine.Vm;
@@ -1237,6 +1238,25 @@ public sealed class VirtualMachine
case "halve-strlen": // 0x1a6: strlen(native encoded bytes) >> 1
Write(a[0], NativeStringByteLength(ReadStr(a[1])) >> 1);
return pc + 1;
case "edit-fullwidth-string-dialog": // 0x144: blocking AGERc command-10 editor
{
string current = ReadStr(a[0]);
string initial = ReadStr(a[1]);
FullwidthTextEditResult result =
_host.EditFullwidthString(new(current, initial));
if (result.Accepted) WriteStr(a[0], result.Text);
return pc + 1;
}
case "cp932-character-length": // 0x2c6: Japanese-locale _mbstrlen
Write(a[0], Cp932Text.CharacterLength(ReadStr(a[1]), _nativeStringEncoding));
return pc + 1;
case "cp932-substring": // 0x2c8: multibyte-character interval [start,start+count)
WriteStr(a[0], Cp932Text.Substring(
ReadStr(a[1]),
unchecked((int)Read(a[2])),
unchecked((int)Read(a[3])),
_nativeStringEncoding));
return pc + 1;
case "u00425790": // upstream ABI label
case "append-diagnostic-value": // 0x1b2: generic operand text -> EngineCtx accumulator
_diagnosticOutput.Append(FormatDiagnosticOperand(a[0]));

View File

@@ -0,0 +1,87 @@
using System;
using Age.Engine.Sys4;
using Godot;
/// <summary>Godot presentation for AGERc command 10's blocking INPUTNAME editor.</summary>
public partial class FullwidthTextEditorDialog : Window
{
private readonly LineEdit _edit = new() { MaxLength = 255 };
private readonly Label _error = new()
{
AutowrapMode = TextServer.AutowrapMode.WordSmart,
CustomMinimumSize = new Vector2(0, 28),
};
private bool _completed;
public event Action<bool, string>? EditCompleted;
public FullwidthTextEditorDialog()
{
Title = "文字入力";
Exclusive = true;
Transient = true;
Unresizable = true;
var margin = new MarginContainer();
margin.AddThemeConstantOverride("margin_left", 16);
margin.AddThemeConstantOverride("margin_top", 16);
margin.AddThemeConstantOverride("margin_right", 16);
margin.AddThemeConstantOverride("margin_bottom", 16);
AddChild(margin);
margin.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
var column = new VBoxContainer();
margin.AddChild(column);
column.AddChild(new Label { Text = "全角文字で名前を入力してください。" });
_edit.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill;
column.AddChild(_edit);
_error.AddThemeColorOverride("font_color", new Color(1.0f, 0.35f, 0.35f));
column.AddChild(_error);
var actions = new HBoxContainer { Alignment = BoxContainer.AlignmentMode.End };
var cancel = new Button { Text = "キャンセル" };
var accept = new Button { Text = "決定" };
actions.AddChild(cancel);
actions.AddChild(accept);
column.AddChild(actions);
accept.Pressed += TryAccept;
cancel.Pressed += Cancel;
_edit.TextSubmitted += _ => TryAccept();
CloseRequested += Cancel;
}
public void Open(string initialText)
{
_completed = false;
_error.Text = "";
_edit.Text = initialText;
PopupCentered(new Vector2I(430, 160));
_edit.GrabFocus();
_edit.SelectAll();
}
private void TryAccept()
{
FullwidthTextValidationError error = Cp932Text.ValidateFullwidthName(_edit.Text);
if (error != FullwidthTextValidationError.None)
{
_error.Text = error == FullwidthTextValidationError.TooLong
? "字数オーバーです"
: "半角文字は使用できません";
_edit.GrabFocus();
return;
}
Complete(true);
}
private void Cancel() => Complete(false);
private void Complete(bool accepted)
{
if (_completed) return;
_completed = true;
Hide();
EditCompleted?.Invoke(accepted, _edit.Text);
}
}

View File

@@ -27,6 +27,9 @@ public sealed class GodotAdvHost : IHost
private readonly ScriptPresentationBarrier _presentationBarrier = new();
private readonly AutoResetEvent _presentationRequestConsumed = new(false);
private readonly AutoResetEvent _diagnosticMessageCompleted = new(false);
private readonly AutoResetEvent _fullwidthTextEditCompleted = new(false);
private readonly object _fullwidthTextEditLock = new();
private FullwidthTextEditResult _fullwidthTextEditResult;
private readonly bool _synchronizeExplicitPresentation;
private long _explicitPresentationRequestGeneration;
private long _consumedPresentationRequestGeneration;
@@ -132,6 +135,28 @@ public sealed class GodotAdvHost : IHost
public void CompleteDiagnosticMessage() => _diagnosticMessageCompleted.Set();
public FullwidthTextEditResult EditFullwidthString(FullwidthTextEditRequest request)
{
if (_stopping) return new(false, request.CurrentText);
lock (_fullwidthTextEditLock)
_fullwidthTextEditResult = new(false, request.CurrentText);
_timeline?.Event("fullwidth-text-edit", new()
{
["current"] = request.CurrentText,
["initial"] = request.InitialText,
});
_main.CallDeferred("ShowAgeFullwidthTextEditor", request.InitialText);
while (!_stopping && !_fullwidthTextEditCompleted.WaitOne(50)) { }
lock (_fullwidthTextEditLock) return _fullwidthTextEditResult;
}
public void CompleteFullwidthTextEdit(bool accepted, string text)
{
lock (_fullwidthTextEditLock)
_fullwidthTextEditResult = new(accepted, text);
_fullwidthTextEditCompleted.Set();
}
private string CurrentScene
{
get { lock (_scriptContextLock) return _scriptContexts.TryPeek(out var scene) ? scene : _rootScene; }
@@ -966,6 +991,7 @@ public sealed class GodotAdvHost : IHost
_inputCallbackSignal.Set();
_frameSignal.Set();
_diagnosticMessageCompleted.Set();
_fullwidthTextEditCompleted.Set();
}
public void ResetSceneContext()

View File

@@ -69,6 +69,7 @@ public partial class Main : Godot.Control
private Sys4RegIniStore? _sys4RegIniStore;
private VirtualMachine _vm = null!;
private GodotAdvHost _host = null!;
private FullwidthTextEditorDialog? _fullwidthTextEditor;
private Sys4ScriptProvider? _scripts;
private DebugSceneLauncher? _debugSceneLauncher;
private IReadOnlyList<DebugSceneEntry> _debugSceneEntries = System.Array.Empty<DebugSceneEntry>();
@@ -928,6 +929,26 @@ public partial class Main : Godot.Control
}
}
public void ShowAgeFullwidthTextEditor(string initialText)
{
if (_fullwidthTextEditor != null)
{
GD.PushWarning("[inputname] replaced an already-open full-width text editor");
_fullwidthTextEditor.QueueFree();
}
var editor = new FullwidthTextEditorDialog();
_fullwidthTextEditor = editor;
editor.EditCompleted += (accepted, text) =>
{
if (_fullwidthTextEditor == editor) _fullwidthTextEditor = null;
_host?.CompleteFullwidthTextEdit(accepted, text);
editor.QueueFree();
};
AddChild(editor);
editor.Open(initialText);
}
public override void _ExitTree()
{
bool vmStopped = true;

View File

@@ -3034,6 +3034,11 @@ This is INPUTNAME's optional native keyboard-entry button, not the surrounding s
character palette. The VM can use a synchronous host seam like the existing diagnostic dialog because
the Godot host runs script execution on its worker thread while the main thread owns the modal UI.
Compatibility requires the native full-width-only and 16-CP932-byte limits before accepting the result.
Port status (2026-07-29): implemented. The VM issues an explicit accept/cancel request through IHost;
headless hosts preserve the inout value by default. Godot parks the VM worker, opens a main-thread modal
LineEdit, keeps it open on either native validation error, and resumes the worker only after valid accept
or cancel. Accept replaces operand 1 and cancel leaves it untouched; operand 2 is never modified.
"""
[[opcode.semantics.args]]
@@ -6901,7 +6906,7 @@ source = "investigation"
confidence = "high"
depends_on = []
evidence = "The real /v2 dispatch slot registers op_0x2c6_cp932_character_length@0x42a6d0. The handler sets LC_ALL to `japanese`, resolves operand 2, calls MSVC _mbstrlen, and writes the result to operand 1. INPUTNAME's sole site at 0x1002 uses this count as the loop bound before slicing each character with opcode 0x2c8."
details = "This is character count rather than .NET UTF-16 length or raw CP932 byte length. The implementation should use VmOptions.NativeStringCodePage and preserve valid CP932 multibyte boundaries."
details = "This is character count rather than .NET UTF-16 length or raw CP932 byte length. Port status (2026-07-29): implemented by encoding the C-string prefix through VmOptions.NativeStringCodePage and counting valid CP932 lead/trail pairs as one character; focused tests cover mixed single-byte and double-byte characters."
[[opcode.semantics.args]]
i = 1
@@ -6928,7 +6933,7 @@ source = "investigation"
confidence = "high"
depends_on = []
evidence = "The real /v2 dispatch slot registers op_0x2c8_cp932_substring@0x42c420. It copies operand 2 into a 256-byte buffer, sets LC_ALL to `japanese`, obtains _mbstrlen, reads start and count, clamps end=start+count to the character length when end is below 1 or beyond that length, and walks bytes with _mbbtype so CP932 lead/trail pairs are copied together. It writes the selected byte interval back through operand 1. INPUTNAME's sole site at 0x1021 loops substring(name,index,1) into its eight local character cells."
details = "The release call uses nonnegative in-range indices and count 1. A compatible general implementation should reproduce native end clamping (`end = length` when start+count < 1 or > length) and select the half-open character interval [start,end)."
details = "The release call uses nonnegative in-range indices and count 1. Port status (2026-07-29): implemented over encoded byte spans, preserving every valid lead/trail pair and reproducing native end clamping (`end = length` when start+count < 1 or > length) before selecting the half-open character interval [start,end)."
[[opcode.semantics.args]]
i = 1