using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using DatReaderWriter.Types; using Xunit.Sdk; namespace AcDream.App.Tests.UI; /// /// Campaign CH slice CH6c: pins the window-opacity ALPHA CHOKEPOINT — /// 's private ApplyAlpha, reached by every /// public draw call (, /// /, /// and — new this slice — and /// ). Retail's ChatInterface::SetOpacity /// @0x004F3120 sets ONE alpha on the whole composited window surface, chrome /// AND text together — before this slice, DrawStringDat passed /// applyAlpha: false so glyphs stayed opaque over a translucent window. /// /// /// Builds a real over the in-memory /// test double (no live GPU, no shader /// compile — RecordingGpuDevice.CreatePipeline just wraps the /// description) so TextRenderer.DebugSpriteSegments can be read back /// directly instead of asserting through a GPU flush. /// /// public sealed class UiRenderContextAlphaTests { private sealed class NullGpuFrameSource : ICurrentGpuFrameSource { public IGpuFrame? CurrentFrame => null; } private static (TextRenderer renderer, UiRenderContext ctx) Build() { var device = new RecordingGpuDevice(); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(800f, 600f)); var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); return (renderer, ctx); } private static UiDatFont BuildFont() => new( fgTex: 1, fgW: 64, fgH: 64, bgTex: 0, bgW: 0, bgH: 0, lineHeight: 16f, baselineOffset: 12f, glyphs: new Dictionary { ['A'] = new FontCharDesc { Unicode = 'A', Width = 8, Height = 16, OffsetX = 0, OffsetY = 0, HorizontalOffsetBefore = 0, HorizontalOffsetAfter = 0, VerticalOffsetBefore = 0, }, }); /// Same shape as but with a non-zero /// background (outline) atlas, so 's /// outline: true branch actually emits its background sprite pass. private static UiDatFont BuildOutlinedFont() => new( fgTex: 1, fgW: 64, fgH: 64, bgTex: 2, bgW: 64, bgH: 64, lineHeight: 16f, baselineOffset: 12f, glyphs: new Dictionary { ['A'] = new FontCharDesc { Unicode = 'A', Width = 8, Height = 16, OffsetX = 0, OffsetY = 0, HorizontalOffsetBefore = 0, HorizontalOffsetAfter = 0, VerticalOffsetBefore = 0, }, }); /// Bakes a real from a system TTF so /// (the BitmapFont path, distinct /// from the dat-font above) can /// be exercised end-to-end. Skips rather than fails on a machine with none /// of 's well-known /// paths — matches the skip pattern other environment-dependent tests in /// this suite already use (e.g. RetailSelectionAssetTests). private static BitmapFont BuildBitmapFontOrSkip(IGpuDevice device) { byte[]? ttf = BitmapFont.TryLoadSystemMonospaceFont(); if (ttf is null) throw SkipException.ForSkip("No system TTF font found for BitmapFont construction."); return new BitmapFont(device, ttf, pixelHeight: 16f); } // -- DrawSprite: full-opacity identity --------------------------------- [Fact] public void FullOpacity_DrawSprite_MatchesRequestedAlpha_Identity() { // Pin: with no window opacity pushed (AlphaMod == 1, matching every // production window today), output is byte-identical to a tint's own // alpha — this slice must not change ANY existing full-opacity render. (TextRenderer renderer, UiRenderContext ctx) = Build(); Assert.Equal(1f, ctx.AlphaMod); ctx.DrawSprite(7u, 0, 0, 10, 10, 0, 0, 1, 1, new Vector4(1f, 1f, 1f, 1f)); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(7u, seg.Texture); Assert.Equal(1f, seg.Alpha); } [Fact] public void HalfOpacityWindow_MultipliesEverySpriteEmission() { (TextRenderer renderer, UiRenderContext ctx) = Build(); ctx.PushAlpha(0.5f); Assert.Equal(0.5f, ctx.AlphaMod); ctx.DrawSprite(7u, 0, 0, 10, 10, 0, 0, 1, 1, new Vector4(1f, 1f, 1f, 1f)); ctx.PopAlpha(); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(0.5f, seg.Alpha); // Pop restores full opacity for whatever draws next. Assert.Equal(1f, ctx.AlphaMod); } [Fact] public void NestedPushAlpha_ComposesMultiplicatively() { (TextRenderer renderer, UiRenderContext ctx) = Build(); ctx.PushAlpha(0.5f); ctx.PushAlpha(0.4f); Assert.Equal(0.2f, ctx.AlphaMod, 5); ctx.DrawSprite(7u, 0, 0, 10, 10, 0, 0, 1, 1, new Vector4(1f, 1f, 1f, 1f)); ctx.PopAlpha(); ctx.PopAlpha(); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(0.2f, seg.Alpha, 5); } [Fact] public void NestedPushAlpha_MultipliesAgainstAnAlreadyTintedColor() { // A widget that already draws at partial alpha (e.g. a translucent // background sprite, tint.W = 0.8) fades FURTHER when its window is // also translucent — the two multipliers compose, they don't clobber. (TextRenderer renderer, UiRenderContext ctx) = Build(); ctx.PushAlpha(0.5f); ctx.DrawSprite(7u, 0, 0, 10, 10, 0, 0, 1, 1, new Vector4(1f, 1f, 1f, 0.8f)); ctx.PopAlpha(); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(0.4f, seg.Alpha, 5); } [Fact] public void PopAlpha_WithoutMatchingPush_IsANoOp() { (TextRenderer renderer, UiRenderContext ctx) = Build(); ctx.PopAlpha(); Assert.Equal(1f, ctx.AlphaMod); } // -- DrawStringDat: the CH6c fix (text now respects window alpha) ----- [Fact] public void FullOpacity_DrawStringDat_MatchesRequestedAlpha_Identity() { (TextRenderer renderer, UiRenderContext ctx) = Build(); UiDatFont font = BuildFont(); ctx.DrawStringDat(font, "A", 0, 0, new Vector4(1f, 1f, 1f, 1f)); // bgTex == 0, so only the foreground (fill) pass draws — one segment // on the font's foreground texture (id 1). var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(1u, seg.Texture); Assert.Equal(1f, seg.Alpha); } [Fact] public void HalfOpacityWindow_MultipliesDatFontGlyphAlpha() { // The retail-faithful fix: ChatInterface::SetOpacity (0x004F3120) fades // the WHOLE composited window surface, glyphs included — before this // slice, DrawStringDat's applyAlpha:false meant text stayed sharp over // a translucent window (see the retired class-doc comment this test // replaces the assumption of). (TextRenderer renderer, UiRenderContext ctx) = Build(); UiDatFont font = BuildFont(); ctx.PushAlpha(0.5f); ctx.DrawStringDat(font, "A", 0, 0, new Vector4(1f, 1f, 1f, 1f)); ctx.PopAlpha(); var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(0.5f, seg.Alpha); } [Fact] public void HalfOpacityWindow_MultipliesDatFontOutlineAndForegroundPassAlpha() { // CH6c review NIT: BuildFont() above has bgTex == 0, so every existing // DrawStringDat alpha test only ever exercised the foreground (fill) // sprite pass. The background (outline) pass is a SEPARATE // DrawSpriteAbsolute(applyAlpha: true) call site — this pins it too, // with a font whose background atlas is actually present (bgTex != 0) // and outline: true so both passes fire. (TextRenderer renderer, UiRenderContext ctx) = Build(); UiDatFont font = BuildOutlinedFont(); ctx.PushAlpha(0.5f); ctx.DrawStringDat(font, "A", 0, 0, new Vector4(1f, 1f, 1f, 1f), outline: true); ctx.PopAlpha(); // Background pass (texture 2) submitted first, then foreground (texture 1) — // both routes guarded by the same ApplyAlpha chokepoint. Assert.Equal(2, renderer.DebugSpriteSegments.Count); foreach (var seg in renderer.DebugSpriteSegments) Assert.Equal(0.5f, seg.Alpha); } // -- DrawString (BitmapFont path): the same alpha chokepoint, guarded --- [Fact] public void FullOpacity_DrawString_BitmapFontPath_MatchesRequestedAlpha_Identity() { // CH6c review NIT: DrawStringDat (retail dat-font glyphs) had its own // alpha regression tests above; UiRenderContext.DrawString — the // BitmapFont path used for D.6 world-space HUD text — had none. Both // route through the SAME private ApplyAlpha, but nothing pinned it for // this path specifically. var device = new RecordingGpuDevice(); using BitmapFont font = BuildBitmapFontOrSkip(device); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(800f, 600f)); var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); ctx.DrawString("A", 0, 0, new Vector4(1f, 1f, 1f, 1f), font); (int vertexCount, float alpha) = renderer.DebugTextBuffer; Assert.True(vertexCount > 0); Assert.Equal(1f, alpha); } [Fact] public void HalfOpacityWindow_MultipliesBitmapFontGlyphAlpha() { var device = new RecordingGpuDevice(); using BitmapFont font = BuildBitmapFontOrSkip(device); var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); renderer.Begin(new Vector2(800f, 600f)); var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); ctx.PushAlpha(0.5f); ctx.DrawString("A", 0, 0, new Vector4(1f, 1f, 1f, 1f), font); ctx.PopAlpha(); (int vertexCount, float alpha) = renderer.DebugTextBuffer; Assert.True(vertexCount > 0); Assert.Equal(0.5f, alpha); } }