diff --git a/PRJ2 Extractor/Core/Prj2Exporter.cs b/PRJ2 Extractor/Core/Prj2Exporter.cs index d03b019..1a39425 100644 --- a/PRJ2 Extractor/Core/Prj2Exporter.cs +++ b/PRJ2 Extractor/Core/Prj2Exporter.cs @@ -128,6 +128,7 @@ public static List Export(TrLevel trLevel, string prj2FilePath) } room.NormalizeRoomY(); + ExportLights(room, trLevel.Rooms[i], pr); level.Rooms[i] = room; tombRooms[i] = room; } @@ -220,7 +221,252 @@ public static List Export(TrLevel trLevel, string prj2FilePath) } } + ExportSoundSources(trLevel, tombRooms, warnings); + ExportSinks(trLevel, tombRooms, warnings); + ExportCameras(trLevel, tombRooms, warnings); + ExportFlybyCameras(trLevel, tombRooms, warnings); + Prj2Writer.SaveToPrj2(prj2FilePath, level); return warnings; } + + /// + /// Converts raw tr4_room_light entries (TR world-coordinate convention) into TombLib + /// LightInstance objects, inverting the exact formulas used by TombLib's own compiler + /// (Compilers/Rooms.cs ConvertLights / BuildRoom) so a round-trip through TombLib reproduces + /// the original TR4 light data. + /// + private static void ExportLights(Room room, LevelRoom r1, PrjRoom pr) + { + foreach (var l in r1.Lights) + { + LightType type = l.LightType switch + { + 0 => LightType.Sun, + 1 => LightType.Point, + 2 => LightType.Spot, + 3 => LightType.Shadow, + 4 => LightType.FogBulb, + _ => LightType.Point, + }; + var light = new LightInstance(type) + { + // Position is room-relative in TombLib; world position (TR convention) is + // RoomInfo.X/Z + Position.X/Z, and -(Position.Y + RoomWorldY) for Y. + Position = new Vector3(l.X - r1.X, -l.Y - room.Position.Y, l.Z - r1.Z), + Color = new Vector3(l.ColourR / 128.0f, l.ColourG / 128.0f, l.ColourB / 128.0f), + }; + + // Intensity: raw ushort = round(abs(floatIntensity) * 8191), sign restored for Shadow type + // (TombLib negates Intensity for Shadow lights on load/construction). + light.Intensity = l.Intensity / 8191.0f; + if (type == LightType.Shadow) light.Intensity *= -1; + + switch (type) + { + case LightType.Point: + case LightType.Shadow: + light.InnerRange = l.In / Level.SectorSizeUnit; + light.OuterRange = l.Out / Level.SectorSizeUnit; + break; + case LightType.Spot: + light.InnerAngle = (float)(Math.Acos(Math.Clamp(l.In, -1.0, 1.0)) * (180.0 / Math.PI)); + light.OuterAngle = (float)(Math.Acos(Math.Clamp(l.Out, -1.0, 1.0)) * (180.0 / Math.PI)); + light.InnerRange = l.Length / Level.SectorSizeUnit; + light.OuterRange = l.CutOff / Level.SectorSizeUnit; + SetDirection(light, l.DirX, l.DirY, l.DirZ); + break; + case LightType.Sun: + SetDirection(light, l.DirX, l.DirY, l.DirZ); + break; + case LightType.FogBulb: + light.InnerRange = l.In / Level.SectorSizeUnit; + light.OuterRange = l.Out / Level.SectorSizeUnit; + light.Intensity = l.Length; // TR5-native storage; TR4 uses a color hack instead + break; + } + + room.AddObject(room.Level, light); + } + } + + private static void SetDirection(IRotateableYX light, float dx, float dy, float dz) + { + // Inverse of GetDirection()/compiler's DirectionX=-dir.X, DirectionY=dir.Y, DirectionZ=-dir.Z + // (light-specific encoding). + ApplyDirection(light, -dx, dy, -dz); + } + + private static void ApplyDirection(IRotateableYX obj, float dirX, float dirY, float dirZ) + { + float rx = (float)Math.Asin(Math.Clamp(dirY, -1.0, 1.0)); + float ry = (float)Math.Atan2(dirX, dirZ); + obj.SetArbitaryRotationsYX(ry * (180.0f / (float)Math.PI), rx * (180.0f / (float)Math.PI)); + } + + /// + /// Sound sources have no room field in the raw tr_sound_source struct; the containing room is + /// found by testing the raw world position against each room's X/Z/Y bounds (matches the + /// compiler's own room.WorldPos + instance.Position relationship, inverted). + /// + private static int FindContainingRoom(TrLevel trLevel, int x, int y, int z) + { + for (int i = 0; i < trLevel.Rooms.Length; i++) + { + var r1 = trLevel.Rooms[i]; + if (r1.NumX == 0 || r1.NumZ == 0) continue; + if (x < r1.X || x >= r1.X + r1.NumX * 1024) continue; + if (z < r1.Z || z >= r1.Z + r1.NumZ * 1024) continue; + if (y < r1.YTop || y > r1.YBottom) continue; + return i; + } + return -1; + } + + private static void ExportSoundSources(TrLevel trLevel, Room?[] tombRooms, List warnings) + { + foreach (var s in trLevel.SoundSources) + { + int roomIdx = FindContainingRoom(trLevel, s.X, s.Y, s.Z); + if (roomIdx < 0 || tombRooms[roomIdx] == null) + { + warnings.Add($"Sound source (SoundID={s.SoundId}) at ({s.X},{s.Y},{s.Z}) skipped: no containing room found"); + continue; + } + var room = tombRooms[roomIdx]!; + var r1 = trLevel.Rooms[roomIdx]; + var sound = new SoundSourceInstance + { + Position = new Vector3(s.X - r1.X, -s.Y - room.Position.Y, s.Z - r1.Z), + SoundId = s.SoundId, + // Flags 0xC0 covers both "Always" and "Automatic in a non-alternated room" (identical + // encoding); 0x40/0x80 mark automatic play tied to a specific alternate-room state. + // Empirically (verified against reference data) 0xC0 is used for Automatic in practice, + // so default there rather than Always. + PlayMode = s.Flags switch + { + 0x80 => SoundSourcePlayMode.OnlyInBaseRoom, + 0x40 => SoundSourcePlayMode.OnlyInAlternateRoom, + _ => SoundSourcePlayMode.Automatic, + }, + }; + room.AddObject(room.Level, sound); + } + } + + /// + /// Sinks share the raw tr_camera array with Camera trigger targets; which indices are which is + /// only knowable by scanning trigger ActionLists (done once during TrLevel.Load, see + /// TrLevel.cs's Trigger FloorData handling). For entries classified as sinks, the raw "Room" + /// field is repurposed by the TombLib compiler to hold Strength (not a room index) and "Flags" + /// to hold a pathfinding box index -- see LevelCompilerClassicTR.cs's sink-writing code, which + /// this inverts. The containing room itself must be found by position, same as sound sources. + /// + private static void ExportSinks(TrLevel trLevel, Room?[] tombRooms, List warnings) + { + foreach (int idx in trLevel.SinkFloorDataIndices) + { + if (idx < 0 || idx >= trLevel.Cameras.Count) + { + warnings.Add($"Sink index {idx} out of range of the raw Cameras[] array ({trLevel.Cameras.Count} entries)"); + continue; + } + var c = trLevel.Cameras[idx]; + int roomIdx = FindContainingRoom(trLevel, c.X, c.Y, c.Z); + if (roomIdx < 0 || tombRooms[roomIdx] == null) + { + warnings.Add($"Sink (index {idx}, strength {c.Room}) at ({c.X},{c.Y},{c.Z}) skipped: no containing room found"); + continue; + } + var room = tombRooms[roomIdx]!; + var r1 = trLevel.Rooms[roomIdx]; + var sink = new SinkInstance + { + Position = new Vector3(c.X - r1.X, -c.Y - room.Position.Y, c.Z - r1.Z), + // Empirically confirmed (Francy): raw Strength is stored 1-based, TombLib's is 0-based. + Strength = (short)(c.Room - 1), + }; + room.AddObject(room.Level, sink); + } + } + + /// + /// Static (non-flyby) cameras, found via the CameraFloorDataIndices set collected while parsing + /// Trigger FloorData (TrigAction 0x01) during TrLevel.Load. Unlike sinks, tr_camera's Room field + /// is a genuine room index for camera entries, so no position-based search is needed. + /// + private static void ExportCameras(TrLevel trLevel, Room?[] tombRooms, List warnings) + { + foreach (int idx in trLevel.CameraFloorDataIndices) + { + if (idx < 0 || idx >= trLevel.Cameras.Count) + { + warnings.Add($"Camera index {idx} out of range of the raw Cameras[] array ({trLevel.Cameras.Count} entries)"); + continue; + } + var c = trLevel.Cameras[idx]; + if (c.Room < 0 || c.Room >= tombRooms.Length || tombRooms[c.Room] == null) + { + warnings.Add($"Camera (index {idx}) at ({c.X},{c.Y},{c.Z}) skipped: invalid room {c.Room}"); + continue; + } + var room = tombRooms[c.Room]!; + var r1 = trLevel.Rooms[c.Room]; + var camera = new CameraInstance + { + Position = new Vector3(c.X - r1.X, -c.Y - room.Position.Y, c.Z - r1.Z), + // Flags: 0x3 (bits 0+1 both set) = Sniper; bit0 alone = Locked; bit2 = GlideOut. + CameraMode = (c.Flags & 0x3) == 0x3 ? CameraInstanceMode.Sniper + : (c.Flags & 0x1) != 0 ? CameraInstanceMode.Locked + : CameraInstanceMode.Default, + GlideOut = (c.Flags & 0x4) != 0, + }; + room.AddObject(room.Level, camera); + } + } + + /// + /// Flyby cameras, from the raw tr4_flyby_camera array. Unlike static cameras, Room is a genuine + /// direct room index here too, so no position search is needed. + /// + private static void ExportFlybyCameras(TrLevel trLevel, Room?[] tombRooms, List warnings) + { + foreach (var c in trLevel.FlybyCameras) + { + int roomIdx = (int)c.RoomId; + if (roomIdx < 0 || roomIdx >= tombRooms.Length || tombRooms[roomIdx] == null) + { + warnings.Add($"Flyby camera (seq {c.Sequence}, idx {c.Index}) at ({c.X},{c.Y},{c.Z}) skipped: invalid room {roomIdx}"); + continue; + } + var room = tombRooms[roomIdx]!; + var r1 = trLevel.Rooms[roomIdx]; + + var position = new Vector3(c.X - r1.X, -c.Y - room.Position.Y, c.Z - r1.Z); + // DirX/Y/Z encode a world-space "look at" point: position + SectorSizeUnit*direction (with + // the same Y sign convention as the position fields). Invert to recover the direction. + float dirX = (c.DirX - c.X) / (float)Level.SectorSizeUnit; + float dirY = (c.Y - c.DirY) / (float)Level.SectorSizeUnit; + float dirZ = (c.DirZ - c.Z) / (float)Level.SectorSizeUnit; + + var flyby = new FlybyCameraInstance + { + Position = position, + Sequence = c.Sequence, + Number = c.Index, + Timer = (short)c.Timer, + Flags = c.Flags, + Fov = c.Fov * (360.0f / 65536.0f), + Speed = c.Speed / 655.0f, + }; + // Roll: encoded as rollTo65536 = (65536 - round(Roll*65536/360)) mod 65536, stored as a + // reinterpreted (unchecked) int16. Invert by reading it back as unsigned first. + ushort rollRaw = unchecked((ushort)c.Roll); + int rollX = (65536 - rollRaw) % 65536; + flyby.Roll = rollX * (360.0f / 65536.0f); + ApplyDirection(flyby, dirX, dirY, dirZ); + + room.AddObject(room.Level, flyby); + } + } } diff --git a/PRJ2 Extractor/Core/TrLevel.cs b/PRJ2 Extractor/Core/TrLevel.cs index 2adcaf4..59790e3 100644 --- a/PRJ2 Extractor/Core/TrLevel.cs +++ b/PRJ2 Extractor/Core/TrLevel.cs @@ -22,6 +22,11 @@ public class TrLevel : IDisposable public uint NumMeshtrees, SizeKeyframes, NumMoveables, NumStatics; public uint NumFloorData; public uint NumBoxes; + public List SoundSources = []; + public List Cameras = []; + public List FlybyCameras = []; + public HashSet CameraFloorDataIndices = []; + public HashSet SinkFloorDataIndices = []; public ushort[] FloorData = []; public LevelRoom[] Rooms = []; public LevelBox[] Boxes = []; @@ -69,10 +74,30 @@ static void ParseFloorType(ushort arg, out int f, out int sub, out int e) } else if (fd.Tipo == FloorType.Trigger) { + bool isFirst = true; do { data = FloorData[fdIndex + k]; k++; + if (isFirst) { isFirst = false; continue; } // TriggerSetup word, not an ActionList entry + + int trigAction = (data & 0x7C00) >> 10; + int parameter = data & 0x03FF; + if (trigAction == 0x01) // Camera: uses Parameter as Cameras[] index, plus one extra word + { + CameraFloorDataIndices.Add(parameter & 0x7F); + data = FloorData[fdIndex + k]; // the end/cont bit for 2-word actions lives here, not on the entry word above + k++; + } + else if (trigAction == 0x02) // Underwater Current (Sink): Parameter is Cameras[] index + { + SinkFloorDataIndices.Add(parameter); + } + else if (trigAction == 0x0C) // Flyby: also has one extra word + { + data = FloorData[fdIndex + k]; + k++; + } } while ((data & 0x8000) != 0x8000); } else if (fd.Tipo == FloorType.Climb) @@ -251,7 +276,8 @@ public byte Load(string filename, IProgress? progress = null) r.Colour.R = br2.ReadByte(); r.Colour.A = br2.ReadByte(); ushort lightCount = br2.ReadUInt16(); - geometry.Seek(lightCount * 46, SeekOrigin.Current); + for (int li = 0; li < lightCount; li++) + r.Lights.Add(ReadLight(br2)); ushort staticCount = br2.ReadUInt16(); geometry.Seek(staticCount * 20, SeekOrigin.Current); r.AltRoom = br2.ReadInt16(); @@ -305,11 +331,47 @@ public byte Load(string filename, IProgress? progress = null) size = br2.ReadUInt32(); geometry.Seek(size * 8, SeekOrigin.Current); size = br2.ReadUInt32(); - geometry.Seek(size * 16, SeekOrigin.Current); + Cameras = new List((int)size); + for (int i = 0; i < size; i++) + Cameras.Add(new LevelCamera + { + X = br2.ReadInt32(), + Y = br2.ReadInt32(), + Z = br2.ReadInt32(), + Room = br2.ReadInt16(), + Flags = br2.ReadUInt16(), + }); size = br2.ReadUInt32(); - geometry.Seek(size * 40, SeekOrigin.Current); + FlybyCameras = new List((int)size); + for (int i = 0; i < size; i++) + FlybyCameras.Add(new LevelFlybyCamera + { + X = br2.ReadInt32(), + Y = br2.ReadInt32(), + Z = br2.ReadInt32(), + DirX = br2.ReadInt32(), + DirY = br2.ReadInt32(), + DirZ = br2.ReadInt32(), + Sequence = br2.ReadByte(), + Index = br2.ReadByte(), + Fov = br2.ReadUInt16(), + Roll = br2.ReadInt16(), + Timer = br2.ReadUInt16(), + Speed = br2.ReadUInt16(), + Flags = br2.ReadUInt16(), + RoomId = br2.ReadUInt32(), + }); size = br2.ReadUInt32(); - geometry.Seek(size * 16, SeekOrigin.Current); + SoundSources = new List((int)size); + for (int i = 0; i < size; i++) + SoundSources.Add(new LevelSoundSource + { + X = br2.ReadInt32(), + Y = br2.ReadInt32(), + Z = br2.ReadInt32(), + SoundId = br2.ReadUInt16(), + Flags = br2.ReadUInt16(), + }); NumBoxes = br2.ReadUInt32(); Boxes = new LevelBox[NumBoxes]; for (int i = 0; i < NumBoxes; i++) @@ -425,6 +487,30 @@ private static ObjectTexture ReadObjectTexture(BinaryReader br) return texture; } + private static LevelLight ReadLight(BinaryReader br) + { + // tr4_room_light, 46 bytes, per TRosettaStone. + var l = new LevelLight + { + X = br.ReadInt32(), + Y = br.ReadInt32(), + Z = br.ReadInt32(), + ColourR = br.ReadByte(), + ColourG = br.ReadByte(), + ColourB = br.ReadByte(), + LightType = br.ReadByte(), + }; + l.Intensity = br.ReadUInt16(); + l.In = br.ReadSingle(); + l.Out = br.ReadSingle(); + l.Length = br.ReadSingle(); + l.CutOff = br.ReadSingle(); + l.DirX = br.ReadSingle(); + l.DirY = br.ReadSingle(); + l.DirZ = br.ReadSingle(); + return l; + } + private static Portal ReadPortal(BinaryReader br) { var p = new Portal { ToRoom = br.ReadUInt16() }; diff --git a/PRJ2 Extractor/MainWindow.xaml b/PRJ2 Extractor/MainWindow.xaml index 07fc416..c534430 100644 --- a/PRJ2 Extractor/MainWindow.xaml +++ b/PRJ2 Extractor/MainWindow.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="TR4 to PRJ" + Title="PRJ2 Extractor" Height="355" Width="479" MinHeight="355" MinWidth="479" AllowDrop="True" diff --git a/PRJ2 Extractor/MainWindow.xaml.cs b/PRJ2 Extractor/MainWindow.xaml.cs index d5303dd..6ced086 100644 --- a/PRJ2 Extractor/MainWindow.xaml.cs +++ b/PRJ2 Extractor/MainWindow.xaml.cs @@ -20,7 +20,7 @@ public partial class MainWindow : Window public MainWindow() { InitializeComponent(); - Title = $"TR4 to PRJ v{ProgramVersion:F2}"; + Title = $"PRJ2 Extractor v{ProgramVersion:F2}"; SetPlaceholderTexture(); } diff --git a/PRJ2 Extractor/Models/TrLevelModels.cs b/PRJ2 Extractor/Models/TrLevelModels.cs index 2fcdab8..34ed148 100644 --- a/PRJ2 Extractor/Models/TrLevelModels.cs +++ b/PRJ2 Extractor/Models/TrLevelModels.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace PRJ2_Extractor.Models; public enum FloorType @@ -95,6 +97,51 @@ public class LevelRoom public byte WaterScheme, Reverb, AltGroup; public bool IsFlipRoom; public short OriginalRoom = -1; + public List Lights = []; +} + +public class LevelLight +{ + // Raw tr4_room_light fields, TR world-coordinate convention (Y down-positive), absolute world position. + public int X, Y, Z; + public byte ColourR, ColourG, ColourB; + public byte LightType; // 0=Sun 1=Point 2=Spot 3=Shadow 4=FogBulb + public ushort Intensity; + public float In, Out, Length, CutOff; + public float DirX, DirY, DirZ; +} + +public class LevelSoundSource +{ + // Raw tr_sound_source fields (16 bytes), TR world-coordinate convention, absolute world position. + // No room field in the raw struct -- the containing room must be located by position. + public int X, Y, Z; + public ushort SoundId; + public ushort Flags; +} + +public class LevelCamera +{ + // Raw tr_camera fields (16 bytes). Shared struct for both Camera and Sink entries (TR engines + // use the same array for both); for Sink entries, Room holds Strength and Flags holds a box + // index instead of their nominal meaning -- see TrLevel.cs ExportSinks. + public int X, Y, Z; + public short Room; + public ushort Flags; +} + +public class LevelFlybyCamera +{ + // Raw tr4_flyby_camera fields (40 bytes). + public int X, Y, Z; + public int DirX, DirY, DirZ; + public byte Sequence, Index; + public ushort Fov; + public short Roll; + public ushort Timer; + public ushort Speed; + public ushort Flags; + public uint RoomId; } public static class PortalExtensions diff --git a/PrjDiag/Program.cs b/PrjDiag/Program.cs index f2f7750..90c3fe7 100644 --- a/PrjDiag/Program.cs +++ b/PrjDiag/Program.cs @@ -1,25 +1,46 @@ +using PRJ2_Extractor.Core; using System.IO; using System.Linq; using System.Threading; using TombLib.LevelData; using TombLib.LevelData.IO; using TombLib.Utils; -using PRJ2_Extractor.Core; - -var reporter = new ProgressReporterSimple(); -var settings = new Prj2Loader.Settings { IgnoreWads = true, IgnoreTextures = true, IgnoreSoundsCatalogs = true }; using var level = new TrLevel(); level.Load(@"C:\Users\Checkm8ra1n\Documents\alexhub2.tr4", new Progress(v => { })); +Console.WriteLine($"Raw FlybyCameras[]: {level.FlybyCameras.Count}"); + var warnings = Prj2Exporter.Export(level, @"C:\Users\Checkm8ra1n\Documents\alexhub2.prj2"); Console.WriteLine($"Export warnings: {warnings.Count}"); +foreach (var w in warnings) Console.WriteLine(" " + w); +var reporter = new ProgressReporterSimple(); +var settings = new Prj2Loader.Settings { IgnoreWads = true, IgnoreTextures = true, IgnoreSoundsCatalogs = true }; var ours = Prj2Loader.LoadFromPrj2(@"C:\Users\Checkm8ra1n\Documents\alexhub2.prj2", reporter, CancellationToken.None, settings); + +int fbTotal = ours.Rooms.Where(r => r != null).Sum(r => r.Objects.OfType().Count()); +Console.WriteLine($"Total flyby cameras: {fbTotal}"); +foreach (var room in ours.Rooms.Where(r => r != null)) +foreach (var fb in room.Objects.OfType()) + Console.WriteLine($" {room.Name}: Seq={fb.Sequence} Num={fb.Number} Pos=({fb.Position.X:F1},{fb.Position.Y:F1},{fb.Position.Z:F1}) Rot=({fb.RotationX:F1},{fb.RotationY:F1}) Roll={fb.Roll:F1} FOV={fb.Fov:F1} Speed={fb.Speed:F2}"); + var orig = Prj2Loader.LoadFromPrj2(@"C:\Users\Checkm8ra1n\Documents\alexhub2_orig.prj2", reporter, CancellationToken.None, settings); +int origCam = orig.Rooms.Where(r => r != null).Sum(r => r.Objects.OfType().Count()); +Console.WriteLine($"Reference generic 'cameras' (likely mis-imported flybys): {origCam}"); +foreach (var room in orig.Rooms.Where(r => r != null)) +foreach (var c in room.Objects.OfType()) +{ + var wp = room.Position + c.Position; + Console.WriteLine($" {room.Name}: world=({wp.X:F1},{wp.Y:F1},{wp.Z:F1})"); +} +foreach (var room in ours.Rooms.Where(r => r != null)) +foreach (var fb in room.Objects.OfType()) +{ + var wp = room.Position + fb.Position; + Console.WriteLine($" OURS FLYBY {room.Name}: world=({wp.X:F1},{wp.Y:F1},{wp.Z:F1})"); +} -int totalRooms = 0, matchingRooms = 0; -int mismatchSectors = 0, comparedSectors = 0; -var worstRooms = new List<(string name, int mismatches, int total)>(); +int totalRooms = 0, matchingRooms = 0, mismatchSectors = 0, comparedSectors = 0; foreach (var rOrig in orig.Rooms) { if (rOrig == null) continue; @@ -27,14 +48,13 @@ if (rOurs == null) continue; totalRooms++; if (rOurs.NumXSectors != rOrig.NumXSectors || rOurs.NumZSectors != rOrig.NumZSectors) continue; - int roomMismatch = 0, roomTotal = 0; + int roomMismatch = 0; for (int x = 0; x < rOrig.NumXSectors; x++) for (int z = 0; z < rOrig.NumZSectors; z++) { - var so = rOurs.Sectors[x, z]; - var sr = rOrig.Sectors[x, z]; + var so = rOurs.Sectors[x, z]; var sr = rOrig.Sectors[x, z]; if (so.IsAnyWall && sr.IsAnyWall) continue; - roomTotal++; comparedSectors++; + comparedSectors++; bool mismatch = Math.Abs(so.Floor.XpZn - sr.Floor.XpZn) > 4 || Math.Abs(so.Floor.XnZn - sr.Floor.XnZn) > 4 || Math.Abs(so.Floor.XnZp - sr.Floor.XnZp) > 4 || Math.Abs(so.Floor.XpZp - sr.Floor.XpZp) > 4 || Math.Abs(so.Ceiling.XpZn - sr.Ceiling.XpZn) > 4 || Math.Abs(so.Ceiling.XnZn - sr.Ceiling.XnZn) > 4 || @@ -42,11 +62,7 @@ if (mismatch) { mismatchSectors++; roomMismatch++; } } if (roomMismatch == 0) matchingRooms++; - worstRooms.Add((rOrig.Name, roomMismatch, roomTotal)); } -Console.WriteLine($"Rooms compared: {totalRooms}, fully matching (non-wall): {matchingRooms}"); -Console.WriteLine($"Non-wall sectors compared: {comparedSectors}, mismatched: {mismatchSectors} ({(comparedSectors>0?100.0*mismatchSectors/comparedSectors:0):F1}%)"); -Console.WriteLine("Worst rooms:"); -foreach (var w in worstRooms.OrderByDescending(w => w.mismatches).Take(10)) - Console.WriteLine($" {w.name}: {w.mismatches}/{w.total}"); +Console.WriteLine($"Rooms: {totalRooms}, fully matching: {matchingRooms}"); +Console.WriteLine($"Sectors: {comparedSectors}, mismatched: {mismatchSectors} ({(comparedSectors>0?100.0*mismatchSectors/comparedSectors:0):F1}%)"); return 0; diff --git a/README.md b/README.md index f9ca9a9..777f9fe 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,19 @@ PRJ2 Extractor is a tool for extracting from Tomb Raider levels, prj2 files that ## Disclaimer -This code is based on sapper-trle's TR42PRJ source code, this program is his code translated to C#, the original code is this [](https://github.com/sapper-trle/TR42PRJ). +The TRLE PRJ extraction, the TR4 level parser and the UI is based on sapper-trle's TR42PRJ source code, this program is his code translated to C#, the original code is this ()[https://github.com/sapper-trle/TR42PRJ]. ## TODO -- [ ] Add support for TR1, 2, 3 and 5. -- [ ] Add PRJ to PRJ2 conversion. \ No newline at end of file +- Add support for other Tomb Raider versions: + - [ ] TR1 + - [ ] TR2 + - [ ] TR3 + - [x] TR4 + - [ ] TR5 +- [x] Add PRJ2 extraction. +- [x] Write Geometry of rooms +- [x] Add level elements (sound sources, light points, sinks, etc.) +- [ ] Assign Textures to rooms faces +- [ ] Add objects from the level file +- [ ] Place Triggers \ No newline at end of file