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; 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, }, }); // -- 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); } }