fix #213: complete suicide and framerate presentation
Translate suicide response 0x004A as retail's successful self-kill notice. Port /framerate onto the authored SmartBox FPS element with live two-decimal FPS and DEG values, keep diagnostic window chrome independent, and synchronize command-driven settings without discarding panel drafts. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
9ea579bdd0
commit
43f7c7807c
17 changed files with 729 additions and 39 deletions
|
|
@ -48,7 +48,7 @@ Copy this block when adding a new issue:
|
||||||
|
|
||||||
## #213 — Retail client commands were sent to ACE as chat text
|
## #213 — Retail client commands were sent to ACE as chat text
|
||||||
|
|
||||||
**Status:** IN-PROGRESS — full named-retail command-family port complete 2026-07-13, pending live gate
|
**Status:** IN-PROGRESS — command-family gate passed except two corrective fixes, pending re-gate
|
||||||
**Severity:** MEDIUM
|
**Severity:** MEDIUM
|
||||||
**Component:** retained UI / chat commands / net
|
**Component:** retained UI / chat commands / net
|
||||||
|
|
||||||
|
|
@ -74,6 +74,15 @@ dialog, and unknown `/` commands still publish `SendServerCommandCmd` in
|
||||||
canonical `@` form so ACE administrator commands continue to work. Both chat
|
canonical `@` form so ACE administrator commands continue to work. Both chat
|
||||||
backends use the same router.
|
backends use the same router.
|
||||||
|
|
||||||
|
**Corrective pass (2026-07-13):** The first connected gate passed all other
|
||||||
|
representative commands and exposed two presentation gaps. Suicide success
|
||||||
|
arrives as WeenieError `0x004A`; it now resolves to retail's informational
|
||||||
|
"Ack! You killed yourself!" instead of the raw numeric fallback. `/framerate`
|
||||||
|
now mounts and toggles SmartBox LayoutDesc `0x2100000F` element `0x10000047`,
|
||||||
|
showing the live `FPS` and `DEG` values with the authored font and two-decimal
|
||||||
|
format. `/framrate` remains unknown because retail registers no misspelled
|
||||||
|
alias.
|
||||||
|
|
||||||
**Research:**
|
**Research:**
|
||||||
`docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`;
|
`docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`;
|
||||||
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`;
|
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`;
|
||||||
|
|
@ -85,7 +94,9 @@ backends use the same router.
|
||||||
server-backed commands execute without appearing as speech: `/loc`,
|
server-backed commands execute without appearing as speech: `/loc`,
|
||||||
`/framerate`, `/saveui test`, `/loadui test`, `/age`, `/friends`, `/afk on`,
|
`/framerate`, `/saveui test`, `/loadui test`, `/age`, `/friends`, `/afk on`,
|
||||||
and `/marketplace`. `/ls now` shows local usage and sends nothing. An ACE
|
and `/marketplace`. `/ls now` shows local usage and sends nothing. An ACE
|
||||||
command such as `/ci 629 1` still works.
|
command such as `/ci 629 1` still works. `/die` ends with the retail success
|
||||||
|
text rather than a raw error code, and exact `/framerate` toggles the two-line
|
||||||
|
in-world SmartBox readout.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -501,7 +501,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
||||||
- **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
|
- **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
|
||||||
- **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
|
- **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
|
||||||
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
||||||
- **Retail client command families implemented 2026-07-13; live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets, and confirmation requests mount imported LayoutDesc `0x2100003C`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
|
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets, and confirmation requests mount imported LayoutDesc `0x2100003C`. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
|
||||||
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
|
||||||
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
|
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
|
||||||
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
|
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
|
||||||
|
|
|
||||||
|
|
@ -484,7 +484,7 @@ include dungeons.
|
||||||
automatic candidates to hostile non-player monsters; live gate passed
|
automatic candidates to hostile non-player monsters; live gate passed
|
||||||
2026-07-12. See
|
2026-07-12. See
|
||||||
`docs/research/2026-07-12-death-and-auto-target-pseudocode.md`.
|
`docs/research/2026-07-12-death-and-auto-target-pseudocode.md`.
|
||||||
- **Retail client command families (implemented 2026-07-13; live gate pending)** —
|
- **Retail client command families (implemented 2026-07-13; corrective live gate pending)** —
|
||||||
both chat surfaces now route a named-decomp command catalog into an App-layer
|
both chat surfaces now route a named-decomp command catalog into an App-layer
|
||||||
controller, keeping ACE administrator verbs on the explicit server-command
|
controller, keeping ACE administrator verbs on the explicit server-command
|
||||||
path. The implemented set covers travel, character/time queries, local UI and
|
path. The implemented set covers travel, character/time queries, local UI and
|
||||||
|
|
@ -492,6 +492,9 @@ include dungeons.
|
||||||
friends, squelch/filter, and fill-components, with exact Core.Net messages and
|
friends, squelch/filter, and fill-components, with exact Core.Net messages and
|
||||||
authoritative social-state parsing. See
|
authoritative social-state parsing. See
|
||||||
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
|
`docs/research/2026-07-13-retail-client-command-families-pseudocode.md`.
|
||||||
|
The first connected gate passed the family except for raw suicide success
|
||||||
|
code `0x004A` and the incomplete title-bar-only FPS toggle; the corrective
|
||||||
|
pass maps retail's success text and mounts the authored SmartBox FPS element.
|
||||||
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
|
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
|
||||||
|
|
||||||
**Freeze on landing:**
|
**Freeze on landing:**
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ cross-check.
|
||||||
- `ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0`
|
- `ClientCommunicationSystem::DoMarketplace @ 0x0056FCE0`
|
||||||
- `ClientCommunicationSystem::DoLockUI @ 0x005703B0`
|
- `ClientCommunicationSystem::DoLockUI @ 0x005703B0`
|
||||||
- `ClientCommunicationSystem::DoFrameRate @ 0x005707D0`
|
- `ClientCommunicationSystem::DoFrameRate @ 0x005707D0`
|
||||||
|
- `gmSmartBoxUI::UpdateFPSMeter @ 0x004D63A0`
|
||||||
|
- `gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0`
|
||||||
|
- `gmSmartBoxUI::UseTime @ 0x004D6E30`
|
||||||
|
- `ClientCommunicationSystem::DoDie @ 0x00580050`
|
||||||
|
- `ClientCommunicationSystem::DieDialogCallback @ 0x0057BA70`
|
||||||
|
- `CM_Character::Event_Suicide @ 0x006A1B00`
|
||||||
- `ClientCommunicationSystem::DoPKArena @ 0x005788D0`
|
- `ClientCommunicationSystem::DoPKArena @ 0x005788D0`
|
||||||
- `ClientCommunicationSystem::DoPKLArena @ 0x005789D0`
|
- `ClientCommunicationSystem::DoPKLArena @ 0x005789D0`
|
||||||
- `ClientCommunicationSystem::DoAge @ 0x0057C5A0`
|
- `ClientCommunicationSystem::DoAge @ 0x0057C5A0`
|
||||||
|
|
@ -104,7 +110,18 @@ framerate(arguments):
|
||||||
if arguments: display local usage
|
if arguments: display local usage
|
||||||
else:
|
else:
|
||||||
show_framerate = !show_framerate
|
show_framerate = !show_framerate
|
||||||
notify UI so the display changes immediately
|
notify gmSmartBoxUI
|
||||||
|
if enabled:
|
||||||
|
resolve UIElement_Text 0x10000047 from LayoutDesc 0x2100000F
|
||||||
|
show it and update it immediately
|
||||||
|
else:
|
||||||
|
hide it and clear the active FPS-display pointer
|
||||||
|
|
||||||
|
smartbox_use_time():
|
||||||
|
if the FPS-display pointer is active:
|
||||||
|
update the display with two-precision values:
|
||||||
|
"FPS: {Render.GetFramerate():F2}"
|
||||||
|
"DEG: {active_degrade_multiplier:F2}"
|
||||||
|
|
||||||
lockui(arguments):
|
lockui(arguments):
|
||||||
if arguments: display local usage
|
if arguments: display local usage
|
||||||
|
|
@ -113,6 +130,29 @@ lockui(arguments):
|
||||||
broadcast the UI-lock state change
|
broadcast the UI-lock state change
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The two-line template is portal string-table entry `0x0DCFFF73` in table
|
||||||
|
`0x23000001`; its authored text fragments are `FPS: ` and `\nDEG: `. The
|
||||||
|
display is the exact 128×36 SmartBox text element at `(1,1)`, using font
|
||||||
|
`0x4000001A`. Retail registers only the exact command name `/framerate`;
|
||||||
|
`/framrate` is not an alias.
|
||||||
|
|
||||||
|
```text
|
||||||
|
die(arguments):
|
||||||
|
if arguments: display local usage
|
||||||
|
else:
|
||||||
|
show the authored local confirmation dialog
|
||||||
|
if accepted: send parameterless game action 0x0279
|
||||||
|
|
||||||
|
suicide_response(weenie_error):
|
||||||
|
if code == 0x004A:
|
||||||
|
display "Ack! You killed yourself!"
|
||||||
|
```
|
||||||
|
|
||||||
|
`0x004A` is an informational success response, despite arriving through the
|
||||||
|
WeenieError message family. Retail's own error-string table contains that exact
|
||||||
|
text; exposing the numeric fallback makes a successful `/die` look like a
|
||||||
|
failure.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
query_age_response(name, duration):
|
query_age_response(name, duration):
|
||||||
if name is empty: display "You have played for {duration}."
|
if name is empty: display "You have played for {duration}."
|
||||||
|
|
|
||||||
|
|
@ -2150,6 +2150,15 @@ public sealed class GameWindow : IDisposable
|
||||||
SetRetailCombatGameplay),
|
SetRetailCombatGameplay),
|
||||||
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
JumpPowerbar: new AcDream.App.UI.JumpPowerbarRuntimeBindings(
|
||||||
() => _playerController?.JumpCharge ?? default),
|
() => _playerController?.JumpCharge ?? default),
|
||||||
|
Fps: new AcDream.App.UI.FpsRuntimeBindings(
|
||||||
|
() => _lastFps,
|
||||||
|
// Retail displays either the user degrade bias or the live
|
||||||
|
// distance-driven multiplier. acdream does not yet implement
|
||||||
|
// adaptive distance degradation (tracked by TS-15), so its
|
||||||
|
// effective multiplier is exactly 1.0.
|
||||||
|
() => 1.0,
|
||||||
|
() => _settingsVm?.DisplayDraft.ShowFps
|
||||||
|
?? _persistedDisplay.ShowFps),
|
||||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||||
Objects,
|
Objects,
|
||||||
() => Shortcuts,
|
() => Shortcuts,
|
||||||
|
|
@ -9988,20 +9997,16 @@ public sealed class GameWindow : IDisposable
|
||||||
int entityCount = _worldState.Entities.Count;
|
int entityCount = _worldState.Entities.Count;
|
||||||
int animatedCount = _animatedEntities.Count;
|
int animatedCount = _animatedEntities.Count;
|
||||||
|
|
||||||
// L.0 Display tab: ShowFps gates the perf string in the
|
// Keep the developer diagnostic title independent from retail's
|
||||||
// title bar. Default is true (matches pre-L.0 behaviour);
|
// /framerate switch. That command owns the in-world SmartBox readout;
|
||||||
// unchecking the toggle in Display tab collapses the title
|
// conflating it with window chrome was the original incomplete port.
|
||||||
// to just "acdream" for a cleaner alt-tab experience.
|
|
||||||
//
|
//
|
||||||
// When perf is shown, also include the in-game calendar/time —
|
// Also include the in-game calendar/time —
|
||||||
// matches retail's @timestamp output ("Date: <Month> <Day>,
|
// matches retail's @timestamp output ("Date: <Month> <Day>,
|
||||||
// PY <Year> Time: <HourName>"). Uses NowTicks (server-synced
|
// PY <Year> Time: <HourName>"). Uses NowTicks (server-synced
|
||||||
// + wall-clock interpolation) so the user can read the same
|
// + wall-clock interpolation) so the user can read the same
|
||||||
// fields off both acdream and retail and confirm clock parity
|
// fields off both acdream and retail and confirm clock parity
|
||||||
// directly. Drift > 1 hour = real bug.
|
// directly. Drift > 1 hour = real bug.
|
||||||
bool showFps = _settingsVm?.DisplayDraft.ShowFps ?? true;
|
|
||||||
if (showFps)
|
|
||||||
{
|
|
||||||
double tNow = WorldTime.NowTicks;
|
double tNow = WorldTime.NowTicks;
|
||||||
var titleCal = AcDream.Core.World.DerethDateTime.ToCalendar(tNow);
|
var titleCal = AcDream.Core.World.DerethDateTime.ToCalendar(tNow);
|
||||||
double df = WorldTime.DayFraction;
|
double df = WorldTime.DayFraction;
|
||||||
|
|
@ -10009,11 +10014,6 @@ public sealed class GameWindow : IDisposable
|
||||||
$"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | "
|
$"acdream | {fps:F0} fps | {avgFrameTime:F1} ms | "
|
||||||
+ $"lb {visibleLandblocks}/{totalLandblocks} | ent {entityCount}/anim {animatedCount} | "
|
+ $"lb {visibleLandblocks}/{totalLandblocks} | ent {entityCount}/anim {animatedCount} | "
|
||||||
+ $"PY{titleCal.Year} {titleCal.Month} {titleCal.Day} {titleCal.Hour} (df={df:F4})";
|
+ $"PY{titleCal.Year} {titleCal.Month} {titleCal.Day} {titleCal.Hour} (df={df:F4})";
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_window!.Title = "acdream";
|
|
||||||
}
|
|
||||||
_lastFps = fps;
|
_lastFps = fps;
|
||||||
_lastFrameMs = avgFrameTime;
|
_lastFrameMs = avgFrameTime;
|
||||||
_perfAccum = 0;
|
_perfAccum = 0;
|
||||||
|
|
@ -11482,7 +11482,7 @@ public sealed class GameWindow : IDisposable
|
||||||
ShowFps = !_persistedDisplay.ShowFps,
|
ShowFps = !_persistedDisplay.ShowFps,
|
||||||
};
|
};
|
||||||
if (_settingsVm is not null)
|
if (_settingsVm is not null)
|
||||||
_settingsVm.SetDisplay(_settingsVm.DisplayDraft with
|
_settingsVm.ApplyExternalDisplayChange(display => display with
|
||||||
{
|
{
|
||||||
ShowFps = _persistedDisplay.ShowFps,
|
ShowFps = _persistedDisplay.ShowFps,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
79
src/AcDream.App/UI/Layout/RetailFpsController.cs
Normal file
79
src/AcDream.App/UI/Layout/RetailFpsController.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds retail's SmartBox frame-rate display to the authored text element from
|
||||||
|
/// <c>LayoutDesc 0x2100000F</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Faithful port of <c>gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0</c>,
|
||||||
|
/// <c>gmSmartBoxUI::UpdateFPSMeter @ 0x004D63A0</c>, and
|
||||||
|
/// <c>gmSmartBoxUI::UseTime @ 0x004D6E30</c>. Portal string table 0x23000001,
|
||||||
|
/// string 0x0DCFFF73 supplies the two labels <c>FPS:</c> and <c>DEG:</c>;
|
||||||
|
/// retail inserts both floating-point values with two decimal places.
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class RetailFpsController
|
||||||
|
{
|
||||||
|
public const uint LayoutId = 0x2100000Fu;
|
||||||
|
public const uint DisplayElementId = 0x10000047u;
|
||||||
|
|
||||||
|
private readonly UiText _display;
|
||||||
|
private readonly Func<double> _framesPerSecond;
|
||||||
|
private readonly Func<double> _degradeMultiplier;
|
||||||
|
private readonly Func<bool> _isVisible;
|
||||||
|
|
||||||
|
private RetailFpsController(
|
||||||
|
UiText display,
|
||||||
|
Func<double> framesPerSecond,
|
||||||
|
Func<double> degradeMultiplier,
|
||||||
|
Func<bool> isVisible)
|
||||||
|
{
|
||||||
|
_display = display;
|
||||||
|
_framesPerSecond = framesPerSecond;
|
||||||
|
_degradeMultiplier = degradeMultiplier;
|
||||||
|
_isVisible = isVisible;
|
||||||
|
|
||||||
|
// The selected catalog element is authored at (1,1) inside gmSmartBoxUI.
|
||||||
|
// Selective LayoutDesc import makes it a root, so restore that parent-relative
|
||||||
|
// placement explicitly instead of letting it drift to (0,0).
|
||||||
|
_display.Left = 1f;
|
||||||
|
_display.Top = 1f;
|
||||||
|
_display.Padding = 1f;
|
||||||
|
_display.Selectable = false;
|
||||||
|
_display.LinesProvider = BuildLines;
|
||||||
|
Tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UiText Display => _display;
|
||||||
|
|
||||||
|
public static RetailFpsController? Bind(
|
||||||
|
ImportedLayout layout,
|
||||||
|
Func<double> framesPerSecond,
|
||||||
|
Func<double> degradeMultiplier,
|
||||||
|
Func<bool> isVisible)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(layout);
|
||||||
|
ArgumentNullException.ThrowIfNull(framesPerSecond);
|
||||||
|
ArgumentNullException.ThrowIfNull(degradeMultiplier);
|
||||||
|
ArgumentNullException.ThrowIfNull(isVisible);
|
||||||
|
|
||||||
|
return layout.FindElement(DisplayElementId) is UiText display
|
||||||
|
? new RetailFpsController(display, framesPerSecond, degradeMultiplier, isVisible)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mirrors the retail notice-driven show/hide state each UI tick.</summary>
|
||||||
|
public void Tick() => _display.Visible = _isVisible();
|
||||||
|
|
||||||
|
private IReadOnlyList<UiText.Line> BuildLines()
|
||||||
|
{
|
||||||
|
string fps = _framesPerSecond().ToString("F2", CultureInfo.InvariantCulture);
|
||||||
|
string degrade = _degradeMultiplier().ToString("F2", CultureInfo.InvariantCulture);
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new UiText.Line($"FPS: {fps}", _display.DefaultColor),
|
||||||
|
new UiText.Line($"DEG: {degrade}", _display.DefaultColor),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -45,6 +45,11 @@ public sealed record CombatRuntimeBindings(
|
||||||
|
|
||||||
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
||||||
|
|
||||||
|
public sealed record FpsRuntimeBindings(
|
||||||
|
Func<double> FramesPerSecond,
|
||||||
|
Func<double> DegradeMultiplier,
|
||||||
|
Func<bool> IsVisible);
|
||||||
|
|
||||||
public sealed record ToolbarRuntimeBindings(
|
public sealed record ToolbarRuntimeBindings(
|
||||||
ClientObjectTable Objects,
|
ClientObjectTable Objects,
|
||||||
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
Func<IReadOnlyList<ShortcutEntry>> Shortcuts,
|
||||||
|
|
@ -110,6 +115,7 @@ public sealed record RetailUiRuntimeBindings(
|
||||||
RadarRuntimeBindings Radar,
|
RadarRuntimeBindings Radar,
|
||||||
CombatRuntimeBindings Combat,
|
CombatRuntimeBindings Combat,
|
||||||
JumpPowerbarRuntimeBindings JumpPowerbar,
|
JumpPowerbarRuntimeBindings JumpPowerbar,
|
||||||
|
FpsRuntimeBindings Fps,
|
||||||
ToolbarRuntimeBindings Toolbar,
|
ToolbarRuntimeBindings Toolbar,
|
||||||
CharacterRuntimeBindings Character,
|
CharacterRuntimeBindings Character,
|
||||||
InventoryRuntimeBindings Inventory,
|
InventoryRuntimeBindings Inventory,
|
||||||
|
|
@ -135,6 +141,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
|
||||||
{
|
{
|
||||||
_bindings = bindings;
|
_bindings = bindings;
|
||||||
|
MountFpsDisplay();
|
||||||
MountVitals();
|
MountVitals();
|
||||||
MountRadar();
|
MountRadar();
|
||||||
MountChat();
|
MountChat();
|
||||||
|
|
@ -180,6 +187,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
public ToolbarInputController? ToolbarInputController { get; private set; }
|
public ToolbarInputController? ToolbarInputController { get; private set; }
|
||||||
public CombatUiController? CombatUiController { get; private set; }
|
public CombatUiController? CombatUiController { get; private set; }
|
||||||
public JumpPowerbarController? JumpPowerbarController { get; private set; }
|
public JumpPowerbarController? JumpPowerbarController { get; private set; }
|
||||||
|
public RetailFpsController? FpsController { get; private set; }
|
||||||
public SelectedObjectController? SelectedObjectController { get; private set; }
|
public SelectedObjectController? SelectedObjectController { get; private set; }
|
||||||
public UiViewport? PaperdollViewportWidget { get; private set; }
|
public UiViewport? PaperdollViewportWidget { get; private set; }
|
||||||
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
public UiNineSlicePanel? InventoryFrame { get; private set; }
|
||||||
|
|
@ -201,6 +209,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
|
|
||||||
public void Tick(double deltaSeconds)
|
public void Tick(double deltaSeconds)
|
||||||
{
|
{
|
||||||
|
FpsController?.Tick();
|
||||||
JumpPowerbarController?.Tick();
|
JumpPowerbarController?.Tick();
|
||||||
SelectedObjectController?.Tick(deltaSeconds);
|
SelectedObjectController?.Tick(deltaSeconds);
|
||||||
ConfirmationDialogs?.Tick();
|
ConfirmationDialogs?.Tick();
|
||||||
|
|
@ -269,6 +278,47 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Assets.ResolveFont);
|
_bindings.Assets.ResolveFont);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ImportedLayout? Import(uint layoutId, uint rootElementId)
|
||||||
|
{
|
||||||
|
lock (_bindings.Assets.DatLock)
|
||||||
|
return LayoutImporter.Import(
|
||||||
|
_bindings.Assets.Dats,
|
||||||
|
layoutId,
|
||||||
|
rootElementId,
|
||||||
|
_bindings.Assets.ResolveSprite,
|
||||||
|
_bindings.Assets.DefaultFont,
|
||||||
|
_bindings.Assets.ResolveFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MountFpsDisplay()
|
||||||
|
{
|
||||||
|
ImportedLayout? layout = Import(
|
||||||
|
RetailFpsController.LayoutId,
|
||||||
|
RetailFpsController.DisplayElementId);
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[D.2b] FPS display: SmartBox element 0x10000047 not found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FpsRuntimeBindings b = _bindings.Fps;
|
||||||
|
FpsController = RetailFpsController.Bind(
|
||||||
|
layout,
|
||||||
|
b.FramesPerSecond,
|
||||||
|
b.DegradeMultiplier,
|
||||||
|
b.IsVisible);
|
||||||
|
if (FpsController is null)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[D.2b] FPS display: SmartBox element is not UIElement_Text.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmartBox HUD content is mounted before movable windows, matching retail's
|
||||||
|
// screen-layer ordering and ensuring ordinary panels can cover the readout.
|
||||||
|
Host.Root.AddChild(layout.Root);
|
||||||
|
Console.WriteLine("[D.2b] retail FPS display from SmartBox LayoutDesc 0x2100000F.");
|
||||||
|
}
|
||||||
|
|
||||||
private void MountVitals()
|
private void MountVitals()
|
||||||
{
|
{
|
||||||
ImportedLayout? layout = Import(0x2100006Cu);
|
ImportedLayout? layout = Import(0x2100006Cu);
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ public static class WeenieErrorMessages
|
||||||
// Movement / teleport / housing
|
// Movement / teleport / housing
|
||||||
[0x0036] = "Action cancelled!", // ActionCancelled
|
[0x0036] = "Action cancelled!", // ActionCancelled
|
||||||
[0x003D] = "You charged too far!", // YouChargedTooFar
|
[0x003D] = "You charged too far!", // YouChargedTooFar
|
||||||
|
[0x004A] = "Ack! You killed yourself!", // YouKilledYourself
|
||||||
[0x0498] = "You have moved too far!", // YouHaveMovedTooFar
|
[0x0498] = "You have moved too far!", // YouHaveMovedTooFar
|
||||||
[0x0499] = "That is not a valid destination!", // TeleToInvalidPosition
|
[0x0499] = "That is not a valid destination!", // TeleToInvalidPosition
|
||||||
[0x0532] = "You must wait 30 days after purchasing a house before you may purchase another with any character on the same account.",
|
[0x0532] = "You must wait 30 days after purchasing a house before you may purchase another with any character on the same account.",
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,17 @@ public sealed record DisplaySettings(
|
||||||
QualityPreset Quality)
|
QualityPreset Quality)
|
||||||
{
|
{
|
||||||
/// <summary>Values used on first launch / when settings.json is absent.
|
/// <summary>Values used on first launch / when settings.json is absent.
|
||||||
/// All defaults pinned to the pre-L.0 runtime state — Resolution
|
/// Geometry/render defaults preserve the pre-L.0 runtime state — Resolution
|
||||||
/// matches the WindowOptions startup size (1280×720), FieldOfView
|
/// matches the WindowOptions startup size (1280×720), FieldOfView
|
||||||
/// matches camera FovY (60°), VSync matches WindowOptions (false),
|
/// matches camera FovY (60°), VSync matches WindowOptions (false),
|
||||||
/// ShowFps preserves the perf string in the title bar. Net effect:
|
/// ShowFps matches retail's initially-hidden SmartBox FPS readout.</summary>
|
||||||
/// opening Display + Save without touching anything is a complete
|
|
||||||
/// visual no-op.</summary>
|
|
||||||
public static DisplaySettings Default { get; } = new(
|
public static DisplaySettings Default { get; } = new(
|
||||||
Resolution: "1280x720",
|
Resolution: "1280x720",
|
||||||
Fullscreen: false,
|
Fullscreen: false,
|
||||||
VSync: false,
|
VSync: false,
|
||||||
FieldOfView: 60f,
|
FieldOfView: 60f,
|
||||||
Gamma: 1.0f,
|
Gamma: 1.0f,
|
||||||
ShowFps: true,
|
ShowFps: false,
|
||||||
Quality: QualityPreset.High);
|
Quality: QualityPreset.High);
|
||||||
|
|
||||||
/// <summary>16:9 resolution presets offered in the dropdown.</summary>
|
/// <summary>16:9 resolution presets offered in the dropdown.</summary>
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,20 @@ public sealed class SettingsVM
|
||||||
_displayDraft = value ?? throw new ArgumentNullException(nameof(value));
|
_displayDraft = value ?? throw new ArgumentNullException(nameof(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply one externally persisted display-option change to both snapshots.
|
||||||
|
/// Existing unsaved edits to other fields remain drafts rather than being
|
||||||
|
/// accidentally promoted to persisted state.
|
||||||
|
/// </summary>
|
||||||
|
public void ApplyExternalDisplayChange(Func<DisplaySettings, DisplaySettings> update)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(update);
|
||||||
|
_displayPersisted = update(_displayPersisted)
|
||||||
|
?? throw new InvalidOperationException("Display update returned null.");
|
||||||
|
_displayDraft = update(_displayDraft)
|
||||||
|
?? throw new InvalidOperationException("Display update returned null.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace the entire Audio draft with <paramref name="value"/>.
|
/// Replace the entire Audio draft with <paramref name="value"/>.
|
||||||
/// Live audio preview is achieved at the host layer by pushing
|
/// Live audio preview is achieved at the host layer by pushing
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,12 @@ public static class FixtureLoader
|
||||||
public static ElementInfo LoadConfirmationDialogInfos()
|
public static ElementInfo LoadConfirmationDialogInfos()
|
||||||
=> LoadInfos("dialogs_2100003C.json");
|
=> LoadInfos("dialogs_2100003C.json");
|
||||||
|
|
||||||
|
public static ImportedLayout LoadFpsDisplay()
|
||||||
|
=> LayoutImporter.Build(LoadFpsDisplayInfos(), _ => (0u, 0, 0), null);
|
||||||
|
|
||||||
|
public static ElementInfo LoadFpsDisplayInfos()
|
||||||
|
=> LoadInfos("smartbox_fps_2100000F.json");
|
||||||
|
|
||||||
// ── Shared loader ────────────────────────────────────────────────────────
|
// ── Shared loader ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
private static AcDream.App.UI.Layout.ElementInfo LoadInfos(string fileName)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
using AcDream.App.UI;
|
||||||
|
using AcDream.App.UI.Layout;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.UI.Layout;
|
||||||
|
|
||||||
|
public sealed class RetailFpsControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AuthoredSmartBoxDisplay_FormatsUpdatesAndTogglesLikeRetail()
|
||||||
|
{
|
||||||
|
double fps = 59.994;
|
||||||
|
double degrade = 0.875;
|
||||||
|
bool visible = false;
|
||||||
|
ImportedLayout layout = FixtureLoader.LoadFpsDisplay();
|
||||||
|
ElementInfo info = FixtureLoader.LoadFpsDisplayInfos();
|
||||||
|
|
||||||
|
Assert.Equal(0x4000001Au, info.FontDid);
|
||||||
|
Assert.Equal(128, info.Width);
|
||||||
|
Assert.Equal(36, info.Height);
|
||||||
|
Assert.Equal(1u, info.Left);
|
||||||
|
Assert.Equal(1u, info.Top);
|
||||||
|
|
||||||
|
var controller = RetailFpsController.Bind(
|
||||||
|
layout, () => fps, () => degrade, () => visible)!;
|
||||||
|
|
||||||
|
UiText display = controller.Display;
|
||||||
|
Assert.Equal(RetailFpsController.DisplayElementId, display.ElementId);
|
||||||
|
Assert.Equal(128f, display.Width);
|
||||||
|
Assert.Equal(36f, display.Height);
|
||||||
|
Assert.Equal(1f, display.Left);
|
||||||
|
Assert.Equal(1f, display.Top);
|
||||||
|
Assert.False(display.Visible);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
["FPS: 59.99", "DEG: 0.88"],
|
||||||
|
display.LinesProvider().Select(line => line.Text));
|
||||||
|
|
||||||
|
fps = 144.0;
|
||||||
|
degrade = 1.0;
|
||||||
|
visible = true;
|
||||||
|
controller.Tick();
|
||||||
|
|
||||||
|
Assert.True(display.Visible);
|
||||||
|
Assert.Equal(
|
||||||
|
["FPS: 144.00", "DEG: 1.00"],
|
||||||
|
display.LinesProvider().Select(line => line.Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Bind_RejectsLayoutWithoutAuthoredTextElement()
|
||||||
|
{
|
||||||
|
var root = new UiPanel();
|
||||||
|
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>());
|
||||||
|
|
||||||
|
Assert.Null(RetailFpsController.Bind(layout, () => 60, () => 1, () => true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,25 @@ public sealed class RetailLayoutFixtureGenerator
|
||||||
Path.Combine(FixtureDirectory(), $"dialogs_{dialogsLayoutDid:X8}.json"),
|
Path.Combine(FixtureDirectory(), $"dialogs_{dialogsLayoutDid:X8}.json"),
|
||||||
dialogsJson);
|
dialogsJson);
|
||||||
|
|
||||||
|
const uint smartboxLayoutDid = 0x2100000Fu;
|
||||||
|
var fps = LayoutImporter.ImportInfos(
|
||||||
|
dats,
|
||||||
|
smartboxLayoutDid,
|
||||||
|
0x10000047u);
|
||||||
|
Assert.NotNull(fps);
|
||||||
|
Assert.Equal(0x4000001Au, fps!.FontDid);
|
||||||
|
var fpsStrings = new DatStringResolver(dats);
|
||||||
|
Assert.Equal("FPS: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 0));
|
||||||
|
Assert.Equal("\nDEG: ", fpsStrings.Resolve(0x23000001u, 0x0DCFFF73u, 1));
|
||||||
|
var fpsJson = JsonSerializer.Serialize(fps, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
IncludeFields = true,
|
||||||
|
WriteIndented = true,
|
||||||
|
});
|
||||||
|
File.WriteAllText(
|
||||||
|
Path.Combine(FixtureDirectory(), $"smartbox_fps_{smartboxLayoutDid:X8}.json"),
|
||||||
|
fpsJson);
|
||||||
|
|
||||||
foreach (var (layoutId, fileName) in Layouts)
|
foreach (var (layoutId, fileName) in Layouts)
|
||||||
{
|
{
|
||||||
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
var info = LayoutImporter.ImportInfos(dats, layoutId);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,395 @@
|
||||||
|
{
|
||||||
|
"Id": 268435527,
|
||||||
|
"Type": 12,
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Width": 128,
|
||||||
|
"Height": 36,
|
||||||
|
"OriginalParentWidth": 0,
|
||||||
|
"OriginalParentHeight": 0,
|
||||||
|
"HasOriginalParentSize": false,
|
||||||
|
"Left": 1,
|
||||||
|
"Top": 1,
|
||||||
|
"Right": 2,
|
||||||
|
"Bottom": 2,
|
||||||
|
"ReadOrder": 15,
|
||||||
|
"ZLevel": 0,
|
||||||
|
"States": {
|
||||||
|
"4294967295": {
|
||||||
|
"Id": 4294967295,
|
||||||
|
"Name": "",
|
||||||
|
"PassToChildren": false,
|
||||||
|
"IncorporationFlags": 30,
|
||||||
|
"Image": null,
|
||||||
|
"Cursor": null,
|
||||||
|
"Properties": {
|
||||||
|
"Values": {
|
||||||
|
"33": {
|
||||||
|
"Kind": 1,
|
||||||
|
"MasterPropertyId": 33,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": true,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"35": {
|
||||||
|
"Kind": 4,
|
||||||
|
"MasterPropertyId": 35,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 2,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"81": {
|
||||||
|
"Kind": 1,
|
||||||
|
"MasterPropertyId": 81,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": true,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"59": {
|
||||||
|
"Kind": 1,
|
||||||
|
"MasterPropertyId": 59,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": true,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"37": {
|
||||||
|
"Kind": 4,
|
||||||
|
"MasterPropertyId": 37,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 2,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"26": {
|
||||||
|
"Kind": 7,
|
||||||
|
"MasterPropertyId": 26,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [
|
||||||
|
{
|
||||||
|
"Kind": 2,
|
||||||
|
"MasterPropertyId": 24,
|
||||||
|
"UnsignedValue": 1073741850,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"27": {
|
||||||
|
"Kind": 7,
|
||||||
|
"MasterPropertyId": 27,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [
|
||||||
|
{
|
||||||
|
"Kind": 6,
|
||||||
|
"MasterPropertyId": 25,
|
||||||
|
"UnsignedValue": 0,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 255,
|
||||||
|
"Green": 255,
|
||||||
|
"Red": 255,
|
||||||
|
"Alpha": 255
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"205": {
|
||||||
|
"Kind": 0,
|
||||||
|
"MasterPropertyId": 205,
|
||||||
|
"UnsignedValue": 3,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"20": {
|
||||||
|
"Kind": 0,
|
||||||
|
"MasterPropertyId": 20,
|
||||||
|
"UnsignedValue": 2,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
},
|
||||||
|
"21": {
|
||||||
|
"Kind": 0,
|
||||||
|
"MasterPropertyId": 21,
|
||||||
|
"UnsignedValue": 4,
|
||||||
|
"IntegerValue": 0,
|
||||||
|
"FloatValue": 0,
|
||||||
|
"BoolValue": false,
|
||||||
|
"StringInfoValue": {
|
||||||
|
"Token": 0,
|
||||||
|
"StringId": 0,
|
||||||
|
"TableId": 0,
|
||||||
|
"Override": 0,
|
||||||
|
"English": 0,
|
||||||
|
"Comment": 0
|
||||||
|
},
|
||||||
|
"ColorValue": {
|
||||||
|
"Blue": 0,
|
||||||
|
"Green": 0,
|
||||||
|
"Red": 0,
|
||||||
|
"Alpha": 0
|
||||||
|
},
|
||||||
|
"VectorValue": {
|
||||||
|
"X": 0,
|
||||||
|
"Y": 0,
|
||||||
|
"Z": 0
|
||||||
|
},
|
||||||
|
"ArrayValue": [],
|
||||||
|
"StructValue": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DefaultStateId": 0,
|
||||||
|
"FontDid": 1073741850,
|
||||||
|
"HJustify": 0,
|
||||||
|
"VJustify": 2,
|
||||||
|
"FontColor": {
|
||||||
|
"X": 1,
|
||||||
|
"Y": 1,
|
||||||
|
"Z": 1,
|
||||||
|
"W": 1
|
||||||
|
},
|
||||||
|
"StateMedia": {},
|
||||||
|
"StateCursors": {},
|
||||||
|
"DefaultStateName": "",
|
||||||
|
"Children": []
|
||||||
|
}
|
||||||
|
|
@ -99,6 +99,7 @@ public sealed class WeenieErrorMessagesTests
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0x0036u, "Action cancelled!")]
|
[InlineData(0x0036u, "Action cancelled!")]
|
||||||
[InlineData(0x003Du, "You charged too far!")]
|
[InlineData(0x003Du, "You charged too far!")]
|
||||||
|
[InlineData(0x004Au, "Ack! You killed yourself!")]
|
||||||
public void Format_CombatMovementErrors(uint code, string expected)
|
public void Format_CombatMovementErrors(uint code, string expected)
|
||||||
=> Assert.Equal(expected, WeenieErrorMessages.Format(code, null));
|
=> Assert.Equal(expected, WeenieErrorMessages.Format(code, null));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,14 @@ public sealed class DisplaySettingsTests
|
||||||
// · Resolution matches WindowOptions (1280×720 in GameWindow.Run)
|
// · Resolution matches WindowOptions (1280×720 in GameWindow.Run)
|
||||||
// · FieldOfView matches camera FovY (60° = π/3)
|
// · FieldOfView matches camera FovY (60° = π/3)
|
||||||
// · VSync matches WindowOptions (false during dev)
|
// · VSync matches WindowOptions (false during dev)
|
||||||
// · ShowFps true preserves the perf string in the title bar
|
// · ShowFps false matches retail's initially-hidden SmartBox meter
|
||||||
// Net effect: opening Display + Save with no edits is a visual
|
|
||||||
// no-op (no window resize, no camera FovY change, no title
|
|
||||||
// bar change).
|
|
||||||
var d = DisplaySettings.Default;
|
var d = DisplaySettings.Default;
|
||||||
Assert.Equal("1280x720", d.Resolution);
|
Assert.Equal("1280x720", d.Resolution);
|
||||||
Assert.False(d.Fullscreen);
|
Assert.False(d.Fullscreen);
|
||||||
Assert.False(d.VSync);
|
Assert.False(d.VSync);
|
||||||
Assert.Equal(60f, d.FieldOfView);
|
Assert.Equal(60f, d.FieldOfView);
|
||||||
Assert.Equal(1.0f, d.Gamma);
|
Assert.Equal(1.0f, d.Gamma);
|
||||||
Assert.True(d.ShowFps);
|
Assert.False(d.ShowFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -65,7 +62,7 @@ public sealed class DisplaySettingsTests
|
||||||
// Other fields untouched.
|
// Other fields untouched.
|
||||||
Assert.Equal("1280x720", d.Resolution);
|
Assert.Equal("1280x720", d.Resolution);
|
||||||
Assert.False(d.VSync);
|
Assert.False(d.VSync);
|
||||||
Assert.True(d.ShowFps);
|
Assert.False(d.ShowFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ParseWidth(string res)
|
private static int ParseWidth(string res)
|
||||||
|
|
|
||||||
|
|
@ -271,12 +271,31 @@ public sealed class SettingsVMTests
|
||||||
public void SetDisplay_marks_unsaved_changes()
|
public void SetDisplay_marks_unsaved_changes()
|
||||||
{
|
{
|
||||||
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
||||||
// Default ShowFps is true → flip to false to ensure the with-
|
// Default ShowFps is false → flip to true to ensure the with-
|
||||||
// expression actually mutates a field.
|
// expression actually mutates a field.
|
||||||
vm.SetDisplay(vm.DisplayDraft with { ShowFps = false });
|
vm.SetDisplay(vm.DisplayDraft with { ShowFps = true });
|
||||||
Assert.True(vm.HasUnsavedChanges);
|
Assert.True(vm.HasUnsavedChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyExternalDisplayChange_updates_both_snapshots_and_preserves_other_drafts()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
||||||
|
|
||||||
|
vm.SetDisplay(vm.DisplayDraft with { FieldOfView = 90f });
|
||||||
|
Assert.True(vm.HasUnsavedChanges);
|
||||||
|
|
||||||
|
vm.ApplyExternalDisplayChange(display => display with { ShowFps = true });
|
||||||
|
|
||||||
|
Assert.True(vm.DisplayDraft.ShowFps);
|
||||||
|
Assert.Equal(90f, vm.DisplayDraft.FieldOfView);
|
||||||
|
Assert.True(vm.HasUnsavedChanges);
|
||||||
|
|
||||||
|
vm.Cancel();
|
||||||
|
Assert.True(vm.DisplayDraft.ShowFps);
|
||||||
|
Assert.Equal(DisplaySettings.Default.FieldOfView, vm.DisplayDraft.FieldOfView);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Save_invokes_display_callback_with_draft()
|
public void Save_invokes_display_callback_with_draft()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue