diff --git a/Source/ACE.DatLoader.Tests/SurfaceTextureTests.cs b/Source/ACE.DatLoader.Tests/SurfaceTextureTests.cs new file mode 100644 index 0000000000..c0951e5b84 --- /dev/null +++ b/Source/ACE.DatLoader.Tests/SurfaceTextureTests.cs @@ -0,0 +1,103 @@ +using System.IO; +using System.Linq; +using ACE.DatLoader.FileTypes; +using ACE.Entity.Enum; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace ACE.DatLoader.Tests +{ + /// + /// Tests the SurfaceTexture header and texture-reference layout without + /// depending on installed DAT archives or the DatManager singleton. + /// + [TestClass] + public class SurfaceTextureTests + { + [TestMethod] + public void Unpack_TwoTextureReferences_PreservesRetailLayout() + { + // The IDs mirror an observed HighRes/Portal pair. Header bytes are + // constructed in-memory so the test can run without client DAT files. + var data = BuildRecord(0x0500000C, 0, TextureType.Texture2D, + 0x06003B9D, 0x06003B9E); + + var result = UnpackCompletely(data); + + Assert.AreEqual(0x0500000Cu, result.Id); + Assert.AreEqual(0u, result.DataCategory); + Assert.AreEqual(TextureType.Texture2D, result.TextureType); + CollectionAssert.AreEqual(new uint[] { 0x06003B9D, 0x06003B9E }, result.Textures.ToArray()); + } + + [TestMethod] + public void Unpack_NonzeroDataCategory_RetainsUnsignedBits() + { + // A synthetic boundary case: retail SurfaceTexture categories + // observed so far were all zero, but the field is a UInt32 on disk. + var data = BuildRecord(0x05000001, 0xF0000001u, TextureType.Cube, + 0x06000001); + + var result = UnpackCompletely(data); + + Assert.AreEqual(0xF0000001u, result.DataCategory); + Assert.AreEqual(TextureType.Cube, result.TextureType); + CollectionAssert.AreEqual(new uint[] { 0x06000001 }, result.Textures.ToArray()); + } + + [TestMethod] + public void Unpack_ZeroTextureReferences_ConsumesCompleteRecord() + { + var data = BuildRecord(0x05000002, 0, TextureType.Texture2D); + + var result = UnpackCompletely(data); + + Assert.AreEqual(0u, result.DataCategory); + Assert.AreEqual(TextureType.Texture2D, result.TextureType); + Assert.AreEqual(0, result.Textures.Count); + } + + [TestMethod] + public void Unpack_HeaderMatchesExistingRenderTextureLayout() + { + // RenderTexture is an independently implemented reader for the + // same binary field layout. Compare their decoded values. + var data = BuildRecord(0x05000003, 0x80000000u, TextureType.Movie2D, + 0x06000010, 0x06000011); + + var surface = UnpackCompletely(data); + var render = UnpackCompletely(data); + + Assert.AreEqual(render.Id, surface.Id); + Assert.AreEqual(render.DataCategory, surface.DataCategory); + Assert.AreEqual(render.TextureType, surface.TextureType); + CollectionAssert.AreEqual(render.Textures.ToArray(), surface.Textures.ToArray()); + } + + private static T UnpackCompletely(byte[] bytes) where T : FileType, new() + { + using var stream = new MemoryStream(bytes, writable: false); + using var reader = new BinaryReader(stream); + var value = new T(); + value.Unpack(reader); + Assert.AreEqual(stream.Length, stream.Position, + "The parser must consume exactly the bytes in the record."); + return value; + } + + private static byte[] BuildRecord(uint id, uint category, + TextureType textureType, params uint[] textureIds) + { + using var stream = new MemoryStream(); + using (var writer = new BinaryWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true)) + { + writer.Write(id); // UInt32 resource ID + writer.Write(category); // UInt32 DataCategory + writer.Write((byte)textureType); // Byte TextureType + writer.Write(textureIds.Length); // Int32 List count + foreach (var textureId in textureIds) + writer.Write(textureId); + } + return stream.ToArray(); + } + } +} diff --git a/Source/ACE.DatLoader/FileTypes/SurfaceTexture.cs b/Source/ACE.DatLoader/FileTypes/SurfaceTexture.cs index 6bd609820d..53fd6f0607 100644 --- a/Source/ACE.DatLoader/FileTypes/SurfaceTexture.cs +++ b/Source/ACE.DatLoader/FileTypes/SurfaceTexture.cs @@ -1,5 +1,6 @@ using ACE.Common; using ACE.DatLoader.Entity; +using ACE.Entity.Enum; using System; using System.Collections.Generic; using System.IO; @@ -10,17 +11,24 @@ namespace ACE.DatLoader.FileTypes [DatFileType(DatFileType.SurfaceTexture)] public class SurfaceTexture : FileType { - // public int Id { get; private set; } - public int Unknown { get; private set; } - public byte UnknownByte { get; private set; } - // public int TextureCount { get; private set; } - public List Textures { get; private set; } = new List(); // These values correspond to a Surface (0x06) entry + // The original client identifies ImgTex as SURFACETEXTURE (DB type 0x0B). + // DBObj::Serialize reads/writes this field for categorized resource types. + // All examined retail SurfaceTexture records have DataCategory == 0. + public uint DataCategory { get; private set; } + + // ImgTex::Serialize writes a fixed byte value of 2. + // RenderTexture uses TextureType.Texture2D for this value. + // On read, the original ImgTex serializer skips the byte without interpreting it. + public TextureType TextureType { get; private set; } + + // References to Texture resources (IDs in the 0x06 and 0x07 ranges). + public List Textures { get; private set; } = new List(); // References to Texture (0x06) entries. public override void Unpack(BinaryReader reader) { Id = reader.ReadUInt32(); - Unknown = reader.ReadInt32(); - UnknownByte = reader.ReadByte(); + DataCategory = reader.ReadUInt32(); + TextureType = (TextureType)reader.ReadByte(); Textures.Unpack(reader); } }