Render DEBUGMAP tiled field surfaces
This commit is contained in:
@@ -86,6 +86,16 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
remains correct for ordinary SC texture/voice ids. ROOM voice `0x3365`, for example, resolves as raw
|
||||
`EUA0016.OGG`; treating it only as a ROOM-local id produces no asset because ROOM owns no SC range.
|
||||
|
||||
**Explicit raw texture loader (identified and implemented 2026-07-21).** Opcode `0x249` is the
|
||||
unambiguous packed raw-id texture path even while a scene section is active. It shares `0x1f9`'s surface
|
||||
replacement, AGF decode, and RGB colorkey contract, but passes native surface mode 1 and does not apply
|
||||
the executing frame's section base. FIELD uses `0x32da..0x32dd`, the universal SYS4INI indexes for
|
||||
`SO005.AGF`, `SO007.AGF`, `SO008A.AGF`, and `SO007A.AGF`, to populate map-sheet surfaces `0x3e..0x41`.
|
||||
The port therefore forwards those ids directly to `ResolveRawTexture`; it must not run them through
|
||||
`ResolveTextureResourceId` first. Native mode 1 is a large-image wrapper which tiles the same decoded
|
||||
logical pixels over ordinary child textures; it is not a different AGF/spritesheet interpretation or
|
||||
blend rule. The port's contiguous CPU image is therefore equivalent for rendering purposes.
|
||||
|
||||
*How we got here (condensed):* first confirmed `resId == file_number` via Frida load-order correlation
|
||||
for SC0000's opening, but `file_number` is not globally unique so a per-scene "scope" was needed. A long
|
||||
hunt for the selector (thought it was native scene state; even tried reading `G[0x62424]` live — the
|
||||
|
||||
@@ -941,6 +941,61 @@ Separate scale/rotation/translation state and timing are implemented. Anchor sem
|
||||
cyclic wrapping, 2D projection, and affine raster coverage have focused native-oracle tests. Native D3D9 filtering
|
||||
and render-target command execution remain separate fidelity work.
|
||||
|
||||
#### Raw mode-1 surface load — opcode `0x249` (2026-07-21)
|
||||
|
||||
`op_0x249_load_raw_texture_surface@0x424b20` has ABI
|
||||
`(universal_packed_catalog_id, surface_slot, RGB_colorkey)`. Its release, movie detach, indexed-asset open,
|
||||
colorkey conversion, failure exception, and stream cleanup are the same as `0x1f9`. The only native loader
|
||||
difference is the last argument to `gfx_surface_load_asset@0x477c40`: `0x1f9` passes mode 0, while `0x249`
|
||||
passes mode 1. `gfx_surface_decode_and_create@0x474e90` selects a base 0x450-byte texture object for mode 0
|
||||
and a derived 0x460-byte texture object for mode 1. A successful mode-0 load records the resource id in its
|
||||
device-reload record; mode 1 records `-1`.
|
||||
|
||||
The derived class is specifically a **tiled large-image surface**, not an alternate pixel format,
|
||||
spritesheet interpretation, or blend mode. `gfx_tiled_surface_create@0x432ff0` divides the logical image into
|
||||
`ceil(width / DAT_005b15b0) × ceil(height / DAT_005b15b0)` ordinary mode-0 child surfaces.
|
||||
`gfx_tiled_surface_upload_agf@0x431a10` decodes indexed 1/4/8-bpp and 24/32-bpp input and uploads each tile;
|
||||
`gfx_tiled_surface_blit@0x4316b0` divides any requested logical source rectangle across the intersecting
|
||||
children and adjusts their destinations. A CPU compositor can therefore keep one contiguous decoded RGBA
|
||||
image without losing the mode-1 behavior relevant to Himegari.
|
||||
|
||||
The corpus makes the addressing and first gameplay consequence concrete. FIELD calls `0x249` with raw ids
|
||||
`0x32da..0x32dd`, which are SYS4INI entries `SO005`, `SO007`, `SO008A`, and `SO007A`, into slots
|
||||
`0x3e..0x41`. `DRAWMAP.BIN` then creates the dungeon tile objects almost entirely from slots `0x3e` and
|
||||
`0x3f`. A skipped `0x249` therefore leaves the surrounding UI operational but the central map black. After
|
||||
implementing it, the first DEBUGMAP retest exposed a separate host blit error: FIELD intentionally binds a
|
||||
zero-width/zero-height SO005 prototype object, while the port expanded zero dimensions to the full texture.
|
||||
Native `gfx_object_blit_d3d9@0x4774c0` clips the explicit source rectangle and returns when
|
||||
`right<=left || bottom<=top`; mode-1's tiled blit likewise visits no children for an empty rectangle. The
|
||||
port now preserves that empty draw rather than leaking the complete SO005 sheet.
|
||||
|
||||
#### Selected retained-object range transform — opcodes `0x229`/`0x22a`/`0x22c`/`0x22d` (2026-07-21)
|
||||
|
||||
`0x229` was formerly misclassified as a second per-object position setter. Native
|
||||
`op_0x229_set_gfx_range_transform@0x423700` instead resets an embedded gfx-object record at retained-gfx
|
||||
owner `+0x428`, writes `(first_handle,count)` to owner `+0x420/+0x424`, and writes operands 3..5 as that
|
||||
embedded object's anchor at owner `+0x440..+0x448`. The actual per-object direct-position opcode remains
|
||||
`0x22f`.
|
||||
|
||||
On every render frame, `gfx_range_transform_sample_frame@0x476df0` samples the embedded object's ordinary
|
||||
scale/rotation/translation channels into owner matrix `+0xb5b4`. `gfx_object_composite@0x47f650`
|
||||
post-multiplies an object's normal matrix by this shared matrix only when its handle is in
|
||||
`[first_handle, first_handle+count)`. The sibling setters are:
|
||||
|
||||
- `0x22a`: current scale, three integer percentages divided by 100;
|
||||
- `0x22b`: current axis-angle rotation (present in the native dispatch table, zero Himegari corpus calls);
|
||||
- `0x22c`: current translation in pixels;
|
||||
- `0x22d`: delayed/duration scale target, using the embedded object's ordinary one-shot scale channel;
|
||||
- `0x22e`: delayed/duration axis-angle target (native-dispatch-only, zero Himegari corpus calls).
|
||||
|
||||
FIELD's camera helper selects handles `[1,50000)`, anchors the transform at the current camera world
|
||||
coordinate `(G[0x767e],G[0x767f])`, sets translation to `(400-camera_x,300-camera_y,0)`, and applies the
|
||||
zoom percentage from `G[0xccc09]`. Thus the map layer is centered/scaled while handles `>=50000`—the dungeon
|
||||
UI—remain screen-fixed. FIELD's sole `0x22d` call animates a zoom over 300 ms. LOOK reuses the immediate
|
||||
camera helper. Across the corpus, `0x229` occurs 693 times in 309 scripts: 590 all-zero disables, 101
|
||||
identity-range selections, and the two FIELD/LOOK camera selections. Correcting the contract therefore
|
||||
removes spurious object-zero mutations without changing established ADV output.
|
||||
|
||||
### Blend & transparency — colorkey + `0x202`/`0x203` color/alpha (2026-07-08)
|
||||
|
||||
Reversed for graphics slice A (spec `docs/superpowers/specs/2026-07-08-blend-transparency-design.md`;
|
||||
@@ -1048,7 +1103,7 @@ annotated in Ghidra, saved.
|
||||
| op | handler / worker | semantics |
|
||||
|---|---|---|
|
||||
| `0x22f` | `gfx_op_0x22f_set_position_anim` → `gfx_worker_set_translation` | set object **position** (translation vec `obj+0x5d4`); base transform, not a ping-pong channel |
|
||||
| `0x229` | `gfx_op_0x229_set_position` (`FUN_00472bb0`+`FUN_00472be0`) | set object **position/geometry** immediately (`obj+0x420/0x424` + vec `obj+0x440..0x448`) |
|
||||
| `0x229` | `op_0x229_set_gfx_range_transform` → `gfx_range_transform_reset` / `select_handles` / `set_anchor` | reset/select the shared **retained-object range transform**; not a per-object position setter (superseded finding above) |
|
||||
| `0x239` | `gfx_op_0x239_set_srcrect_cell` → `gfx_worker_set_srcrect_cell` | one-shot **spritesheet-cell** channel: delay/duration `obj+0x48/+0x5c`, total frames/columns `obj+0x238/+0x23c`, target frame `obj+0x234` |
|
||||
| `0x231` | `gfx_op_0x231_anim_srcrect` → `gfx_worker_anim_srcrect` | looping **spritesheet-cell** channel: milliseconds per frame `obj+0x230`, total frames `obj+0x238`, columns `obj+0x23c`; row-major and wraps, not ping-pong |
|
||||
| `0x232` | `gfx_op_0x232_anim_color` → `gfx_worker_anim_color` | **animate color**: bit2 active, period `obj+0x220`, target `obj+0x240` → interpolator COLOR channel (ping-pong). Negative alpha/RGB preserve corresponding bytes from static color `obj+0x60`; alpha >255 clamps. Distinct from one-shot `0x202`/static `0x203` |
|
||||
|
||||
@@ -594,9 +594,25 @@ Implemented through IHost.PlayModalMovieToSurface. ResourceMap.ResolveRawMovie d
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2 handler gfx_op_0x228_query_position@0x42a3a0 calls gfx_object_query_translation_target@0x47cdd0. The worker copies the complete 0xb5-dword object record, passes copied obj+0x17c to matrix4_decompose_affine@0x48d7c8, and returns its translation outputs; the decomposition reads matrix elements +0x30/+0x34/+0x38, corresponding to obj+0x1ac/+0x1b0/+0x1b4. SC0000 AE001H queries this before each 0x220 leg. C# regression covers targets (40,-20), (50,-80), (130,-100), plus the missing-object output-preservation path.
|
||||
|
||||
### 0x229 `u004219E0` (u004219E0, argc 5)
|
||||
- **summary:** 0x229 set-position2 (handle)(op2)(x)(y)(z): set object position/geometry directly (FUN_00472bb0/be0). C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
### 0x229 `set-gfx-range-transform` (set-gfx-range-transform, argc 5)
|
||||
- **summary:** (first_handle)(count)(anchor_x)(anchor_y)(anchor_z) — reset and select the retained-gfx range transform applied after each ordinary object matrix for handles in [first, first+count), then set its anchor/pivot. A zero count disables it.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x229_set_gfx_range_transform@0x423700 first calls gfx_range_transform_reset@0x472b80, then writes operands 1/2 to retained-gfx owner+0x420/+0x424 and operands 3..5 to the embedded transform object's anchor at owner+0x440..+0x448. gfx_object_composite@0x47f650 post-multiplies the sampled owner+0xb5b4 matrix only for handles in that selected range. Corpus: 693 calls/309 scripts; 590 disable with all zeroes, 101 select from handle 1 with a script-computed count, and FIELD/LOOK supply camera anchors. This supersedes the former incorrect per-object-position interpretation; per-object direct position is 0x22f.
|
||||
|
||||
### 0x22a `set-gfx-range-scale-current` (set-gfx-range-scale-current, argc 3)
|
||||
- **summary:** (scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the selected retained-gfx range transform's current scale matrix.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x22a_set_gfx_range_scale_current@0x4237b0 divides all three operands by 100 and calls gfx_range_transform_set_scale_current@0x472c10, which builds owner+0x494. FIELD and LOOK each call it once after 0x229/0x22c; FIELD's zoom percent is G[0xccc09].
|
||||
|
||||
### 0x22c `set-gfx-range-translation-current` (set-gfx-range-translation-current, argc 3)
|
||||
- **summary:** (translate_x)(translate_y)(translate_z) — immediately replace the selected retained-gfx range transform's current translation matrix.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x22c_set_gfx_range_translation_current@0x423900 passes the three integer operands as floats to gfx_range_transform_set_translation_current@0x472d00, which builds owner+0x594. FIELD computes (400-camera_x, 300-camera_y, 0), making the selected map anchor land at screen center; LOOK uses the same camera helper.
|
||||
|
||||
### 0x22d `set-gfx-range-scale-target` (set-gfx-range-scale-target, argc 5)
|
||||
- **summary:** (delay_ms)(duration_ms)(scale_x_percent)(scale_y_percent)(scale_z_percent) — animate the selected retained-gfx range transform's scale from its current matrix to the target.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x22d_set_gfx_range_scale_target@0x423990 divides operands 3..5 by 100 and calls gfx_range_transform_set_scale_target@0x472d50. The worker arms the embedded transform object's ordinary scale channel (delay obj+0x3c, duration +0x50, target matrix +0xac), which gfx_range_transform_sample_frame@0x476df0 samples before range composition. FIELD has the sole corpus call, a 300 ms camera zoom.
|
||||
|
||||
### 0x22f `u00421DD0` (u00421DD0, argc 5)
|
||||
- **summary:** 0x22f set-position (handle)(op2)(x)(y)(z): set the object base position (direct transform, not ping-pong). Worker gfx_worker_set_translation @0x472e90. C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster.
|
||||
@@ -658,6 +674,11 @@ The setter get-or-creates the object and writes the complete operand. During ret
|
||||
- **depends on:** 0x242
|
||||
- **evidence:** Ghidra handler 0x4182d0: if !(ctx+0x51b80 & 2), set retained-gfx owner+0xb55c (EngineCtx+0x51b70)=1 and zero owner+0xb564/+0xb568. gfx_object_apply_transform_channels treats force value 1 as immediate completion unless obj+0x2d0 bit 0 is set. SC0000 label_1235a calls it before present-frame.
|
||||
|
||||
### 0x249 `load-raw-texture-surface` (load-raw-texture-surface, argc 3)
|
||||
- **summary:** Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same RGB colorkey contract as set-texture (0x1f9).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its mode-1 gfx_surface_mode1_ctor selects a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits the logical dimensions into DAT_005b15b0-sized ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes and uploads each region; gfx_tiled_surface_blit@0x4316b0 subdivides a requested logical source rectangle across those tiles. It is not a spritesheet interpretation or alternate blend mode, so the port's contiguous CPU image is behaviorally equivalent. Corpus literals are universal raw indexes, including FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A, and therefore bypass scene-section normalization.
|
||||
|
||||
## input
|
||||
|
||||
### 0x86 `set-cursor-resource` (u0041B210, argc 1)
|
||||
@@ -1181,18 +1202,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x22a `u00421A90` (u00421A90, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x22c `u00421BD0` (u00421BD0, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x22d `u00421C60` (u00421C60, argc 5)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x230 `u00421E70` (u00421E70, argc 1)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
@@ -1221,10 +1230,6 @@ op 0x90 (u0041BEB0, argc 7): `0x90 x y w h tgt_a tgt_b tgt_c`. Kelebek left it "
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x249 `u00422EB0` (u00422EB0, argc 3)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
### 0x24d `u00422E90` (u00422E90, argc 12)
|
||||
- **summary:** —
|
||||
- **grounding:** source=kelebek, confidence=low
|
||||
|
||||
@@ -383,6 +383,39 @@ requires them.
|
||||
Completion evidence combines original-game observation, executed-opcode/call traces, visible map/UI output,
|
||||
and before/after global-state comparisons for the action.
|
||||
|
||||
### DEBUGMAP field-entry result (2026-07-21; manually validated)
|
||||
|
||||
The shipped `DEBUGMAP.BIN` path is useful for the first bounded field slice, but it is not treated as a
|
||||
replacement for the natural campaign path. It performs substantial script-authored setup itself: it creates
|
||||
the test units, writes stage id `0xa5` to `G[0x4dfbc]`, fills the field-mode globals, writes system-flow
|
||||
request `G[0]=3`, and returns so SYSTEM4 enters `FIELD.BIN`. Runs launched from TITLE also retain the real
|
||||
SYSTEM4/INIT tables. A future discrepancy in party, inventory, stage, or progression state may still be an
|
||||
unpublished debug-level prerequisite; do not invent a seed unless its missing producer is proven.
|
||||
|
||||
The first observed field discrepancy was a black central map while the surrounding field UI and minimap
|
||||
input remained alive. `DRAWMAP.BIN` is 23/23 opcodes handled and `RENDERMAP.BIN` is 31/31. FIELD's missing
|
||||
opcode `0x249` loads universal raw map sheets `0x32da..0x32dd`
|
||||
(`SO005`/`SO007`/`SO008A`/`SO007A`) into surfaces `0x3e..0x41`, after which DRAWMAP binds its generated tile
|
||||
objects to those surfaces. Skipping the loader left valid retained objects pointing at empty surfaces.
|
||||
|
||||
The first retest after adding `0x249` showed the complete SO005 sheet enlarged over a grey field. The native
|
||||
mode-1 class is now fully identified as a large-image tiled wrapper over the same decoded pixels, ruling out
|
||||
a special spritesheet or blend interpretation. Two independent presentation gaps caused the retest:
|
||||
|
||||
- native treats a zero-area draw-texture source rectangle as an empty draw; the port incorrectly expanded
|
||||
it to the entire source image, exposing FIELD's intentionally invisible SO005 prototype object;
|
||||
- FIELD's camera depends on the shared retained-object range transform. Corrected `0x229` selects the map
|
||||
handle range and anchor (it is not a per-object position opcode), while newly implemented `0x22a`,
|
||||
`0x22c`, and `0x22d` apply immediate zoom, immediate translation, and animated zoom to that range without
|
||||
moving the surrounding UI.
|
||||
|
||||
The related native `0x22b`/`0x22e` range-rotation setters have zero Himegari corpus calls and need no runtime
|
||||
implementation yet. The remaining `DRAWMINIMAP` gap is `0x207` (eight calls) and is confined to minimap
|
||||
work; FIELD's other 14 static gaps do not produce the main terrain layer. Installed-asset decode,
|
||||
range-isolation/animation, VM dispatch, full engine tests, and the threaded Godot selftest pass. Manual
|
||||
acceptance confirms that DEBUGMAP now displays the dungeon map correctly; the earlier full-sheet overlay is
|
||||
gone and the field presentation remains operational after the camera-transform correction.
|
||||
|
||||
## Later Phase B breadth
|
||||
|
||||
Once the natural spine and first gameplay loop are trustworthy, broaden in independent tracks:
|
||||
|
||||
@@ -81,6 +81,22 @@ public class AgfDecoderTests
|
||||
Assert.Contains(image.Pixels.Where((_, i) => (i & 3) == 3), a => a is > 0 and < 255);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x32da, "SO005.AGF")]
|
||||
[InlineData(0x32db, "SO007.AGF")]
|
||||
[InlineData(0x32dc, "SO008A.AGF")]
|
||||
[InlineData(0x32dd, "SO007A.AGF")]
|
||||
public void InstalledFieldMapSheetsResolveAndDecodeByRawCatalogIndex(int rawId, string name)
|
||||
{
|
||||
var resources = ResourceMap.Load();
|
||||
var asset = resources.ResolveRawTexture(rawId);
|
||||
Assert.NotNull(asset);
|
||||
Assert.Equal(name, asset.Name);
|
||||
var image = resources.DecodeTexture(asset);
|
||||
Assert.True(image.Width > 0);
|
||||
Assert.True(image.Height > 0);
|
||||
}
|
||||
|
||||
private sealed class MemoryStore(byte[] bytes) : IAssetStore
|
||||
{
|
||||
public Stream Open(AssetEntry entry) => new MemoryStream(bytes, writable: false);
|
||||
|
||||
@@ -102,6 +102,8 @@ public class GfxCommandBufferTests
|
||||
=> (0x1fb, new[] { G(handle), G(slot), I(0), I(0), G(w), G(h), G(dx), G(dy) });
|
||||
|
||||
private static (int, Operand[]) SetTex(int resId, int slot) => (0x1f9, new[] { G(resId), G(slot), I(0) });
|
||||
private static (int, Operand[]) SetRawTex(long resId, long slot, long colorKey)
|
||||
=> (0x249, new[] { I(resId), I(slot), I(colorKey) });
|
||||
|
||||
[Fact]
|
||||
public void SetThenDrawTextureMakesAVisibleObjectFromTheSurface()
|
||||
@@ -122,4 +124,26 @@ public class GfxCommandBufferTests
|
||||
Assert.Equal(0x25, vis[0].SurfaceResId); // resolved from the object's live source slot
|
||||
Assert.Equal((800, 600, 0, 0), (vis[0].W, vis[0].H, vis[0].DstX, vis[0].DstY));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawTextureLoadBypassesSceneResourceNormalizationAndFeedsRetainedDraws()
|
||||
{
|
||||
var t = T();
|
||||
var scene = ScriptAssembler.Assemble(t, "RAW-GFX", new List<(int, Operand[])>
|
||||
{
|
||||
SetRawTex(0x32da, 0x3e, 0),
|
||||
(0x1fb, new[] { I(0x100), I(0x3e), I(0), I(0), I(100), I(100), I(20), I(30) }),
|
||||
Exit(),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost { TextureResourceIdOffset = 0x1000 };
|
||||
var vm = new VirtualMachine(scene, t, host);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal((0x32daL, 0x3e), Assert.Single(host.Textures));
|
||||
var visible = Assert.Single(vm.Gfx.SnapshotVisibleObjects());
|
||||
Assert.Equal(0x32da, visible.SurfaceResId);
|
||||
Assert.Equal(0, visible.ColorKey);
|
||||
Assert.Equal((100, 100, 20, 30), (visible.W, visible.H, visible.DstX, visible.DstY));
|
||||
}
|
||||
}
|
||||
|
||||
76
engine/Age.Engine.Tests/GfxRangeTransformTests.cs
Normal file
76
engine/Age.Engine.Tests/GfxRangeTransformTests.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class GfxRangeTransformTests
|
||||
{
|
||||
[Fact]
|
||||
public void RangeCameraCentersItsAnchorAndLeavesUiHandlesUnchanged()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(1, 1, -1);
|
||||
gfx.BindDraw(10, 1, 0, 0, 1, 1, 500, 350); // selected map object at the camera anchor
|
||||
gfx.BindDraw(20, 1, 0, 0, 1, 1, 500, 350); // screen-fixed UI object outside the range
|
||||
gfx.SetRangeTransform(10, 1, (500, 350, 0));
|
||||
gfx.SetRangeTranslationCurrent((-100, -50, 0));
|
||||
gfx.SetRangeScaleCurrent((150, 150, 100));
|
||||
|
||||
var objects = gfx.SnapshotVisibleObjects();
|
||||
var map = objects.Single(x => x.Handle == 10);
|
||||
var ui = objects.Single(x => x.Handle == 20);
|
||||
var mapOrigin = Transform2DMath.Build(map.Transform).FromLocalOrigin(map.DstX, map.DstY)
|
||||
.Then(map.RangeTransform!.Value).Apply(0, 0);
|
||||
var uiOrigin = Transform2DMath.Build(ui.Transform).FromLocalOrigin(ui.DstX, ui.DstY).Apply(0, 0);
|
||||
|
||||
Assert.Equal((400.0, 300.0), mapOrigin);
|
||||
Assert.Null(ui.RangeTransform);
|
||||
Assert.Equal((500.0, 350.0), uiOrigin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RangeScaleTargetUsesTheOrdinaryOneShotClock()
|
||||
{
|
||||
var gfx = new GfxState();
|
||||
gfx.SetSurface(1, 1, -1);
|
||||
gfx.BindDraw(10, 1, 0, 0, 1, 1, 410, 300);
|
||||
gfx.SetRangeTransform(10, 1, (400, 300, 0));
|
||||
gfx.SetRangeScaleCurrent((100, 100, 100));
|
||||
gfx.SetRangeScaleChannel(delayMs: 0, durationMs: 300, percent: (200, 200, 100));
|
||||
|
||||
gfx.SnapshotVisibleObjects(1000); // seeds the native-style shared channel start
|
||||
var halfway = gfx.SnapshotVisibleObjects(1150).Single();
|
||||
var p = Transform2DMath.Build(halfway.Transform).FromLocalOrigin(halfway.DstX, halfway.DstY)
|
||||
.Then(halfway.RangeTransform!.Value).Apply(0, 0);
|
||||
|
||||
Assert.Equal((415.0, 300.0), p);
|
||||
Assert.True(gfx.HasActiveTimedPresentation(1150));
|
||||
gfx.SnapshotVisibleObjects(1300);
|
||||
Assert.False(gfx.HasActiveTimedPresentation(1300));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Opcode229SelectsRangeWithoutCreatingAnOrdinaryObject()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "RANGE", new List<(int, Operand[])>
|
||||
{
|
||||
(0x229, new[] { I(1), I(100), I(400), I(300), I(0) }),
|
||||
(0x22c, new[] { I(0), I(0), I(0) }),
|
||||
(0x22a, new[] { I(100), I(100), I(100) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(script, table, new RecordingHost());
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Null(vm.Gfx.TryGet(1));
|
||||
vm.Gfx.SetSurface(1, 1, -1);
|
||||
vm.Gfx.BindDraw(1, 1, 0, 0, 1, 1, 400, 300);
|
||||
Assert.NotNull(vm.Gfx.SnapshotVisibleObjects().Single().RangeTransform);
|
||||
}
|
||||
|
||||
private static Operand I(long v) => new(0, v);
|
||||
}
|
||||
@@ -42,12 +42,15 @@ internal class RecordingHost : IHost
|
||||
public readonly List<int> ClearedRenderTargets = new();
|
||||
public readonly List<(int First, int Count)> ReleasedSurfaceRanges = new();
|
||||
public readonly List<(int Source, int Target, long Interval)> SurfaceCrossfades = new();
|
||||
public readonly List<(long Resource, int Slot)> Textures = new();
|
||||
public readonly List<bool> MessageSkipChanges = new();
|
||||
public readonly List<bool> PhysicalMessageSkipChanges = new();
|
||||
public readonly List<long> CursorResources = new();
|
||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||
public int CursorClearCount;
|
||||
public int SceneContextResets;
|
||||
public long TextureResourceIdOffset;
|
||||
public long ResolveTextureResourceId(long resourceId) => resourceId + TextureResourceIdOffset;
|
||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||
@@ -123,7 +126,7 @@ internal class RecordingHost : IHost
|
||||
public void CrossfadeSurfaces(GfxState gfx, int sourceSurface, int targetSurface, long intervalArgument)
|
||||
=> SurfaceCrossfades.Add((sourceSurface, targetSurface, intervalArgument));
|
||||
public void CreateTexture(int slot, int w, int h) { }
|
||||
public void SetTexture(long resId, int slot) { }
|
||||
public void SetTexture(long resId, int slot) => Textures.Add((resId, slot));
|
||||
public void ClearRenderTarget(int surfaceSlot) => ClearedRenderTargets.Add(surfaceSlot);
|
||||
public void ReleaseSurfaceRange(int firstSlot, int count) => ReleasedSurfaceRanges.Add((firstSlot, count));
|
||||
public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { }
|
||||
|
||||
@@ -38,7 +38,8 @@ public readonly record struct RenderObject(long Handle, long SurfaceResId, long
|
||||
int Alpha, long Tint, int TintStrength, BlendKind Blend,
|
||||
bool MultiplyTint,
|
||||
SurfaceTransitionState? SurfaceTransition = null,
|
||||
ColorTransitionState? ColorTransition = null);
|
||||
ColorTransitionState? ColorTransition = null,
|
||||
Affine2D? RangeTransform = null);
|
||||
|
||||
/// <summary>Host-agnostic model of the AGE native gfx command-buffer (reversed in
|
||||
/// docs/engine-re.md, gfx op-contract table). One registry maps an object handle to a GfxObject — the
|
||||
@@ -119,6 +120,12 @@ public sealed class GfxState
|
||||
// returns the object's live source slot (obj+4), or -1 when the handle has not been drawn/bound yet.
|
||||
private readonly Dictionary<long, GfxObject> _objects = new();
|
||||
|
||||
// Ops 0x229-0x22e address one embedded gfx-object record outside the ordinary object map. Its sampled
|
||||
// matrix is post-multiplied onto only the selected handle range during native composition. FIELD uses
|
||||
// this as its map camera while the surrounding dungeon UI remains screen-fixed.
|
||||
private long _rangeTransformFirst, _rangeTransformCount;
|
||||
private GfxObject _rangeTransform = new();
|
||||
|
||||
private readonly Dictionary<long, long> _fieldTable = new(); // ctx+0x46d14 (0x216); no family writer -> default 0
|
||||
public long CurrentObject { get; private set; }
|
||||
/// <summary>EngineCtx+0x14e08, selected by op 0x80 and used by op 0x1d9 when its slot is zero.</summary>
|
||||
@@ -155,6 +162,44 @@ public sealed class GfxState
|
||||
lock (_lock) DefaultObjectSlot = slot;
|
||||
}
|
||||
|
||||
/// <summary>Op 0x229: reset the embedded range transform, select [first, first+count), and set its
|
||||
/// anchor/pivot. This does not create or mutate an ordinary retained object.</summary>
|
||||
public void SetRangeTransform(long first, long count, (long X, long Y, long Z) anchor)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransformFirst = first;
|
||||
_rangeTransformCount = System.Math.Max(0, count);
|
||||
_rangeTransform = new GfxObject { V18 = anchor };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22a: immediately replace the embedded range transform's current scale.</summary>
|
||||
public void SetRangeScaleCurrent((long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
_rangeTransform.ScaleCurrent = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22c: immediately replace the embedded range transform's current translation.</summary>
|
||||
public void SetRangeTranslationCurrent((long X, long Y, long Z) translation)
|
||||
{
|
||||
lock (_lock) _rangeTransform.TranslationCurrent = translation;
|
||||
}
|
||||
|
||||
/// <summary>Op 0x22d: arm the range transform's delayed one-shot scale target.</summary>
|
||||
public void SetRangeScaleChannel(long delayMs, long durationMs, (long X, long Y, long Z) percent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rangeTransform.ScaleDelayMs = delayMs;
|
||||
_rangeTransform.ScaleDurationMs = durationMs;
|
||||
_rangeTransform.ScaleTarget = (percent.X / 100.0, percent.Y / 100.0, percent.Z / 100.0);
|
||||
_rangeTransform.ScaleEnabled = durationMs > 0;
|
||||
_rangeTransform.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Op 0x21d: clone the native 0x2d4-byte retained-object record from source to destination.</summary>
|
||||
public bool CloneObject(long sourceHandle, long destinationHandle)
|
||||
{
|
||||
@@ -257,6 +302,9 @@ public sealed class GfxState
|
||||
_surfaceTransitions.Clear();
|
||||
CurrentObject = 0;
|
||||
CurrentRenderTargetSlot = -1;
|
||||
_rangeTransformFirst = 0;
|
||||
_rangeTransformCount = 0;
|
||||
_rangeTransform = new GfxObject();
|
||||
AnimClockDurationTicks = 0;
|
||||
AnimClockGeneration++;
|
||||
}
|
||||
@@ -424,6 +472,8 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
(o.OneShotAnimationControlFlags & 1) == 0 &&
|
||||
(o.OneShotColorEnabled || o.ScaleEnabled ||
|
||||
@@ -441,9 +491,16 @@ public sealed class GfxState
|
||||
/// marked by op 0x242 bit 0 keep sampling asynchronously.</summary>
|
||||
private void ForceCompleteOneShotChannels()
|
||||
{
|
||||
CommitOneShotChannels(_rangeTransform);
|
||||
foreach (var o in _objects.Values)
|
||||
{
|
||||
if ((o.OneShotAnimationControlFlags & 1) != 0) continue;
|
||||
CommitOneShotChannels(o);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CommitOneShotChannels(GfxObject o)
|
||||
{
|
||||
if (o.OneShotColorEnabled)
|
||||
{
|
||||
o.Color = o.OneShotColorTarget & 0xffffffff;
|
||||
@@ -475,7 +532,6 @@ public sealed class GfxState
|
||||
}
|
||||
o.OneShotStartMs = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Whether sampling the retained scene at a later frame can change its pixels without another
|
||||
/// VM mutation. Includes finite presentation work plus the ambient channels that may remain active while
|
||||
@@ -484,6 +540,8 @@ public sealed class GfxState
|
||||
{
|
||||
lock (_lock)
|
||||
return _surfaceTransitions.Values.Any(t => TransitionProgress(t, nowMs) < 1.0) ||
|
||||
_rangeTransform.ScaleEnabled || _rangeTransform.RotationChannelEnabled ||
|
||||
_rangeTransform.TranslationEnabled ||
|
||||
_objects.Values.Any(o => o.Visible &&
|
||||
(o.OneShotColorEnabled || o.ScaleEnabled || o.RotationChannelEnabled ||
|
||||
o.TranslationEnabled ||
|
||||
@@ -651,7 +709,8 @@ public sealed class GfxState
|
||||
|
||||
/// <summary>Visible objects in ascending-handle order (= z-order), each with its source surface resolved
|
||||
/// and its active channels interpolated at <paramref name="nowMs"/>. Position is the base V24 (a direct
|
||||
/// transform, ops 0x22f/0x229); scale and translation are independent one-shot matrix channels. The
|
||||
/// transform (op 0x22f); scale and translation are independent one-shot matrix channels. Ops
|
||||
/// 0x229-0x22e contribute a second sampled matrix only to their selected handle range. The
|
||||
/// src-rect channel (0x239/0x231) selects the spritesheet cell; the color channel (0x232) ping-pongs the
|
||||
/// alpha/tint. Channel Start fields seed to nowMs on first sight.</summary>
|
||||
public IReadOnlyList<RenderObject> SnapshotVisibleObjects(long nowMs)
|
||||
@@ -659,6 +718,28 @@ public sealed class GfxState
|
||||
lock (_lock)
|
||||
{
|
||||
var list = new List<RenderObject>();
|
||||
Affine2D? rangeAffine = null;
|
||||
if (_rangeTransformCount > 0)
|
||||
{
|
||||
var r = _rangeTransform;
|
||||
bool hadRangeOneShot = r.ScaleEnabled || r.RotationChannelEnabled || r.TranslationEnabled;
|
||||
if (hadRangeOneShot && r.OneShotStartMs < 0) r.OneShotStartMs = nowMs;
|
||||
var rangeScale = SampleMatrixChannel(ref r.ScaleCurrent, r.ScaleTarget, r.ScaleDelayMs,
|
||||
r.ScaleDurationMs, r.OneShotStartMs, ref r.ScaleEnabled, nowMs);
|
||||
var rangeRotation = SampleRotationChannel(ref r.RotationCurrent, r.RotationTarget,
|
||||
r.RotationDelayMs, r.RotationDurationMs, r.OneShotStartMs,
|
||||
ref r.RotationChannelEnabled, nowMs);
|
||||
var rangeTranslation = SampleMatrixChannel(ref r.TranslationCurrent, r.TranslationTarget,
|
||||
r.TranslationDelayMs, r.TranslationDurationMs, r.OneShotStartMs,
|
||||
ref r.TranslationEnabled, nowMs);
|
||||
if (!r.ScaleEnabled && !r.RotationChannelEnabled && !r.TranslationEnabled)
|
||||
r.OneShotStartMs = -1;
|
||||
rangeAffine = Transform2DMath.Build(new TransformState(
|
||||
rangeScale.X, rangeScale.Y, rangeScale.Z,
|
||||
rangeTranslation.X, rangeTranslation.Y, rangeTranslation.Z,
|
||||
r.V18.X, r.V18.Y, r.V18.Z,
|
||||
rangeRotation.X, rangeRotation.Y, rangeRotation.Z, rangeRotation.Angle));
|
||||
}
|
||||
foreach (var kv in _objects.OrderBy(k => k.Key))
|
||||
{
|
||||
var o = kv.Value;
|
||||
@@ -762,6 +843,9 @@ public sealed class GfxState
|
||||
|
||||
SurfaceTransitionState? transition = _surfaceTransitions.TryGetValue(o.SourceSlot, out var st)
|
||||
? SampleTransition(st, nowMs) : null;
|
||||
Affine2D? objectRangeTransform = rangeAffine is { } ra &&
|
||||
kv.Key >= _rangeTransformFirst && kv.Key - _rangeTransformFirst < _rangeTransformCount
|
||||
? ra : null;
|
||||
list.Add(new RenderObject(kv.Key, resId, ck, srcX, srcY, w, h,
|
||||
(int)o.V24.X, (int)o.V24.Y,
|
||||
new TransformState(scale.X, scale.Y, scale.Z,
|
||||
@@ -771,7 +855,8 @@ public sealed class GfxState
|
||||
new RotationCycleState(o.RotationEnabled, o.RotationPeriodMs,
|
||||
o.RotationAxis.X, o.RotationAxis.Y,
|
||||
o.RotationAxis.Z, cycleAngle),
|
||||
alpha, tint, strength, blend, multiplyTint, transition, colorTransition));
|
||||
alpha, tint, strength, blend, multiplyTint, transition,
|
||||
colorTransition, objectRangeTransform));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,17 @@ public readonly record struct Affine2D(double XX, double XY, double YX, double Y
|
||||
return new(XX, XY, YX, YY, p.X, p.Y);
|
||||
}
|
||||
|
||||
/// <summary>Compose this row-vector transform followed by <paramref name="next"/>. Native uses this
|
||||
/// order when it post-multiplies an object's matrix by the selected retained-gfx range transform.</summary>
|
||||
public Affine2D Then(Affine2D next)
|
||||
=> new(
|
||||
XX * next.XX + XY * next.YX,
|
||||
XX * next.XY + XY * next.YY,
|
||||
YX * next.XX + YY * next.YX,
|
||||
YX * next.XY + YY * next.YY,
|
||||
TX * next.XX + TY * next.YX + next.TX,
|
||||
TX * next.XY + TY * next.YY + next.TY);
|
||||
|
||||
public bool TryInverse(out Affine2D inverse)
|
||||
{
|
||||
double det = XX * YY - XY * YX;
|
||||
|
||||
@@ -41,10 +41,10 @@ public sealed class ResourceMap
|
||||
return entry is { IsPlaceholder: false } && IsAudio(entry) ? entry : null;
|
||||
}
|
||||
|
||||
/// <summary>Resolve an already-normalized raw catalog id without applying a scene section base.</summary>
|
||||
/// <summary>Resolve an already-normalized packed catalog id without applying a scene section base.</summary>
|
||||
public AssetEntry? ResolveRawTexture(long rawId)
|
||||
{
|
||||
var entry = _catalog.ResolveRaw(rawId);
|
||||
var entry = _catalog.ResolvePacked(rawId);
|
||||
return entry is { IsPlaceholder: false } &&
|
||||
entry.Name.EndsWith(".AGF", StringComparison.OrdinalIgnoreCase) ? entry : null;
|
||||
}
|
||||
|
||||
@@ -1328,6 +1328,20 @@ public sealed class VirtualMachine
|
||||
_host.SetTexture(resolvedResourceId, (int)Read(a[1]));
|
||||
return pc + 1; // host still tracks dims for get-texture-size
|
||||
}
|
||||
case "u00422EB0": // pre-reference compatibility
|
||||
case "load-raw-texture-surface": // 0x249 (raw catalog id)(slot)(colorkey)
|
||||
{
|
||||
// Native shares 0x1f9's release/load/colorkey path, but constructs its mode-1
|
||||
// surface subclass and receives an already-global SYS4INI catalog index. The
|
||||
// CPU compositor does not need the D3D subclass distinction; it does need the
|
||||
// resource id to bypass the executing script's scene-section normalization.
|
||||
long rawResourceId = Read(a[0]);
|
||||
int surfaceSlot = (int)Read(a[1]);
|
||||
_host.ReleaseSurface(surfaceSlot);
|
||||
Gfx.SetSurface(surfaceSlot, rawResourceId, Read(a[2]));
|
||||
_host.SetTexture(rawResourceId, surfaceSlot);
|
||||
return pc + 1;
|
||||
}
|
||||
case "draw-texture": // 0x1fb (handle)(slot)(srcX)(srcY)(w)(h)(dstX)(dstY) — bind object -> surface + rect + pos
|
||||
Gfx.BindDraw(Read(a[0]), (int)Read(a[1]), (int)Read(a[2]), (int)Read(a[3]),
|
||||
(int)Read(a[4]), (int)Read(a[5]), (int)Read(a[6]), (int)Read(a[7]));
|
||||
@@ -1445,8 +1459,21 @@ public sealed class VirtualMachine
|
||||
|
||||
// ---- SC0000 anim/transform/spritesheet cluster (docs/engine-re.md §"SC0000 anim ... cluster") ----
|
||||
case "u00421DD0": // 0x22f set-position: (handle)(op2)(x)(y)(z) -> base position (direct set)
|
||||
case "u004219E0": // 0x229 set-position2: same shape, direct position
|
||||
Gfx.GetOrCreate(Read(a[0])).V24 = (Read(a[2]), Read(a[3]), Read(a[4])); return pc + 1;
|
||||
case "u004219E0": // pre-reference compatibility
|
||||
case "set-gfx-range-transform": // 0x229 (first)(count)(anchor x/y/z)
|
||||
Gfx.SetRangeTransform(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4])));
|
||||
return pc + 1;
|
||||
case "u00421A90": // pre-reference compatibility
|
||||
case "set-gfx-range-scale-current": // 0x22a (sx%)(sy%)(sz%)
|
||||
Gfx.SetRangeScaleCurrent((Read(a[0]), Read(a[1]), Read(a[2]))); return pc + 1;
|
||||
case "u00421BD0": // pre-reference compatibility
|
||||
case "set-gfx-range-translation-current": // 0x22c (tx)(ty)(tz)
|
||||
Gfx.SetRangeTranslationCurrent((Read(a[0]), Read(a[1]), Read(a[2]))); return pc + 1;
|
||||
case "u00421C60": // pre-reference compatibility
|
||||
case "set-gfx-range-scale-target": // 0x22d (delay)(duration)(sx%)(sy%)(sz%)
|
||||
Gfx.SetRangeScaleChannel(Read(a[0]), Read(a[1]), (Read(a[2]), Read(a[3]), Read(a[4])));
|
||||
return pc + 1;
|
||||
case "u004223C0": // 0x239 spritesheet cell: (handle)(delay)(duration)(frame count)(columns)(cell)
|
||||
Gfx.SetSrcRect(Read(a[0]), Read(a[3]), Read(a[4]), Read(a[5]), 0); return pc + 1;
|
||||
case "u00421EA0": // 0x231 looping spritesheet: (handle)(ms per frame)(frame count)(columns)
|
||||
|
||||
@@ -660,6 +660,8 @@ public partial class Main : Godot.Control
|
||||
var t = v.Transform;
|
||||
var affine = Age.Engine.Model.Transform2DMath.Build(t, v.Rotation);
|
||||
var localToDest = affine.FromLocalOrigin(v.DstX, v.DstY);
|
||||
if (v.RangeTransform is { } rangeTransform)
|
||||
localToDest = localToDest.Then(rangeTransform);
|
||||
var projected = localToDest.Apply(0, 0);
|
||||
int dstX = (int)System.Math.Round(projected.X);
|
||||
int dstY = (int)System.Math.Round(projected.Y);
|
||||
@@ -869,6 +871,8 @@ public partial class Main : Godot.Control
|
||||
if (source.Handle < transition.RangeBStart || source.Handle >= end || source.SurfaceTransition != null)
|
||||
continue;
|
||||
var affine = Transform2DMath.Build(source.Transform, source.Rotation).FromLocalOrigin(source.DstX, source.DstY);
|
||||
if (source.RangeTransform is { } rangeTransform)
|
||||
affine = affine.Then(rangeTransform);
|
||||
float opacity = source.Alpha / 255f * (float)transition.Progress;
|
||||
if (source.SurfaceResId == 0)
|
||||
{
|
||||
@@ -927,6 +931,10 @@ public partial class Main : Godot.Control
|
||||
Age.Engine.Model.Affine2D localToDest, float alpha = 1f, bool multiplyTint = false,
|
||||
bool dynamic = false, BlendKind blend = BlendKind.Alpha)
|
||||
{
|
||||
// Native gfx_object_blit_d3d9 clips the explicit source rectangle and returns without drawing when
|
||||
// right<=left or bottom<=top. FIELD deliberately creates zero-area prototype objects from SO005;
|
||||
// expanding those dimensions to the full texture leaks the entire spritesheet onto the map.
|
||||
if (w <= 0 || h <= 0) return;
|
||||
var cacheKey = (assetId, colorKey);
|
||||
int sourceWidth, sourceHeight;
|
||||
byte[] sourcePixels;
|
||||
@@ -961,8 +969,8 @@ public partial class Main : Godot.Control
|
||||
sourcePixels = cached.Rgba;
|
||||
}
|
||||
|
||||
int sw = w > 0 ? w : sourceWidth;
|
||||
int sh = h > 0 ? h : sourceHeight;
|
||||
int sw = w;
|
||||
int sh = h;
|
||||
sw = System.Math.Min(sw, sourceWidth - srcX);
|
||||
sh = System.Math.Min(sh, sourceHeight - srcY);
|
||||
if (sw <= 0 || sh <= 0) return;
|
||||
|
||||
@@ -5804,146 +5804,146 @@ observed_types = ["g-int", "l-int"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x229
|
||||
label = "u004219E0"
|
||||
label = "set-gfx-range-transform"
|
||||
argc = 5
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u004219E0"
|
||||
name = "set-gfx-range-transform"
|
||||
category = "draw"
|
||||
summary = "0x229 set-position2 (handle)(op2)(x)(y)(z): set object position/geometry directly (FUN_00472bb0/be0). C# VM: sets V24. See docs/engine-re.md §SC0000 anim cluster."
|
||||
summary = "(first_handle)(count)(anchor_x)(anchor_y)(anchor_z) — reset and select the retained-gfx range transform applied after each ordinary object matrix for handles in [first, first+count), then set its anchor/pivot. A zero count disables it."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x229_set_gfx_range_transform@0x423700 first calls gfx_range_transform_reset@0x472b80, then writes operands 1/2 to retained-gfx owner+0x420/+0x424 and operands 3..5 to the embedded transform object's anchor at owner+0x440..+0x448. gfx_object_composite@0x47f650 post-multiplies the sampled owner+0xb5b4 matrix only for handles in that selected range. Corpus: 693 calls/309 scripts; 590 disable with all zeroes, 101 select from handle 1 with a script-computed count, and FIELD/LOOK supply camera anchors. This supersedes the former incorrect per-object-position interpretation; per-object direct position is 0x22f."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "first affected object handle"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "affected handle count; zero disables"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "range-transform anchor X"
|
||||
observed_types = ["imm", "g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "range-transform anchor Y"
|
||||
observed_types = ["imm", "g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "range-transform anchor Z"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x22a
|
||||
label = "u00421A90"
|
||||
label = "set-gfx-range-scale-current"
|
||||
argc = 3
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00421A90"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-gfx-range-scale-current"
|
||||
category = "draw"
|
||||
summary = "(scale_x_percent)(scale_y_percent)(scale_z_percent) — immediately replace the selected retained-gfx range transform's current scale matrix."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x22a_set_gfx_range_scale_current@0x4237b0 divides all three operands by 100 and calls gfx_range_transform_set_scale_current@0x472c10, which builds owner+0x494. FIELD and LOOK each call it once after 0x229/0x22c; FIELD's zoom percent is G[0xccc09]."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "X scale percent"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "Y scale percent"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "Z scale percent"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x22c
|
||||
label = "u00421BD0"
|
||||
label = "set-gfx-range-translation-current"
|
||||
argc = 3
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00421BD0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-gfx-range-translation-current"
|
||||
category = "draw"
|
||||
summary = "(translate_x)(translate_y)(translate_z) — immediately replace the selected retained-gfx range transform's current translation matrix."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x22c_set_gfx_range_translation_current@0x423900 passes the three integer operands as floats to gfx_range_transform_set_translation_current@0x472d00, which builds owner+0x594. FIELD computes (400-camera_x, 300-camera_y, 0), making the selected map anchor land at screen center; LOOK uses the same camera helper."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "translation X"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "translation Y"
|
||||
observed_types = ["l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "translation Z"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x22d
|
||||
label = "u00421C60"
|
||||
label = "set-gfx-range-scale-target"
|
||||
argc = 5
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00421C60"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "set-gfx-range-scale-target"
|
||||
category = "draw"
|
||||
summary = "(delay_ms)(duration_ms)(scale_x_percent)(scale_y_percent)(scale_z_percent) — animate the selected retained-gfx range transform's scale from its current matrix to the target."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x22d_set_gfx_range_scale_target@0x423990 divides operands 3..5 by 100 and calls gfx_range_transform_set_scale_target@0x472d50. The worker arms the embedded transform object's ordinary scale channel (delay obj+0x3c, duration +0x50, target matrix +0xac), which gfx_range_transform_sample_frame@0x476df0 samples before range composition. FIELD has the sole corpus call, a 300 ms camera zoom."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "delay milliseconds"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "duration milliseconds"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "target X scale percent"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 4
|
||||
role = ""
|
||||
role = "target Y scale percent"
|
||||
observed_types = ["g-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 5
|
||||
role = ""
|
||||
role = "target Z scale percent"
|
||||
observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
@@ -6509,33 +6509,33 @@ observed_types = ["imm"]
|
||||
|
||||
[[opcode]]
|
||||
op = 0x249
|
||||
label = "u00422EB0"
|
||||
label = "load-raw-texture-surface"
|
||||
argc = 3
|
||||
abi_source = "kelebek+decode-validated"
|
||||
|
||||
[opcode.semantics]
|
||||
name = "u00422EB0"
|
||||
category = "unknown"
|
||||
summary = ""
|
||||
name = "load-raw-texture-surface"
|
||||
category = "draw"
|
||||
summary = "Load an AGF by universal packed SYS4INI/AAI catalog id into a retained surface slot using native surface mode 1 and the same RGB colorkey contract as set-texture (0x1f9)."
|
||||
noop_headless = false
|
||||
source = "kelebek"
|
||||
confidence = "low"
|
||||
source = "investigation"
|
||||
confidence = "high"
|
||||
depends_on = []
|
||||
evidence = ""
|
||||
evidence = "Ghidra /v2: op_0x249_load_raw_texture_surface@0x424b20 is instruction-length 7 and is contract-identical to gfx_op_0x1f9_load_surface through release, asset_open_indexed_entry, RGB colorkey conversion, load failure, and cleanup. Its mode-1 gfx_surface_mode1_ctor selects a tiled large-image wrapper: gfx_tiled_surface_create@0x432ff0 splits the logical dimensions into DAT_005b15b0-sized ordinary mode-0 child textures; gfx_tiled_surface_upload_agf@0x431a10 decodes and uploads each region; gfx_tiled_surface_blit@0x4316b0 subdivides a requested logical source rectangle across those tiles. It is not a spritesheet interpretation or alternate blend mode, so the port's contiguous CPU image is behaviorally equivalent. Corpus literals are universal raw indexes, including FIELD 0x32da..0x32dd -> SO005/SO007/SO008A/SO007A, and therefore bypass scene-section normalization."
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 1
|
||||
role = ""
|
||||
role = "universal packed SYS4INI/AAI catalog id"
|
||||
observed_types = ["imm", "l-int", "l-ptr"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 2
|
||||
role = ""
|
||||
role = "surface slot"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode.semantics.args]]
|
||||
i = 3
|
||||
role = ""
|
||||
role = "RGB888 colorkey; negative disables colorkey"
|
||||
observed_types = ["imm", "l-int"]
|
||||
|
||||
[[opcode]]
|
||||
|
||||
Reference in New Issue
Block a user