fix(chargen): Campaign CC CC6b-MOUNT review fix round — F1-F13
Fixes every finding from the dual-lens review of 34c6fceab0 (architectural
PASS-with-items, retail-fidelity FAIL). Re-derived every decomp citation
against docs/research/named-retail/acclient_2013_pseudo_c.txt directly
rather than trusting the reviewer's transcription.
Wrap/normalize semantics (F1): CycleIndex's decrement-from-Unset landed on
0; the decomp's shared decrement tail (label_47f065/label_47f6d9, the same
switch the headgear ring was ported from) computes new=cur-1=-2 on the raw
signed int32, which wraps to count-1 — matching headgear's own ring shape.
Also ports the spin body-click normalize-and-write-back retail's cases
0xa5-0xae all share (NormalizeChoiceOnSelect), which acdream had dropped
entirely. Flips the one test that pinned the wrong expectation and adds
select-zone coverage no prior test isolated.
Heritage gate (F3): Update's Gearknight/Olthoi/OlthoiAcid branches reset
SetChoice(FACE)/SetSelection(HAIR) unconditionally, not only when Clothes
was showing — a conditional gate stranded Nose/Mouth as the current part
under a Face-tab session.
Doc corrections propagated everywhere they repeated (F4, F5, F6, plus the
plan doc's own CC6b-MOUNT ledger row for F1/F3): the gmBarberUI heading
citation conflated PostInit with InitializePage; Random's Appearance
disable was mislabeled a placeholder when it's really AP-212's unported
RandomizeAppearance/RandomizeClothing gap; the master-page doc still called
the Appearance page content-inert after this campaign made it real.
Visual substitutions widened (F2): AP-215 named only two of the Appearance
page's swatch/spin substitutions. Ports the two cheap ones directly —
current-part highlight via SetSelection's SetState(1)/SetState(6), routed
through the existing UiButtonStateMachine.Normal/Highlight ids and
IUiDatStateful.TrySetRetailState seam (installed-DAT-confirmed
ToggleBehavior=true on all nine spins); the shade scrollbar's SetVisible(0)
for Eyes vs acdream's Enabled=false. Files the other five (DoColorSpots,
the inert GradCircle, spin-caption/heritage-caption loss, the Skin-spin
MoveTo reposition, the Gearknight-boundary randomize calls) as new register
rows AP-216..AP-220 and corrects the plan doc's false claim that AP-215
already named the GradCircle.
Unlocked DAT read (F7, BLOCKER): ChargenPreviewController.Rebuild called
ChargenAppearanceFactory.TryCompose outside _datLock while the very next
line correctly locked TryBuildAnimated — CC6a's own F4 class of bug,
reintroduced at this catalog's first production call site. Wrapped in the
same lock; documented the invariant on ChargenAppearanceCatalog itself.
One-shot preview mount (F8): LivePresentationComposition reads
ChargenPreviewViewportWidget once, but its underlying mount
(CharacterCreationUiMountCoordinator) is explicitly retryable while this
GPU-resource composition pass is not — unlike PaperdollViewportWidget,
which IS eager/non-retryable, so the "mirrors Paperdoll" doc claim was
false. Retrofitting cross-frame retry here would mean restructuring this
composition's one-shot contract for every private viewport (paperdoll,
creature appraisal) and FrameRootComposition's fixed frame-group array —
out of this round's blast radius. Corrected the doc and made the failure
loud (a diagnostic log) instead of silent.
Dispose leak (F9): ChargenPreviewController.Dispose left the preview
WorldEntity referenced by the leased renderer until the renderer's own,
later disposal. Releases it on its own teardown now.
Test-quality items (F10, F11, F13): pinned the spin arrow widths
(47px, both arrows) the 174 zone boundary is derived from, plus a
controller test for the previously-uncovered select zone. Measured the
shade scrollbar's authored orientation instead of assuming it — it is
VERTICAL (33x85) — which is a real production bug: UiScrollbar only routed
scalar-mode mouse events when Horizontal was true, so the shade control
never fired in production. Added OnVerticalScalarEvent/DrawVerticalScalar
mirroring the existing horizontal scalar path. Converted
ChargenPreviewControllerTests from silent-pass [Fact] to the shared
InstalledDatFactAttribute skip-reporting pattern.
Adjudication (F12): AD-101's retirement leaves TryBeginFinish's four local
refusals (NoName/AttributeCreditsUnspent/AlreadyPending/RosterFull) with no
heritage/gender gate — currently latent since Finish stays hard-disabled
this round. Amended the campaign plan's CC5 slice scope to require BOTH a
heritage/gender refusal AND a real RandomizeCharacter port before the
connected user gate opens Finish; noted the interaction on AP-214's own
register row. No CC5 implementation in this commit.
Gates: dotnet build -c Release green across the full solution. App suite
(Release, ACDREAM_PROBE_LIVE_MOUNT=1) 5223/3 skips, Runtime suite
1713/0 — both clean across repeated runs. A full-solution run surfaced
three pre-existing, previously-documented flakes unrelated to this change
(Streaming.LandblockBuildFactoryTests/LandblockPresentationPipelineTests
#402, Core.Net.Tests.NakEmissionTests loss soak) — each confirmed passing
in isolation, consistent with their known full-suite-parallelism-timing
history; none touch any file this commit changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
34c6fceab0
commit
d2a71152d2
14 changed files with 571 additions and 54 deletions
|
|
@ -249,6 +249,12 @@ public sealed class UiScrollbar : UiElement
|
|||
return;
|
||||
}
|
||||
|
||||
if (ScalarChanged is not null)
|
||||
{
|
||||
DrawVerticalScalar(ctx, resolve);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Model is not { } m) return;
|
||||
|
||||
// Track background — TILED vertically (retail DrawMode=Normal). The native track
|
||||
|
|
@ -313,6 +319,30 @@ public sealed class UiScrollbar : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix round F11 (Campaign CC CC6b-MOUNT review): the mirror-image
|
||||
/// counterpart of the horizontal scalar draw block above, for scalar-mode
|
||||
/// bars authored VERTICAL (taller than wide) — retail's chargen shade
|
||||
/// scrollbar (<c>0x10000321</c>) is one, measured against the installed
|
||||
/// EoR dat (<c>Width=33 Height=85</c>). Retail's own
|
||||
/// <c>UIElement_Scrollbar</c> is one class handling both a model-driven
|
||||
/// list scroll and a scalar-value slider on EITHER axis; this class only
|
||||
/// had the horizontal half of the scalar shape before this fix, so a
|
||||
/// vertically-authored scalar bar (like the shade control) drew nothing
|
||||
/// scalar-specific and fell through to the model-mode branch below,
|
||||
/// which requires a <see cref="UiScrollable"/> <see cref="Model"/> a
|
||||
/// scalar-mode bar never has.
|
||||
/// </summary>
|
||||
private void DrawVerticalScalar(
|
||||
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve)
|
||||
{
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
float thumbHeight = ScalarThumbExtent(resolve, Height);
|
||||
float travel = MathF.Max(0f, Height - thumbHeight);
|
||||
float y = travel * ScalarPosition;
|
||||
DrawSprite(ctx, resolve, ThumbSprite, 0f, y, Width, thumbHeight);
|
||||
}
|
||||
|
||||
/// <summary>Draw a sprite stretched 1:1 to the dest rect.</summary>
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
|
|
@ -412,8 +442,17 @@ public sealed class UiScrollbar : UiElement
|
|||
if (e.Type == UiEventType.MouseMove)
|
||||
_hoveredButton = ButtonAt(e.Data1, e.Data2);
|
||||
|
||||
if (Horizontal && ScalarChanged is not null)
|
||||
return OnScalarEvent(e);
|
||||
// Fix round F11: retail's chargen shade scrollbar (0x10000321) is
|
||||
// authored VERTICAL (measured against the installed dat), but a
|
||||
// scalar-mode bar (ScalarChanged set, no Model) has always been
|
||||
// possible on either axis in retail's own UIElement_Scrollbar.
|
||||
// Gating this dispatch on Horizontal silently dropped every mouse
|
||||
// event for a vertical scalar bar — it fell through the Horizontal
|
||||
// Model branch below too, then hit "Model is not {} m => return
|
||||
// false" since a scalar bar has no Model, so NOTHING ever routed to
|
||||
// ScalarChanged in production for this orientation.
|
||||
if (ScalarChanged is not null)
|
||||
return Horizontal ? OnScalarEvent(e) : OnVerticalScalarEvent(e);
|
||||
|
||||
if (Horizontal && Model is not null)
|
||||
return OnHorizontalModelEvent(e);
|
||||
|
|
@ -590,14 +629,77 @@ public sealed class UiScrollbar : UiElement
|
|||
return false;
|
||||
}
|
||||
|
||||
private float ScalarThumbWidth(Func<uint, (uint tex, int w, int h)>? resolve)
|
||||
/// <summary>F11: the vertical mirror of <see cref="OnScalarEvent"/> —
|
||||
/// same click-thumb-to-drag / click-track-to-jump shape, along Y/Height
|
||||
/// instead of X/Width. Reuses <see cref="_dragOffsetY"/> (otherwise only
|
||||
/// touched by the vertical MODEL-mode drag, mutually exclusive with
|
||||
/// scalar mode on one instance) rather than adding a third offset field.
|
||||
/// </summary>
|
||||
private bool OnVerticalScalarEvent(in UiEvent e)
|
||||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
case UiEventType.MouseDown:
|
||||
{
|
||||
float thumbHeight = ScalarThumbExtent(SpriteResolve, Height);
|
||||
float travel = MathF.Max(1f, Height - thumbHeight);
|
||||
float thumbY = travel * ScalarPosition;
|
||||
float y = e.Data2;
|
||||
// OP5 re-check R2 (mirrored from OnScalarEvent): latch
|
||||
// before the jump so the jump's own tick defers its flush
|
||||
// to MouseUp's DragCompleted.
|
||||
_draggingThumb = true;
|
||||
if (y >= thumbY && y <= thumbY + thumbHeight)
|
||||
{
|
||||
_dragOffsetY = y - thumbY;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dragOffsetY = thumbHeight * 0.5f;
|
||||
ChangeScalarPosition((y - _dragOffsetY) / travel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
case UiEventType.MouseMove when _draggingThumb:
|
||||
{
|
||||
float thumbHeight = ScalarThumbExtent(SpriteResolve, Height);
|
||||
float travel = MathF.Max(1f, Height - thumbHeight);
|
||||
ChangeScalarPosition(((float)e.Data2 - _dragOffsetY) / travel);
|
||||
return true;
|
||||
}
|
||||
|
||||
case UiEventType.MouseUp:
|
||||
{
|
||||
bool wasDragging = _draggingThumb;
|
||||
_draggingThumb = false;
|
||||
_pressedButton = EndButton.None;
|
||||
if (wasDragging) DragCompleted?.Invoke();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float ScalarThumbWidth(Func<uint, (uint tex, int w, int h)>? resolve) =>
|
||||
ScalarThumbExtent(resolve, Width);
|
||||
|
||||
/// <summary>F11: generalized over <see cref="ScalarThumbWidth"/> so
|
||||
/// <see cref="DrawVerticalScalar"/> can size the thumb along the
|
||||
/// authored axis (native sprite width for a horizontal bar, native
|
||||
/// sprite height for a vertical one) instead of assuming horizontal.
|
||||
/// </summary>
|
||||
private float ScalarThumbExtent(
|
||||
Func<uint, (uint tex, int w, int h)>? resolve, float axisLength)
|
||||
{
|
||||
if (resolve is not null && ThumbSprite != 0)
|
||||
{
|
||||
var (_, width, _) = resolve(ThumbSprite);
|
||||
if (width > 0) return MathF.Min(width, Width);
|
||||
var (_, width, height) = resolve(ThumbSprite);
|
||||
int native = Horizontal ? width : height;
|
||||
if (native > 0) return MathF.Min(native, axisLength);
|
||||
}
|
||||
return MathF.Min(16f, Width);
|
||||
return MathF.Min(16f, axisLength);
|
||||
}
|
||||
|
||||
private void ChangeScalarPosition(float position)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue