Both dual-lens reviewers of `9cf6c522`+`ddcbf1fb` returned PASS-with-items. This round closes all nine findings: F1 files AP-229 for the screen-layering divergence (retail destroys/ reconstructs the current UI framework via UIFlow::UseNewMode; acdream keeps both CharacterManagementUiController and CharacterCreationUiController mounted for the whole lifetime and reveals/occludes) plus its narrow residual risk (the shared RetailDialogFactory can hand UiRoot.Modal to a dialog opened by the still-ticking, occluded management screen on an inbound CharacterError) and what already matches retail (selection/ world-name persistence, click-through isolation, one coherent Modal stack). F2 rewrites the connected-gate script's roster-full step with the exact `@modifylong max_chars_per_account` recipe and the pending-delete-counts note. F3 adds AP-221's console-diagnostic lines to the known-gaps paragraph. F4 adds an empty-name/AP-227 step. F9 notes that a uniform Random pick over 13 heritages can repeat. F5 adds an App-layer source-text pin (GameWindowLiveSessionOwnershipTests.LiveSessionRuntimeFactoryBinds CharacterCreatedAndCreationFailedToTheStatusWriter) for the delegate wiring the reviewer proved was deletable without breaking any test — no practical seam exists to construct LiveSessionRuntimeFactory without a GameWindow, so this follows the file's own established source-text-pin pattern; the payload shape is already pinned separately at SessionStatusWriterTests. F6 corrects the CC7 ledger's checksum-assertion wording (it is a round-trip purity check, not an independent golden — the golden is CharacterCreateTests.ComputeChecksum_ExactRetailAccumulationSet) and cross-references it from the test's own doc comment. F7 corrects the CC7 ledger's fixture-ordering claim (it had chargen constructing first, backwards from RetailUiRuntime.Tick's real management-then-chargen order) and reorders CharacterScreensFixedCanvas ArbiterTests to match production, adding ClickThrough/ZOrder assertions that pin the occlusion the reviewer previously verified only by hand. F8 records a known flake (RuntimeCollisionReportingStateTests. WarmedSteadyContactRefreshDoesNotAllocate, allocation-assertion load sensitivity, pre-existing) seen under full-solution parallel load on both reviewer runs. Campaign status: all seven slices (CC1-CC7) are REVIEW-CLOSED; the campaign is CODE-COMPLETE pending the user's own connected gate. Runtime 1735/0 (unchanged), App 5257/3 skips (+1: the new F5 pin). Full Release build: 0 warnings, 0 errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
506 lines
22 KiB
C#
506 lines
22 KiB
C#
using System.Numerics;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.CharGen;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Campaign CC slice CC4 review-fix round R1 (2026-08-15):
|
|
/// <see cref="CharacterCreationUiController"/> and
|
|
/// <see cref="CharacterManagementUiController"/> can be SIMULTANEOUSLY
|
|
/// active against the SAME <see cref="UiRoot"/> — character-creation opens
|
|
/// on top of character-management, which keeps ticking underneath it. Both
|
|
/// controllers share <see cref="UiRoot.FixedCanvasSize"/>, and before this
|
|
/// fix each wrote it directly — a raw write from either screen was a last-
|
|
/// writer-wins race with no owner: the F1 fix's own chargen
|
|
/// <c>Close()</c> nulled the canvas out from under a STILL-ACTIVE
|
|
/// character-management screen underneath it (the exact AD-98 gate-round-2
|
|
/// defect resurfacing one layer up). These tests drive
|
|
/// <see cref="UiRoot.DeclareFixedCanvas"/>/<see cref="UiRoot.RevokeFixedCanvas"/>
|
|
/// across BOTH controllers on one shared host, per the reviewer's required
|
|
/// sequence.
|
|
/// </summary>
|
|
public sealed class CharacterScreensFixedCanvasArbiterTests
|
|
{
|
|
private static readonly Vector2 AuthoredCanvas = new(800f, 600f);
|
|
|
|
[Fact]
|
|
public void CanvasStaysSetWhileEitherScreenIsActive_AndNullsOnlyWhenBothRevoke()
|
|
{
|
|
using var environment = new TwoControllerHarness();
|
|
|
|
// char-mgmt alone: declared on its own activation edge (Tick's
|
|
// `if (!_active)` arm).
|
|
Assert.Equal(AuthoredCanvas, environment.Host.FixedCanvasSize);
|
|
|
|
// chargen opens ON TOP of the still-active char-mgmt screen.
|
|
environment.Chargen.Controller.Open();
|
|
Assert.Equal(AuthoredCanvas, environment.Host.FixedCanvasSize);
|
|
|
|
// chargen Exit-confirms and closes -- char-mgmt is STILL ACTIVE, so
|
|
// the canvas must stay set. This is R1's regression: the pre-fix
|
|
// Close() nulled UiRoot.FixedCanvasSize unconditionally here,
|
|
// stripping it from char-management underneath.
|
|
environment.Chargen.Button(CharacterCreationUiController.ExitElementId)
|
|
.OnClick!();
|
|
environment.Chargen.ConfirmActiveDialog(confirmed: true);
|
|
Assert.Equal(AuthoredCanvas, environment.Host.FixedCanvasSize);
|
|
Assert.False(environment.Chargen.Controller.Root.Visible);
|
|
|
|
// char-mgmt deactivates (e.g. entering the world) -- now NEITHER
|
|
// screen declares, so the canvas nulls.
|
|
environment.Management.Runtime.SetLifecycle(
|
|
RuntimeCharacterSelectionLifecycle.InWorld);
|
|
environment.Management.Controller.Tick();
|
|
Assert.Null(environment.Host.FixedCanvasSize);
|
|
}
|
|
|
|
/// <summary>The original F1 defect's own covering case: both screens
|
|
/// revoke together (world entry while chargen was ALSO still open)
|
|
/// still nulls the canvas -- not just "one revokes while the other
|
|
/// holds," the scenario above.</summary>
|
|
[Fact]
|
|
public void CanvasNulls_WhenBothScreensRevokeAtWorldEntry()
|
|
{
|
|
using var environment = new TwoControllerHarness();
|
|
environment.Chargen.Controller.Open();
|
|
Assert.Equal(AuthoredCanvas, environment.Host.FixedCanvasSize);
|
|
|
|
environment.Management.Runtime.SetLifecycle(
|
|
RuntimeCharacterSelectionLifecycle.InWorld);
|
|
environment.Management.Controller.Tick();
|
|
environment.Chargen.Runtime.ProvideView = false;
|
|
environment.Chargen.Controller.Tick();
|
|
|
|
Assert.Null(environment.Host.FixedCanvasSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign CC slice CC7 item 1: the real Create-button wire — retail's
|
|
/// <c>gmCharacterManagementUI::ListenToElementMessage @ 0x004ed5a0</c>
|
|
/// case 3 -> <c>QueueUIMode(0x1000000b)</c> — and the return path on
|
|
/// chargen Exit (<c>DoExit @ 0x004e8650</c> ->
|
|
/// <c>QueueUIMode(0x1000000a)</c>). Character-management is never
|
|
/// hidden by chargen opening on top of it (see the canvas-arbiter test
|
|
/// above), so "return to character management" needs no separate
|
|
/// Runtime action beyond chargen's own <c>Close()</c> — this proves
|
|
/// that architecture claim end to end rather than by inspection alone.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CreateButtonClick_OpensChargen_AndExitConfirmReturnsToManagement()
|
|
{
|
|
using var environment = new TwoControllerHarness();
|
|
|
|
// Character-management is active and visible before Create is ever
|
|
// clicked (AttachAndTick already ran in its own harness ctor).
|
|
Assert.True(environment.Management.Controller.Root.Visible);
|
|
Assert.False(environment.Chargen.Controller.Root.Visible);
|
|
|
|
UiButton create = environment.Management.Button(
|
|
CharacterManagementUiController.CreateElementId);
|
|
Assert.True(create.Enabled);
|
|
create.OnClick!();
|
|
environment.Chargen.Controller.Tick();
|
|
|
|
Assert.True(environment.Chargen.Controller.Root.Visible);
|
|
// Character-management stays active/visible underneath -- chargen
|
|
// opening on top never deactivates or hides it.
|
|
Assert.True(environment.Management.Controller.Root.Visible);
|
|
// F7 (2026-08-16): pin the occlusion the reviewer verified only by
|
|
// hand. Chargen's root is click-opaque over the full authored
|
|
// canvas, so no input reaches management underneath it...
|
|
Assert.False(environment.Chargen.Controller.Root.ClickThrough);
|
|
// ...and BringToFront (called every tick chargen is open, see
|
|
// CharacterCreationUiController.Tick) keeps chargen's ZOrder
|
|
// strictly above management's, so it paints on top too.
|
|
Assert.True(
|
|
environment.Chargen.Controller.Root.ZOrder
|
|
> environment.Management.Controller.Root.ZOrder);
|
|
|
|
environment.Chargen.Button(CharacterCreationUiController.ExitElementId)
|
|
.OnClick!();
|
|
environment.Chargen.ConfirmActiveDialog(confirmed: true);
|
|
|
|
Assert.False(environment.Chargen.Controller.Root.Visible);
|
|
// No separate "return" action was needed -- management was never
|
|
// hidden, so it is simply what remains visible.
|
|
Assert.True(environment.Management.Controller.Root.Visible);
|
|
}
|
|
|
|
// ── Fixture: one shared UiRoot, both controllers ────────────────────
|
|
|
|
private sealed class TwoControllerHarness : IDisposable
|
|
{
|
|
public TwoControllerHarness()
|
|
{
|
|
Host = new UiRoot { Width = 800f, Height = 600f };
|
|
// Campaign CC slice CC7 review-fix round, F7 (2026-08-16):
|
|
// construct in PRODUCTION order — management, then chargen —
|
|
// matching RetailUiRuntime.Tick's real sequence
|
|
// (`_characterManagementMount?.Tick(); ...
|
|
// _characterCreationMount?.Tick(); CharacterCreationController
|
|
// ?.Tick();`). An earlier version of this fixture built chargen
|
|
// FIRST and claimed that matched production; it did not — it
|
|
// was the opposite order. Management still needs a RequestCreate
|
|
// callback before Chargen's Controller exists, so this closure
|
|
// resolves Chargen lazily per-call, the SAME trick production's
|
|
// own RetailUiRuntime.ConfigureCharacterManagement uses
|
|
// (`bindings with { RequestCreate = () =>
|
|
// CharacterCreationController?.Open() }`, closing over `this`
|
|
// rather than capturing a not-yet-built controller).
|
|
// Chargen is assigned below, before this closure can ever be
|
|
// invoked (Management's own ctor only stores the callback, it
|
|
// does not call it) -- the null-forgiving operator documents
|
|
// that ordering guarantee for the nullable analyzer.
|
|
Management = new ManagementHarness(Host, () => Chargen!.Controller.Open());
|
|
Chargen = new ChargenHarness(Host);
|
|
}
|
|
|
|
public UiRoot Host { get; }
|
|
public ManagementHarness Management { get; }
|
|
public ChargenHarness Chargen { get; }
|
|
|
|
public void Dispose()
|
|
{
|
|
Chargen.Dispose();
|
|
Management.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class ManagementHarness : IDisposable
|
|
{
|
|
private readonly RetailDialogFactory _dialogs;
|
|
|
|
public ManagementHarness(UiRoot host, Action requestCreate)
|
|
{
|
|
Screen = BuildManagementScreen();
|
|
Runtime = new ManagementFakeRuntime(requestCreate);
|
|
_dialogs = new RetailDialogFactory(
|
|
host,
|
|
type => RetailDialogFactoryTests.BuildDialogLayout(type));
|
|
Controller = Assert.IsType<CharacterManagementUiController>(
|
|
CharacterManagementUiController.Bind(
|
|
host,
|
|
Screen,
|
|
static (_, _) => BuildRow(),
|
|
_dialogs,
|
|
Runtime.Bindings,
|
|
new CharacterManagementUiController.DialogStrings(
|
|
name => $"WARNING! {name}",
|
|
"DELETE",
|
|
"Please Wait",
|
|
"Entering World",
|
|
"Are you sure you want to leave?")));
|
|
}
|
|
|
|
public ImportedLayout Screen { get; }
|
|
public ManagementFakeRuntime Runtime { get; }
|
|
public CharacterManagementUiController Controller { get; }
|
|
|
|
public UiButton Button(uint id) =>
|
|
Assert.IsType<UiButton>(Screen.FindElement(id));
|
|
|
|
public void Dispose()
|
|
{
|
|
Controller.Dispose();
|
|
_dialogs.Dispose();
|
|
}
|
|
|
|
private static UiElement BuildRow() => LayoutImporter.Build(
|
|
new ElementInfo { Id = 0x100003A5u, Type = 1u, Width = 160f, Height = 16f },
|
|
_ => (0u, 0, 0),
|
|
null).Root;
|
|
|
|
private static ImportedLayout BuildManagementScreen()
|
|
{
|
|
var root = new ElementInfo
|
|
{
|
|
Id = CharacterManagementUiController.RootElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 600f,
|
|
};
|
|
var list = new ElementInfo
|
|
{
|
|
Id = CharacterManagementUiController.ListElementId,
|
|
Type = 5u,
|
|
X = 42f,
|
|
Y = 212f,
|
|
Width = 160f,
|
|
Height = 320f,
|
|
};
|
|
list.TemplateList.Add(new UiTemplateListEntry(0x21000004u, 0x100003A5u));
|
|
root.Children.Add(list);
|
|
root.Children.Add(new ElementInfo
|
|
{
|
|
Id = CharacterManagementUiController.WorldTextElementId,
|
|
Type = 12u,
|
|
Width = 193f,
|
|
Height = 110f,
|
|
});
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.CreateElementId));
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.EnterElementId));
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.DeleteElementId));
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.RestoreElementId));
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.CreditsElementId));
|
|
root.Children.Add(ButtonInfo(CharacterManagementUiController.ExitElementId));
|
|
return LayoutImporter.Build(root, _ => (0u, 0, 0), null);
|
|
}
|
|
}
|
|
|
|
private sealed class ManagementFakeRuntime
|
|
{
|
|
private static readonly RuntimeGenerationToken Generation = new(11u);
|
|
private readonly FakeManagementView _view = new();
|
|
|
|
public ManagementFakeRuntime(Action requestCreate)
|
|
{
|
|
_view.Entries = [new RuntimeCharacterSelectionEntry(0, 0x50000001u, "Alpha", 0u)];
|
|
_view.Snapshot = new RuntimeCharacterSelectionSnapshot(
|
|
Generation,
|
|
RuntimeCharacterSelectionLifecycle.AwaitingSelection,
|
|
Revision: 1,
|
|
AccountName: "account",
|
|
SlotCount: 5,
|
|
RosterCount: _view.Entries.Length,
|
|
WorldName: "sawato",
|
|
HighlightedCharacterId: 0x50000001u,
|
|
HighlightedDisplayIndex: 0,
|
|
PendingDeleteCharacterId: 0u,
|
|
LastRestoreRequestedCharacterId: 0u,
|
|
Operation: RuntimeCharacterSelectionOperation.None,
|
|
Error: null,
|
|
Buttons: new RuntimeCharacterSelectionButtons(true, true, false, true, false, true));
|
|
Bindings = new CharacterSelectionRuntimeBindings(
|
|
View: () => _view,
|
|
Highlight: _ => Result(),
|
|
Enter: Result,
|
|
RequestDelete: Result,
|
|
ConfirmDelete: Result,
|
|
Restore: Result,
|
|
Cancel: Result,
|
|
RequestExit: () => { },
|
|
RequestCreate: requestCreate);
|
|
}
|
|
|
|
public CharacterSelectionRuntimeBindings Bindings { get; }
|
|
|
|
private static RuntimeCommandResult Result() =>
|
|
new(RuntimeCommandStatus.Accepted, Generation);
|
|
|
|
public void SetLifecycle(RuntimeCharacterSelectionLifecycle lifecycle)
|
|
{
|
|
RuntimeCharacterSelectionSnapshot current = _view.Snapshot;
|
|
_view.Snapshot = current with { Lifecycle = lifecycle, Revision = current.Revision + 1 };
|
|
}
|
|
|
|
private sealed class FakeManagementView : IRuntimeCharacterSelectionView
|
|
{
|
|
public RuntimeCharacterSelectionEntry[] Entries { get; set; } = [];
|
|
public RuntimeCharacterSelectionSnapshot Snapshot { get; set; }
|
|
|
|
public bool TryGetAt(int displayIndex, out RuntimeCharacterSelectionEntry character)
|
|
{
|
|
if ((uint)displayIndex >= (uint)Entries.Length)
|
|
{
|
|
character = default;
|
|
return false;
|
|
}
|
|
character = Entries[displayIndex];
|
|
return true;
|
|
}
|
|
|
|
public bool TryGet(uint characterId, out RuntimeCharacterSelectionEntry character)
|
|
{
|
|
int index = Array.FindIndex(Entries, entry => entry.CharacterId == characterId);
|
|
if (index < 0)
|
|
{
|
|
character = default;
|
|
return false;
|
|
}
|
|
character = Entries[index];
|
|
return true;
|
|
}
|
|
|
|
public void Visit(IRuntimeCharacterSelectionVisitor visitor)
|
|
{
|
|
foreach (RuntimeCharacterSelectionEntry character in Entries)
|
|
visitor.Visit(in character);
|
|
}
|
|
|
|
public IDisposable Subscribe(IRuntimeCharacterSelectionObserver observer) =>
|
|
NullSubscription.Instance;
|
|
|
|
private sealed class NullSubscription : IDisposable
|
|
{
|
|
public static readonly NullSubscription Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class ChargenHarness : IDisposable
|
|
{
|
|
private readonly List<ImportedLayout> _dialogLayouts = [];
|
|
private readonly RetailDialogFactory _dialogs;
|
|
|
|
public ChargenHarness(UiRoot host)
|
|
{
|
|
Screen = BuildChargenScreen();
|
|
Runtime = new ChargenFakeRuntime();
|
|
_dialogs = new RetailDialogFactory(host, type =>
|
|
{
|
|
ImportedLayout layout = RetailDialogFactoryTests.BuildDialogLayout(type);
|
|
_dialogLayouts.Add(layout);
|
|
return layout;
|
|
});
|
|
Controller = Assert.IsType<CharacterCreationUiController>(
|
|
CharacterCreationUiController.CreateDetached(
|
|
host,
|
|
Screen,
|
|
static (_, _) => null,
|
|
_dialogs,
|
|
Runtime.Bindings,
|
|
new CharacterCreationUiController.DialogStrings(
|
|
"Are you sure you want to leave?",
|
|
"No name", "Unspent credits", "Randomize?", "Name too long")));
|
|
Controller.AttachAndTick();
|
|
}
|
|
|
|
public ImportedLayout Screen { get; }
|
|
public ChargenFakeRuntime Runtime { get; }
|
|
public CharacterCreationUiController Controller { get; }
|
|
|
|
public UiButton Button(uint id) =>
|
|
Assert.IsType<UiButton>(Screen.FindElement(id));
|
|
|
|
public void ConfirmActiveDialog(bool confirmed)
|
|
{
|
|
ImportedLayout dialog = _dialogLayouts[^1];
|
|
uint buttonId = confirmed
|
|
? RetailConfirmationDialogView.AcceptButtonId
|
|
: RetailConfirmationDialogView.RejectButtonId;
|
|
UiButton button = Assert.IsType<UiButton>(dialog.FindElement(buttonId));
|
|
button.OnClick!();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Controller.Dispose();
|
|
_dialogs.Dispose();
|
|
}
|
|
|
|
private static ImportedLayout BuildChargenScreen()
|
|
{
|
|
var root = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.RootElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 600f,
|
|
};
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.ProgressBarElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.BackElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.NextElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.FinishElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.HelpElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.ExitElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.RandomElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.MasterPageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.HeritagePageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.ProfessionPageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.SkillsPageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.AppearancePageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.TownPageElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.SummaryPageElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.HeritageTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.ProfessionTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.SkillsTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.AppearanceTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.TownTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.SummaryTabElementId));
|
|
return LayoutImporter.Build(root, _ => (0u, 0, 0), null);
|
|
}
|
|
|
|
private static ElementInfo ContainerInfo(uint id) =>
|
|
new() { Id = id, Type = 3u, Width = 200f, Height = 60f };
|
|
}
|
|
|
|
private sealed class ChargenFakeRuntime
|
|
{
|
|
private static readonly RuntimeGenerationToken Generation = new(13u);
|
|
|
|
public ChargenFakeRuntime()
|
|
{
|
|
View = new FakeChargenView();
|
|
Bindings = new CharacterCreationRuntimeBindings(
|
|
() => ProvideView ? View : null,
|
|
_ => Result(),
|
|
_ => Result(),
|
|
_ => Result(),
|
|
(_, _) => Result(),
|
|
(_, _) => Result(),
|
|
_ => Result(),
|
|
_ => Result(),
|
|
_ => Result(),
|
|
_ => Result(),
|
|
_ => Result(),
|
|
RequestExit: () => { },
|
|
ResolveText: _ => null,
|
|
OpenOnStart: false);
|
|
}
|
|
|
|
public FakeChargenView View { get; }
|
|
public CharacterCreationRuntimeBindings Bindings { get; }
|
|
public bool ProvideView { get; set; } = true;
|
|
|
|
private static RuntimeCommandResult Result() =>
|
|
new(RuntimeCommandStatus.Accepted, Generation);
|
|
|
|
public sealed class FakeChargenView : IRuntimeCharacterCreationView
|
|
{
|
|
public RuntimeCharacterCreationSnapshot Snapshot { get; set; } =
|
|
new(
|
|
Generation,
|
|
IsActive: true,
|
|
Revision: 1,
|
|
HeritageId: 0u,
|
|
GenderKey: 0u,
|
|
Appearance: RuntimeCharacterCreationAppearance.Default,
|
|
Template: RuntimeCharacterCreationSnapshot.TemplateUnset,
|
|
Attributes: default,
|
|
AttributeLockMask: 0u,
|
|
TotalAttributeCredits: 0u,
|
|
RemainingAttributeCredits: 0,
|
|
TotalSkillCredits: 0u,
|
|
RemainingSkillCredits: 0,
|
|
Name: string.Empty,
|
|
StartArea: -1,
|
|
Slot: 0u,
|
|
VerificationPending: false,
|
|
LastLocalRefusal: default,
|
|
LastRejection: null,
|
|
LastCreated: null);
|
|
|
|
public ChargenOptions Options => ChargenOptions.Empty;
|
|
|
|
public ChargenSkillAdvancementClass GetSkillLevel(uint skillId) =>
|
|
ChargenSkillAdvancementClass.Inactive;
|
|
|
|
public IDisposable Subscribe(IRuntimeCharacterCreationObserver observer) =>
|
|
NullSubscription.Instance;
|
|
|
|
private sealed class NullSubscription : IDisposable
|
|
{
|
|
public static readonly NullSubscription Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
}
|
|
|
|
private static ElementInfo ButtonInfo(uint id) =>
|
|
new() { Id = id, Type = 1u, Width = 100f, Height = 30f };
|
|
}
|