using AcDream.Core.Textures; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Types; namespace AcDream.Core.Tests.Textures; public class SurfaceDecoderTests { [Fact] public void Decode_A8R8G8B8_ConvertsToRgba8() { // Source format is B, G, R, A in memory (little-endian ARGB). // One 2x2 image: red, green, blue, white pixels. var src = new byte[] { 0x00, 0x00, 0xFF, 0xFF, // red (B=0, G=0, R=255, A=255) 0x00, 0xFF, 0x00, 0xFF, // green 0xFF, 0x00, 0x00, 0xFF, // blue 0xFF, 0xFF, 0xFF, 0xFF, // white }; var rs = new RenderSurface { Width = 2, Height = 2, Format = PixelFormat.PFID_A8R8G8B8, SourceData = src, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Equal(2, decoded.Width); Assert.Equal(2, decoded.Height); Assert.Equal(16, decoded.Rgba8.Length); // 2*2*4 // red pixel, in RGBA: 255, 0, 0, 255 Assert.Equal(0xFF, decoded.Rgba8[0]); Assert.Equal(0x00, decoded.Rgba8[1]); Assert.Equal(0x00, decoded.Rgba8[2]); Assert.Equal(0xFF, decoded.Rgba8[3]); } [Fact] public void Decode_UnsupportedFormat_ReturnsMagenta() { var rs = new RenderSurface { Width = 4, Height = 4, Format = PixelFormat.PFID_INDEX16, // not implemented path SourceData = new byte[32], }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } [Fact] public void Decode_A8_NonAdditive_ProducesWhitePlusAlpha() { // Default (isAdditive: false) = WB FillA8 semantics: R=G=B=255, A=val. // Used for non-additive entity surfaces where A8 is a pure alpha channel. var src = new byte[] { 0x00, 0x40, 0x80, 0xFF }; // 2x2 image var rs = new RenderSurface { Width = 2, Height = 2, Format = PixelFormat.PFID_A8, SourceData = src, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Equal(2, decoded.Width); Assert.Equal(2, decoded.Height); Assert.Equal(16, decoded.Rgba8.Length); // Each input byte expands to (255, 255, 255, val) — white with varying alpha Assert.Equal(new byte[] { 255, 255, 255, 0x00, 255, 255, 255, 0x40, 255, 255, 255, 0x80, 255, 255, 255, 0xFF, }, decoded.Rgba8); } [Fact] public void Decode_A8_Additive_ReplicatesByteToAllChannels() { // isAdditive=true = WB FillA8Additive semantics: R=G=B=A=val. // Used for terrain blending alpha masks (TerrainAtlas always passes isAdditive:true). var src = new byte[] { 0x00, 0x40, 0x80, 0xFF }; // 2x2 image var rs = new RenderSurface { Width = 2, Height = 2, Format = PixelFormat.PFID_A8, SourceData = src, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: true); Assert.Equal(16, decoded.Rgba8.Length); // Each input byte fans out to all four channels Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x40, 0x40, 0x80, 0x80, 0x80, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, }, decoded.Rgba8); } [Fact] public void Decode_CustomLscapeAlpha_TreatedIdenticallyToA8() { // PFID_CUSTOM_LSCAPE_ALPHA (0xF4) is AC's custom format for terrain // blending alpha maps. Pixel layout is identical to PFID_A8 — one // byte of alpha per pixel — so the decoder routes both through the // same DecodeA8 implementation. Default (isAdditive:false) → R=G=B=255, A=val. var src = new byte[] { 0x10, 0x20, 0x30, 0x40 }; // 2x2 var rs = new RenderSurface { Width = 2, Height = 2, Format = PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA, SourceData = src, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Equal(16, decoded.Rgba8.Length); Assert.Equal(new byte[] { 255, 255, 255, 0x10, 255, 255, 255, 0x20, 255, 255, 255, 0x30, 255, 255, 255, 0x40, }, decoded.Rgba8); } [Fact] public void Decode_A8_WithShortSourceData_ReturnsMagenta() { var rs = new RenderSurface { Width = 4, Height = 4, Format = PixelFormat.PFID_A8, SourceData = new byte[8], // expects 16 }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } [Fact] public void Decode_NullSourceData_ReturnsMagenta() { var rs = new RenderSurface { Width = 4, Height = 4, Format = PixelFormat.PFID_A8R8G8B8, SourceData = null!, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } [Fact] public void Decode_TruncatedA8R8G8B8_ReturnsMagenta() { // Buffer too small for width*height*4. var rs = new RenderSurface { Width = 2, Height = 2, Format = PixelFormat.PFID_A8R8G8B8, SourceData = new byte[8], // should be 16 }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } [Fact] public void DecodeSolidColor_Opaque_PreservesAlpha() { var color = new ColorARGB { Alpha = 0xFF, Red = 0x11, Green = 0x22, Blue = 0x33 }; var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 0f); Assert.Equal(1, decoded.Width); Assert.Equal(1, decoded.Height); Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0xFF }, decoded.Rgba8); } [Fact] public void DecodeSolidColor_FullyTranslucent_AlphaGoesToZero() { // Surfaces marked Base1Solid + Translucent with Translucency=1.0 are // AC's convention for "invisible placeholder surfaces" — the engine renders // them as nothing. Alpha must go to 0 so the mesh shader's discard rule // makes them invisible. var color = new ColorARGB { Alpha = 0xFF, Red = 0xC8, Green = 0xC8, Blue = 0xC8 }; var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 1f); Assert.Equal(0, decoded.Rgba8[3]); // alpha must be zero } [Fact] public void DecodeIndex16_ClipMap_ZerosAlphaForLowIndices() { // Build a 4x1 INDEX16 surface with indices 0, 1, 7, 8. // On a clipmap surface, indices 0..7 should be fully transparent and // index 8 should render with its palette color. var rs = new RenderSurface { Width = 4, Height = 1, Format = PixelFormat.PFID_INDEX16, SourceData = new byte[] { 0x00, 0x00, // index 0 0x01, 0x00, // index 1 0x07, 0x00, // index 7 0x08, 0x00, // index 8 }, }; var palette = new Palette(); for (int i = 0; i < 16; i++) palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xAA, Green = 0xBB, Blue = 0xCC }); var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap: true); // Pixels 0, 1, 2 (indices 0, 1, 7) should be fully transparent. Assert.Equal(0, decoded.Rgba8[3]); // pixel 0 alpha Assert.Equal(0, decoded.Rgba8[7]); // pixel 1 alpha Assert.Equal(0, decoded.Rgba8[11]); // pixel 2 alpha // Pixel 3 (index 8) should have the palette alpha. Assert.Equal(0xFF, decoded.Rgba8[15]); Assert.Equal(0xAA, decoded.Rgba8[12]); } // ---- PFID_P8 tests ------------------------------------------------------- [Fact] public void Decode_P8_LooksUpPaletteForEachByte() { // 2x1 surface: pixel 0 → palette index 0 (red), pixel 1 → palette index 1 (blue). var palette = new Palette(); palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xFF, Green = 0x00, Blue = 0x00 }); // index 0 = red palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0x00, Green = 0x00, Blue = 0xFF }); // index 1 = blue var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_P8, SourceData = new byte[] { 0x00, 0x01 }, // indices }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); Assert.Equal(8, decoded.Rgba8.Length); // 2 pixels * 4 channels // Pixel 0: red Assert.Equal(new byte[] { 0xFF, 0x00, 0x00, 0xFF }, decoded.Rgba8[0..4]); // Pixel 1: blue Assert.Equal(new byte[] { 0x00, 0x00, 0xFF, 0xFF }, decoded.Rgba8[4..8]); } [Fact] public void Decode_P8_ClipMap_ZerosAlphaForLowIndices() { // 4x1 surface with indices 0, 3, 7, 8. // isClipMap=true → indices 0..7 should be fully transparent; index 8 opaque. var palette = new Palette(); for (int i = 0; i < 16; i++) palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xCC, Green = 0xDD, Blue = 0xEE }); var rs = new RenderSurface { Width = 4, Height = 1, Format = PixelFormat.PFID_P8, SourceData = new byte[] { 0x00, 0x03, 0x07, 0x08 }, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap: true); // Indices 0, 3, 7 should be transparent. Assert.Equal(0, decoded.Rgba8[3]); // pixel 0 alpha Assert.Equal(0, decoded.Rgba8[7]); // pixel 1 alpha Assert.Equal(0, decoded.Rgba8[11]); // pixel 2 alpha // Index 8 should be opaque with palette color. Assert.Equal(0xFF, decoded.Rgba8[15]); Assert.Equal(0xCC, decoded.Rgba8[12]); } [Fact] public void Decode_P8_WithoutPalette_ReturnsMagenta() { // P8 without palette passed → falls through to magenta. var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_P8, SourceData = new byte[] { 0x00, 0x01 }, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } [Fact] public void Decode_P8_TruncatedData_ReturnsMagenta() { var palette = new Palette(); palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xAA, Green = 0xBB, Blue = 0xCC }); var rs = new RenderSurface { Width = 4, Height = 1, Format = PixelFormat.PFID_P8, SourceData = new byte[] { 0x00, 0x00 }, // expects 4 bytes }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); Assert.Same(DecodedTexture.Magenta, decoded); } // ---- PFID_R8G8B8 tests --------------------------------------------------- [Fact] public void Decode_R8G8B8_ConvertsToRgba8WithOpaqueAlpha() { // PFID_R8G8B8 is stored on disk as B,G,R (little-endian 24-bit BGR). // 2x1 surface: first pixel = red (B=0,G=0,R=255), second = green (B=0,G=255,R=0). var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_R8G8B8, SourceData = new byte[] { 0x00, 0x00, 0xFF, // B=0, G=0, R=255 → red 0x00, 0xFF, 0x00, // B=0, G=255, R=0 → green }, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Equal(8, decoded.Rgba8.Length); // Red pixel → R=255, G=0, B=0, A=255 Assert.Equal(new byte[] { 0xFF, 0x00, 0x00, 0xFF }, decoded.Rgba8[0..4]); // Green pixel → R=0, G=255, B=0, A=255 Assert.Equal(new byte[] { 0x00, 0xFF, 0x00, 0xFF }, decoded.Rgba8[4..8]); } [Fact] public void Decode_R8G8B8_TruncatedData_ReturnsMagenta() { var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_R8G8B8, SourceData = new byte[] { 0x00, 0x00 }, // expects 6 bytes }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } // ---- PFID_X8R8G8B8 tests ------------------------------------------------- [Fact] public void Decode_X8R8G8B8_ConvertsToRgba8DiscardingXByte() { // PFID_X8R8G8B8 is stored on disk as B,G,R,X (DirectX little-endian 32-bit). // The X byte is unused padding — NOT alpha. Output alpha must be 255. // 2x1: first pixel = blue (B=255,G=0,R=0,X=0xDE), second = white (B=255,G=255,R=255,X=0xAD). var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_X8R8G8B8, SourceData = new byte[] { 0xFF, 0x00, 0x00, 0xDE, // B=255, G=0, R=0, X=0xDE → blue, alpha forced 255 0xFF, 0xFF, 0xFF, 0xAD, // B=255, G=255, R=255, X=0xAD → white, alpha forced 255 }, }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Equal(8, decoded.Rgba8.Length); // Blue pixel → R=0, G=0, B=255, A=255 (X byte discarded) Assert.Equal(new byte[] { 0x00, 0x00, 0xFF, 0xFF }, decoded.Rgba8[0..4]); // White pixel → R=255, G=255, B=255, A=255 (X byte discarded) Assert.Equal(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }, decoded.Rgba8[4..8]); } [Fact] public void Decode_X8R8G8B8_TruncatedData_ReturnsMagenta() { var rs = new RenderSurface { Width = 2, Height = 1, Format = PixelFormat.PFID_X8R8G8B8, SourceData = new byte[] { 0xFF, 0x00, 0x00, 0xDE }, // expects 8 bytes (2 pixels) }; var decoded = SurfaceDecoder.DecodeRenderSurface(rs); Assert.Same(DecodedTexture.Magenta, decoded); } }