Fix menu cursor auto-move

This commit is contained in:
gamer147
2026-08-16 11:07:09 -04:00
parent d4d9def8f4
commit eef1225b56
11 changed files with 75 additions and 10 deletions

View File

@@ -2727,6 +2727,25 @@ deferred ReadTextDB work should remain a seam rather than a storage dependency.
names/comments the cursor, callback dispatch, retained-history navigation/render/metadata, and history-voice
opcode paths.
### Menu default cursor auto-move (2026-08-16)
The menu option stored in `G[0x3238]` is `menu_cursor_auto_move_enabled`. `INITCONFIG.BIN` defaults it to
one and `CONFIG.BIN` writes the paired enabled/disabled values. At `BUNKI.BIN@0x2b6` and
`SBUNKI.BIN@0x289`, an enabled value makes the script compute a point in the default choice row and invoke
`set-cursor-virtual` (`0x10a`); the disabled path instead samples the existing physical position through
`get-cursor-virtual` (`0x109`). `BUNKIMOVE` and `SBUNKIMOVE` use the same toggle while animating the menu.
The highlighted default row and the physical pointer are therefore two consequences of the same script
path, not one frontend-owned selection operation.
The `/v2` handler `op_0x10a_set_cursor_virtual@0x421590` converts the client origin to screen space,
applies active VirtualFullScreen scale/letterbox geometry, and calls USER32 `SetCursorPos`. The port now
preserves its immediate virtual-pointer update and also forwards a cursor-warp request to the interactive
host. Godot marshals that request onto its main thread, inversely maps the 800x600 AGE coordinate into the
current viewport, and calls `Viewport.WarpMouse`. Godot supports that operation on the port's Windows,
macOS, and Linux desktop targets; it is documented as a no-op on Android, iOS, and Web, where the VM's
logical position still remains correct. A synthetic opcode regression covers both the VM position and host
request. The Ghidra handler annotation was updated and the `/v2` program saved on 2026-08-16.
### ADV Hide Window implementation (2026-07-18)
The x=772 callback now follows the original control flow rather than a Godot-only visibility shortcut.

View File

@@ -3730,6 +3730,7 @@
| `0x6cb` | adv_hover_message_skip | high | investigation | Pointer-hover flag for the standard ADV all-message Skip button at (728,572). See adv_hover_history. |
| `0x6cc` | adv_hover_read_message_skip | high | investigation | Pointer-hover flag for the standard ADV read-message-only Skip button at (750,572). See adv_hover_history. |
| `0x6cd` | adv_hover_hide_window | high | investigation | Pointer-hover flag for the standard ADV Hide-window button at (772,572). See adv_hover_history. |
| `0x3238` | menu_cursor_auto_move_enabled | high | investigation | Persistent game option controlling whether a newly opened BUNKI/SBUNKI choice menu moves the physical cursor to its default row. INITCONFIG defaults it to 1; CONFIG writes 1/0 for the paired choices. BUNKI@0x2b6 and SBUNKI@0x289 call set-cursor-virtual when enabled and sample the existing cursor with get-cursor-virtual when disabled; BUNKIMOVE/SBUNKIMOVE use the same toggle during menu entrance motion. Not a scene story flag. |
| `0x62425` | adv_hide_window_enabled | high | investigation | Native ADV-scheduler permission for the standard Hide Window action. After op 0x199 enters the registered yield-A handler, every standard ADV scene calls HIDEWIN.BIN only while this value is nonzero. No script writes it and the complete boot-to-SC0000 VM-write capture does not contain it, so it is native-owned inherited state rather than saved-game or script boot data. The Godot scene bootstrap mirrors the original enabled value 1. |
| `0x15a096` | information_message_handled | high | investigation | INFOMES clears this before walking handler rows and stops when it becomes one. CIMES, EIMES, and VIMES set it after rendering a matching character, enemy, or glossary message, implementing a first-handler-wins extension chain. |
@@ -3737,7 +3738,6 @@
| address | name | conf | source | usage |
|---|---|---|---|---|
| `0x3238` | — | low | inference | Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 5 scripts; compared against [1]. |
| `0x3301` | — | low | inference | Config/settings global (INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 6 scripts; compared against [1, 2]. |
| `0x3303` | — | low | inference | Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 8 scripts; compared against [1]. |
| `0x3304` | — | low | inference | Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 13 scripts; compared against [1]. |

View File

@@ -1282,7 +1282,7 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
### 0x10a `set-cursor-virtual` (u0041E540, argc 2)
- **summary:** (x)(y) - convert AGE virtual-screen coordinates to client/screen coordinates and move the OS cursor.
- **grounding:** source=investigation, confidence=high
- **evidence:** Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly.
- **evidence:** Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls USER32 SetCursorPos. BUNKI@0x2b6 and SBUNKI@0x289 test G[0x3238], compute the default choice point, and call 0x10a when menu cursor auto-move is enabled; their disabled branches retain the physical position through 0x109. SC0000 also alternates the cursor by one vertical pixel after state-changing ADV button clicks so hover re-enters cleanly.
### 0x10b `map-mouse-button` (u0041E5A0, argc 2)
- **summary:** (button_slot)(physical_button) - map a physical mouse button to a logical button slot; polling emits the slot as logical action slot+4.

View File

@@ -47,6 +47,25 @@ public class InputBindingTests
Assert.NotEqual(0, vm.InputBindings.JoystickButtonActionMask(3) & (1 << 4));
}
[Fact]
public void SetCursorVirtualUpdatesVmCoordinatesAndRequestsAHostWarp()
{
var script = ScriptAssembler.Assemble(Table, "CURSOR_WARP", new List<(int, Operand[])>
{
(0x10a, new[] { I(321), I(234) }),
(0x109, new[] { G(0x710), G(0x711) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var host = new RecordingHost();
var vm = new VirtualMachine(script, Table, host);
vm.Run();
Assert.Equal((321, 234), Assert.Single(host.CursorWarps));
Assert.Equal(321, vm.Globals.GetValueOrDefault(0x710));
Assert.Equal(234, vm.Globals.GetValueOrDefault(0x711));
}
[Fact]
[Trait("Category", "Workspace")]
public void System4BootstrapReplaysAllSixteenInputConfigurationCalls()

View File

@@ -76,6 +76,7 @@ internal class RecordingHost : IHost
public readonly List<FullwidthTextEditRequest> FullwidthTextEdits = new();
public System.Func<FullwidthTextEditRequest, FullwidthTextEditResult>? OnFullwidthTextEdit;
public readonly List<long> CursorResources = new();
public readonly List<(int X, int Y)> CursorWarps = new();
public readonly List<bool> AdvPagePresentationSuspended = new();
public readonly List<(AdvLiveTextRun Run, int GlyphDelayMilliseconds)> LiveTextRuns = new();
public readonly Dictionary<int, RgbaImage> SurfacePixels = new();
@@ -176,6 +177,7 @@ internal class RecordingHost : IHost
public virtual long InputClockMilliseconds => Environment.TickCount64;
public void SetCursorResource(long resourceId) => CursorResources.Add(resourceId);
public void ClearCursorResource() => CursorClearCount++;
public void WarpCursor(int virtualX, int virtualY) => CursorWarps.Add((virtualX, virtualY));
public virtual void Sleep(long duration) => SleptDurations.Add(duration);
public virtual void WaitForTimedCallbackDeadline(long duration)
{

View File

@@ -32,6 +32,7 @@ public interface IInputHost
void SetCursorResource(long resourceId) { }
void ClearCursorResource() { }
void WarpCursor(int virtualX, int virtualY) { }
// Native 0x1c7/0x1cc query two distinct ADV skip channels. Headless and non-interactive
// hosts default to normal playback; the Godot host supplies the live interactive values.

View File

@@ -159,8 +159,14 @@ public sealed partial class VirtualMachine
return pc + 1;
}
case "u0041E540":
case "set-cursor-virtual": // 0x10a; retain the virtual position even without OS warping
UpdatePointer((int)Read(a[0]), (int)Read(a[1])); return pc + 1;
case "set-cursor-virtual": // 0x10a: update AGE state immediately, then request the native warp
{
int x = (int)Read(a[0]);
int y = (int)Read(a[1]);
UpdatePointer(x, y);
_host.WarpCursor(x, y);
return pc + 1;
}
case "u0041E5A0":
case "map-mouse-button": // 0x10b: physical button -> slot, polled action is slot+4
InputBindings.MapMouseButton(unchecked((int)Read(a[0])), unchecked((int)Read(a[1])));

View File

@@ -482,6 +482,13 @@ public sealed partial class GodotAdvHost
_timeline?.Event("cursor-clear", new());
}
public void WarpCursor(int virtualX, int virtualY)
{
// Script execution is on the VM thread; Godot input/display APIs belong to the main thread.
_main.CallDeferred("WarpAgeCursor", virtualX, virtualY);
_timeline?.Event("cursor-warp", new() { ["x"] = virtualX, ["y"] = virtualY });
}
public void WaitForForegroundTransition(GfxState gfx)
{
_foregroundTransitionWaitBypassed = false;

View File

@@ -256,6 +256,17 @@ public partial class Main
(int)System.Math.Floor(position.Y * _screenHeight / size.Y));
}
public void WarpAgeCursor(int virtualX, int virtualY)
{
// Viewport.WarpMouse expects viewport coordinates. This is the inverse of ToNativeScreen and
// lets Godot account for the platform window origin before moving the desktop pointer.
Vector2 size = GetViewport().GetVisibleRect().Size;
if (size.X <= 0 || size.Y <= 0 || _screenWidth <= 0 || _screenHeight <= 0) return;
GetViewport().WarpMouse(new Vector2(
virtualX * size.X / _screenWidth,
virtualY * size.Y / _screenHeight));
}
public void SetAgeCursor(byte[] rgba, int width, int height, int hotspotX, int hotspotY)
{
var image = Image.CreateFromData(width, height, false, Image.Format.Rgba8, rgba);

View File

@@ -2419,13 +2419,13 @@ depends_on = ["0x3234"]
[[global]]
address = "0x3238"
name = ""
category = "unknown"
name = "menu_cursor_auto_move_enabled"
category = "ui-toggle"
type = "int"
value_domain = "{0,1}"
usage = "Config/settings global (CONFIG.BIN/INITCONFIG.BIN writer, scene-reach 0) — NOT a scene story flag; miner over-tagged it. Branch-read in 5 scripts; compared against [1]."
source = "inference"
confidence = "low"
usage = "Persistent game option controlling whether a newly opened BUNKI/SBUNKI choice menu moves the physical cursor to its default row. INITCONFIG defaults it to 1; CONFIG writes 1/0 for the paired choices. BUNKI@0x2b6 and SBUNKI@0x289 call set-cursor-virtual when enabled and sample the existing cursor with get-cursor-virtual when disabled; BUNKIMOVE/SBUNKIMOVE use the same toggle during menu entrance motion. Not a scene story flag."
source = "investigation"
confidence = "high"
depends_on = []
[[global]]

View File

@@ -2426,7 +2426,7 @@ noop_headless = false
source = "investigation"
confidence = "high"
depends_on = []
evidence = "Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls SetCursorPos. SC0000 alternates the cursor by one vertical pixel after state-changing ADV button clicks so the hover state re-enters cleanly."
evidence = "Ghidra /v2: op_0x10a_set_cursor_virtual@0x421590 maps virtual coordinates through the active VirtualFullScreen geometry and calls USER32 SetCursorPos. BUNKI@0x2b6 and SBUNKI@0x289 test G[0x3238], compute the default choice point, and call 0x10a when menu cursor auto-move is enabled; their disabled branches retain the physical position through 0x109. SC0000 also alternates the cursor by one vertical pixel after state-changing ADV button clicks so hover re-enters cleanly."
[[opcode.semantics.args]]
i = 1