fix(chargen): Campaign CC gate round 1 re-test 2 — R3-5/R3-6 color-key swatch/gradient textures
Re-derived gmCGAppearancePage::DoColorSpots @0x0047d850 and DoGradDisk @0x0047da90: retail does NOT multiply-tint the swatch/grad-circle's authored sprite. It builds a fresh composited surface once (CreateLocalSurface + Blit), then calls SurfaceWindow::ReplaceColor against old-color RGBAColor(0,0,0,1) (opaque black — the spot template's own placeholder fill, live-DAT-pixel-confirmed: the 37x44 "spot" resource has a genuine solid-black CENTER and a genuine non-black RING) — swapping every exact opaque-black pixel for the swatch's real color while leaving the ring untouched. A multiply-tint (Batch G's mechanism) is architecturally wrong: black multiplied by any color stays black (never recolors the center), and multiplying the ring's own non-black pixels corrupts them — exactly the reported "we tint the ring" symptom. Beyond-count swatches (R3-5b) use a COMPLETELY DIFFERENT authored resource (enum 0x1000000f, "blank" — pixel-confirmed almost no black at all, i.e. genuinely different art) shown untinted, and retail's own pColor->SetVisible(1) is unconditional for all 9 swatches (never hidden). For Eyes (R3-6), DoGradDisk's Eyes branch blits the "grad plug" icon (enum 0x10000010) untinted, and SetSelection's own Eyes/non-Eyes tail never hides m_pGradCircle at all — a correction to this port's prior "_gradCircle.Visible = !isEyes" line. Ported via a new ChargenColorSpotComposer (CPU-side decode-once + per-color bake-and-cache-once through the existing TextureCache.UploadRgba8 seam — the same shape IconComposer.GetSpellComponentIcon already established for item icons, just matching black instead of white) and a new opt-in UiButton.ColorKeyFaceResolver / reuse of the existing UiDatElement.RuntimeImageTexture seam — both additive. Tint keeps its existing meaning for every reader/test; the grad circle's Tint stays a genuine multiply for the non-Eyes case (retail's own Blit_Multiply there). Wired as a fourth late-bound composition seam (SwatchTextureSource), same pattern/site as the existing three color-computation seams. Code-complete, unit/live-DAT-tested (including pixel-level proof of the spot/blank templates' actual content); the user's connected visual gate is owed — no client launches this batch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c9313edc63
commit
2886f79f37
7 changed files with 455 additions and 22 deletions
|
|
@ -236,6 +236,28 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
internal IChargenClothingTableSource? ClothingTableSource { get; set; }
|
||||
internal IChargenPaletteColorSource? PaletteColorSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// R3-5/R3-6 (Campaign CC gate round 1 re-test 2) late-bound seam, same
|
||||
/// pattern as the three above: null (the default) leaves every swatch/
|
||||
/// the gradient disc falling back to their ordinary FaceFileOverride/
|
||||
/// ActiveFile draw — live-DAT-confirmed, each swatch and the grad
|
||||
/// circle DOES author its own DirectState sprite (the RAW,
|
||||
/// un-recolored spot/gradDisk template respectively — the exact same
|
||||
/// RenderSurface DIDs <c>ChargenColorSpotComposer</c> resolves by
|
||||
/// enum), so the unwired fallback shows that authored art untinted
|
||||
/// (Tint stays Vector4.One when no color data exists), not literally
|
||||
/// nothing. Wired by the composition root to a
|
||||
/// <see cref="ChargenColorSpotComposer"/> (same site as
|
||||
/// <see cref="PalSetSource"/> et al) once a <c>TextureCache</c> exists.
|
||||
/// See <see cref="UiButton.ColorKeyFaceResolver"/>'s own doc for WHY a
|
||||
/// fourth seam is needed beyond the three color-computation ones above:
|
||||
/// those three answer "what RGB is this swatch", this one answers
|
||||
/// "what actual bitmap should this element's face show" — a materially
|
||||
/// different question once the answer can no
|
||||
/// longer be a plain multiply-tint (see R3-5's finding).
|
||||
/// </summary>
|
||||
internal IChargenSwatchTextureSource? SwatchTextureSource { get; set; }
|
||||
|
||||
/// <summary>The authored viewport (<c>0x100003bb</c>) — the composition
|
||||
/// root assigns its <c>Renderer</c> once the graphics backend exists,
|
||||
/// mirroring the paperdoll's own late <c>viewport.Renderer = ...</c>
|
||||
|
|
@ -759,29 +781,42 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
if (_swatches[i] is not { } swatch)
|
||||
continue;
|
||||
bool visible = i < displayCount;
|
||||
swatch.Visible = visible;
|
||||
// Closeout Group 1: a genuine multiplicative sprite tint on the
|
||||
// swatch's own authored spot art (UiButton.Tint), replacing the
|
||||
// Batch G flat-fill overlay. Vector4.One (identity) reproduces
|
||||
// the swatch's bare authored art untouched — both "no color data
|
||||
// yet" (sources unwired) and "beyond this part's color count".
|
||||
// R3-5 correction: retail's own pColor->SetVisible(1)
|
||||
// (DoColorSpots' own loop) is UNCONDITIONAL for all 9 swatches
|
||||
// — beyond displayCount shows the BLANK/blocked art (still
|
||||
// visible), never hides the element. Batch C's own "beyond
|
||||
// count -> hide" half is retired by this correction.
|
||||
swatch.Visible = true;
|
||||
// Closeout Group 1 (Tint) + R3-5 correction (ColorKeyFaceResolver):
|
||||
// Tint keeps communicating "this swatch's color is X" for every
|
||||
// existing reader/test; the resolver is the SEPARATE decision
|
||||
// of which pre-baked bitmap actually draws (see
|
||||
// UiButton.ColorKeyFaceResolver's own doc for why a multiply
|
||||
// over the authored sprite is retail-wrong here).
|
||||
ChargenSwatchRgb? rgb = visible ? swatchColors[i] : null;
|
||||
swatch.Tint = rgb is { } c ? ToTintColor(c) : Vector4.One;
|
||||
swatch.ColorKeyFaceResolver = BuildSwatchTextureResolver(visible, rgb);
|
||||
}
|
||||
|
||||
// AP-217 (Batch C PARTIAL -> Batch G, R2-5, FULL):
|
||||
// gmCGAppearancePage::DoGradDisk @0x0047da90 blits the blank "grad
|
||||
// plug" for Eyes (DoGradDisk(this, 1), called from SetSelection
|
||||
// AP-217 (Batch C PARTIAL -> Batch G, R2-5, FULL) + R3-6 correction:
|
||||
// gmCGAppearancePage::DoGradDisk @0x0047da90 blits the "grad plug"
|
||||
// icon for Eyes (DoGradDisk(this, 1), called from SetSelection
|
||||
// @0x0047e85d) and a gradient graphic TINTED with the CURRENTLY
|
||||
// SELECTED swatch's own color otherwise (SetColor @0x0047dd50's
|
||||
// tail, DoGradDisk(this, 0) after m_iCurColor is already updated —
|
||||
// @0x0047de18). Nose/Mouth/Skin always tint from swatch index 0
|
||||
// (SetSelection hard-codes eyeColor = 0 for those three cases,
|
||||
// matching displayCount's own reasoning above).
|
||||
// matching displayCount's own reasoning above). R3-6: SetSelection
|
||||
// @0x0047e260's own Eyes/non-Eyes branches (@0x0047e859-0047e878)
|
||||
// call ONLY DoGradDisk + m_pShadeScroll->SetVisible — NEITHER
|
||||
// branch ever calls m_pGradCircle->SetVisible; the disc element
|
||||
// itself is never hidden for Eyes, only its CONTENT (source image)
|
||||
// changes. The prior "_gradCircle.Visible = !isEyes" line was a
|
||||
// misreading — corrected to always-visible.
|
||||
if (_gradCircle is not null)
|
||||
{
|
||||
bool isEyes = _currentPart == Part.Eyes;
|
||||
_gradCircle.Visible = !isEyes;
|
||||
_gradCircle.Visible = true;
|
||||
// Closeout Group 1: same Tint mechanism as the swatches above —
|
||||
// the gradient disc's own authored art is multiplied by the
|
||||
// currently-selected swatch's color instead of an overlay child.
|
||||
|
|
@ -793,6 +828,16 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
ChargenSwatchRgb? gradColor =
|
||||
gradIndex >= 0 && gradIndex < swatchColors.Length ? swatchColors[gradIndex] : null;
|
||||
_gradCircle.Tint = gradColor is { } gc ? ToTintColor(gc) : Vector4.One;
|
||||
// R3-6: the disc's own authored media is empty (live-DAT-
|
||||
// confirmed) — supply the missing base bitmap. Non-Eyes shows
|
||||
// gradDisk (multiplied by Tint above, matching retail's own
|
||||
// Blit_Multiply); Eyes shows the static plug icon UNTINTED
|
||||
// (Tint is already Vector4.One for Eyes via gradIndex==-1
|
||||
// above, matching retail's plain Blit_Normal).
|
||||
uint gradTexture = SwatchTextureSource is { } textures
|
||||
? (isEyes ? textures.GradPlugTexture : textures.GradDiskTexture)
|
||||
: 0u;
|
||||
_gradCircle.RuntimeImageTexture = gradTexture;
|
||||
}
|
||||
|
||||
ChargenShadeSlot? shadeSlot = ShadeSlotFor(_currentPart);
|
||||
|
|
@ -812,6 +857,40 @@ internal sealed class CharacterCreationAppearancePage : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-5: which pre-baked bitmap (if any) a swatch's own
|
||||
/// <see cref="UiButton.ColorKeyFaceResolver"/> should resolve to on
|
||||
/// THIS refresh. A fresh closure per call (not a cached delegate) so a
|
||||
/// later <see cref="SwatchTextureSource"/> assignment (the composition
|
||||
/// root wires it once the graphics backend exists, strictly after this
|
||||
/// page's own construction — same ordering as
|
||||
/// <see cref="PreviewControl"/>) is picked up the next time the
|
||||
/// resolver actually RUNS (at draw time), not frozen at the OLD (null)
|
||||
/// value from an earlier refresh.
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Beyond <c>displayCount</c> (<paramref name="visible"/>
|
||||
/// false): always resolve to the BLOCKED/blank art — retail shows this
|
||||
/// for every swatch its current color count doesn't reach.</description></item>
|
||||
/// <item><description>In range with a real color: resolve to that
|
||||
/// color's own baked spot.</description></item>
|
||||
/// <item><description>In range but no color data yet (palette seams
|
||||
/// unwired): null — falls back to the ordinary FaceFileOverride/
|
||||
/// ActiveFile draw, which shows the swatch's own AUTHORED DirectState
|
||||
/// sprite (the raw, un-recolored spot template — live-DAT-confirmed
|
||||
/// present, see <see cref="SwatchTextureSource"/>'s own doc) untinted,
|
||||
/// matching this page's pre-R3-5 "fully inert until wired"
|
||||
/// disposition for the OTHER three palette seams.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private Func<uint>? BuildSwatchTextureResolver(bool visible, ChargenSwatchRgb? rgb)
|
||||
{
|
||||
if (!visible)
|
||||
return () => SwatchTextureSource?.BlankSpotTexture ?? 0u;
|
||||
if (rgb is { } c)
|
||||
return () => SwatchTextureSource?.GetActiveSpotTexture(c) ?? 0u;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Real swatch/gradient colors (R2-5) ──────────────────────────────
|
||||
|
||||
private static readonly ChargenSwatchRgb?[] EmptySwatchColors = new ChargenSwatchRgb?[SwatchIds.Length];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue