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