From 5b47872365cb3db2e4a61dd1b749966c194410c4 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Tue, 28 Jul 2026 14:16:24 -0400 Subject: [PATCH] Implement native text face selection --- docs/engine-ctx-reference.md | 11 +++ docs/engine-re.md | 61 ++++++++++++++ docs/opcode-reference.md | 21 +++-- docs/phase-a-slice-plan.md | 14 ++++ docs/platform-portability.md | 2 +- engine/Age.Engine.Tests/AdvTextOpsTests.cs | 26 ++++++ .../HistoryPresentationOpsTests.cs | 6 +- engine/Age.Engine/Model/AdvTextHistory.cs | 5 +- engine/Age.Engine/Vm/VirtualMachine.cs | 1 + godot/Main.cs | 79 ++++++++++++++++--- vm-map/engine-ctx.toml | 55 +++++++++++++ vm-map/opcodes.toml | 22 +++--- 12 files changed, 270 insertions(+), 33 deletions(-) diff --git a/docs/engine-ctx-reference.md b/docs/engine-ctx-reference.md index ba1af23..fbd89f8 100644 --- a/docs/engine-ctx-reference.md +++ b/docs/engine-ctx-reference.md @@ -19,7 +19,17 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x144e0` | `sfx_channel_resource_ids` | `int` | base of ten packed resource ids retained by the sound-effect facade; op 0xb4 loads a channel, op 0xb6 clears it, and numbered-save layouts restore then reopen every positive id | | `0x14d54` | `adv_text_layout_ptr_table` | `void*` | text_manager at ctx+0x14940 plus layout pointer table +0x414; ops 0x212/0x213 configure each layout's wait-indicator handle and retained glyph-object range at layout+0x64/+0x68/+0x6c | | `0x14e08` | `gfx_default_object_slot` | `int` | op 0x80 selected slot; op 0x1d9 substitutes it when its explicit object-slot operand is zero | +| `0x14e0c` | `text_primary_font_height` | `int` | primary LOGFONTA.lfHeight at text-manager+0x4cc; stored as a negative pixel height and updated by op 0x75 | +| `0x14e10` | `text_primary_font_width` | `int` | primary LOGFONTA.lfWidth at text-manager+0x4d0; op 0x75 stores negative half the requested font size | +| `0x14e1c` | `text_primary_font_weight` | `int` | primary LOGFONTA.lfWeight at text-manager+0x4dc; op 0x2bd writes 700 for enabled or zero for disabled | +| `0x14e84` | `text_antialias_enabled` | `int` | text-manager+0x544; message:UseAntiFont selects GGO_GRAY4_BITMAP when nonzero and GGO_BITMAP when zero | +| `0x14e88` | `text_antialias_version` | `int` | text-manager+0x548 loaded from set:AntiFontVersion; Himegari SYS4INI supplies version 3 | +| `0x14e8c` | `text_primary_color_bgr` | `uint` | text-manager+0x54c internal GDI BGR/COLORREF primary glyph color; op 0x76 converts script RGB into it | +| `0x14e90` | `text_effect_color_bgr` | `uint` | text-manager+0x550 internal GDI BGR/COLORREF effect glyph color; op 0x77 converts script RGB into it | +| `0x14e98` | `text_render_mode` | `int` | text-manager+0x558; op 0x78 selects primary-only, displaced shadow, reduced-coverage repeat, or sampled ellipse outline | | `0x14ea0` | `text_line_spacing` | `int` | extra pixel leading between text lines; defaults to 6, op 0x8b writes it | +| `0x14ea4` | `text_effect_offset_x` | `int` | text-manager+0x564; op 0x1a4 horizontal displacement for mode 1 or ellipse radius for mode 3 | +| `0x14ea8` | `text_effect_offset_y` | `int` | text-manager+0x568; op 0x1a4 vertical displacement for mode 1 or ellipse radius for mode 3 | | `0x14f45` | `script_frame_index` | `int` | call-script frame index (0x1e-dword frames) | | `0x46188` | `renderer_device_state` | `int` | embedded D3D9 device/render state; op 0x20e passes this base to the target/depth clear worker | | `0x46598` | `d3d9_device` | `void*` | IDirect3DDevice9 pointer at renderer_device_state+0x410; op 0x20e invokes vtable Clear +0xac | @@ -94,6 +104,7 @@ Struct `EngineCtx`, size `0xa1000`. Applied to the Ghidra `/v2` image (dispatch- | `0x6f8b4` | `script_launch_queue_high_water` | `int` | highest consumed cursor retained across FIFO compaction | | `0x6f8b8` | `script_launch_dispatch_active` | `int` | suppresses immediate dispatch while op 0x143 batches mounted append record-zero ids and while a queued script is active | | `0x9928c` | `save_frame_boundary_index` | `int` | highest script-frame index included by numbered-save layouts 2/3; -1 falls back to cur_ctx_index; op 0x1ad marks current frame and op 0x2 clears after unwinding below it | +| `0x99290` | `gfx_draw_mode` | `int` | loaded from set:DrawMode during runtime initialization; Himegari uses mode 1, selecting the cached GetGlyphOutline text path | | `0x9b24c` | `dispatch_table` | `void*` | opcode->handler table base [0x400]; handler(op) = *(0x9b24c + op*4) | | `0x9c658` | `sys4ini_count` | `int` | SYS4INI record count at embedded FileDB+0x40c | | `0x9c65c` | `archive_name_table` | `void*` | archive-name table base at embedded FileDB+0x410; arc_id*0x100 indexes it | diff --git a/docs/engine-re.md b/docs/engine-re.md index f99c33f..253a3b6 100644 --- a/docs/engine-re.md +++ b/docs/engine-re.md @@ -2453,6 +2453,67 @@ Matching evidence: `build/native-adv-text-trace.jsonl` and Godot timeline captur non-overlapping name/dialogue bands at y=447–468, 478–500, and 507–530. A manual run progressed 14 pages: 11 clicks completed active reveals and 14 later clicks released 14 distinct waits through `0xe0c`. +#### Native font face, weight, antialiasing, and effects (2026-07-28) + +The earlier retained-text work established *where* glyphs appear but deliberately left their final raster +appearance to Godot `Label`. Static reconstruction now identifies the complete native style selected by +Himegari and the specific reasons the port's text looks only approximate. + +Himegari's decompressed SYS4INI settings trailer supplies `FONT=MS 明朝`, `DRAWMODE=1`, +`ENABLEANTIFONT=1`, and `ANTIFONTVERSION=3`. `engine_initialize_subsystems_from_settings@0x415390` +passes the face to `adv_text_manager_initialize@0x456800`, stores draw mode 1 at `ctx+0x99290`, applies +the enabled `message:UseAntiFont` default of 1 through `text_set_antialias_enabled@0x4137e0`, and stores +AntiFont version 3 at text-manager `+0x548`. The ordinary primary `LOGFONTA` begins as: + +- `lfHeight=-24`, `lfWidth=-12`, `lfWeight=0`; +- no escapement/orientation, italic, underline, or strikeout; +- `DEFAULT_CHARSET`, default output/clip precision, default quality, and default pitch/family; +- `lfFaceName="MS 明朝"`. + +Size opcode `0x75` keeps the negative pixel height and a negative half-width in both primary LOGFONT +states. It rounds ordinary odd requested heights down by one and special-cases 32/33 to 31, although every +Himegari call uses an even size from 8 through 24. Bold opcode `0x2bd` sets `lfWeight` to exactly 700, not +an arbitrary stroke expansion. Face opcode `0x1a5` is also effectful: +`op_0x1a5_set_font_face@0x42bbf0` calls `text_set_primary_font_face@0x42b1d0`, which copies the requested +face and constructs an `@%s` vertical-writing companion before +`text_rebuild_primary_font_resources@0x44f7f0` recreates the `CreateFontIndirectA` handles, metrics, and +glyph buffers. The 207 corpus calls use only `MS 明朝` (168) and `MS ゴシック` (39). + +With Himegari's DrawMode 1 and AntiFont enabled, `GetGlyphOutlineA` requests +`GGO_GRAY4_BITMAP` with an identity `MAT2`. `text_raster_string_cached@0x45b600` caches the returned +0..16 coverage bitmap and composites it into the locked 16- or 32-bit destination with ordinary +source-over color/alpha interpolation. Layout measurement separately uses `GetTextExtentPoint32A` against +the selected GDI font, so face, explicit `lfWidth`, and weight affect advances and wrapping as well as +pixels. + +The shipped effect profiles use only modes 1 and 3: + +- mode 1 draws one effect-color copy at `(x+effect_x,y+effect_y)`, then the primary glyph; all 43 + Himegari sites pair it with `(0,0)`; +- mode 3 samples an ellipse around the glyph. `text_compute_outline_ellipse_offset@0x459860` computes + `cos(angle)*effect_x` / `sin(angle)*effect_y`; the caller steps by + `360 / (sqrt(effect_x²+effect_y²)*8)`, blits the effect-colored glyph at every rounded offset, then + draws the primary glyph. All 164 paired sites use `(1,1)`, producing the normal one-pixel outline. + +The ordinary ADV preset is therefore 24-pixel `MS 明朝`, weight 700, white fill, mode-3 `(1,1)` outline +in `0x606060`, with 8 pixels of line leading. The small menu-description preset is 16-pixel +`MS ゴシック`, weight 0, white fill, mode 1 with zero displacement, and 9 pixels of leading. + +The port now carries opcode `0x1a5` through `AdvTextStyle` into live, retained, History, and surface-text +presentation. `godot/Main.cs` recognizes Himegari's two requested families and loads +`C:/Windows/Fonts/msmincho.ttc` or `msgothic.ttc` when present. Missing or unknown requested faces fall +back to the existing best-effort Japanese font probe, and regular/bold variants are cached per resolved +face so a runtime face switch cannot reuse the preceding family's synthetic-bold font. This restores the +important Mincho/Gothic distinction on a normal Windows installation without treating the proprietary +fonts as game assets. + +The remaining raster behavior is still an approximation: weight 700 uses Godot +`VariationEmbolden=1.2`, and every nonzero render mode becomes a symmetric Godot outline. Mode 1 is +therefore modeled as an outline rather than its native displaced-copy operation. Exact GDI-compatible +advances, grayscale coverage, and mode-3 edge pixels require a dedicated glyph backend (or a deliberately +calibrated approximation) behind the existing engine-owned layout/style contract. Portable configurable +face substitutions/defaults are intentionally deferred to the broader runtime configuration design. + #### ADV wait indicator -- ops `0x73` / `0x72` (2026-07-11) The small bat marker is a configured ADV-layout sprite, not a glyph or part of SO001. `SYSTEM4.BIN` diff --git a/docs/opcode-reference.md b/docs/opcode-reference.md index 76f3b5c..eefeb76 100644 --- a/docs/opcode-reference.md +++ b/docs/opcode-reference.md @@ -45,7 +45,7 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que - **summary:** (pixels) - set the primary text font height and rebuild its native rasterization state. - **grounding:** source=investigation, confidence=high - **depended on by:** 0x205 -- **evidence:** Ghidra /v2: op_0x75_handler@0x41ea10 calls text_set_primary_font_size@0x415bd0 on ctx+0x14940. The worker writes the negated pixel height to both primary LOGFONT states, derives width, and rebuilds font resources. HISTORY.BIN selects 22 pixels while active and restores 24 on exit. +- **evidence:** Ghidra /v2: op_0x75_set_font_size@0x41ea10 calls text_set_primary_font_size@0x415bd0 on ctx+0x14940. The worker writes the negated pixel height to both primary LOGFONT states, sets lfWidth to negative half the requested size, applies AGE's odd/32/33-pixel height adjustment, and rebuilds both GDI font resources plus glyph buffers. All shipped Himegari sizes are even and avoid the adjustment; HISTORY.BIN selects 22 pixels while active and restores 24 on exit. ### 0x76 `set-text-color` (set-text-color, argc 1) - **summary:** (rgb) - set the current primary text color. @@ -60,7 +60,9 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que ### 0x78 `set-text-render-mode` (set-text-render-mode, argc 1) - **summary:** (mode) - select the current text raster/effect mode. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x78_handler@0x41eb10 writes operand 1 to text-manager+0x558 and rebuilds font state. Direct draw and text_history_render_records branch on this field (including a distinct mode-3 offset path). HISTORY.BIN selects mode 3 and restores mode 3 with the normal ADV font preset. +- **evidence:** Ghidra /v2: op_0x78_set_text_render_mode@0x41eb10 writes operand 1 to text-manager+0x558 and rebuilds font state. Himegari's DrawMode=1 path reaches text_raster_string_cached@0x45b600: mode 1 blits effect color at (+x,+y); mode 2 repeats a quarter-coverage glyph bitmap in primary color; mode 3 samples text_compute_outline_ellipse_offset@0x459860 around 360 degrees before the primary blit. The corpus uses only mode 1 (43 sites) and mode 3 (165 sites). + +Mode 0 draws only the primary glyph. Mode 1 draws one effect-colored copy displaced by the configured x/y offset, then the primary glyph. Mode 2 adds a reduced-coverage primary-color bitmap pass after the ordinary glyph and is unused by Himegari scripts. Mode 3 repeatedly draws the effect glyph around an ellipse whose radii are the configured x/y extents, then draws the primary glyph; Himegari's (1,1) profile produces its one-pixel outline. ### 0x79 `set-adv-text-reset-cursor` (set-adv-text-reset-cursor, argc 3) - **summary:** (layout_slot)(x)(y) - configure the cursor coordinates restored by a later reset of the selected ADV text layout. Slot 0 selects the current layout. This does not move the live cursor immediately; opcode 0x7a does that. @@ -101,7 +103,14 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que ### 0x1a4 `set-text-effect-offset` (set-text-effect-offset, argc 2) - **summary:** (x)(y) - set the horizontal and vertical extent/offset used by the current text effect mode. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x1a4_handler@0x41f440 writes operands 1/2 to text-manager+0x564/+0x568. Native raster/bounds paths expand or shift text by these values when an effect mode is active; mode 3 subtracts both. HISTORY.BIN uses (1,1). +- **evidence:** Ghidra /v2: op_0x1a4_set_text_effect_offset@0x41f440 writes operands 1/2 to text-manager+0x564/+0x568. Mode 1 uses them as a single displaced effect-color shadow. Mode 3 treats them as ellipse radii and samples x=cos(angle)*offset_x, y=sin(angle)*offset_y around the glyph before the primary draw. The Himegari corpus pairs mode 1 with (0,0) at all 43 sites and mode 3 with (1,1) at all 164 paired sites. + +### 0x1a5 `set-font` (set-font, argc 1) +- **summary:** (face) - select the primary GDI font face, derive its vertical-writing companion, and rebuild the native font/glyph state. +- **grounding:** source=investigation, confidence=high +- **evidence:** Ghidra /v2: op_0x1a5_set_font_face@0x42bbf0 resolves operand 1 and calls text_set_primary_font_face@0x42b1d0 on ctx+0x14940. The worker copies the face to manager+0x4e8, formats @%s into the vertical LOGFONT state at +0x18e74, rebuilds CreateFontIndirectA handles/metrics/glyph buffers through text_rebuild_primary_font_resources@0x44f7f0, and maintains both names in its font-family caches. SYS4INI seeds FONT=MS 明朝. + +The requested CP932 face is copied into the primary LOGFONT lfFaceName and AGE also constructs an @-prefixed vertical face. Himegari uses only the full-width Windows family names MS 明朝 (168 sites) and MS ゴシック (39 sites). This is live raster state, not a declaration/no-op. ### 0x1b5 `set-message-glyph-delay` (u0041B5F0, argc 1) - **summary:** (milliseconds) - set the per-glyph ADV text reveal delay; zero makes retained UI text publish immediately. @@ -173,7 +182,7 @@ Port status (2026-07-24): after the blocking host releases this wait, the VM que ### 0x2bd `set-font-bold` (set-font-bold, argc 1) - **summary:** (enabled) - set the current primary text font weight to 700 when enabled or 0 when disabled, then rebuild the native font state. - **grounding:** source=investigation, confidence=high -- **evidence:** Ghidra /v2: op_0x2bd_handler@0x4251c0 writes enabled?700:0 to LOGFONT weight at text-manager+0x4dc and calls the font rebuild worker. It immediately follows set-font throughout the UI corpus; HISTORY.BIN selects the Mincho face with bold enabled. +- **evidence:** Ghidra /v2: op_0x2bd_set_font_bold@0x4251c0 writes enabled?700:0 to LOGFONT weight at text-manager+0x4dc and calls text_rebuild_primary_font_resources@0x44f7f0. It immediately follows set-font throughout the UI corpus; HISTORY.BIN selects the Mincho face with bold enabled. ## audio @@ -1339,10 +1348,6 @@ Port status (2026-07-24): implemented through the same profile-lifetime setting - **summary:** — - **grounding:** source=kelebek, confidence=med -### 0x1a5 `set-font` (set-font, argc 1) -- **summary:** — -- **grounding:** source=kelebek, confidence=med - ### 0x1a7 `comment` (comment, argc 1) - **summary:** — - **grounding:** source=kelebek, confidence=med diff --git a/docs/phase-a-slice-plan.md b/docs/phase-a-slice-plan.md index fb12605..c07a9a4 100644 --- a/docs/phase-a-slice-plan.md +++ b/docs/phase-a-slice-plan.md @@ -3329,6 +3329,20 @@ shared-profile registration. `G[0x3ebe]` (21 references across CALCREVISE, CHMENU, DRAWTIP, GAMECLEAR, GAMESTART, IMPROVE, and TUNE). +## ADV requested-face selection implemented (2026-07-28) + +Opcode `0x1a5` is no longer a presentation no-op. The VM retains its requested face in +`AdvTextStyle`, so live text, retained backlog records, History replay, and surface strings all preserve +the active family. Godot resolves Himegari's two authored requests (`MS 明朝` and `MS ゴシック`) to +the corresponding Windows TTC files and caches regular/bold variants independently. If a requested face +is unavailable or unknown, presentation falls back to the existing best-effort Japanese font rather than +dropping the text or inventing a bundled font dependency. + +Configurable defaults and per-face substitutions remain deferred to the general runtime configuration +design, especially for Linux/macOS where the proprietary Microsoft faces are not normally installed. +Focused regressions cover face changes flowing into both live and retained styles; the engine tests and +Godot C# build pass. User visual acceptance passed on 2026-07-28. + ## Persistence native-format reconnaissance complete (2026-07-24) The deferred save/profile ownership question now has a compatibility-mode answer. The remaining native diff --git a/docs/platform-portability.md b/docs/platform-portability.md index 8dd0580..eec5924 100644 --- a/docs/platform-portability.md +++ b/docs/platform-portability.md @@ -31,7 +31,7 @@ or replaced before claiming portable exports. | AGE movie decode (`0x236` scene movies; `0x20f` modal LOGO/OP/ED) | `FfmpegMovieDecoder` is the sole factory over the project-owned `native/age_movie_ffmpeg` ABI | Windows-x64 passes the complete 213-payload installed video/audio corpus gate plus audible LOGO/OP/CHAPTER playback | Add target-specific native builds and export packaging | | Movie integration | Each surface owns a unique playback-instance id; `MovieRuntime` owns `IMovieDecoder` from an injected factory; video-only streams use monotonic pacing while audio-bearing streams use the Godot output clock | Concurrent/restarted uses of one asset have independent frame/audio/completion/teardown state; managed code is no longer Windows-annotated, while only the win-x64 native bundle exists today | Add Linux/macOS native builds and smoke gates | | Movie audio | ABI v2 returns timestamped stereo float PCM; bounded managed buffering feeds a per-playback Godot `AudioStreamGenerator` and routes native movie flags to engine buses | All 29 installed audio-bearing streams decode with signal; synchronized LOGO/OP/CHAPTER playback is audibly accepted | Treat absent, distorted, or unsynchronized audio from an audio-bearing movie as a runtime bug | -| ADV font discovery | `godot/Main.cs` probes `C:/Windows/Fonts` for Japanese fonts | Harmless fallback today, but appearance depends on host fonts | Bundle/configure a redistributable font or add platform-specific discovery | +| ADV font discovery/raster fidelity | Opcode `0x1a5` now reaches presentation; `godot/Main.cs` loads Windows `MS 明朝`/`MS ゴシック` from their known TTC files when available, otherwise uses the existing Japanese-font/default fallback; Godot still supplies shaping and outline synthesis | A normal Windows install now preserves Himegari's Mincho/Gothic distinction and face-specific bold caches; Linux/macOS normally lack these proprietary fonts, while advances, wrapping, boldness, and outline coverage still depend on the substitute/backend | Add configurable default and requested-face substitutions as part of the broader runtime configuration design; for portable distribution bundle licensed metric-compatible faces or define profile substitutions, then decide whether exact AGE grayscale/effect rasterization warrants a custom glyph backend | | Filesystem semantics | Several filename and containment comparisons use `OrdinalIgnoreCase`; installed assets are conventionally uppercase | Needs validation on case-sensitive filesystems; may hide casing or containment mistakes | Add Linux/macOS tests with mixed-case synthetic roots and use filesystem-appropriate containment rules | | Save/profile storage | Managed `DirectoryNativeDatStore` under Godot `user://SAVE`; native S3SD/S4SD/S3RT files and 24-bit BMP thumbnails | No Win32 path API at runtime; port saves remain isolated from the original installation | Validate replace/flush, case, permissions, and interrupted-write behavior on each export target | | Install/repository discovery | `engine/Age.Engine/Sys4/Paths.cs` finds `age-reimpl` above `AppContext.BaseDirectory` and assumes the current workspace sibling layout | Suitable for development, not packaged exports on any OS | Replace runtime discovery with a user-selected game root/profile; retain repository paths only for developer tools/tests | diff --git a/engine/Age.Engine.Tests/AdvTextOpsTests.cs b/engine/Age.Engine.Tests/AdvTextOpsTests.cs index cadb61a..42cbfa2 100644 --- a/engine/Age.Engine.Tests/AdvTextOpsTests.cs +++ b/engine/Age.Engine.Tests/AdvTextOpsTests.cs @@ -9,6 +9,32 @@ using Xunit; public class AdvTextOpsTests { + [Fact] + public void SetFontFaceFlowsIntoLiveAndRetainedTextStyles() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + static Operand I(long value) => new(0, value); + static Operand S(int index) => new(2, index); + var script = ScriptAssembler.Assemble(table, "ADV_FONT_FACE", + new List<(int, Operand[])> + { + (0x1a5, new[] { S(0) }), + (0x6e, new[] { I(0), S(2) }), + (0x1a5, new[] { S(1) }), + (0x6e, new[] { I(0), S(3) }), + (0x2, Array.Empty()), + }, new[] { "MS 明朝", "MS ゴシック", "mincho", "gothic" }); + var host = new RecordingHost(); + var vm = new VirtualMachine(script, table, host); + + vm.Run(); + + Assert.Equal(new[] { "MS 明朝", "MS ゴシック" }, + host.LiveTextRuns.Select(run => run.Run.Style.FontFace)); + Assert.Equal(new[] { "MS 明朝", "MS ゴシック" }, + vm.TextHistory.Records.Select(record => record.Style.FontFace)); + } + [Fact] public void TextCursorAndDrawStringReachHostWithLocalStringPointer() { diff --git a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs index 500909e..fbc2eab 100644 --- a/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs +++ b/engine/Age.Engine.Tests/HistoryPresentationOpsTests.cs @@ -17,7 +17,7 @@ public class HistoryPresentationOpsTests history.DefineLayout(1, 640, 160, 80, 430); history.AppendMetadata(9, 2, AdvTextStyle.Default); history.AppendText(0, 10, "retained dialogue", new AdvTextStyle(24, 8, false, - 0xffffff, 0x606060, 3, 1, 1, 9)); + 0xffffff, 0x606060, 3, 1, 1, 9, "MS 明朝")); history.ResetLayout(1); history.AppendText(0, 11, "next group", AdvTextStyle.Default); history.SetRecordingEnabled(false); @@ -42,9 +42,9 @@ public class HistoryPresentationOpsTests Assert.Equal((4, 0, "retained dialogue"), (render.LayoutSlot, render.FirstRecordIndex, render.Text)); Assert.Equal(new AdvTextLayoutSnapshot(4, 600, 150, 65, 150, 45, 42, 600, 150), render.Layout); - Assert.Equal((24, 0xffffffL, 0x606060L, 8), + Assert.Equal((24, 0xffffffL, 0x606060L, 8, "MS 明朝"), (render.Style.PrimaryFontSize, render.Style.TextColor, render.Style.EffectColor, - render.Style.LineSpacing)); + render.Style.LineSpacing, render.Style.FontFace)); } [Fact] diff --git a/engine/Age.Engine/Model/AdvTextHistory.cs b/engine/Age.Engine/Model/AdvTextHistory.cs index f069a36..f9a8041 100644 --- a/engine/Age.Engine/Model/AdvTextHistory.cs +++ b/engine/Age.Engine/Model/AdvTextHistory.cs @@ -27,9 +27,10 @@ public readonly record struct AdvTextStyle( int RenderMode, int EffectOffsetX, int EffectOffsetY, - int LineSpacing) + int LineSpacing, + string FontFace) { - public static AdvTextStyle Default => new(0, 0, false, 0, 0, 0, 0, 0, 6); + public static AdvTextStyle Default => new(0, 0, false, 0, 0, 0, 0, 0, 6, ""); } /// A stable snapshot of the layout state associated with a retained record. diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 8e2519a..b16d8f7 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -2269,6 +2269,7 @@ public sealed class VirtualMachine case "end-text-line": TextHistory.EndLine((int)Read(a[0]), _advTextStyle); return pc + 1; case "set-font": + _advTextStyle = _advTextStyle with { FontFace = ReadStr(a[0]) }; return pc + 1; case "comment": case "display-furigana": case "dev_ukn": return pc + 1; case "create-texture": // 0x1f8 (slot)(w)(h) — allocate a blank surface at the slot diff --git a/godot/Main.cs b/godot/Main.cs index 4dc5415..cc356d9 100644 --- a/godot/Main.cs +++ b/godot/Main.cs @@ -34,8 +34,13 @@ public partial class Main : Godot.Control private readonly System.Collections.Generic.List