From 335c989980d58ae1903440fe4dc1af8d1a1ad7af Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Fri, 21 Feb 2025 22:12:20 +0000 Subject: [PATCH] Handle TR4R/TR5R PDP data This updates the model builder to support TR4 and TR5 remastered, which has some changes in frame rotations and "null" model entries. --- TRLevelControl/Build/TRModelBuilder.cs | 63 ++++++++++++++++--- TRLevelControl/Control/TR4/TR4PDPControl.cs | 13 ++++ TRLevelControl/Control/TR5/TR5PDPControl.cs | 13 ++++ .../Model/Common/Enums/TRAngleMode.cs | 1 + .../Model/Common/TRAnimFrameRotation.cs | 1 + TRLevelControlTests/Base/TestBase.cs | 14 +++++ TRLevelControlTests/TR4/IOTests.cs | 7 +++ TRLevelControlTests/TR5/IOTests.cs | 7 +++ 8 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 TRLevelControl/Control/TR4/TR4PDPControl.cs create mode 100644 TRLevelControl/Control/TR5/TR5PDPControl.cs diff --git a/TRLevelControl/Build/TRModelBuilder.cs b/TRLevelControl/Build/TRModelBuilder.cs index 95cedb3e3..5eee74b8b 100644 --- a/TRLevelControl/Build/TRModelBuilder.cs +++ b/TRLevelControl/Build/TRModelBuilder.cs @@ -207,6 +207,15 @@ private void ReadModels(TRLevelReader reader) } List animatedModels = _placeholderModels.FindAll(m => m.Animation != TRConsts.NoAnimation); + if (_dataType == TRModelDataType.PDP && _version == TRGameVersion.TR5) + { + // TR5 PDP has entries for what seem like null models, with no animations, frames or meshes. These are not setup in the conventional way + // per OG, with jumps in anim idx, so we address this here to avoid skewing other animation counts below. + List invalidAnimModels = animatedModels.FindAll(m => m.ID > (uint)TR5Type.Lara && m.Animation == 0); + invalidAnimModels.ForEach(m => m.Animation = TRConsts.NoAnimation); + animatedModels.RemoveAll(invalidAnimModels.Contains); + } + for (int i = 0; i < animatedModels.Count; i++) { PlaceholderModel model = animatedModels[i]; @@ -511,6 +520,14 @@ private TRAnimFrame BuildFrame(ref int frameIndex, int numRotations) : GetSingleRotation(rot0); } + if (_dataType == TRModelDataType.PDP && _version > TRGameVersion.TR3) + { + // Some TR4+ rots are marked as all despite only having one value set, so when we write + // them back, we restore this mode to keep tests happy. If changing values otherwise, care + // should be taken to set the mode to Auto. + rot.Mode = rotMode; + } + switch (rotMode) { case TRAngleMode.X: @@ -548,16 +565,42 @@ private void TestTR5Changes(IEnumerable models) } } - private void DeconstructModel(T type, TRModel model) + private PlaceholderModel CreateDeconstructedModel(T type, TRModel model) { - PlaceholderModel placeholderModel = new() + PlaceholderModel placeholder = new() { ID = (uint)(object)type, - Animation = model.Animations.Count == 0 ? TRConsts.NoAnimation : (ushort)_placeholderAnimations.Count, - FrameOffset = (_dataType == TRModelDataType.PDP || _observer == null) - && model.Animations.Count == 0 ? 0 : (uint)_frames.Count * sizeof(short), NumMeshes = (ushort)model.Meshes.Count, }; + + if (model.Animations.Count == 0) + { + if (_dataType == TRModelDataType.PDP && _version == TRGameVersion.TR5 && model.Meshes.Count == 0) + { + placeholder.Animation = 0; + placeholder.IsNullTR5Model = true; + } + else + { + placeholder.Animation = TRConsts.NoAnimation; + } + + placeholder.FrameOffset = _dataType == TRModelDataType.PDP || _observer == null + ? 0 + : (uint)_frames.Count * sizeof(short); + } + else + { + placeholder.Animation = (ushort)_placeholderAnimations.Count; + placeholder.FrameOffset = (uint)_frames.Count * sizeof(short); + } + + return placeholder; + } + + private void DeconstructModel(T type, TRModel model) + { + PlaceholderModel placeholderModel = CreateDeconstructedModel(type, model); _placeholderModels.Add(placeholderModel); _trees.AddRange(model.MeshTrees); @@ -880,8 +923,8 @@ private void WriteModels(TRLevelWriter writer, TRDictionary models) writer.Write(placeholderModel.ID); writer.Write(placeholderModel.NumMeshes); - writer.Write(startingMesh); - writer.Write(treePointer); + writer.Write(placeholderModel.IsNullTR5Model ? (ushort)0 : startingMesh); + writer.Write(placeholderModel.IsNullTR5Model ? 0 : treePointer); writer.Write(placeholderModel.FrameOffset); writer.Write(placeholderModel.Animation); @@ -897,6 +940,11 @@ private void WriteModels(TRLevelWriter writer, TRDictionary models) private TRAngleMode GetMode(TRAnimFrameRotation rot) { + if (rot.Mode != TRAngleMode.Auto) + { + return rot.Mode; + } + if (rot.X == 0 && rot.Y == 0) { // OG TR2+ levels (and TRR levels) use Z here, PDP uses X. Makes no difference @@ -961,6 +1009,7 @@ class PlaceholderModel public uint FrameOffset { get; set; } public ushort Animation { get; set; } public int AnimCount { get; set; } + public bool IsNullTR5Model { get; set; } } class PlaceholderAnimation diff --git a/TRLevelControl/Control/TR4/TR4PDPControl.cs b/TRLevelControl/Control/TR4/TR4PDPControl.cs new file mode 100644 index 000000000..f5a2cd060 --- /dev/null +++ b/TRLevelControl/Control/TR4/TR4PDPControl.cs @@ -0,0 +1,13 @@ +using TRLevelControl.Build; +using TRLevelControl.Model; + +namespace TRLevelControl; + +public class TR4PDPControl : TRPDPControlBase +{ + public TR4PDPControl(ITRLevelObserver observer = null) + : base(observer) { } + + protected override TRModelBuilder CreateBuilder() + => new(TRGameVersion.TR4, TRModelDataType.PDP, null, true); +} diff --git a/TRLevelControl/Control/TR5/TR5PDPControl.cs b/TRLevelControl/Control/TR5/TR5PDPControl.cs new file mode 100644 index 000000000..bdeefaff9 --- /dev/null +++ b/TRLevelControl/Control/TR5/TR5PDPControl.cs @@ -0,0 +1,13 @@ +using TRLevelControl.Build; +using TRLevelControl.Model; + +namespace TRLevelControl; + +public class TR5PDPControl : TRPDPControlBase +{ + public TR5PDPControl(ITRLevelObserver observer = null) + : base(observer) { } + + protected override TRModelBuilder CreateBuilder() + => new(TRGameVersion.TR5, TRModelDataType.PDP, null, true); +} diff --git a/TRLevelControl/Model/Common/Enums/TRAngleMode.cs b/TRLevelControl/Model/Common/Enums/TRAngleMode.cs index 9577e0fa3..6744e6490 100644 --- a/TRLevelControl/Model/Common/Enums/TRAngleMode.cs +++ b/TRLevelControl/Model/Common/Enums/TRAngleMode.cs @@ -2,6 +2,7 @@ public enum TRAngleMode { + Auto = -1, All = 0, X = 0x4000, Y = 0x8000, diff --git a/TRLevelControl/Model/Common/TRAnimFrameRotation.cs b/TRLevelControl/Model/Common/TRAnimFrameRotation.cs index 7b0482704..c173ddbf4 100644 --- a/TRLevelControl/Model/Common/TRAnimFrameRotation.cs +++ b/TRLevelControl/Model/Common/TRAnimFrameRotation.cs @@ -2,6 +2,7 @@ public class TRAnimFrameRotation : ICloneable { + public TRAngleMode Mode { get; set; } = TRAngleMode.Auto; public short X { get; set; } public short Y { get; set; } public short Z { get; set; } diff --git a/TRLevelControlTests/Base/TestBase.cs b/TRLevelControlTests/Base/TestBase.cs index d989077ad..db9d1c787 100644 --- a/TRLevelControlTests/Base/TestBase.cs +++ b/TRLevelControlTests/Base/TestBase.cs @@ -202,6 +202,20 @@ public static void ReadWritePDP(string levelName, TRGameVersion version) control3.Write(models3, outputStream); break; + case TRGameVersion.TR4: + observer = new TR4Observer(true); + TR4PDPControl control4 = new(observer); + TRDictionary models4 = control4.Read(inputStream); + control4.Write(models4, outputStream); + break; + + case TRGameVersion.TR5: + observer = new TR5Observer(true); + TR5PDPControl control5 = new(observer); + TRDictionary models5 = control5.Read(inputStream); + control5.Write(models5, outputStream); + break; + default: throw new NotImplementedException(); } diff --git a/TRLevelControlTests/TR4/IOTests.cs b/TRLevelControlTests/TR4/IOTests.cs index dc171e55c..6d98b2837 100644 --- a/TRLevelControlTests/TR4/IOTests.cs +++ b/TRLevelControlTests/TR4/IOTests.cs @@ -24,6 +24,13 @@ public void TestRemasteredReadWrite(string levelName) ReadWriteLevel(levelName, TRGameVersion.TR4, true); } + [TestMethod] + [DynamicData(nameof(GetAllLevels), DynamicDataSourceType.Method)] + public void TestPDPReadWrite(string levelName) + { + ReadWritePDP(levelName, TRGameVersion.TR4); + } + [TestMethod] [DynamicData(nameof(GetAllLevels), DynamicDataSourceType.Method)] public void TestAgressiveFloorData(string levelName) diff --git a/TRLevelControlTests/TR5/IOTests.cs b/TRLevelControlTests/TR5/IOTests.cs index 307501520..4366f10f1 100644 --- a/TRLevelControlTests/TR5/IOTests.cs +++ b/TRLevelControlTests/TR5/IOTests.cs @@ -24,6 +24,13 @@ public void TestRemasteredReadWrite(string levelName) ReadWriteLevel(levelName, TRGameVersion.TR5, true); } + [TestMethod] + [DynamicData(nameof(GetAllLevels), DynamicDataSourceType.Method)] + public void TestPDPReadWrite(string levelName) + { + ReadWritePDP(levelName, TRGameVersion.TR5); + } + [TestMethod] [DynamicData(nameof(GetAllLevels), DynamicDataSourceType.Method)] public void TestAgressiveFloorData(string levelName)