From 1667d668a421c677cd21f20cd70f918151c72fb3 Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:39:12 +0100 Subject: [PATCH] Refactor TR4/5 to hide compression (#621) Compression implementation details are now hidden. Tests for TR4/5 improved. --- TRFDControl/FDControl.cs | 8 +- TRLevelControl/Build/TRObjectMeshBuilder.cs | 149 ++++++ TRLevelControl/Control/TR1LevelControl.cs | 3 + TRLevelControl/Control/TR2LevelControl.cs | 3 + TRLevelControl/Control/TR3LevelControl.cs | 3 + TRLevelControl/Control/TR4LevelControl.cs | 468 ++++++++++++++--- TRLevelControl/Control/TR5LevelControl.cs | 493 ++++++++++++++---- TRLevelControl/IO/TRLevelReader.cs | 50 +- TRLevelControl/IO/TRLevelWriter.cs | 50 +- TRLevelControl/ITRLevelObserver.cs | 11 + .../Model/Common/Enums/TRChunkType.cs | 9 + TRLevelControl/Model/TR4/TR4Chunk.cs | 8 - TRLevelControl/Model/TR4/TR4Level.cs | 38 +- TRLevelControl/Model/TR4/TR4LevelDataChunk.cs | 225 -------- TRLevelControl/Model/TR4/TR4Mesh.cs | 60 +-- .../Model/TR4/TR4SkyAndFont32Chunk.cs | 6 - TRLevelControl/Model/TR4/TR4Textiles.cs | 45 ++ TRLevelControl/Model/TR4/TR4Texture16Chunk.cs | 8 - TRLevelControl/Model/TR4/TR4Texture32Chunk.cs | 8 - TRLevelControl/Model/TR4/TR4TextureData.cs | 8 + TRLevelControl/Model/TR5/TR5Level.cs | 41 +- TRLevelControl/Model/TR5/TR5LevelDataChunk.cs | 221 -------- TRLevelControl/Model/TR5/TR5Textiles.cs | 43 ++ TRLevelControl/TR4FileReadUtilities.cs | 243 +++------ TRLevelControl/TR5FileReadUtilities.cs | 145 ++---- TRLevelControl/TRLevelControlBase.cs | 10 +- .../Base/Observers/ObserverBase.cs | 25 + .../Base/Observers/TR45Observer.cs | 80 +++ TRLevelControlTests/Base/TestBase.cs | 109 ++-- TRLevelControlTests/TR4/IOTests.cs | 14 +- TRLevelControlTests/TR5/IOTests.cs | 6 +- 31 files changed, 1459 insertions(+), 1131 deletions(-) create mode 100644 TRLevelControl/Build/TRObjectMeshBuilder.cs create mode 100644 TRLevelControl/ITRLevelObserver.cs create mode 100644 TRLevelControl/Model/Common/Enums/TRChunkType.cs delete mode 100644 TRLevelControl/Model/TR4/TR4Chunk.cs delete mode 100644 TRLevelControl/Model/TR4/TR4LevelDataChunk.cs delete mode 100644 TRLevelControl/Model/TR4/TR4SkyAndFont32Chunk.cs create mode 100644 TRLevelControl/Model/TR4/TR4Textiles.cs delete mode 100644 TRLevelControl/Model/TR4/TR4Texture16Chunk.cs delete mode 100644 TRLevelControl/Model/TR4/TR4Texture32Chunk.cs create mode 100644 TRLevelControl/Model/TR4/TR4TextureData.cs delete mode 100644 TRLevelControl/Model/TR5/TR5LevelDataChunk.cs create mode 100644 TRLevelControl/Model/TR5/TR5Textiles.cs create mode 100644 TRLevelControlTests/Base/Observers/ObserverBase.cs create mode 100644 TRLevelControlTests/Base/Observers/TR45Observer.cs diff --git a/TRFDControl/FDControl.cs b/TRFDControl/FDControl.cs index 595355d67..62bb94c5e 100644 --- a/TRFDControl/FDControl.cs +++ b/TRFDControl/FDControl.cs @@ -66,12 +66,12 @@ public void ParseFromLevel(TR3Level level) public void ParseFromLevel(TR4Level level) { - ParseLevel(level.LevelDataChunk.Rooms.SelectMany(r => r.Sectors), level.LevelDataChunk.FloorData); + ParseLevel(level.Rooms.SelectMany(r => r.Sectors), level.FloorData); } public void ParseFromLevel(TR5Level level) { - ParseLevel(level.LevelDataChunk.Rooms.SelectMany(r => r.RoomData.SectorList), level.LevelDataChunk.FloorData); + ParseLevel(level.Rooms.SelectMany(r => r.RoomData.SectorList), level.FloorData); } private void ParseLevel(IEnumerable roomSectors, List floorData) @@ -267,12 +267,12 @@ public void WriteToLevel(TR3Level level) public void WriteToLevel(TR4Level level) { - Flatten(level.LevelDataChunk.Rooms.SelectMany(r => r.Sectors), level.LevelDataChunk.FloorData); + Flatten(level.Rooms.SelectMany(r => r.Sectors), level.FloorData); } public void WriteToLevel(TR5Level level) { - Flatten(level.LevelDataChunk.Rooms.SelectMany(r => r.RoomData.SectorList), level.LevelDataChunk.FloorData); + Flatten(level.Rooms.SelectMany(r => r.RoomData.SectorList), level.FloorData); } private void Flatten(IEnumerable sectors, List data) diff --git a/TRLevelControl/Build/TRObjectMeshBuilder.cs b/TRLevelControl/Build/TRObjectMeshBuilder.cs new file mode 100644 index 000000000..6d51409e6 --- /dev/null +++ b/TRLevelControl/Build/TRObjectMeshBuilder.cs @@ -0,0 +1,149 @@ +using TRLevelControl.Model; + +namespace TRLevelControl.Build; + +public class TRObjectMeshBuilder +{ + private readonly ITRLevelObserver _observer; + + public List Meshes { get; private set; } + public List MeshPointers { get; private set; } + + public TRObjectMeshBuilder(ITRLevelObserver observer = null) + { + _observer = observer; + } + + public void BuildObjectMeshes(TRLevelReader reader) + { + uint numMeshData = reader.ReadUInt32(); + byte[] meshData = reader.ReadBytes((int)numMeshData * sizeof(short)); + + uint numMeshPointers = reader.ReadUInt32(); + MeshPointers = new(reader.ReadUInt32s(numMeshPointers)); + + // The mesh pointer list can contain duplicates so we must make + // sure to iterate over distinct values only + uint[] pointers = MeshPointers.Distinct().ToArray(); + + Meshes = new(); + + using MemoryStream ms = new(meshData); + using TRLevelReader meshReader = new(ms); + + for (int i = 0; i < pointers.Length; i++) + { + uint meshPointer = pointers[i]; + meshReader.BaseStream.Position = meshPointer; + + TR4Mesh mesh = new(); + Meshes.Add(mesh); + + mesh.Pointer = meshPointer; + + mesh.Centre = TR2FileReadUtilities.ReadVertex(meshReader); + mesh.CollRadius = meshReader.ReadInt32(); + + mesh.NumVertices = meshReader.ReadInt16(); + mesh.Vertices = new TRVertex[mesh.NumVertices]; + for (int j = 0; j < mesh.NumVertices; j++) + { + mesh.Vertices[j] = TR2FileReadUtilities.ReadVertex(meshReader); + } + + mesh.NumNormals = meshReader.ReadInt16(); + if (mesh.NumNormals > 0) + { + mesh.Normals = new TRVertex[mesh.NumNormals]; + for (int j = 0; j < mesh.NumNormals; j++) + { + mesh.Normals[j] = TR2FileReadUtilities.ReadVertex(meshReader); + } + } + else + { + mesh.Lights = new short[Math.Abs(mesh.NumNormals)]; + for (int j = 0; j < mesh.Lights.Length; j++) + { + mesh.Lights[j] = meshReader.ReadInt16(); + } + } + + mesh.NumTexturedRectangles = meshReader.ReadInt16(); + mesh.TexturedRectangles = new TR4MeshFace4[mesh.NumTexturedRectangles]; + for (int j = 0; j < mesh.NumTexturedRectangles; j++) + { + mesh.TexturedRectangles[j] = TR4FileReadUtilities.ReadTR4MeshFace4(meshReader); + } + + mesh.NumTexturedTriangles = meshReader.ReadInt16(); + mesh.TexturedTriangles = new TR4MeshFace3[mesh.NumTexturedTriangles]; + for (int j = 0; j < mesh.NumTexturedTriangles; j++) + { + mesh.TexturedTriangles[j] = TR4FileReadUtilities.ReadTR4MeshFace3(meshReader); + } + + // Padding - TR1-3 use 0s, TR4 and 5 use random values. This allows tests to observe and restore. + if (_observer != null && meshReader.BaseStream.Position % 4 != 0) + { + long nextPointer = i < pointers.Length - 1 ? pointers[i + 1] : meshReader.BaseStream.Length; + long paddingSize = nextPointer - meshReader.BaseStream.Position; + _observer.OnMeshPaddingRead(meshPointer, meshReader.ReadBytes((int)paddingSize).ToList()); + } + } + } + + public byte[] Serialize(TR4Mesh mesh) + { + using MemoryStream stream = new(); + TRLevelWriter writer = new(stream); + + writer.Write(mesh.Centre.Serialize()); + writer.Write(mesh.CollRadius); + + writer.Write(mesh.NumVertices); + foreach (TRVertex vert in mesh.Vertices) + { + writer.Write(vert.Serialize()); + } + + writer.Write(mesh.NumNormals); + if (mesh.NumNormals > 0) + { + foreach (TRVertex normal in mesh.Normals) + { + writer.Write(normal.Serialize()); + } + } + else + { + foreach (short light in mesh.Lights) + { + writer.Write(light); + } + } + + writer.Write(mesh.NumTexturedRectangles); + foreach (TR4MeshFace4 face in mesh.TexturedRectangles) + { + writer.Write(face.Serialize()); + } + + writer.Write(mesh.NumTexturedTriangles); + foreach (TR4MeshFace3 face in mesh.TexturedTriangles) + { + writer.Write(face.Serialize()); + } + + // 4-byte alignment for mesh data + int paddingSize = (int)writer.BaseStream.Position % 4; + if (paddingSize != 0) + { + IEnumerable padding = _observer?.GetMeshPadding(mesh.Pointer) ?? + Enumerable.Repeat((byte)0, paddingSize); + writer.Write(padding.ToArray()); + } + + return stream.ToArray(); + } +} diff --git a/TRLevelControl/Control/TR1LevelControl.cs b/TRLevelControl/Control/TR1LevelControl.cs index 23df8684f..c730abc2a 100644 --- a/TRLevelControl/Control/TR1LevelControl.cs +++ b/TRLevelControl/Control/TR1LevelControl.cs @@ -6,6 +6,9 @@ namespace TRLevelControl; public class TR1LevelControl : TRLevelControlBase { + public TR1LevelControl(ITRLevelObserver observer = null) + : base(observer) { } + protected override TR1Level CreateLevel(TRFileVersion version) { TR1Level level = new() diff --git a/TRLevelControl/Control/TR2LevelControl.cs b/TRLevelControl/Control/TR2LevelControl.cs index 05e656657..ed279cce2 100644 --- a/TRLevelControl/Control/TR2LevelControl.cs +++ b/TRLevelControl/Control/TR2LevelControl.cs @@ -6,6 +6,9 @@ namespace TRLevelControl; public class TR2LevelControl : TRLevelControlBase { + public TR2LevelControl(ITRLevelObserver observer = null) + : base(observer) { } + protected override TR2Level CreateLevel(TRFileVersion version) { TR2Level level = new() diff --git a/TRLevelControl/Control/TR3LevelControl.cs b/TRLevelControl/Control/TR3LevelControl.cs index d041c1f6a..76123b8c5 100644 --- a/TRLevelControl/Control/TR3LevelControl.cs +++ b/TRLevelControl/Control/TR3LevelControl.cs @@ -6,6 +6,9 @@ namespace TRLevelControl; public class TR3LevelControl : TRLevelControlBase { + public TR3LevelControl(ITRLevelObserver observer = null) + : base(observer) { } + protected override TR3Level CreateLevel(TRFileVersion version) { TR3Level level = new() diff --git a/TRLevelControl/Control/TR4LevelControl.cs b/TRLevelControl/Control/TR4LevelControl.cs index 266cf8928..a593c2224 100644 --- a/TRLevelControl/Control/TR4LevelControl.cs +++ b/TRLevelControl/Control/TR4LevelControl.cs @@ -1,11 +1,14 @@ using System.Diagnostics; -using TRLevelControl.Compression; +using TRLevelControl.Build; using TRLevelControl.Model; namespace TRLevelControl; public class TR4LevelControl : TRLevelControlBase { + public TR4LevelControl(ITRLevelObserver observer = null) + : base(observer) { } + protected override TR4Level CreateLevel(TRFileVersion version) { TR4Level level = new() @@ -23,39 +26,393 @@ protected override TR4Level CreateLevel(TRFileVersion version) protected override void Read(TRLevelReader reader) { - // Texture chunk + ReadImages(reader); + ReadLevelDataChunk(reader); + ReadWAVData(reader); + } + + protected override void Write(TRLevelWriter writer) + { + WriteImages(writer); + WriteLevelDataChunk(writer); + WriteWAVData(writer); + } + + private void ReadImages(TRLevelReader reader) + { + _level.Images = new(); ushort roomCount = reader.ReadUInt16(); ushort objectCount = reader.ReadUInt16(); ushort bumpCount = reader.ReadUInt16(); - _level.Texture32Chunk = new(); - using TRLevelReader reader32 = reader.Inflate(_level.Texture32Chunk); - _level.Texture32Chunk.Rooms = reader32.ReadImage32s(roomCount); - _level.Texture32Chunk.Objects = reader32.ReadImage32s(objectCount); - _level.Texture32Chunk.Bump = reader32.ReadImage32s(bumpCount); + using TRLevelReader reader32 = reader.Inflate(TRChunkType.Images32); + _level.Images.Rooms.Images32 = reader32.ReadImage32s(roomCount); + _level.Images.Objects.Images32 = reader32.ReadImage32s(objectCount); + _level.Images.Bump.Images32 = reader32.ReadImage32s(bumpCount); + + using TRLevelReader reader16 = reader.Inflate(TRChunkType.Images16); + _level.Images.Rooms.Images16 = reader16.ReadImage16s(roomCount); + _level.Images.Objects.Images16 = reader16.ReadImage16s(objectCount); + _level.Images.Bump.Images16 = reader16.ReadImage16s(bumpCount); + + using TRLevelReader skyReader = reader.Inflate(TRChunkType.SkyFont); + _level.Images.Font = skyReader.ReadImage32(); + _level.Images.Sky = skyReader.ReadImage32(); + } + + private void WriteImages(TRLevelWriter writer) + { + Debug.Assert(_level.Images.Rooms.Images32.Count == _level.Images.Rooms.Images16.Count); + Debug.Assert(_level.Images.Objects.Images32.Count == _level.Images.Objects.Images16.Count); + Debug.Assert(_level.Images.Bump.Images32.Count == _level.Images.Bump.Images16.Count); + + writer.Write((ushort)_level.Images.Rooms.Images32.Count); + writer.Write((ushort)_level.Images.Objects.Images32.Count); + writer.Write((ushort)_level.Images.Bump.Images32.Count); + + using TRLevelWriter writer32 = new(); + writer32.Write(_level.Images.Rooms.Images32); + writer32.Write(_level.Images.Objects.Images32); + writer32.Write(_level.Images.Bump.Images32); + writer.Deflate(writer32, TRChunkType.Images32); + + using TRLevelWriter writer16 = new(); + writer16.Write(_level.Images.Rooms.Images16); + writer16.Write(_level.Images.Objects.Images16); + writer16.Write(_level.Images.Bump.Images16); + writer.Deflate(writer16, TRChunkType.Images16); + + using TRLevelWriter skyWriter = new(); + skyWriter.Write(_level.Images.Font); + skyWriter.Write(_level.Images.Sky); + writer.Deflate(skyWriter, TRChunkType.SkyFont); + } + + private void ReadLevelDataChunk(TRLevelReader mainReader) + { + using TRLevelReader reader = mainReader.Inflate(TRChunkType.LevelData); + + reader.ReadUInt32(); // Unused, always 0 + + ReadRooms(reader); + + ReadMeshData(reader); + ReadModelData(reader); + + ReadStaticMeshes(reader); + + ReadSprites(reader); + + ReadCameras(reader); + ReadSoundSources(reader); + + ReadBoxes(reader); + + ReadAnimatedTextures(reader); + ReadObjectTextures(reader); + + ReadEntities(reader); + + ReadSoundEffects(reader); + + reader.ReadUInt16s(3); // Always 0s + Debug.Assert(reader.BaseStream.Position == reader.BaseStream.Length); + } + + private void WriteLevelDataChunk(TRLevelWriter mainWriter) + { + using TRLevelWriter writer = new(); + + writer.Write((uint)0); // Unused, always 0 + + WriteRooms(writer); + + WriteMeshData(writer); + WriteModelData(writer); + + WriteStaticMeshes(writer); + + WriteSprites(writer); + + WriteCameras(writer); + WriteSoundSources(writer); + + WriteBoxes(writer); + + WriteAnimatedTextures(writer); + WriteObjectTextures(writer); + + WriteEntities(writer); + + WriteSoundEffects(writer); + + writer.Write(Enumerable.Repeat((ushort)0, 3).ToArray()); + + mainWriter.Deflate(writer, TRChunkType.LevelData); + } + + private void ReadRooms(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateRooms(reader, _level); + TR4FileReadUtilities.PopulateFloordata(reader, _level); + } + + private void WriteRooms(TRLevelWriter writer) + { + writer.Write((ushort)_level.Rooms.Count); + foreach (TR4Room room in _level.Rooms) + { + writer.Write(room.Serialize()); + } + + writer.Write((uint)_level.FloorData.Count); + writer.Write(_level.FloorData); + } + + private void ReadMeshData(TRLevelReader reader) + { + TRObjectMeshBuilder builder = new(_observer); + builder.BuildObjectMeshes(reader); + + _level.Meshes = builder.Meshes; + _level.MeshPointers = builder.MeshPointers; + } - _level.Texture16Chunk = new(); - using TRLevelReader reader16 = reader.Inflate(_level.Texture16Chunk); - _level.Texture16Chunk.Rooms = reader16.ReadImage16s(roomCount); - _level.Texture16Chunk.Objects = reader16.ReadImage16s(objectCount); - _level.Texture16Chunk.Bump = reader16.ReadImage16s(bumpCount); + private void WriteMeshData(TRLevelWriter writer) + { + TRObjectMeshBuilder builder = new(_observer); + List meshData = _level.Meshes.SelectMany(m => builder.Serialize(m)).ToList(); - _level.SkyAndFont32Chunk = new(); - using TRLevelReader skyReader = reader.Inflate(_level.SkyAndFont32Chunk); - _level.SkyAndFont32Chunk.Textiles = skyReader.ReadImage32s(2); // Font, sky + writer.Write((uint)meshData.Count / 2); + writer.Write(meshData.ToArray()); - //Level Data Chunk - //Get Raw Chunk Data - _level.LevelDataChunk = new TR4LevelDataChunk + writer.Write((uint)_level.MeshPointers.Count); + foreach (uint data in _level.MeshPointers) { - UncompressedSize = reader.ReadUInt32(), - CompressedSize = reader.ReadUInt32() - }; - _level.LevelDataChunk.CompressedChunk = reader.ReadBytes((int)_level.LevelDataChunk.CompressedSize); + writer.Write(data); + } + } + + private void ReadModelData(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateAnimations(reader, _level); + TR4FileReadUtilities.PopulateMeshTreesFramesModels(reader, _level); + } + + private void WriteModelData(TRLevelWriter writer) + { + writer.Write((uint)_level.Animations.Count); + foreach (TR4Animation anim in _level.Animations) + { + writer.Write(anim.Serialize()); + } + + writer.Write((uint)_level.StateChanges.Count); + foreach (TRStateChange sc in _level.StateChanges) + { + writer.Write(sc.Serialize()); + } + + writer.Write((uint)_level.AnimDispatches.Count); + foreach (TRAnimDispatch ad in _level.AnimDispatches) + { + writer.Write(ad.Serialize()); + } + + writer.Write((uint)_level.AnimCommands.Count); + foreach (TRAnimCommand ac in _level.AnimCommands) + { + writer.Write(ac.Serialize()); + } + + writer.Write((uint)_level.MeshTrees.Count * 4); //To get the correct number /= 4 is done during read, make sure to reverse it here. + foreach (TRMeshTreeNode node in _level.MeshTrees) + { + writer.Write(node.Serialize()); + } + + writer.Write((uint)_level.Frames.Count); + foreach (ushort frame in _level.Frames) + { + writer.Write(frame); + } + + writer.Write((uint)_level.Models.Count); + foreach (TRModel model in _level.Models) + { + writer.Write(model.Serialize()); + } + } + + private void ReadStaticMeshes(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateStaticMeshes(reader, _level); + } + + private void WriteStaticMeshes(TRLevelWriter writer) + { + writer.Write((uint)_level.StaticMeshes.Count); + foreach (TRStaticMesh sm in _level.StaticMeshes) + { + writer.Write(sm.Serialize()); + } + } + + private void ReadSprites(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateSprites(reader, _level); + } + + private void WriteSprites(TRLevelWriter writer) + { + writer.Write(TR4FileReadUtilities.SPRMarker.ToCharArray()); + + writer.Write((uint)_level.SpriteTextures.Count); + foreach (TRSpriteTexture st in _level.SpriteTextures) + { + writer.Write(st.Serialize()); + } + + writer.Write((uint)_level.SpriteSequences.Count); + foreach (TRSpriteSequence seq in _level.SpriteSequences) + { + writer.Write(seq.Serialize()); + } + } + + private void ReadCameras(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateCameras(reader, _level); + } + + private void WriteCameras(TRLevelWriter writer) + { + writer.Write((uint)_level.Cameras.Count); + foreach (TRCamera cam in _level.Cameras) + { + writer.Write(cam.Serialize()); + } + + writer.Write((uint)_level.FlybyCameras.Count); + foreach (TR4FlyByCamera flycam in _level.FlybyCameras) + { + writer.Write(flycam.Serialize()); + } + } + + private void ReadSoundSources(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateSoundSources(reader, _level); + } + + private void WriteSoundSources(TRLevelWriter writer) + { + writer.Write((uint)_level.SoundSources.Count); + foreach (TRSoundSource ssrc in _level.SoundSources) + { + writer.Write(ssrc.Serialize()); + } + } + + private void ReadBoxes(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateBoxesOverlapsZones(reader, _level); + } + + private void WriteBoxes(TRLevelWriter writer) + { + writer.Write((uint)_level.Boxes.Count); + foreach (TR2Box box in _level.Boxes) + { + writer.Write(box.Serialize()); + } + + writer.Write((uint)_level.Overlaps.Count); + foreach (ushort overlap in _level.Overlaps) + { + writer.Write(overlap); + } + + foreach (short zone in _level.Zones) + { + writer.Write(zone); + } + } + + private void ReadAnimatedTextures(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateAnimatedTextures(reader, _level); + } + + private void WriteAnimatedTextures(TRLevelWriter writer) + { + byte[] animTextureData = _level.AnimatedTextures.SelectMany(a => a.Serialize()).ToArray(); + writer.Write((uint)(animTextureData.Length / sizeof(ushort)) + 1); + writer.Write((ushort)_level.AnimatedTextures.Count); + writer.Write(animTextureData); + writer.Write(_level.AnimatedTexturesUVCount); + } + + private void ReadObjectTextures(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateObjectTextures(reader, _level); + } + + private void WriteObjectTextures(TRLevelWriter writer) + { + writer.Write(TR4FileReadUtilities.TEXMarker.ToCharArray()); + + writer.Write((uint)_level.ObjectTextures.Count); + foreach (TR4ObjectTexture otex in _level.ObjectTextures) + { + writer.Write(otex.Serialize()); + } + } + + private void ReadEntities(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateEntitiesAndAI(reader, _level); + } + + private void WriteEntities(TRLevelWriter writer) + { + writer.Write((uint)_level.Entities.Count); + writer.Write(_level.Entities); + + writer.Write((uint)_level.AIEntities.Count); + writer.Write(_level.AIEntities); + } + + private void ReadSoundEffects(TRLevelReader reader) + { + TR4FileReadUtilities.PopulateDemoSoundSampleIndices(reader, _level); + } - //Decompress - DecompressLevelDataChunk(_level); + private void WriteSoundEffects(TRLevelWriter writer) + { + writer.Write((ushort)_level.DemoData.Length); + writer.Write(_level.DemoData); + + foreach (short sound in _level.SoundMap) + { + writer.Write(sound); + } + + writer.Write((uint)_level.SoundDetails.Count); + foreach (TR3SoundDetails snd in _level.SoundDetails) + { + writer.Write(snd.Serialize()); + } + + writer.Write((uint)_level.SampleIndices.Count); + foreach (uint sampleindex in _level.SampleIndices) + { + writer.Write(sampleindex); + } + } + private void ReadWAVData(TRLevelReader reader) + { uint numSamples = reader.ReadUInt32(); _level.Samples = new(); @@ -67,45 +424,12 @@ protected override void Read(TRLevelReader reader) CompSize = reader.ReadUInt32(), }); - //Compressed chunk is actually NOT zlib compressed - it is simply a WAV file. _level.Samples[i].CompressedChunk = reader.ReadBytes((int)_level.Samples[i].CompSize); } } - protected override void Write(TRLevelWriter writer) + private void WriteWAVData(TRLevelWriter writer) { - // Texture chunk - Debug.Assert(_level.Texture32Chunk.Rooms.Count == _level.Texture16Chunk.Rooms.Count); - Debug.Assert(_level.Texture32Chunk.Objects.Count == _level.Texture16Chunk.Objects.Count); - Debug.Assert(_level.Texture32Chunk.Bump.Count == _level.Texture16Chunk.Bump.Count); - Debug.Assert(_level.SkyAndFont32Chunk.Textiles.Count == 2); - - writer.Write((ushort)_level.Texture32Chunk.Rooms.Count); - writer.Write((ushort)_level.Texture32Chunk.Objects.Count); - writer.Write((ushort)_level.Texture32Chunk.Bump.Count); - - using TRLevelWriter writer32 = new(); - writer32.Write(_level.Texture32Chunk.Rooms); - writer32.Write(_level.Texture32Chunk.Objects); - writer32.Write(_level.Texture32Chunk.Bump); - writer.Deflate(writer32, _level.Texture32Chunk); - - using TRLevelWriter writer16 = new(); - writer16.Write(_level.Texture16Chunk.Rooms); - writer16.Write(_level.Texture16Chunk.Objects); - writer16.Write(_level.Texture16Chunk.Bump); - writer.Deflate(writer16, _level.Texture16Chunk); - - using TRLevelWriter skyWriter = new(); - skyWriter.Write(_level.SkyAndFont32Chunk.Textiles); - writer.Deflate(skyWriter, _level.SkyAndFont32Chunk); - - // LevelData chunk - byte[] chunk = _level.LevelDataChunk.Serialize(); - writer.Write(_level.LevelDataChunk.UncompressedSize); - writer.Write(_level.LevelDataChunk.CompressedSize); - writer.Write(chunk); - writer.Write((uint)_level.Samples.Count); foreach (TR4Sample sample in _level.Samples) { @@ -114,32 +438,4 @@ protected override void Write(TRLevelWriter writer) writer.Write(sample.CompressedChunk); } } - - private static void DecompressLevelDataChunk(TR4Level lvl) - { - byte[] buffer = TRZlib.Decompress(lvl.LevelDataChunk.CompressedChunk); - - //Is the decompressed chunk the size we expected? - Debug.Assert(buffer.Length == lvl.LevelDataChunk.UncompressedSize); - - using MemoryStream stream = new(buffer, false); - using TRLevelReader lvlChunkReader = new(stream); - TR4FileReadUtilities.PopulateRooms(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateFloordata(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateMeshes(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateAnimations(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateMeshTreesFramesModels(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateStaticMeshes(lvlChunkReader, lvl); - TR4FileReadUtilities.VerifySPRMarker(lvlChunkReader); - TR4FileReadUtilities.PopulateSprites(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateCameras(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateSoundSources(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateBoxesOverlapsZones(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateAnimatedTextures(lvlChunkReader, lvl); - TR4FileReadUtilities.VerifyTEXMarker(lvlChunkReader); - TR4FileReadUtilities.PopulateObjectTextures(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateEntitiesAndAI(lvlChunkReader, lvl); - TR4FileReadUtilities.PopulateDemoSoundSampleIndices(lvlChunkReader, lvl); - TR4FileReadUtilities.VerifyLevelDataFinalSeperator(lvlChunkReader, lvl); - } } diff --git a/TRLevelControl/Control/TR5LevelControl.cs b/TRLevelControl/Control/TR5LevelControl.cs index a1f393daa..2834f5792 100644 --- a/TRLevelControl/Control/TR5LevelControl.cs +++ b/TRLevelControl/Control/TR5LevelControl.cs @@ -1,10 +1,14 @@ using System.Diagnostics; +using TRLevelControl.Build; using TRLevelControl.Model; namespace TRLevelControl; public class TR5LevelControl : TRLevelControlBase { + public TR5LevelControl(ITRLevelObserver observer = null) + : base(observer) { } + protected override TR5Level CreateLevel(TRFileVersion version) { TR5Level level = new() @@ -22,42 +26,420 @@ protected override TR5Level CreateLevel(TRFileVersion version) protected override void Read(TRLevelReader reader) { - // Texture chunk + ReadImages(reader); + ReadLaraAndWeather(reader); + ReadLevelDataChunk(reader); + } + + protected override void Write(TRLevelWriter writer) + { + WriteImages(writer); + WriteLaraAndWeather(writer); + WriteLevelDataChunk(writer); + } + + private void ReadImages(TRLevelReader reader) + { + _level.Images = new(); ushort roomCount = reader.ReadUInt16(); ushort objectCount = reader.ReadUInt16(); reader.ReadUInt16(); // Previously bump in TR4, no longer used - _level.Texture32Chunk = new(); - using TRLevelReader reader32 = reader.Inflate(_level.Texture32Chunk); - _level.Texture32Chunk.Rooms = reader32.ReadImage32s(roomCount); - _level.Texture32Chunk.Objects = reader32.ReadImage32s(objectCount); + using TRLevelReader reader32 = reader.Inflate(TRChunkType.Images32); + _level.Images.Rooms.Images32 = reader32.ReadImage32s(roomCount); + _level.Images.Objects.Images32 = reader32.ReadImage32s(objectCount); + + using TRLevelReader reader16 = reader.Inflate(TRChunkType.Images16); + _level.Images.Rooms.Images16 = reader16.ReadImage16s(roomCount); + _level.Images.Objects.Images16 = reader16.ReadImage16s(objectCount); - _level.Texture16Chunk = new(); - using TRLevelReader reader16 = reader.Inflate(_level.Texture16Chunk); - _level.Texture16Chunk.Rooms = reader16.ReadImage16s(roomCount); - _level.Texture16Chunk.Objects = reader16.ReadImage16s(objectCount); + using TRLevelReader skyReader = reader.Inflate(TRChunkType.SkyFont); + _level.Images.Shine = skyReader.ReadImage32(); + _level.Images.Font = skyReader.ReadImage32(); + _level.Images.Sky = skyReader.ReadImage32(); + } - _level.SkyAndFont32Chunk = new(); - using TRLevelReader skyReader = reader.Inflate(_level.SkyAndFont32Chunk); - _level.SkyAndFont32Chunk.Textiles = skyReader.ReadImage32s(3); // Shine, Font, Sky + private void WriteImages(TRLevelWriter writer) + { + Debug.Assert(_level.Images.Rooms.Images32.Count == _level.Images.Rooms.Images16.Count); + Debug.Assert(_level.Images.Objects.Images32.Count == _level.Images.Objects.Images16.Count); - //TR5 Specific + writer.Write((ushort)_level.Images.Rooms.Images32.Count); + writer.Write((ushort)_level.Images.Objects.Images32.Count); + writer.Write((ushort)0); // No bump + + using TRLevelWriter writer32 = new(); + writer32.Write(_level.Images.Rooms.Images32); + writer32.Write(_level.Images.Objects.Images32); + writer.Deflate(writer32, TRChunkType.Images32); + + using TRLevelWriter writer16 = new(); + writer16.Write(_level.Images.Rooms.Images16); + writer16.Write(_level.Images.Objects.Images16); + writer.Deflate(writer16, TRChunkType.Images16); + + using TRLevelWriter skyWriter = new(); + skyWriter.Write(_level.Images.Shine); + skyWriter.Write(_level.Images.Font); + skyWriter.Write(_level.Images.Sky); + writer.Deflate(skyWriter, TRChunkType.SkyFont); + } + + private void ReadLaraAndWeather(TRLevelReader reader) + { _level.LaraType = reader.ReadUInt16(); _level.WeatherType = reader.ReadUInt16(); - _level.Padding = reader.ReadBytes(28); - //Level Data Chunk - //Get Raw Chunk Data - _level.LevelDataChunk = new TR5LevelDataChunk + reader.ReadBytes(28); // Padding + } + + private void WriteLaraAndWeather(TRLevelWriter writer) + { + writer.Write(_level.LaraType); + writer.Write(_level.WeatherType); + + writer.Write(Enumerable.Repeat((byte)0, 28).ToArray()); + } + + private void ReadLevelDataChunk(TRLevelReader reader) + { + // TR5 level chunk is not compressed. + uint expectedLength = reader.ReadUInt32(); + uint compressedLength = reader.ReadUInt32(); + Debug.Assert(expectedLength == compressedLength); + + reader.ReadUInt32(); // Unused, always 0 + + ReadRooms(reader); + + ReadMeshData(reader); + ReadModelData(reader); + + ReadStaticMeshes(reader); + + ReadSprites(reader); + + ReadCameras(reader); + ReadSoundSources(reader); + + ReadBoxes(reader); + + ReadAnimatedTextures(reader); + ReadObjectTextures(reader); + + ReadEntities(reader); + + //reader.ReadUInt16(); // Unused, always 0 //IF we eliminate demodata + + ReadSoundEffects(reader); + ReadWAVData(reader); + } + + private void WriteLevelDataChunk(TRLevelWriter mainWriter) + { + using TRLevelWriter writer = new(); + + writer.Write((uint)0); // Unused, always 0 + + WriteRooms(writer); + + WriteMeshData(writer); + WriteModelData(writer); + + WriteStaticMeshes(writer); + + WriteSprites(writer); + + WriteCameras(writer); + WriteSoundSources(writer); + + WriteBoxes(writer); + + WriteAnimatedTextures(writer); + WriteObjectTextures(writer); + + WriteEntities(writer); + + //writer.Write((ushort)0); //IF we eliminate demodata + + WriteSoundEffects(writer); + + // Simulate zipping + byte[] data = (writer.BaseStream as MemoryStream).ToArray(); + mainWriter.Write((uint)data.Length); + mainWriter.Write((uint)data.Length); + mainWriter.Write(data); + + WriteWAVData(mainWriter); + } + + private void ReadRooms(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateRooms(reader, _level); + TR5FileReadUtilities.PopulateFloordata(reader, _level); + } + + private void WriteRooms(TRLevelWriter writer) + { + writer.Write((uint)_level.Rooms.Count); + foreach (TR5Room room in _level.Rooms) { - UncompressedSize = reader.ReadUInt32(), - CompressedSize = reader.ReadUInt32() - }; - _level.LevelDataChunk.CompressedChunk = reader.ReadBytes((int)_level.LevelDataChunk.CompressedSize); + writer.Write(room.Serialize()); + } + + writer.Write((uint)_level.FloorData.Count); + writer.Write(_level.FloorData); + } - //Decompress - DecompressLevelDataChunk(_level); + private void ReadMeshData(TRLevelReader reader) + { + TRObjectMeshBuilder builder = new(_observer); + builder.BuildObjectMeshes(reader); + + _level.Meshes = builder.Meshes; + _level.MeshPointers = builder.MeshPointers; + } + + private void WriteMeshData(TRLevelWriter writer) + { + TRObjectMeshBuilder builder = new(_observer); + List meshData = _level.Meshes.SelectMany(m => builder.Serialize(m)).ToList(); + + writer.Write((uint)meshData.Count / 2); + writer.Write(meshData.ToArray()); + + writer.Write((uint)_level.MeshPointers.Count); + foreach (uint data in _level.MeshPointers) + { + writer.Write(data); + } + } + + private void ReadModelData(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateAnimations(reader, _level); + TR5FileReadUtilities.PopulateMeshTreesFramesModels(reader, _level); + } + + private void WriteModelData(TRLevelWriter writer) + { + writer.Write((uint)_level.Animations.Count); + foreach (TR4Animation anim in _level.Animations) + { + writer.Write(anim.Serialize()); + } + + writer.Write((uint)_level.StateChanges.Count); + foreach (TRStateChange sc in _level.StateChanges) + { + writer.Write(sc.Serialize()); + } + + writer.Write((uint)_level.AnimDispatches.Count); + foreach (TRAnimDispatch ad in _level.AnimDispatches) + { + writer.Write(ad.Serialize()); + } + + writer.Write((uint)_level.AnimCommands.Count); + foreach (TRAnimCommand ac in _level.AnimCommands) + { + writer.Write(ac.Serialize()); + } + + writer.Write((uint)_level.MeshTrees.Count * 4); //To get the correct number /= 4 is done during read, make sure to reverse it here. + foreach (TRMeshTreeNode node in _level.MeshTrees) + { + writer.Write(node.Serialize()); + } + + writer.Write((uint)_level.Frames.Count); + foreach (ushort frame in _level.Frames) + { + writer.Write(frame); + } + + writer.Write((uint)_level.Models.Count); + foreach (TR5Model model in _level.Models) + { + writer.Write(model.Serialize()); + } + } + + private void ReadStaticMeshes(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateStaticMeshes(reader, _level); + } + private void WriteStaticMeshes(TRLevelWriter writer) + { + writer.Write((uint)_level.StaticMeshes.Count); + foreach (TRStaticMesh sm in _level.StaticMeshes) + { + writer.Write(sm.Serialize()); + } + } + + private void ReadSprites(TRLevelReader reader) + { + TR5FileReadUtilities.VerifySPRMarker(reader); + TR5FileReadUtilities.PopulateSprites(reader, _level); + } + + private void WriteSprites(TRLevelWriter writer) + { + writer.Write(TR5FileReadUtilities.SPRMarker.ToCharArray()); + + writer.Write((uint)_level.SpriteTextures.Count); + foreach (TRSpriteTexture st in _level.SpriteTextures) + { + writer.Write(st.Serialize()); + } + + writer.Write((uint)_level.SpriteSequences.Count); + foreach (TRSpriteSequence seq in _level.SpriteSequences) + { + writer.Write(seq.Serialize()); + } + } + + private void ReadCameras(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateCameras(reader, _level); + } + + private void WriteCameras(TRLevelWriter writer) + { + writer.Write((uint)_level.Cameras.Count); + foreach (TRCamera cam in _level.Cameras) + { + writer.Write(cam.Serialize()); + } + + writer.Write((uint)_level.FlybyCameras.Count); + foreach (TR4FlyByCamera flycam in _level.FlybyCameras) + { + writer.Write(flycam.Serialize()); + } + } + + private void ReadSoundSources(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateSoundSources(reader, _level); + } + + private void WriteSoundSources(TRLevelWriter writer) + { + writer.Write((uint)_level.SoundSources.Count); + foreach (TRSoundSource ssrc in _level.SoundSources) + { + writer.Write(ssrc.Serialize()); + } + } + + private void ReadBoxes(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateBoxesOverlapsZones(reader, _level); + } + + private void WriteBoxes(TRLevelWriter writer) + { + writer.Write((uint)_level.Boxes.Count); + foreach (TR2Box box in _level.Boxes) + { + writer.Write(box.Serialize()); + } + + writer.Write((uint)_level.Overlaps.Count); + foreach (ushort overlap in _level.Overlaps) + { + writer.Write(overlap); + } + + foreach (short zone in _level.Zones) + { + writer.Write(zone); + } + } + + private void ReadAnimatedTextures(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateAnimatedTextures(reader, _level); + } + + private void WriteAnimatedTextures(TRLevelWriter writer) + { + byte[] animTextureData = _level.AnimatedTextures.SelectMany(a => a.Serialize()).ToArray(); + writer.Write((uint)(animTextureData.Length / sizeof(ushort)) + 1); + writer.Write((ushort)_level.AnimatedTextures.Count); + writer.Write(animTextureData); + writer.Write(_level.AnimatedTexturesUVCount); + } + + private void ReadObjectTextures(TRLevelReader reader) + { + TR5FileReadUtilities.VerifyTEXMarker(reader); + TR5FileReadUtilities.PopulateObjectTextures(reader, _level); + } + + private void WriteObjectTextures(TRLevelWriter writer) + { + writer.Write(TR5FileReadUtilities.TEXMarker.ToCharArray()); + + writer.Write((uint)_level.ObjectTextures.Count); + foreach (TR5ObjectTexture otex in _level.ObjectTextures) + { + writer.Write(otex.Serialize()); + } + } + + private void ReadEntities(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateEntitiesAndAI(reader, _level); + } + + private void WriteEntities(TRLevelWriter writer) + { + writer.Write((uint)_level.Entities.Count); + writer.Write(_level.Entities); + + writer.Write((uint)_level.AIEntities.Count); + writer.Write(_level.AIEntities); + } + + private void ReadSoundEffects(TRLevelReader reader) + { + TR5FileReadUtilities.PopulateDemoSoundSampleIndices(reader, _level); + reader.ReadBytes(6); // Always 0xCD + } + + private void WriteSoundEffects(TRLevelWriter writer) + { + writer.Write((ushort)_level.DemoData.Length); + writer.Write(_level.DemoData); + + foreach (short sound in _level.SoundMap) + { + writer.Write(sound); + } + + writer.Write((uint)_level.SoundDetails.Count); + foreach (TR3SoundDetails snd in _level.SoundDetails) + { + writer.Write(snd.Serialize()); + } + + writer.Write((uint)_level.SampleIndices.Count); + foreach (uint sampleindex in _level.SampleIndices) + { + writer.Write(sampleindex); + } + + writer.Write(Enumerable.Repeat((byte)0xCD, 6).ToArray()); + } + + private void ReadWAVData(TRLevelReader reader) + { uint numSamples = reader.ReadUInt32(); _level.Samples = new(); @@ -69,46 +451,12 @@ protected override void Read(TRLevelReader reader) CompSize = reader.ReadUInt32(), }); - //Compressed chunk is actually NOT zlib compressed - it is simply a WAV file. _level.Samples[i].CompressedChunk = reader.ReadBytes((int)_level.Samples[i].CompSize); } } - protected override void Write(TRLevelWriter writer) + private void WriteWAVData(TRLevelWriter writer) { - // Texture chunk - Debug.Assert(_level.Texture32Chunk.Rooms.Count == _level.Texture16Chunk.Rooms.Count); - Debug.Assert(_level.Texture32Chunk.Objects.Count == _level.Texture16Chunk.Objects.Count); - Debug.Assert(_level.SkyAndFont32Chunk.Textiles.Count == 3); - - writer.Write((ushort)_level.Texture32Chunk.Rooms.Count); - writer.Write((ushort)_level.Texture32Chunk.Objects.Count); - writer.Write((ushort)0); // No bump - - using TRLevelWriter writer32 = new(); - writer32.Write(_level.Texture32Chunk.Rooms); - writer32.Write(_level.Texture32Chunk.Objects); - writer.Deflate(writer32, _level.Texture32Chunk); - - using TRLevelWriter writer16 = new(); - writer16.Write(_level.Texture16Chunk.Rooms); - writer16.Write(_level.Texture16Chunk.Objects); - writer.Deflate(writer16, _level.Texture16Chunk); - - using TRLevelWriter skyWriter = new(); - skyWriter.Write(_level.SkyAndFont32Chunk.Textiles); - writer.Deflate(skyWriter, _level.SkyAndFont32Chunk); - - writer.Write(_level.LaraType); - writer.Write(_level.WeatherType); - writer.Write(_level.Padding); - - //Note - a TR5 Level data chunk is not compressed. - byte[] chunk = _level.LevelDataChunk.Serialize(); - writer.Write(_level.LevelDataChunk.UncompressedSize); - writer.Write(_level.LevelDataChunk.CompressedSize); - writer.Write(chunk); - writer.Write((uint)_level.Samples.Count); foreach (TR4Sample sample in _level.Samples) { @@ -117,33 +465,4 @@ protected override void Write(TRLevelWriter writer) writer.Write(sample.CompressedChunk); } } - - private static void DecompressLevelDataChunk(TR5Level lvl) - { - //TR5 level chunk is not compressed - byte[] buffer = lvl.LevelDataChunk.CompressedChunk; - - //Is the decompressed chunk the size we expected? - Debug.Assert(buffer.Length == lvl.LevelDataChunk.UncompressedSize); - - using MemoryStream stream = new(buffer, false); - using TRLevelReader lvlChunkReader = new(stream); - TR5FileReadUtilities.PopulateRooms(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateFloordata(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateMeshes(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateAnimations(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateMeshTreesFramesModels(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateStaticMeshes(lvlChunkReader, lvl); - TR5FileReadUtilities.VerifySPRMarker(lvlChunkReader); - TR5FileReadUtilities.PopulateSprites(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateCameras(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateSoundSources(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateBoxesOverlapsZones(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateAnimatedTextures(lvlChunkReader, lvl); - TR5FileReadUtilities.VerifyTEXMarker(lvlChunkReader); - TR5FileReadUtilities.PopulateObjectTextures(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateEntitiesAndAI(lvlChunkReader, lvl); - TR5FileReadUtilities.PopulateDemoSoundSampleIndices(lvlChunkReader, lvl); - TR5FileReadUtilities.VerifyLevelDataFinalSeperator(lvlChunkReader, lvl); - } } diff --git a/TRLevelControl/IO/TRLevelReader.cs b/TRLevelControl/IO/TRLevelReader.cs index 8e378283b..02a946a65 100644 --- a/TRLevelControl/IO/TRLevelReader.cs +++ b/TRLevelControl/IO/TRLevelReader.cs @@ -5,24 +5,39 @@ namespace TRLevelControl; public class TRLevelReader : BinaryReader { - public TRLevelReader(Stream stream) - : base(stream) { } + private readonly ITRLevelObserver _observer; - public TRLevelReader Inflate(TR4Chunk chunk) + public TRLevelReader(Stream stream, ITRLevelObserver observer = null) + : base(stream) { - chunk.UncompressedSize = ReadUInt32(); - chunk.CompressedSize = ReadUInt32(); - chunk.CompressedChunk = ReadBytes((int)chunk.CompressedSize); + _observer = observer; + } + + public TRLevelReader Inflate(TRChunkType chunkType) + { + long position = BaseStream.Position; + uint expectedLength = ReadUInt32(); + uint compressedLength = ReadUInt32(); - MemoryStream inflatedStream = new(); - using MemoryStream ms = new(chunk.CompressedChunk); + byte[] data = new byte[compressedLength]; + for (uint i = 0; i < compressedLength; i++) + { + data[i] = ReadByte(); + } + + MemoryStream inflatedStream; + + inflatedStream = new(); + using MemoryStream ms = new(data); using InflaterInputStream inflater = new(ms); + inflater.CopyTo(inflatedStream); - if (inflatedStream.Length != chunk.UncompressedSize) + _observer?.OnChunkRead(position, BaseStream.Position, chunkType, inflatedStream.ToArray()); + + if (inflatedStream.Length != expectedLength) { - throw new InvalidDataException( - $"Inflated stream length mismatch: got {inflatedStream.Length}, expected {chunk.UncompressedSize}"); + throw new InvalidDataException($"Inflated stream length mismatch: got {inflatedStream.Length}, expected {expectedLength}"); } inflatedStream.Position = 0; @@ -82,14 +97,19 @@ public List ReadImage32s(long numImages) List images = new((int)numImages); for (long i = 0; i < numImages; i++) { - images.Add(new() - { - Pixels = ReadUInt32s(TRConsts.TPageSize) - }); + images.Add(ReadImage32()); } return images; } + public TRTexImage32 ReadImage32() + { + return new() + { + Pixels = ReadUInt32s(TRConsts.TPageSize) + }; + } + public List ReadColours(long numColours) { List colours = new((int)numColours); diff --git a/TRLevelControl/IO/TRLevelWriter.cs b/TRLevelControl/IO/TRLevelWriter.cs index 7ed7703d8..ffeb4181f 100644 --- a/TRLevelControl/IO/TRLevelWriter.cs +++ b/TRLevelControl/IO/TRLevelWriter.cs @@ -5,36 +5,35 @@ namespace TRLevelControl; public class TRLevelWriter : BinaryWriter { - public TRLevelWriter() - : base(new MemoryStream()) { } + private readonly ITRLevelObserver _observer; - public TRLevelWriter(Stream stream) - : base(stream) { } + public TRLevelWriter(ITRLevelObserver observer = null) + : this(new MemoryStream(), observer) { } - public void Deflate(TRLevelWriter inflatedWriter, TR4Chunk chunk) + public TRLevelWriter(Stream input, ITRLevelObserver observer = null) + : base(input) { + _observer = observer; + } + + public void Deflate(TRLevelWriter inflatedWriter, TRChunkType chunkType) + { + byte[] data = (inflatedWriter.BaseStream as MemoryStream).ToArray(); + using MemoryStream outStream = new(); using DeflaterOutputStream deflater = new(outStream); + using MemoryStream inStream = new(data); - long position = inflatedWriter.BaseStream.Position; - try - { - inflatedWriter.BaseStream.Position = 0; - inflatedWriter.BaseStream.CopyTo(deflater); - deflater.Finish(); + inStream.CopyTo(deflater); + deflater.Finish(); - byte[] zippedData = outStream.ToArray(); - chunk.UncompressedSize = (uint)inflatedWriter.BaseStream.Length; - chunk.CompressedSize = (uint)zippedData.Length; + byte[] zippedData = outStream.ToArray(); + long startPosition = BaseStream.Position; + Write((uint)data.Length); + Write((uint)zippedData.Length); + Write(zippedData); - Write(chunk.UncompressedSize); - Write(chunk.CompressedSize); - Write(zippedData); - } - finally - { - inflatedWriter.BaseStream.Position = position; - } + _observer?.OnChunkWritten(startPosition, BaseStream.Position, chunkType, data); } public void Write(IEnumerable data) @@ -73,10 +72,15 @@ public void Write(IEnumerable images) { foreach (TRTexImage32 image in images) { - Write(image.Pixels); + Write(image); } } + public void Write(TRTexImage32 image) + { + Write(image.Pixels); + } + public void Write(IEnumerable colours) { foreach (TRColour colour in colours) diff --git a/TRLevelControl/ITRLevelObserver.cs b/TRLevelControl/ITRLevelObserver.cs new file mode 100644 index 000000000..2bcd9cc7d --- /dev/null +++ b/TRLevelControl/ITRLevelObserver.cs @@ -0,0 +1,11 @@ +using TRLevelControl.Model; + +namespace TRLevelControl; + +public interface ITRLevelObserver +{ + void OnChunkRead(long startPosition, long endPosition, TRChunkType chunkType, byte[] data); + void OnChunkWritten(long startPosition, long endPosition, TRChunkType chunkType, byte[] data); + void OnMeshPaddingRead(uint meshPointer, List values); + List GetMeshPadding(uint meshPointer); +} diff --git a/TRLevelControl/Model/Common/Enums/TRChunkType.cs b/TRLevelControl/Model/Common/Enums/TRChunkType.cs new file mode 100644 index 000000000..ace6a683d --- /dev/null +++ b/TRLevelControl/Model/Common/Enums/TRChunkType.cs @@ -0,0 +1,9 @@ +namespace TRLevelControl.Model; + +public enum TRChunkType +{ + Images32 = 0, + Images16 = 1, + SkyFont = 2, + LevelData = 3, +} diff --git a/TRLevelControl/Model/TR4/TR4Chunk.cs b/TRLevelControl/Model/TR4/TR4Chunk.cs deleted file mode 100644 index 8bb648e5a..000000000 --- a/TRLevelControl/Model/TR4/TR4Chunk.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TRLevelControl.Model; - -public class TR4Chunk -{ - public uint UncompressedSize { get; set; } - public uint CompressedSize { get; set; } - public byte[] CompressedChunk { get; set; } -} diff --git a/TRLevelControl/Model/TR4/TR4Level.cs b/TRLevelControl/Model/TR4/TR4Level.cs index cb414b56e..c557d915a 100644 --- a/TRLevelControl/Model/TR4/TR4Level.cs +++ b/TRLevelControl/Model/TR4/TR4Level.cs @@ -2,13 +2,35 @@ public class TR4Level : TRLevelBase { - public TR4Texture32Chunk Texture32Chunk { get; set; } - - public TR4Texture16Chunk Texture16Chunk { get; set; } - - public TR4SkyAndFont32Chunk SkyAndFont32Chunk { get; set; } - - public TR4LevelDataChunk LevelDataChunk { get; set; } - + public TR4Textiles Images { get; set; } + public List Rooms { get; set; } + public List FloorData { get; set; } + public List Meshes { get; set; } + public List MeshPointers { get; set; } + public List Animations { get; set; } + public List StateChanges { get; set; } + public List AnimDispatches { get; set; } + public List AnimCommands { get; set; } + public List MeshTrees { get; set; } + public List Frames { get; set; } + public List Models { get; set; } + public List StaticMeshes { get; set; } + public List SpriteTextures { get; set; } + public List SpriteSequences { get; set; } + public List Cameras { get; set; } + public List FlybyCameras { get; set; } + public List SoundSources { get; set; } + public List Boxes { get; set; } + public List Overlaps { get; set; } + public List Zones { get; set; } + public List AnimatedTextures { get; set; } + public byte AnimatedTexturesUVCount { get; set; } + public List ObjectTextures { get; set; } + public List Entities { get; set; } + public List AIEntities { get; set; } + public byte[] DemoData { get; set; } + public short[] SoundMap { get; set; } + public List SoundDetails { get; set; } + public List SampleIndices { get; set; } public List Samples { get; set; } } diff --git a/TRLevelControl/Model/TR4/TR4LevelDataChunk.cs b/TRLevelControl/Model/TR4/TR4LevelDataChunk.cs deleted file mode 100644 index b938e670b..000000000 --- a/TRLevelControl/Model/TR4/TR4LevelDataChunk.cs +++ /dev/null @@ -1,225 +0,0 @@ -using TRLevelControl.Serialization; -using TRLevelControl.Compression; - -namespace TRLevelControl.Model; - -public class TR4LevelDataChunk : ISerializableCompact -{ - public static readonly string SPRMarker = "SPR"; - public static readonly string TEXMarker = "TEX"; - - public uint UncompressedSize { get; set; } - - public uint CompressedSize { get; set; } - - public uint Unused { get; set; } - public List Rooms { get; set; } - public List FloorData { get; set; } - public List Meshes { get; set; } - public List MeshPointers { get; set; } - public List Animations { get; set; } - public List StateChanges { get; set; } - public List AnimDispatches { get; set; } - public List AnimCommands { get; set; } - public List MeshTrees { get; set; } - public List Frames { get; set; } - public List Models { get; set; } - public List StaticMeshes { get; set; } - public List SpriteTextures { get; set; } - public List SpriteSequences { get; set; } - public List Cameras { get; set; } - public List FlybyCameras { get; set; } - public List SoundSources { get; set; } - public List Boxes { get; set; } - public List Overlaps { get; set; } - public List Zones { get; set; } - public List AnimatedTextures { get; set; } - public byte AnimatedTexturesUVCount { get; set; } - public List ObjectTextures { get; set; } - public List Entities { get; set; } - public List AIEntities { get; set; } - public byte[] DemoData { get; set; } - public short[] SoundMap { get; set; } - public List SoundDetails { get; set; } - public List SampleIndices { get; set; } - - public byte[] Seperator { get; set; } - - //Optional - mainly just for testing, this is just to store the raw zlib compressed chunk. - public byte[] CompressedChunk { get; set; } - - public byte[] Serialize() - { - using MemoryStream stream = new(); - using (TRLevelWriter writer = new(stream)) - { - writer.Write(Unused); - - writer.Write((ushort)Rooms.Count); - foreach (TR4Room room in Rooms) - { - writer.Write(room.Serialize()); - } - - writer.Write((uint)FloorData.Count); - writer.Write(FloorData); - - List meshData = Meshes.SelectMany(m => m.Serialize()).ToList(); - writer.Write((uint)meshData.Count / 2); - writer.Write(meshData.ToArray()); - - writer.Write((uint)MeshPointers.Count); - foreach (uint data in MeshPointers) - { - writer.Write(data); - } - - writer.Write((uint)Animations.Count); - foreach (TR4Animation anim in Animations) - { - writer.Write(anim.Serialize()); - } - - writer.Write((uint)StateChanges.Count); - foreach (TRStateChange sc in StateChanges) - { - writer.Write(sc.Serialize()); - } - - writer.Write((uint)AnimDispatches.Count); - foreach (TRAnimDispatch ad in AnimDispatches) - { - writer.Write(ad.Serialize()); - } - - writer.Write((uint)AnimCommands.Count); - foreach (TRAnimCommand ac in AnimCommands) - { - writer.Write(ac.Serialize()); - } - - writer.Write((uint)MeshTrees.Count * 4); //To get the correct number /= 4 is done during read, make sure to reverse it here. - foreach (TRMeshTreeNode node in MeshTrees) - { - writer.Write(node.Serialize()); - } - - writer.Write((uint)Frames.Count); - foreach (ushort frame in Frames) - { - writer.Write(frame); - } - - writer.Write((uint)Models.Count); - foreach (TRModel model in Models) - { - writer.Write(model.Serialize()); - } - - writer.Write((uint)StaticMeshes.Count); - foreach (TRStaticMesh sm in StaticMeshes) - { - writer.Write(sm.Serialize()); - } - - writer.Write(SPRMarker.ToCharArray()); - - writer.Write((uint)SpriteTextures.Count); - foreach (TRSpriteTexture st in SpriteTextures) - { - writer.Write(st.Serialize()); - } - - writer.Write((uint)SpriteSequences.Count); - foreach (TRSpriteSequence seq in SpriteSequences) - { - writer.Write(seq.Serialize()); - } - - writer.Write((uint)Cameras.Count); - foreach (TRCamera cam in Cameras) - { - writer.Write(cam.Serialize()); - } - - writer.Write((uint)FlybyCameras.Count); - foreach (TR4FlyByCamera flycam in FlybyCameras) - { - writer.Write(flycam.Serialize()); - } - - writer.Write((uint)SoundSources.Count); - foreach (TRSoundSource ssrc in SoundSources) - { - writer.Write(ssrc.Serialize()); - } - - writer.Write((uint)Boxes.Count); - foreach (TR2Box box in Boxes) - { - writer.Write(box.Serialize()); - } - - writer.Write((uint)Overlaps.Count); - foreach (ushort overlap in Overlaps) - { - writer.Write(overlap); - } - - foreach (short zone in Zones) - { - writer.Write(zone); - } - - byte[] animTextureData = AnimatedTextures.SelectMany(a => a.Serialize()).ToArray(); - writer.Write((uint)(animTextureData.Length / sizeof(ushort)) + 1); - writer.Write((ushort)AnimatedTextures.Count); - writer.Write(animTextureData); - writer.Write(AnimatedTexturesUVCount); - - writer.Write(TEXMarker.ToCharArray()); - - writer.Write((uint)ObjectTextures.Count); - foreach (TR4ObjectTexture otex in ObjectTextures) - { - writer.Write(otex.Serialize()); - } - - writer.Write((uint)Entities.Count); - writer.Write(Entities); - - writer.Write((uint)AIEntities.Count); - writer.Write(AIEntities); - - writer.Write((ushort)DemoData.Length); - writer.Write(DemoData); - - foreach (short sound in SoundMap) - { - writer.Write(sound); - } - - writer.Write((uint)SoundDetails.Count); - foreach (TR3SoundDetails snd in SoundDetails) - { - writer.Write(snd.Serialize()); - } - - writer.Write((uint)SampleIndices.Count); - foreach (uint sampleindex in SampleIndices) - { - writer.Write(sampleindex); - } - - writer.Write(Seperator); - } - - byte[] uncompressed = stream.ToArray(); - UncompressedSize = (uint)uncompressed.Length; - - byte[] compressed = TRZlib.Compress(uncompressed); - CompressedSize = (uint)compressed.Length; - - return compressed; - } -} diff --git a/TRLevelControl/Model/TR4/TR4Mesh.cs b/TRLevelControl/Model/TR4/TR4Mesh.cs index e0e767ddf..fff3ce9a7 100644 --- a/TRLevelControl/Model/TR4/TR4Mesh.cs +++ b/TRLevelControl/Model/TR4/TR4Mesh.cs @@ -1,8 +1,6 @@ -using TRLevelControl.Serialization; +namespace TRLevelControl.Model; -namespace TRLevelControl.Model; - -public class TR4Mesh : ISerializableCompact +public class TR4Mesh { //Held for convenience here but is not included in serialisation //Value matches that in containing pointer array @@ -29,58 +27,4 @@ public class TR4Mesh : ISerializableCompact public short NumTexturedTriangles { get; set; } public TR4MeshFace3[] TexturedTriangles { get; set; } - - public byte[] Serialize() - { - using MemoryStream stream = new(); - using (BinaryWriter writer = new(stream)) - { - writer.Write(Centre.Serialize()); - writer.Write(CollRadius); - writer.Write(NumVertices); - - foreach (TRVertex vert in Vertices) - { - writer.Write(vert.Serialize()); - } - - writer.Write(NumNormals); - - if (NumNormals > 0) - { - foreach (TRVertex normal in Normals) - { - writer.Write(normal.Serialize()); - } - } - else - { - foreach (short light in Lights) - { - writer.Write(light); - } - } - - writer.Write(NumTexturedRectangles); - foreach (TR4MeshFace4 face in TexturedRectangles) - { - writer.Write(face.Serialize()); - } - - writer.Write(NumTexturedTriangles); - foreach (TR4MeshFace3 face in TexturedTriangles) - { - writer.Write(face.Serialize()); - } - - // 4-byte alignment for mesh data - long padding = writer.BaseStream.Position % 4; - for (int i = 0; i < padding; i++) - { - writer.Write((byte)0); - } - } - - return stream.ToArray(); - } } diff --git a/TRLevelControl/Model/TR4/TR4SkyAndFont32Chunk.cs b/TRLevelControl/Model/TR4/TR4SkyAndFont32Chunk.cs deleted file mode 100644 index e62de6e53..000000000 --- a/TRLevelControl/Model/TR4/TR4SkyAndFont32Chunk.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace TRLevelControl.Model; - -public class TR4SkyAndFont32Chunk : TR4Chunk -{ - public List Textiles { get; set; } -} diff --git a/TRLevelControl/Model/TR4/TR4Textiles.cs b/TRLevelControl/Model/TR4/TR4Textiles.cs new file mode 100644 index 000000000..1c25dd76c --- /dev/null +++ b/TRLevelControl/Model/TR4/TR4Textiles.cs @@ -0,0 +1,45 @@ +namespace TRLevelControl.Model; + +public class TR4Textiles +{ + // TRObjectTexture sequencing is based on this order. + public TR4TextureData Rooms { get; set; } + public TR4TextureData Objects { get; set; } + public TR4TextureData Bump { get; set; } + public TRTexImage32 Sky { get; set; } + public TRTexImage32 Font { get; set; } + + public int Count => Rooms.Count + Objects.Count + Bump.Count + 2; + + public TR4Textiles() + { + Rooms = new(); + Objects = new(); + Bump = new(); + } + + public TRTexImage32 GetImage32(int index) + { + if (index < Rooms.Count) + { + return Rooms.Images32[index]; + } + if (index < Rooms.Count + Objects.Count) + { + return Objects.Images32[index - Rooms.Count]; + } + if (index < Rooms.Count + Objects.Count + Bump.Count) + { + return Bump.Images32[index - Rooms.Count - Objects.Count]; + } + if (index == Count - 2) + { + return Sky; + } + if (index == Count - 1) + { + return Font; + } + throw new IndexOutOfRangeException(); + } +} diff --git a/TRLevelControl/Model/TR4/TR4Texture16Chunk.cs b/TRLevelControl/Model/TR4/TR4Texture16Chunk.cs deleted file mode 100644 index 486411443..000000000 --- a/TRLevelControl/Model/TR4/TR4Texture16Chunk.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TRLevelControl.Model; - -public class TR4Texture16Chunk : TR4Chunk -{ - public List Rooms { get; set; } - public List Objects { get; set; } - public List Bump { get; set; } -} diff --git a/TRLevelControl/Model/TR4/TR4Texture32Chunk.cs b/TRLevelControl/Model/TR4/TR4Texture32Chunk.cs deleted file mode 100644 index 4556e8371..000000000 --- a/TRLevelControl/Model/TR4/TR4Texture32Chunk.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace TRLevelControl.Model; - -public class TR4Texture32Chunk : TR4Chunk -{ - public List Rooms { get; set; } - public List Objects { get; set; } - public List Bump { get; set; } -} diff --git a/TRLevelControl/Model/TR4/TR4TextureData.cs b/TRLevelControl/Model/TR4/TR4TextureData.cs new file mode 100644 index 000000000..549c8a58a --- /dev/null +++ b/TRLevelControl/Model/TR4/TR4TextureData.cs @@ -0,0 +1,8 @@ +namespace TRLevelControl.Model; + +public class TR4TextureData +{ + public List Images32 { get; set; } + public List Images16 { get; set; } + public int Count => Images32.Count; +} diff --git a/TRLevelControl/Model/TR5/TR5Level.cs b/TRLevelControl/Model/TR5/TR5Level.cs index 324c570c5..d26a97216 100644 --- a/TRLevelControl/Model/TR5/TR5Level.cs +++ b/TRLevelControl/Model/TR5/TR5Level.cs @@ -2,19 +2,38 @@ public class TR5Level : TRLevelBase { - public TR4Texture32Chunk Texture32Chunk { get; set; } - - public TR4Texture16Chunk Texture16Chunk { get; set; } - - public TR4SkyAndFont32Chunk SkyAndFont32Chunk { get; set; } - + public TR5Textiles Images { get; set; } public ushort LaraType { get; set; } - public ushort WeatherType { get; set; } - - public byte[] Padding { get; set; } - - public TR5LevelDataChunk LevelDataChunk { get; set; } + public List Rooms { get; set; } + public List FloorData { get; set; } + public List Meshes { get; set; } + public List MeshPointers { get; set; } + public List Animations { get; set; } + public List StateChanges { get; set; } + public List AnimDispatches { get; set; } + public List AnimCommands { get; set; } + public List MeshTrees { get; set; } + public List Frames { get; set; } + public List Models { get; set; } + public List StaticMeshes { get; set; } + public List SpriteTextures { get; set; } + public List SpriteSequences { get; set; } + public List Cameras { get; set; } + public List FlybyCameras { get; set; } + public List SoundSources { get; set; } + public List Boxes { get; set; } + public List Overlaps { get; set; } + public List Zones { get; set; } + public List AnimatedTextures { get; set; } + public byte AnimatedTexturesUVCount { get; set; } + public List ObjectTextures { get; set; } + public List Entities { get; set; } + public List AIEntities { get; set; } + public byte[] DemoData { get; set; } + public short[] SoundMap { get; set; } + public List SoundDetails { get; set; } + public List SampleIndices { get; set; } public List Samples { get; set; } } diff --git a/TRLevelControl/Model/TR5/TR5LevelDataChunk.cs b/TRLevelControl/Model/TR5/TR5LevelDataChunk.cs deleted file mode 100644 index 2a0f96bf8..000000000 --- a/TRLevelControl/Model/TR5/TR5LevelDataChunk.cs +++ /dev/null @@ -1,221 +0,0 @@ -namespace TRLevelControl.Model; - -public class TR5LevelDataChunk -{ - public static readonly string SPRMarker = "SPR\0"; - public static readonly string TEXMarker = "TEX\0"; - - public uint UncompressedSize { get; set; } - - public uint CompressedSize { get; set; } - - public uint Unused { get; set; } - public List Rooms { get; set; } - public List FloorData { get; set; } - public List Meshes { get; set; } - public List MeshPointers { get; set; } - public List Animations { get; set; } - public List StateChanges { get; set; } - public List AnimDispatches { get; set; } - public List AnimCommands { get; set; } - public List MeshTrees { get; set; } - public List Frames { get; set; } - public List Models { get; set; } - public List StaticMeshes { get; set; } - public List SpriteTextures { get; set; } - public List SpriteSequences { get; set; } - public List Cameras { get; set; } - public List FlybyCameras { get; set; } - public List SoundSources { get; set; } - public List Boxes { get; set; } - public List Overlaps { get; set; } - public List Zones { get; set; } - public List AnimatedTextures { get; set; } - public byte AnimatedTexturesUVCount { get; set; } - public List ObjectTextures { get; set; } - public List Entities { get; set; } - public List AIEntities { get; set; } - public byte[] DemoData { get; set; } - - public short[] SoundMap { get; set; } - public List SoundDetails { get; set; } - public List SampleIndices { get; set; } - - public byte[] Seperator { get; set; } - - //Optional - mainly just for testing, this is just to store the raw zlib compressed chunk. - public byte[] CompressedChunk { get; set; } - - public byte[] Serialize() - { - using MemoryStream stream = new(); - using (TRLevelWriter writer = new(stream)) - { - writer.Write(Unused); - - writer.Write((uint)Rooms.Count); - foreach (TR5Room room in Rooms) - { - writer.Write(room.Serialize()); - } - - writer.Write((uint)FloorData.Count); - writer.Write(FloorData); - - List meshData = Meshes.SelectMany(m => m.Serialize()).ToList(); - writer.Write((uint)meshData.Count / 2); - writer.Write(meshData.ToArray()); - - writer.Write((uint)MeshPointers.Count); - foreach (uint data in MeshPointers) - { - writer.Write(data); - } - - writer.Write((uint)Animations.Count); - foreach (TR4Animation anim in Animations) - { - writer.Write(anim.Serialize()); - } - - writer.Write((uint)StateChanges.Count); - foreach (TRStateChange sc in StateChanges) - { - writer.Write(sc.Serialize()); - } - - writer.Write((uint)AnimDispatches.Count); - foreach (TRAnimDispatch ad in AnimDispatches) - { - writer.Write(ad.Serialize()); - } - - writer.Write((uint)AnimCommands.Count); - foreach (TRAnimCommand ac in AnimCommands) - { - writer.Write(ac.Serialize()); - } - - writer.Write((uint)MeshTrees.Count * 4); //To get the correct number /= 4 is done during read, make sure to reverse it here. - foreach (TRMeshTreeNode node in MeshTrees) - { - writer.Write(node.Serialize()); - } - - writer.Write((uint)Frames.Count); - foreach (ushort frame in Frames) - { - writer.Write(frame); - } - - writer.Write((uint)Models.Count); - foreach (TR5Model model in Models) - { - writer.Write(model.Serialize()); - } - - writer.Write((uint)StaticMeshes.Count); - foreach (TRStaticMesh sm in StaticMeshes) - { - writer.Write(sm.Serialize()); - } - - writer.Write(SPRMarker.ToCharArray()); - - writer.Write((uint)SpriteTextures.Count); - foreach (TRSpriteTexture st in SpriteTextures) - { - writer.Write(st.Serialize()); - } - - writer.Write((uint)SpriteSequences.Count); - foreach (TRSpriteSequence seq in SpriteSequences) - { - writer.Write(seq.Serialize()); - } - - writer.Write((uint)Cameras.Count); - foreach (TRCamera cam in Cameras) - { - writer.Write(cam.Serialize()); - } - - writer.Write((uint)FlybyCameras.Count); - foreach (TR4FlyByCamera flycam in FlybyCameras) - { - writer.Write(flycam.Serialize()); - } - - writer.Write((uint)SoundSources.Count); - foreach (TRSoundSource ssrc in SoundSources) - { - writer.Write(ssrc.Serialize()); - } - - writer.Write((uint)Boxes.Count); - foreach (TR2Box box in Boxes) - { - writer.Write(box.Serialize()); - } - - writer.Write((uint)Overlaps.Count); - foreach (ushort overlap in Overlaps) - { - writer.Write(overlap); - } - - foreach (short zone in Zones) - { - writer.Write(zone); - } - - byte[] animTextureData = AnimatedTextures.SelectMany(a => a.Serialize()).ToArray(); - writer.Write((uint)(animTextureData.Length / sizeof(ushort)) + 1); - writer.Write((ushort)AnimatedTextures.Count); - writer.Write(animTextureData); - writer.Write(AnimatedTexturesUVCount); - - writer.Write(TEXMarker.ToCharArray()); - - writer.Write((uint)ObjectTextures.Count); - foreach (TR5ObjectTexture otex in ObjectTextures) - { - writer.Write(otex.Serialize()); - } - - writer.Write((uint)Entities.Count); - writer.Write(Entities); - - writer.Write((uint)AIEntities.Count); - writer.Write(AIEntities); - - writer.Write((ushort)DemoData.Length); - writer.Write(DemoData); - - foreach (short sound in SoundMap) - { - writer.Write(sound); - } - - writer.Write((uint)SoundDetails.Count); - foreach (TR3SoundDetails snd in SoundDetails) - { - writer.Write(snd.Serialize()); - } - - writer.Write((uint)SampleIndices.Count); - foreach (uint sampleindex in SampleIndices) - { - writer.Write(sampleindex); - } - - writer.Write(Seperator); - } - - byte[] uncompressed = stream.ToArray(); - UncompressedSize = (uint)uncompressed.Length; - CompressedSize = UncompressedSize; - - return uncompressed; - } -} diff --git a/TRLevelControl/Model/TR5/TR5Textiles.cs b/TRLevelControl/Model/TR5/TR5Textiles.cs new file mode 100644 index 000000000..30c3c9bd0 --- /dev/null +++ b/TRLevelControl/Model/TR5/TR5Textiles.cs @@ -0,0 +1,43 @@ +namespace TRLevelControl.Model; + +public class TR5Textiles +{ + public TR4TextureData Rooms { get; set; } + public TR4TextureData Objects { get; set; } + public TRTexImage32 Shine { get; set; } + public TRTexImage32 Sky { get; set; } + public TRTexImage32 Font { get; set; } + + public int Count => Rooms.Count + Objects.Count + 3; + + public TR5Textiles() + { + Rooms = new(); + Objects = new(); + } + + public TRTexImage32 GetImage32(int index) + { + if (index < Rooms.Count) + { + return Rooms.Images32[index]; + } + if (index < Rooms.Count + Objects.Count) + { + return Objects.Images32[index - Rooms.Count]; + } + if (index == Count - 3) + { + return Shine; + } + if (index == Count - 2) + { + return Sky; + } + if (index == Count - 1) + { + return Font; + } + throw new IndexOutOfRangeException(); + } +} diff --git a/TRLevelControl/TR4FileReadUtilities.cs b/TRLevelControl/TR4FileReadUtilities.cs index 11edb98ce..bb9f79519 100644 --- a/TRLevelControl/TR4FileReadUtilities.cs +++ b/TRLevelControl/TR4FileReadUtilities.cs @@ -6,11 +6,13 @@ namespace TRLevelControl; internal static class TR4FileReadUtilities { + public static readonly string SPRMarker = "SPR"; + public static readonly string TEXMarker = "TEX"; + public static void PopulateRooms(BinaryReader reader, TR4Level lvl) { - lvl.LevelDataChunk.Unused = reader.ReadUInt32(); ushort numRooms = reader.ReadUInt16(); - lvl.LevelDataChunk.Rooms = new(); + lvl.Rooms = new(); for (int i = 0; i < numRooms; i++) { @@ -28,7 +30,7 @@ public static void PopulateRooms(BinaryReader reader, TR4Level lvl) //Grab data NumDataWords = reader.ReadUInt32() }; - lvl.LevelDataChunk.Rooms.Add(room); + lvl.Rooms.Add(room); room.Data = new ushort[room.NumDataWords]; for (int j = 0; j < room.NumDataWords; j++) @@ -63,7 +65,7 @@ public static void PopulateRooms(BinaryReader reader, TR4Level lvl) room.Lights = new TR4RoomLight[room.NumLights]; for (int j = 0; j < room.NumLights; j++) { - room.Lights[j] = TR4FileReadUtilities.ReadRoomLight(reader); + room.Lights[j] = ReadRoomLight(reader); } //Static meshes @@ -85,67 +87,46 @@ public static void PopulateRooms(BinaryReader reader, TR4Level lvl) public static void PopulateFloordata(BinaryReader reader, TR4Level lvl) { uint numFloorData = reader.ReadUInt32(); - lvl.LevelDataChunk.FloorData = new(); + lvl.FloorData = new(); for (int i = 0; i < numFloorData; i++) { - lvl.LevelDataChunk.FloorData.Add(reader.ReadUInt16()); - } - } - - public static void PopulateMeshes(BinaryReader reader, TR4Level lvl) - { - uint numMeshData = reader.ReadUInt32(); - ushort[] rawMeshData = new ushort[numMeshData]; - - for (int i = 0; i < numMeshData; i++) - { - rawMeshData[i] = reader.ReadUInt16(); + lvl.FloorData.Add(reader.ReadUInt16()); } - - uint numMeshPointers = reader.ReadUInt32(); - lvl.LevelDataChunk.MeshPointers = new(); - - for (int i = 0; i < numMeshPointers; i++) - { - lvl.LevelDataChunk.MeshPointers.Add(reader.ReadUInt32()); - } - - lvl.LevelDataChunk.Meshes = ConstructMeshData(lvl.LevelDataChunk.MeshPointers, rawMeshData); } public static void PopulateAnimations(BinaryReader reader, TR4Level lvl) { //Animations uint numAnimations = reader.ReadUInt32(); - lvl.LevelDataChunk.Animations = new(); + lvl.Animations = new(); for (int i = 0; i < numAnimations; i++) { - lvl.LevelDataChunk.Animations.Add(ReadAnimation(reader)); + lvl.Animations.Add(ReadAnimation(reader)); } //State Changes uint numStateChanges = reader.ReadUInt32(); - lvl.LevelDataChunk.StateChanges = new(); + lvl.StateChanges = new(); for (int i = 0; i < numStateChanges; i++) { - lvl.LevelDataChunk.StateChanges.Add(TR2FileReadUtilities.ReadStateChange(reader)); + lvl.StateChanges.Add(TR2FileReadUtilities.ReadStateChange(reader)); } //Animation Dispatches uint numAnimDispatches = reader.ReadUInt32(); - lvl.LevelDataChunk.AnimDispatches = new(); + lvl.AnimDispatches = new(); for (int i = 0; i < numAnimDispatches; i++) { - lvl.LevelDataChunk.AnimDispatches.Add(TR2FileReadUtilities.ReadAnimDispatch(reader)); + lvl.AnimDispatches.Add(TR2FileReadUtilities.ReadAnimDispatch(reader)); } //Animation Commands uint numAnimCommands = reader.ReadUInt32(); - lvl.LevelDataChunk.AnimCommands = new(); + lvl.AnimCommands = new(); for (int i = 0; i < numAnimCommands; i++) { - lvl.LevelDataChunk.AnimCommands.Add(TR2FileReadUtilities.ReadAnimCommand(reader)); + lvl.AnimCommands.Add(TR2FileReadUtilities.ReadAnimCommand(reader)); } } @@ -153,62 +134,59 @@ public static void PopulateMeshTreesFramesModels(BinaryReader reader, TR4Level l { //Mesh Trees uint numMeshTrees = reader.ReadUInt32() / 4; - lvl.LevelDataChunk.MeshTrees = new(); + lvl.MeshTrees = new(); for (int i = 0; i < numMeshTrees; i++) { - lvl.LevelDataChunk.MeshTrees.Add(TR2FileReadUtilities.ReadMeshTreeNode(reader)); + lvl.MeshTrees.Add(TR2FileReadUtilities.ReadMeshTreeNode(reader)); } //Frames uint numFrames = reader.ReadUInt32(); - lvl.LevelDataChunk.Frames = new(); + lvl.Frames = new(); for (int i = 0; i < numFrames; i++) { - lvl.LevelDataChunk.Frames.Add(reader.ReadUInt16()); + lvl.Frames.Add(reader.ReadUInt16()); } //Models uint numModels = reader.ReadUInt32(); - lvl.LevelDataChunk.Models = new(); + lvl.Models = new(); for (int i = 0; i < numModels; i++) { - lvl.LevelDataChunk.Models.Add(TR2FileReadUtilities.ReadModel(reader)); + lvl.Models.Add(TR2FileReadUtilities.ReadModel(reader)); } } public static void PopulateStaticMeshes(BinaryReader reader, TR4Level lvl) { uint numStaticMeshes = reader.ReadUInt32(); - lvl.LevelDataChunk.StaticMeshes = new(); + lvl.StaticMeshes = new(); for (int i = 0; i < numStaticMeshes; i++) { - lvl.LevelDataChunk.StaticMeshes.Add(TR2FileReadUtilities.ReadStaticMesh(reader)); + lvl.StaticMeshes.Add(TR2FileReadUtilities.ReadStaticMesh(reader)); } } - public static void VerifySPRMarker(BinaryReader reader) - { - string sprMarker = new(reader.ReadChars(TR4LevelDataChunk.SPRMarker.Length)); - Debug.Assert(sprMarker == TR4LevelDataChunk.SPRMarker); - } - public static void PopulateSprites(BinaryReader reader, TR4Level lvl) { + string sprMarker = new(reader.ReadChars(SPRMarker.Length)); + Debug.Assert(sprMarker == SPRMarker); + uint numSpriteTextures = reader.ReadUInt32(); - lvl.LevelDataChunk.SpriteTextures = new(); + lvl.SpriteTextures = new(); for (int i = 0; i < numSpriteTextures; i++) { - lvl.LevelDataChunk.SpriteTextures.Add(TR2FileReadUtilities.ReadSpriteTexture(reader)); + lvl.SpriteTextures.Add(TR2FileReadUtilities.ReadSpriteTexture(reader)); } uint numSpriteSequences = reader.ReadUInt32(); - lvl.LevelDataChunk.SpriteSequences = new(); + lvl.SpriteSequences = new(); for (int i = 0; i < numSpriteSequences; i++) { - lvl.LevelDataChunk.SpriteSequences.Add(TR2FileReadUtilities.ReadSpriteSequence(reader)); + lvl.SpriteSequences.Add(TR2FileReadUtilities.ReadSpriteSequence(reader)); } } @@ -216,20 +194,20 @@ public static void PopulateCameras(BinaryReader reader, TR4Level lvl) { //Cameras uint numCameras = reader.ReadUInt32(); - lvl.LevelDataChunk.Cameras = new(); + lvl.Cameras = new(); for (int i = 0; i < numCameras; i++) { - lvl.LevelDataChunk.Cameras.Add(TR2FileReadUtilities.ReadCamera(reader)); + lvl.Cameras.Add(TR2FileReadUtilities.ReadCamera(reader)); } //Flyby Cameras uint numFlybyCameras = reader.ReadUInt32(); - lvl.LevelDataChunk.FlybyCameras = new(); + lvl.FlybyCameras = new(); for (int i = 0; i < numFlybyCameras; i++) { - lvl.LevelDataChunk.FlybyCameras.Add(ReadFlybyCamera(reader)); + lvl.FlybyCameras.Add(ReadFlybyCamera(reader)); } } @@ -237,11 +215,11 @@ public static void PopulateSoundSources(BinaryReader reader, TR4Level lvl) { //Sound Sources uint numSoundSources = reader.ReadUInt32(); - lvl.LevelDataChunk.SoundSources = new(); + lvl.SoundSources = new(); for (int i = 0; i < numSoundSources; i++) { - lvl.LevelDataChunk.SoundSources.Add(TR2FileReadUtilities.ReadSoundSource(reader)); + lvl.SoundSources.Add(TR2FileReadUtilities.ReadSoundSource(reader)); } } @@ -249,58 +227,55 @@ public static void PopulateBoxesOverlapsZones(BinaryReader reader, TR4Level lvl) { //Boxes uint numBoxes = reader.ReadUInt32(); - lvl.LevelDataChunk.Boxes = new(); + lvl.Boxes = new(); for (int i = 0; i < numBoxes; i++) { - lvl.LevelDataChunk.Boxes.Add(TR2FileReadUtilities.ReadBox(reader)); + lvl.Boxes.Add(TR2FileReadUtilities.ReadBox(reader)); } //Overlaps & Zones uint numOverlaps = reader.ReadUInt32(); - lvl.LevelDataChunk.Overlaps = new(); + lvl.Overlaps = new(); short[] zones = new short[10 * numBoxes]; for (int i = 0; i < numOverlaps; i++) { - lvl.LevelDataChunk.Overlaps.Add(reader.ReadUInt16()); + lvl.Overlaps.Add(reader.ReadUInt16()); } for (int i = 0; i < zones.Length; i++) { zones[i] = reader.ReadInt16(); } - lvl.LevelDataChunk.Zones = new(zones); + lvl.Zones = new(zones); } public static void PopulateAnimatedTextures(BinaryReader reader, TR4Level lvl) { reader.ReadUInt32(); // Total count of ushorts ushort numGroups = reader.ReadUInt16(); - lvl.LevelDataChunk.AnimatedTextures = new(); + lvl.AnimatedTextures = new(); for (int i = 0; i < numGroups; i++) { - lvl.LevelDataChunk.AnimatedTextures.Add(TR2FileReadUtilities.ReadAnimatedTexture(reader)); + lvl.AnimatedTextures.Add(TR2FileReadUtilities.ReadAnimatedTexture(reader)); } //TR4+ Specific - lvl.LevelDataChunk.AnimatedTexturesUVCount = reader.ReadByte(); - } - - public static void VerifyTEXMarker(BinaryReader reader) - { - string texMarker = new(reader.ReadChars(TR4LevelDataChunk.TEXMarker.Length)); - Debug.Assert(texMarker == TR4LevelDataChunk.TEXMarker); + lvl.AnimatedTexturesUVCount = reader.ReadByte(); } public static void PopulateObjectTextures(BinaryReader reader, TR4Level lvl) { + string texMarker = new(reader.ReadChars(TEXMarker.Length)); + Debug.Assert(texMarker == TEXMarker); + uint numObjectTextures = reader.ReadUInt32(); - lvl.LevelDataChunk.ObjectTextures = new(); + lvl.ObjectTextures = new(); for (int i = 0; i < numObjectTextures; i++) { - lvl.LevelDataChunk.ObjectTextures.Add(ReadObjectTexture(reader)); + lvl.ObjectTextures.Add(ReadObjectTexture(reader)); } } @@ -308,55 +283,43 @@ public static void PopulateEntitiesAndAI(TRLevelReader reader, TR4Level lvl) { //Entities uint numEntities = reader.ReadUInt32(); - lvl.LevelDataChunk.Entities = reader.ReadTR4Entities(numEntities); + lvl.Entities = reader.ReadTR4Entities(numEntities); //AIObjects numEntities = reader.ReadUInt32(); - lvl.LevelDataChunk.AIEntities = reader.ReadTR4AIEntities(numEntities); + lvl.AIEntities = reader.ReadTR4AIEntities(numEntities); } public static void PopulateDemoSoundSampleIndices(BinaryReader reader, TR4Level lvl) { ushort numDemoData = reader.ReadUInt16(); - lvl.LevelDataChunk.DemoData = reader.ReadBytes(numDemoData); + lvl.DemoData = reader.ReadBytes(numDemoData); //Sound Map (370 shorts) & Sound Details - lvl.LevelDataChunk.SoundMap = new short[370]; + lvl.SoundMap = new short[370]; - for (int i = 0; i < lvl.LevelDataChunk.SoundMap.Length; i++) + for (int i = 0; i < lvl.SoundMap.Length; i++) { - lvl.LevelDataChunk.SoundMap[i] = reader.ReadInt16(); + lvl.SoundMap[i] = reader.ReadInt16(); } uint numSoundDetails = reader.ReadUInt32(); - lvl.LevelDataChunk.SoundDetails = new(); + lvl.SoundDetails = new(); for (int i = 0; i < numSoundDetails; i++) { - lvl.LevelDataChunk.SoundDetails.Add(TR3FileReadUtilities.ReadSoundDetails(reader)); + lvl.SoundDetails.Add(TR3FileReadUtilities.ReadSoundDetails(reader)); } uint numSampleIndices = reader.ReadUInt32(); - lvl.LevelDataChunk.SampleIndices = new(); + lvl.SampleIndices = new(); for (int i = 0; i < numSampleIndices; i++) { - lvl.LevelDataChunk.SampleIndices.Add(reader.ReadUInt32()); + lvl.SampleIndices.Add(reader.ReadUInt32()); } } - public static void VerifyLevelDataFinalSeperator(BinaryReader reader, TR4Level lvl) - { - lvl.LevelDataChunk.Seperator = reader.ReadBytes(6); - - Debug.Assert(lvl.LevelDataChunk.Seperator[0] == 0x00); - Debug.Assert(lvl.LevelDataChunk.Seperator[1] == 0x00); - Debug.Assert(lvl.LevelDataChunk.Seperator[2] == 0x00); - Debug.Assert(lvl.LevelDataChunk.Seperator[3] == 0x00); - Debug.Assert(lvl.LevelDataChunk.Seperator[4] == 0x00); - Debug.Assert(lvl.LevelDataChunk.Seperator[5] == 0x00); - } - private static TR3RoomData ConvertToRoomData(TR4Room room) { int RoomDataOffset = 0; @@ -494,92 +457,6 @@ private static TR4RoomLight ReadRoomLight(BinaryReader reader) }; } - public static List ConstructMeshData(List meshPointers, ushort[] rawMeshData) - { - byte[] target = new byte[rawMeshData.Length * 2]; - Buffer.BlockCopy(rawMeshData, 0, target, 0, target.Length); - - // The mesh pointer list can contain duplicates so we must make - // sure to iterate over distinct values only - meshPointers = new(meshPointers.Distinct()); - - List meshes = new(); - - using (MemoryStream ms = new(target)) - using (BinaryReader br = new(ms)) - { - for (int i = 0; i < meshPointers.Count; i++) - { - TR4Mesh mesh = new(); - meshes.Add(mesh); - - uint meshPointer = meshPointers[i]; - br.BaseStream.Position = meshPointer; - - //Pointer - mesh.Pointer = meshPointer; - - //Centre - mesh.Centre = TR2FileReadUtilities.ReadVertex(br); - - //CollRadius - mesh.CollRadius = br.ReadInt32(); - - //Vertices - mesh.NumVertices = br.ReadInt16(); - mesh.Vertices = new TRVertex[mesh.NumVertices]; - for (int j = 0; j < mesh.NumVertices; j++) - { - mesh.Vertices[j] = TR2FileReadUtilities.ReadVertex(br); - } - - //Lights or Normals - mesh.NumNormals = br.ReadInt16(); - if (mesh.NumNormals > 0) - { - mesh.Normals = new TRVertex[mesh.NumNormals]; - for (int j = 0; j < mesh.NumNormals; j++) - { - mesh.Normals[j] = TR2FileReadUtilities.ReadVertex(br); - } - } - else - { - mesh.Lights = new short[Math.Abs(mesh.NumNormals)]; - for (int j = 0; j < mesh.Lights.Length; j++) - { - mesh.Lights[j] = br.ReadInt16(); - } - } - - //Textured Rectangles - mesh.NumTexturedRectangles = br.ReadInt16(); - mesh.TexturedRectangles = new TR4MeshFace4[mesh.NumTexturedRectangles]; - for (int j = 0; j < mesh.NumTexturedRectangles; j++) - { - mesh.TexturedRectangles[j] = TR4FileReadUtilities.ReadTR4MeshFace4(br); - } - - //Textured Triangles - mesh.NumTexturedTriangles = br.ReadInt16(); - mesh.TexturedTriangles = new TR4MeshFace3[mesh.NumTexturedTriangles]; - for (int j = 0; j < mesh.NumTexturedTriangles; j++) - { - mesh.TexturedTriangles[j] = TR4FileReadUtilities.ReadTR4MeshFace3(br); - } - - // There may be alignment padding at the end of the mesh, but rather than - // storing it, when the mesh is serialized the alignment should be considered. - // It seems to be 4-byte alignment for mesh data. The basestream position is - // moved to the next pointer in the next iteration, so we don't need to process - // the additional data here. - // See https://www.tombraiderforums.com/archive/index.php/t-215247.html - } - } - - return meshes; - } - public static TR4MeshFace4 ReadTR4MeshFace4(BinaryReader reader) { return new TR4MeshFace4 diff --git a/TRLevelControl/TR5FileReadUtilities.cs b/TRLevelControl/TR5FileReadUtilities.cs index b93e8794f..57b014cd0 100644 --- a/TRLevelControl/TR5FileReadUtilities.cs +++ b/TRLevelControl/TR5FileReadUtilities.cs @@ -6,18 +6,20 @@ namespace TRLevelControl; internal static class TR5FileReadUtilities { + public static readonly string SPRMarker = "SPR\0"; + public static readonly string TEXMarker = "TEX\0"; + public static void PopulateRooms(BinaryReader reader, TR5Level lvl) { - lvl.LevelDataChunk.Unused = reader.ReadUInt32(); uint numRooms = reader.ReadUInt32(); - lvl.LevelDataChunk.Rooms = new(); + lvl.Rooms = new(); for (int i = 0; i < numRooms; i++) { TR5Room room = new() { XELALandmark = reader.ReadBytes(4) }; - lvl.LevelDataChunk.Rooms.Add(room); + lvl.Rooms.Add(room); Debug.Assert(room.XELALandmark[0] == 'X'); Debug.Assert(room.XELALandmark[1] == 'E'); @@ -202,67 +204,46 @@ private static TR5FogBulb ReadRoomBulbs(BinaryReader r) public static void PopulateFloordata(BinaryReader reader, TR5Level lvl) { uint numFloorData = reader.ReadUInt32(); - lvl.LevelDataChunk.FloorData = new(); + lvl.FloorData = new(); for (int i = 0; i < numFloorData; i++) { - lvl.LevelDataChunk.FloorData.Add(reader.ReadUInt16()); + lvl.FloorData.Add(reader.ReadUInt16()); } } - public static void PopulateMeshes(BinaryReader reader, TR5Level lvl) - { - uint numMeshData = reader.ReadUInt32(); - ushort[] rawMeshData = new ushort[numMeshData]; - - for (int i = 0; i < numMeshData; i++) - { - rawMeshData[i] = reader.ReadUInt16(); - } - - uint numMeshPointers = reader.ReadUInt32(); - lvl.LevelDataChunk.MeshPointers = new(); - - for (int i = 0; i < numMeshPointers; i++) - { - lvl.LevelDataChunk.MeshPointers.Add(reader.ReadUInt32()); - } - - lvl.LevelDataChunk.Meshes = TR4FileReadUtilities.ConstructMeshData(lvl.LevelDataChunk.MeshPointers, rawMeshData); - } - public static void PopulateAnimations(BinaryReader reader, TR5Level lvl) { //Animations uint numAnimations = reader.ReadUInt32(); - lvl.LevelDataChunk.Animations = new(); + lvl.Animations = new(); for (int i = 0; i < numAnimations; i++) { - lvl.LevelDataChunk.Animations.Add(TR4FileReadUtilities.ReadAnimation(reader)); + lvl.Animations.Add(TR4FileReadUtilities.ReadAnimation(reader)); } //State Changes uint numStateChanges = reader.ReadUInt32(); - lvl.LevelDataChunk.StateChanges = new(); + lvl.StateChanges = new(); for (int i = 0; i < numStateChanges; i++) { - lvl.LevelDataChunk.StateChanges.Add(TR2FileReadUtilities.ReadStateChange(reader)); + lvl.StateChanges.Add(TR2FileReadUtilities.ReadStateChange(reader)); } //Animation Dispatches uint numAnimDispatches = reader.ReadUInt32(); - lvl.LevelDataChunk.AnimDispatches = new(); + lvl.AnimDispatches = new(); for (int i = 0; i < numAnimDispatches; i++) { - lvl.LevelDataChunk.AnimDispatches.Add(TR2FileReadUtilities.ReadAnimDispatch(reader)); + lvl.AnimDispatches.Add(TR2FileReadUtilities.ReadAnimDispatch(reader)); } //Animation Commands uint numAnimCommands = reader.ReadUInt32(); - lvl.LevelDataChunk.AnimCommands = new(); + lvl.AnimCommands = new(); for (int i = 0; i < numAnimCommands; i++) { - lvl.LevelDataChunk.AnimCommands.Add(TR2FileReadUtilities.ReadAnimCommand(reader)); + lvl.AnimCommands.Add(TR2FileReadUtilities.ReadAnimCommand(reader)); } } @@ -270,26 +251,26 @@ public static void PopulateMeshTreesFramesModels(BinaryReader reader, TR5Level l { //Mesh Trees uint numMeshTrees = reader.ReadUInt32() / 4; - lvl.LevelDataChunk.MeshTrees = new(); + lvl.MeshTrees = new(); for (int i = 0; i < numMeshTrees; i++) { - lvl.LevelDataChunk.MeshTrees.Add(TR2FileReadUtilities.ReadMeshTreeNode(reader)); + lvl.MeshTrees.Add(TR2FileReadUtilities.ReadMeshTreeNode(reader)); } //Frames uint numFrames = reader.ReadUInt32(); - lvl.LevelDataChunk.Frames = new(); + lvl.Frames = new(); for (int i = 0; i < numFrames; i++) { - lvl.LevelDataChunk.Frames.Add(reader.ReadUInt16()); + lvl.Frames.Add(reader.ReadUInt16()); } //Models uint numModels = reader.ReadUInt32(); - lvl.LevelDataChunk.Models = new(); + lvl.Models = new(); for (int i = 0; i < numModels; i++) { - lvl.LevelDataChunk.Models.Add(ReadTR5Model(reader)); + lvl.Models.Add(ReadTR5Model(reader)); } } @@ -310,36 +291,36 @@ private static TR5Model ReadTR5Model(BinaryReader reader) public static void PopulateStaticMeshes(BinaryReader reader, TR5Level lvl) { uint numStaticMeshes = reader.ReadUInt32(); - lvl.LevelDataChunk.StaticMeshes = new(); + lvl.StaticMeshes = new(); for (int i = 0; i < numStaticMeshes; i++) { - lvl.LevelDataChunk.StaticMeshes.Add(TR2FileReadUtilities.ReadStaticMesh(reader)); + lvl.StaticMeshes.Add(TR2FileReadUtilities.ReadStaticMesh(reader)); } } public static void VerifySPRMarker(BinaryReader reader) { - string sprMarker = new(reader.ReadChars(TR5LevelDataChunk.SPRMarker.Length)); - Debug.Assert(sprMarker == TR5LevelDataChunk.SPRMarker); + string sprMarker = new(reader.ReadChars(SPRMarker.Length)); + Debug.Assert(sprMarker == SPRMarker); } public static void PopulateSprites(BinaryReader reader, TR5Level lvl) { uint numSpriteTextures = reader.ReadUInt32(); - lvl.LevelDataChunk.SpriteTextures = new(); + lvl.SpriteTextures = new(); for (int i = 0; i < numSpriteTextures; i++) { - lvl.LevelDataChunk.SpriteTextures.Add(TR2FileReadUtilities.ReadSpriteTexture(reader)); + lvl.SpriteTextures.Add(TR2FileReadUtilities.ReadSpriteTexture(reader)); } uint numSpriteSequences = reader.ReadUInt32(); - lvl.LevelDataChunk.SpriteSequences = new(); + lvl.SpriteSequences = new(); for (int i = 0; i < numSpriteSequences; i++) { - lvl.LevelDataChunk.SpriteSequences.Add(TR2FileReadUtilities.ReadSpriteSequence(reader)); + lvl.SpriteSequences.Add(TR2FileReadUtilities.ReadSpriteSequence(reader)); } } @@ -347,20 +328,20 @@ public static void PopulateCameras(BinaryReader reader, TR5Level lvl) { //Cameras uint numCameras = reader.ReadUInt32(); - lvl.LevelDataChunk.Cameras = new(); + lvl.Cameras = new(); for (int i = 0; i < numCameras; i++) { - lvl.LevelDataChunk.Cameras.Add(TR2FileReadUtilities.ReadCamera(reader)); + lvl.Cameras.Add(TR2FileReadUtilities.ReadCamera(reader)); } //Flyby Cameras uint numFlybyCameras = reader.ReadUInt32(); - lvl.LevelDataChunk.FlybyCameras = new(); + lvl.FlybyCameras = new(); for (int i = 0; i < numFlybyCameras; i++) { - lvl.LevelDataChunk.FlybyCameras.Add(TR4FileReadUtilities.ReadFlybyCamera(reader)); + lvl.FlybyCameras.Add(TR4FileReadUtilities.ReadFlybyCamera(reader)); } } @@ -368,11 +349,11 @@ public static void PopulateSoundSources(BinaryReader reader, TR5Level lvl) { //Sound Sources uint numSoundSources = reader.ReadUInt32(); - lvl.LevelDataChunk.SoundSources = new(); + lvl.SoundSources = new(); for (int i = 0; i < numSoundSources; i++) { - lvl.LevelDataChunk.SoundSources.Add(TR2FileReadUtilities.ReadSoundSource(reader)); + lvl.SoundSources.Add(TR2FileReadUtilities.ReadSoundSource(reader)); } } @@ -380,58 +361,58 @@ public static void PopulateBoxesOverlapsZones(BinaryReader reader, TR5Level lvl) { //Boxes uint numBoxes = reader.ReadUInt32(); - lvl.LevelDataChunk.Boxes = new(); + lvl.Boxes = new(); for (int i = 0; i < numBoxes; i++) { - lvl.LevelDataChunk.Boxes.Add(TR2FileReadUtilities.ReadBox(reader)); + lvl.Boxes.Add(TR2FileReadUtilities.ReadBox(reader)); } //Overlaps & Zones uint numOverlaps = reader.ReadUInt32(); - lvl.LevelDataChunk.Overlaps = new(); + lvl.Overlaps = new(); short[] zones = new short[10 * numBoxes]; for (int i = 0; i < numOverlaps; i++) { - lvl.LevelDataChunk.Overlaps.Add(reader.ReadUInt16()); + lvl.Overlaps.Add(reader.ReadUInt16()); } for (int i = 0; i < zones.Length; i++) { zones[i] = reader.ReadInt16(); } - lvl.LevelDataChunk.Zones = new(zones); + lvl.Zones = new(zones); } public static void PopulateAnimatedTextures(BinaryReader reader, TR5Level lvl) { reader.ReadUInt32(); // Total count of ushorts ushort numGroups = reader.ReadUInt16(); - lvl.LevelDataChunk.AnimatedTextures = new(); + lvl.AnimatedTextures = new(); for (int i = 0; i < numGroups; i++) { - lvl.LevelDataChunk.AnimatedTextures.Add(TR2FileReadUtilities.ReadAnimatedTexture(reader)); + lvl.AnimatedTextures.Add(TR2FileReadUtilities.ReadAnimatedTexture(reader)); } //TR4+ Specific - lvl.LevelDataChunk.AnimatedTexturesUVCount = reader.ReadByte(); + lvl.AnimatedTexturesUVCount = reader.ReadByte(); } public static void VerifyTEXMarker(BinaryReader reader) { - string texMarker = new(reader.ReadChars(TR5LevelDataChunk.TEXMarker.Length)); - Debug.Assert(texMarker == TR5LevelDataChunk.TEXMarker); + string texMarker = new(reader.ReadChars(TEXMarker.Length)); + Debug.Assert(texMarker == TEXMarker); } public static void PopulateObjectTextures(BinaryReader reader, TR5Level lvl) { uint numObjectTextures = reader.ReadUInt32(); - lvl.LevelDataChunk.ObjectTextures = new(); + lvl.ObjectTextures = new(); for (int i = 0; i < numObjectTextures; i++) { - lvl.LevelDataChunk.ObjectTextures.Add(ReadTR5ObjectTexture(reader)); + lvl.ObjectTextures.Add(ReadTR5ObjectTexture(reader)); } } @@ -476,52 +457,40 @@ public static void PopulateEntitiesAndAI(TRLevelReader reader, TR5Level lvl) { //Entities uint numEntities = reader.ReadUInt32(); - lvl.LevelDataChunk.Entities = reader.ReadTR5Entities(numEntities); + lvl.Entities = reader.ReadTR5Entities(numEntities); //AIObjects numEntities = reader.ReadUInt32(); - lvl.LevelDataChunk.AIEntities = reader.ReadTR5AIEntities(numEntities); + lvl.AIEntities = reader.ReadTR5AIEntities(numEntities); } public static void PopulateDemoSoundSampleIndices(BinaryReader reader, TR5Level lvl) { ushort numDemoData = reader.ReadUInt16(); - lvl.LevelDataChunk.DemoData = reader.ReadBytes(numDemoData); + lvl.DemoData = reader.ReadBytes(numDemoData); //Sound Map (370 shorts) & Sound Details - lvl.LevelDataChunk.SoundMap = new short[450]; + lvl.SoundMap = new short[450]; - for (int i = 0; i < lvl.LevelDataChunk.SoundMap.Length; i++) + for (int i = 0; i < lvl.SoundMap.Length; i++) { - lvl.LevelDataChunk.SoundMap[i] = reader.ReadInt16(); + lvl.SoundMap[i] = reader.ReadInt16(); } uint numSoundDetails = reader.ReadUInt32(); - lvl.LevelDataChunk.SoundDetails = new(); + lvl.SoundDetails = new(); for (int i = 0; i < numSoundDetails; i++) { - lvl.LevelDataChunk.SoundDetails.Add(TR3FileReadUtilities.ReadSoundDetails(reader)); + lvl.SoundDetails.Add(TR3FileReadUtilities.ReadSoundDetails(reader)); } uint numSampleIndices = reader.ReadUInt32(); - lvl.LevelDataChunk.SampleIndices = new(); + lvl.SampleIndices = new(); for (int i = 0; i < numSampleIndices; i++) { - lvl.LevelDataChunk.SampleIndices.Add(reader.ReadUInt32()); + lvl.SampleIndices.Add(reader.ReadUInt32()); } } - - public static void VerifyLevelDataFinalSeperator(BinaryReader reader, TR5Level lvl) - { - lvl.LevelDataChunk.Seperator = reader.ReadBytes(6); - - Debug.Assert(lvl.LevelDataChunk.Seperator[0] == 0xCD); - Debug.Assert(lvl.LevelDataChunk.Seperator[1] == 0xCD); - Debug.Assert(lvl.LevelDataChunk.Seperator[2] == 0xCD); - Debug.Assert(lvl.LevelDataChunk.Seperator[3] == 0xCD); - Debug.Assert(lvl.LevelDataChunk.Seperator[4] == 0xCD); - Debug.Assert(lvl.LevelDataChunk.Seperator[5] == 0xCD); - } } diff --git a/TRLevelControl/TRLevelControlBase.cs b/TRLevelControl/TRLevelControlBase.cs index 10fbc08c4..3aa4f2648 100644 --- a/TRLevelControl/TRLevelControlBase.cs +++ b/TRLevelControl/TRLevelControlBase.cs @@ -6,14 +6,20 @@ namespace TRLevelControl; public abstract class TRLevelControlBase where L : TRLevelBase { + protected ITRLevelObserver _observer; protected L _level; + public TRLevelControlBase(ITRLevelObserver observer = null) + { + _observer = observer; + } + public L Read(string filePath) => Read(File.OpenRead(filePath)); public L Read(Stream stream) { - using TRLevelReader reader = new(stream); + using TRLevelReader reader = new(stream, _observer); _level = CreateLevel((TRFileVersion)reader.ReadUInt32()); Read(reader); @@ -27,7 +33,7 @@ public void Write(L level, string filePath) public void Write(L level, Stream outputStream) { - using TRLevelWriter writer = new(outputStream); + using TRLevelWriter writer = new(outputStream, _observer); writer.Write((uint)level.Version.File); diff --git a/TRLevelControlTests/Base/Observers/ObserverBase.cs b/TRLevelControlTests/Base/Observers/ObserverBase.cs new file mode 100644 index 000000000..cd96919c2 --- /dev/null +++ b/TRLevelControlTests/Base/Observers/ObserverBase.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TRLevelControl; +using TRLevelControl.Model; + +namespace TRLevelControlTests; + +public class ObserverBase : ITRLevelObserver +{ + public virtual void TestOutput(byte[] input, byte[] output) + { + CollectionAssert.AreEqual(input, output); + } + + public virtual void OnChunkRead(long startPosition, long endPosition, TRChunkType chunkType, byte[] data) + { } + + public virtual void OnChunkWritten(long startPosition, long endPosition, TRChunkType chunkType, byte[] data) + { } + + public virtual void OnMeshPaddingRead(uint meshPointer, List values) + { } + + public virtual List GetMeshPadding(uint meshPointer) + => null; +} diff --git a/TRLevelControlTests/Base/Observers/TR45Observer.cs b/TRLevelControlTests/Base/Observers/TR45Observer.cs new file mode 100644 index 000000000..abf0e53e0 --- /dev/null +++ b/TRLevelControlTests/Base/Observers/TR45Observer.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TRLevelControl.Model; + +namespace TRLevelControlTests; + +public class TR45Observer : ObserverBase +{ + private readonly Dictionary _inflatedReads = new(); + private readonly Dictionary _inflatedWrites = new(); + + private readonly Dictionary> _meshPadding = new(); + + public override void TestOutput(byte[] input, byte[] output) + { + CollectionAssert.AreEquivalent(_inflatedReads.Keys, _inflatedWrites.Keys); + + foreach (TRChunkType type in _inflatedReads.Keys) + { + CollectionAssert.AreEqual(_inflatedReads[type].Data, _inflatedWrites[type].Data); + } + + // At this stage, everything zipped matches. We want to check for unzipped matches, so do + // so by stripping out everything that has been zipped from both streams. + List oldData = new(input); + List newData = new(output); + + List unzips = new(_inflatedReads.Values); + unzips.Sort((z1, z2) => z2.StreamEnd.CompareTo(z1.StreamEnd)); + foreach (ZipWrapper zip in unzips) + { + oldData.RemoveRange((int)zip.StreamStart, (int)(zip.StreamEnd - zip.StreamStart)); + } + + unzips = new(_inflatedWrites.Values); + unzips.Sort((z1, z2) => z2.StreamEnd.CompareTo(z1.StreamEnd)); + foreach (ZipWrapper zip in unzips) + { + newData.RemoveRange((int)zip.StreamStart, (int)(zip.StreamEnd - zip.StreamStart)); + } + + CollectionAssert.AreEqual(oldData, newData); + } + + public override void OnChunkRead(long startPosition, long endPosition, TRChunkType chunkType, byte[] data) + { + _inflatedReads[chunkType] = new() + { + StreamStart = startPosition, + StreamEnd = endPosition, + Data = data + }; + } + + public override void OnChunkWritten(long startPosition, long endPosition, TRChunkType chunkType, byte[] data) + { + _inflatedWrites[chunkType] = new() + { + StreamStart = startPosition, + StreamEnd = endPosition, + Data = data + }; + } + + public override void OnMeshPaddingRead(uint meshPointer, List values) + { + _meshPadding[meshPointer] = values; + } + + public override List GetMeshPadding(uint meshPointer) + { + return _meshPadding.ContainsKey(meshPointer) ? _meshPadding[meshPointer] : null; + } + + class ZipWrapper + { + public long StreamStart { get; set; } + public long StreamEnd { get; set; } + public byte[] Data { get; set; } + } +} diff --git a/TRLevelControlTests/Base/TestBase.cs b/TRLevelControlTests/Base/TestBase.cs index 4c0604290..7cd5159ab 100644 --- a/TRLevelControlTests/Base/TestBase.cs +++ b/TRLevelControlTests/Base/TestBase.cs @@ -1,6 +1,4 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using TRLevelControl; -using TRLevelControl.Compression; +using TRLevelControl; using TRLevelControl.Model; namespace TRLevelControlTests; @@ -83,101 +81,52 @@ public static TR5Level GetTR5Level(string level) public static void ReadWriteLevel(string levelName, TRGameVersion version) { string pathI = GetReadPath(levelName, version); + using FileStream dataStream = File.OpenRead(pathI); + using MemoryStream inputStream = new(); using MemoryStream outputStream = new(); + dataStream.CopyTo(inputStream); + byte[] inputData = inputStream.ToArray(); + inputStream.Position = 0; + + ObserverBase observer; switch (version) { case TRGameVersion.TR1: - TR1LevelControl control1 = new(); - TR1Level level1 = control1.Read(pathI); + observer = new(); + TR1LevelControl control1 = new(observer); + TR1Level level1 = control1.Read(inputStream); control1.Write(level1, outputStream); break; case TRGameVersion.TR2: - TR2LevelControl control2 = new(); + observer = new(); + TR2LevelControl control2 = new(observer); TR2Level level2 = control2.Read(pathI); control2.Write(level2, outputStream); break; case TRGameVersion.TR3: - TR3LevelControl control3 = new(); + observer = new(); + TR3LevelControl control3 = new(observer); TR3Level level3 = control3.Read(pathI); control3.Write(level3, outputStream); break; + case TRGameVersion.TR4: + observer = new TR45Observer(); + TR4LevelControl control4 = new(observer); + TR4Level level4 = control4.Read(pathI); + control4.Write(level4, outputStream); + break; + case TRGameVersion.TR5: + observer = new TR45Observer(); + TR5LevelControl control5 = new(observer); + TR5Level level5 = control5.Read(pathI); + control5.Write(level5, outputStream); + break; default: - throw new Exception("Utility IO method suitable only for TR1-3."); + throw new NotImplementedException(); } - byte[] b1 = File.ReadAllBytes(pathI); - byte[] b2 = outputStream.ToArray(); - - CollectionAssert.AreEqual(b1, b2); - } - - public static void ReadWriteTR4Level(string levelName) - { - TR4LevelControl control = new(); - - string pathI = GetReadPath(levelName, TRGameVersion.TR4); - using MemoryStream outputStream = new(); - - // ZLib produces a slightly more optimal output than OG so we can't compare byte-for-byte - TR4Level level = control.Read(pathI); - TR45LevelSummary originalSummary = new() - { - LevelChunkUncompressedSize = level.LevelDataChunk.UncompressedSize, - Tex32ChunkUncompressedSize = level.Texture32Chunk.UncompressedSize, - Tex16ChunkUncompressedSize = level.Texture16Chunk.UncompressedSize, - Tex32MChunkUncompressedSize = level.SkyAndFont32Chunk.UncompressedSize - }; - - control.Write(level, outputStream); - // Read in again what we wrote out - TR4Level level2 = control.Read(new MemoryStream(outputStream.ToArray())); - - // Verify - have we lost any data? - Assert.AreEqual(originalSummary.LevelChunkUncompressedSize, (uint)TRZlib.Decompress(level2.LevelDataChunk.CompressedChunk).Length); - Assert.AreEqual(originalSummary.Tex32ChunkUncompressedSize, (uint)TRZlib.Decompress(level2.Texture32Chunk.CompressedChunk).Length); - Assert.AreEqual(originalSummary.Tex16ChunkUncompressedSize, (uint)TRZlib.Decompress(level2.Texture16Chunk.CompressedChunk).Length); - Assert.AreEqual(originalSummary.Tex32MChunkUncompressedSize, (uint)TRZlib.Decompress(level2.SkyAndFont32Chunk.CompressedChunk).Length); - - // Test compression against original - CollectionAssert.AreEqual(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, level2.LevelDataChunk.Seperator); - CollectionAssert.AreEqual(TRZlib.Decompress(level.Texture32Chunk.CompressedChunk), TRZlib.Decompress(level2.Texture32Chunk.CompressedChunk)); - CollectionAssert.AreEqual(TRZlib.Decompress(level.Texture16Chunk.CompressedChunk), TRZlib.Decompress(level2.Texture16Chunk.CompressedChunk)); - CollectionAssert.AreEqual(TRZlib.Decompress(level.SkyAndFont32Chunk.CompressedChunk), TRZlib.Decompress(level2.SkyAndFont32Chunk.CompressedChunk)); - } - - public static void ReadWriteTR5Level(string levelName) - { - TR5LevelControl control = new(); - - string pathI = GetReadPath(levelName, TRGameVersion.TR5); - using MemoryStream outputStream = new(); - - // ZLib produces a slightly more optimal output than OG so we can't compare byte-for-byte - TR5Level level = control.Read(pathI); - TR45LevelSummary originalSummary = new() - { - LevelChunkUncompressedSize = level.LevelDataChunk.UncompressedSize, - Tex32ChunkUncompressedSize = level.Texture32Chunk.UncompressedSize, - Tex16ChunkUncompressedSize = level.Texture16Chunk.UncompressedSize, - Tex32MChunkUncompressedSize = level.SkyAndFont32Chunk.UncompressedSize - }; - - control.Write(level, outputStream); - // Read in again what we wrote out - TR5Level level2 = control.Read(new MemoryStream(outputStream.ToArray())); - - // Verify - have we lost any data? - Assert.AreEqual(originalSummary.LevelChunkUncompressedSize, (uint)level2.LevelDataChunk.CompressedChunk.Length); - Assert.AreEqual(originalSummary.Tex32ChunkUncompressedSize, (uint)TRZlib.Decompress(level2.Texture32Chunk.CompressedChunk).Length); - Assert.AreEqual(originalSummary.Tex16ChunkUncompressedSize, (uint)TRZlib.Decompress(level2.Texture16Chunk.CompressedChunk).Length); - Assert.AreEqual(originalSummary.Tex32MChunkUncompressedSize, (uint)TRZlib.Decompress(level2.SkyAndFont32Chunk.CompressedChunk).Length); - - // Test compression against original - CollectionAssert.AreEqual(new byte[] { 0xCD, 0xCD, 0xCD, 0xCD, 0xCD, 0xCD }, level2.LevelDataChunk.Seperator); - CollectionAssert.AreEqual(TRZlib.Decompress(level.Texture32Chunk.CompressedChunk), TRZlib.Decompress(level2.Texture32Chunk.CompressedChunk)); - CollectionAssert.AreEqual(TRZlib.Decompress(level.Texture16Chunk.CompressedChunk), TRZlib.Decompress(level2.Texture16Chunk.CompressedChunk)); - CollectionAssert.AreEqual(TRZlib.Decompress(level.SkyAndFont32Chunk.CompressedChunk), TRZlib.Decompress(level2.SkyAndFont32Chunk.CompressedChunk)); + observer.TestOutput(inputData, outputStream.ToArray()); } public static TR1Level WriteReadTempLevel(TR1Level level) diff --git a/TRLevelControlTests/TR4/IOTests.cs b/TRLevelControlTests/TR4/IOTests.cs index ed09f84db..bc469a2d6 100644 --- a/TRLevelControlTests/TR4/IOTests.cs +++ b/TRLevelControlTests/TR4/IOTests.cs @@ -50,7 +50,7 @@ public class IOTests : TestBase [DataRow(TR4LevelNames.TIMES)] public void TestReadWrite(string levelname) { - ReadWriteTR4Level(levelname); + ReadWriteLevel(levelname, TRGameVersion.TR4); } [TestMethod] @@ -96,13 +96,13 @@ public void TestFloorData(string levelName) { TR4Level level = GetTR4Level(levelName); - List originalData = new(level.LevelDataChunk.FloorData); + List originalData = new(level.FloorData); FDControl fdControl = new(); fdControl.ParseFromLevel(level); fdControl.WriteToLevel(level); - CollectionAssert.AreEqual(originalData, level.LevelDataChunk.FloorData); + CollectionAssert.AreEqual(originalData, level.FloorData); } [TestMethod] @@ -111,7 +111,7 @@ public void Floordata_ReadWrite_MechBeetleTest() TR4Level lvl = GetTR4Level(TR4LevelNames.CLEOPATRA); //Store the original floordata from the level - List originalFData = new(lvl.LevelDataChunk.FloorData); + List originalFData = new(lvl.FloorData); //Parse the floordata using FDControl and re-write the parsed data back FDControl fdataReader = new(); @@ -119,7 +119,7 @@ public void Floordata_ReadWrite_MechBeetleTest() fdataReader.WriteToLevel(lvl); //Compare to make sure the original fdata was written back. - CollectionAssert.AreEqual(originalFData, lvl.LevelDataChunk.FloorData, "Floordata does not match"); + CollectionAssert.AreEqual(originalFData, lvl.FloorData, "Floordata does not match"); } [TestMethod] @@ -128,7 +128,7 @@ public void Floordata_ReadWrite_TriggerTriggererTest() TR4Level lvl = GetTR4Level(TR4LevelNames.ALEXANDRIA); //Store the original floordata from the level - List originalFData = new(lvl.LevelDataChunk.FloorData); + List originalFData = new(lvl.FloorData); //Parse the floordata using FDControl and re-write the parsed data back FDControl fdataReader = new(); @@ -136,6 +136,6 @@ public void Floordata_ReadWrite_TriggerTriggererTest() fdataReader.WriteToLevel(lvl); //Compare to make sure the original fdata was written back. - CollectionAssert.AreEqual(originalFData, lvl.LevelDataChunk.FloorData, "Floordata does not match"); + CollectionAssert.AreEqual(originalFData, lvl.FloorData, "Floordata does not match"); } } diff --git a/TRLevelControlTests/TR5/IOTests.cs b/TRLevelControlTests/TR5/IOTests.cs index 8c6b2196e..12751d53e 100644 --- a/TRLevelControlTests/TR5/IOTests.cs +++ b/TRLevelControlTests/TR5/IOTests.cs @@ -25,7 +25,7 @@ public class IOTests : TestBase [DataRow(TR5LevelNames.REDALERT)] public void TestReadWrite(string levelName) { - ReadWriteTR5Level(levelName); + ReadWriteLevel(levelName, TRGameVersion.TR5); } [TestMethod] @@ -46,12 +46,12 @@ public void TestFloorData(string levelName) { TR5Level level = GetTR5Level(levelName); - List originalData = new(level.LevelDataChunk.FloorData); + List originalData = new(level.FloorData); FDControl fdControl = new(); fdControl.ParseFromLevel(level); fdControl.WriteToLevel(level); - CollectionAssert.AreEqual(originalData, level.LevelDataChunk.FloorData); + CollectionAssert.AreEqual(originalData, level.FloorData); } }