Binds LayoutDesc 0x21000028 (gmCharacterSettingsUI) through OP2's template-list mechanism and OP3's OptionPage model: 6 authored group headers + 50 toggle rows (49 from the 2013 build + D3's "Listen to PK death messages", AP-193) in research doc §2's authored order, each row resolved by PlayerOption id through CharacterOptionTable, seeded from live RuntimeCharacterOptionsState, defaulted from CharacterOptionTable. ClientDefault (byte-verified against UIOption_Checkbox::SetPlayerOption @0x00486e80's own GetDefaultOptionValue call — AP-194 updated to confirm the directive was followed), labels/tooltips resolved by name from string table 0x23000003 (never hard-coded English), and registered with OptionsPanelController.CharacterPage. Apply/Reset/Defaults (0x100001FC/FD/FE) are now wired per-page via a scoped subtree search (UiElement.FindDescendant, promoted from UiTabPanel) since Character/ Chat/Config each author their own physical instance under the SAME element ids. Consumers: Group A (29 ids) wire+store only via the existing SetSingleCharacterOptionRuntimeCmd/TrySetOption seam. Group B: Display Timestamps prefixes new transcript lines (RuntimeCommunicationState. DisplayTimestampsSource); Disable Distance Fog forces FogMode.Off (WeatherSystem.DisableDistanceFogSource, retiring half of TS-73); Run as Default Movement inverts the walk-mode modifier's default (RuntimeLocalPlayerMovementState.RunAsDefaultMovementSource). Group C re-points AutoTarget/AutoRepeatAttack/ViewCombatTarget (CharacterOptionCombatSettingsSource), VividTargetingIndicator/ CoordinatesOnRadar/LockUI/AcceptLootPermits from the client-local GameplaySettings record to the canonical server bit — closing two previously-unfiled divergences where AutoRepeatAttack and AcceptCorpseLootingPermissions never reached the wire despite being retail auto-save ids. TS-73 narrowed to its two still-open cases; TS-75..TS-80 file the genuine gaps (no day/night force, no weather- particle/profanity-filter/salvage/housing/pickup-preference subsystem, fellowship-create's unaudited client-sourced field) rather than inventing stand-ins. Conformance: CharacterOptionsPageControllerTests pins all 50 rows against CharacterOptionTable in both directions (an invented or dropped row fails the build), the authored group/order row-by-row, and the build/seed/Apply/Reset/Defaults/wire-publish behavior end-to-end against the committed fixture. 52 new tests; full solution suite 13,008 passed / 4 skipped / 0 failed (was 12,956/4/0). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.4 KiB
C#
62 lines
2.4 KiB
C#
using AcDream.App.Combat;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Runtime.Gameplay;
|
|
|
|
namespace AcDream.App.Tests.Combat;
|
|
|
|
/// <summary>
|
|
/// Campaign OP slice OP4 (2026-08-11) — D7 Group-C re-point:
|
|
/// <see cref="CharacterOptionCombatSettingsSource"/> reads
|
|
/// AutoTarget/AutoRepeatAttack/ViewCombatTarget from the canonical
|
|
/// <see cref="RuntimeCharacterOptionsState"/> instead of the client-local
|
|
/// <c>GameplaySettings</c> record <see cref="GameplaySettingsState"/> used
|
|
/// to be the only implementation of.
|
|
/// </summary>
|
|
public sealed class CharacterOptionCombatSettingsSourceTests
|
|
{
|
|
[Fact]
|
|
public void ReadsLiveBits_NotAConstructionTimeSnapshot()
|
|
{
|
|
var options = new RuntimeCharacterOptionsState();
|
|
options.SetOptionBit((uint)CharacterOptionId.AutoTarget, false);
|
|
options.SetOptionBit((uint)CharacterOptionId.AutoRepeatAttack, false);
|
|
options.SetOptionBit((uint)CharacterOptionId.ViewCombatTarget, false);
|
|
var source = new CharacterOptionCombatSettingsSource(options);
|
|
|
|
Assert.False(source.AutoTarget);
|
|
Assert.False(source.AutoRepeatAttack);
|
|
Assert.False(source.ViewCombatTarget);
|
|
|
|
// Live: a later write is observed with no re-construction, matching
|
|
// ICombatGameplaySettingsSource's poll-per-call contract.
|
|
options.SetOptionBit((uint)CharacterOptionId.AutoTarget, true);
|
|
Assert.True(source.AutoTarget);
|
|
Assert.False(source.AutoRepeatAttack);
|
|
Assert.False(source.ViewCombatTarget);
|
|
|
|
options.SetOptionBit((uint)CharacterOptionId.AutoRepeatAttack, true);
|
|
options.SetOptionBit((uint)CharacterOptionId.ViewCombatTarget, true);
|
|
Assert.True(source.AutoRepeatAttack);
|
|
Assert.True(source.ViewCombatTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReflectsClientDefaults_ForAFreshCharacterOptionsState()
|
|
{
|
|
// CharacterOptionTable's byte-verified client-Defaults column:
|
|
// AutoTarget/AutoRepeatAttack ON, ViewCombatTarget OFF.
|
|
var options = new RuntimeCharacterOptionsState();
|
|
var source = new CharacterOptionCombatSettingsSource(options);
|
|
|
|
Assert.True(source.AutoTarget);
|
|
Assert.True(source.AutoRepeatAttack);
|
|
Assert.False(source.ViewCombatTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_RejectsNull()
|
|
{
|
|
Assert.Throws<ArgumentNullException>(
|
|
static () => new CharacterOptionCombatSettingsSource(null!));
|
|
}
|
|
}
|