diff --git a/src/010 Templates/trees.bt b/src/010 Templates/trees.bt index 2b7510d..c418b0c 100644 --- a/src/010 Templates/trees.bt +++ b/src/010 Templates/trees.bt @@ -56,7 +56,7 @@ struct InstanceRef { if (isDirt3 || isGridAutosport) { int32 distant; int32 maxInstances; - local int32 numInstances = 0; + local int32 numInstances = 0; } else { int32 maxInstances; diff --git a/src/EgoEngineLibrary/IO/EndianBinaryReaderExtensions.cs b/src/EgoEngineLibrary/IO/EndianBinaryReaderExtensions.cs new file mode 100644 index 0000000..809fd1b --- /dev/null +++ b/src/EgoEngineLibrary/IO/EndianBinaryReaderExtensions.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; + +namespace EgoEngineLibrary.IO; + +public static class EndianBinaryReaderExtensions +{ + extension(EndianBinaryReader reader) + { + public string ReadNullTerminatedString() + { + List bytes = []; + byte b = reader.ReadByte(); + + while (b != 0x00) + { + bytes.Add(b); + b = reader.ReadByte(); + } + + return reader.Encoding.GetString(CollectionsMarshal.AsSpan(bytes)); + } + } +} \ No newline at end of file diff --git a/src/EgoEngineLibrary/Text/EncodingExtensions.cs b/src/EgoEngineLibrary/Text/EncodingExtensions.cs new file mode 100644 index 0000000..3acb13f --- /dev/null +++ b/src/EgoEngineLibrary/Text/EncodingExtensions.cs @@ -0,0 +1,57 @@ +using System.Text; +using EgoEngineLibrary.IO; + +namespace EgoEngineLibrary.Text; + +public static class EncodingExtensions +{ + extension(Encoding encoding) + { + public StringByteBuffer BuildStringByteBuffer(IEnumerable strings) + { + return encoding.BuildStringByteBuffer([.. strings]); + } + + public StringByteBuffer BuildStringByteBuffer(IReadOnlyCollection strings) + { + int totalCharacters = 0; + int largestStringLength = 0; + foreach (var s in strings) + { + totalCharacters += s.Length; + largestStringLength = Math.Max(largestStringLength, s.Length); + } + + int bufferSize = encoding.GetMaxByteCount(totalCharacters); + int singleBufferSize = encoding.GetMaxByteCount(largestStringLength); + int bufferIndex = 0; + byte[] buffer = new byte[bufferSize + strings.Count]; + byte[] singleBuffer = new byte[singleBufferSize + 1]; + var stringOffsetMap = new Dictionary(strings.Count); + foreach (var s in strings) + { + if (stringOffsetMap.ContainsKey(s)) + { + continue; + } + + var bytesWritten = encoding.GetBytes(s, singleBuffer); + singleBuffer[bytesWritten] = 0; + var filenameSpan = singleBuffer.AsSpan(0, bytesWritten + 1); + var index = buffer.IndexOf(filenameSpan); + if (index >= 0) + { + stringOffsetMap.Add(s, index); + } + else + { + stringOffsetMap.Add(s, bufferIndex); + filenameSpan.CopyTo(buffer.AsSpan(bufferIndex)); + bufferIndex += filenameSpan.Length; + } + } + + return new StringByteBuffer(buffer, bufferIndex, stringOffsetMap); + } + } +} \ No newline at end of file diff --git a/src/EgoEngineLibrary/Text/StringByteBuffer.cs b/src/EgoEngineLibrary/Text/StringByteBuffer.cs new file mode 100644 index 0000000..53f9191 --- /dev/null +++ b/src/EgoEngineLibrary/Text/StringByteBuffer.cs @@ -0,0 +1,3 @@ +namespace EgoEngineLibrary.Text; + +public record StringByteBuffer(byte[] Buffer, int BufferSize, Dictionary StringOffsetMap); \ No newline at end of file diff --git a/src/EgoEngineLibrary/Track/TrackTreesFile.cs b/src/EgoEngineLibrary/Track/TrackTreesFile.cs new file mode 100644 index 0000000..30d5e50 --- /dev/null +++ b/src/EgoEngineLibrary/Track/TrackTreesFile.cs @@ -0,0 +1,328 @@ +using System.Diagnostics; +using System.Numerics; +using EgoEngineLibrary.IO; +using EgoEngineLibrary.Text; + +namespace EgoEngineLibrary.Track; + +public class TrackTreesFile +{ + public TrackTreesFileType Type { get; set; } + public Vector3 BoundsMin { get; set; } + public Vector3 BoundsMax { get; set; } + + public List InstanceRefs { get; set; } = []; + + public List Instances { get; set; } = []; + + public static TrackTreesFile ReadBinary(EndianBinaryReader reader, TrackTreesFileType type) + { + var file = new TrackTreesFile { Type = type }; + if (type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + var version = reader.ReadInt32(); + var instanceListOffset = reader.ReadInt32(); + int numInstanceList = reader.ReadInt32(); + + Debug.Assert(version == 0); + Debug.Assert(instanceListOffset == 12); + Debug.Assert(numInstanceList == 1); + reader.BaseStream.Seek(instanceListOffset, SeekOrigin.Begin); + } + + file.BoundsMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + file.BoundsMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + if (type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + reader.ReadInt32(); + } + + reader.ReadInt32(); + + if (type is TrackTreesFileType.Dirt2 or TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + // totalLandmarks + reader.ReadInt32(); + } + + int instanceRefOffset = reader.ReadInt32(); + int numInstanceRef = reader.ReadInt32(); + int instanceOffset = 0; + int numInstance = 0; + if (type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + instanceOffset = reader.ReadInt32(); + numInstance = reader.ReadInt32(); + } + + reader.BaseStream.Seek(instanceRefOffset, SeekOrigin.Begin); + for (var r = 0; r < numInstanceRef; ++r) + { + var filenameOffset = reader.ReadInt32(); + var filename = ReadStringAtOffset(filenameOffset); + var instanceRef = new TrackTreesInstanceRef(filename); + file.InstanceRefs.Add(instanceRef); + + if (type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + instanceRef.ReferenceId = reader.ReadInt32(); + } + else + { + instanceRef.ReferenceId = r; + } + + instanceRef.BoundsMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + instanceRef.BoundsMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + if (type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + instanceRef.Distant = reader.ReadInt32(); + instanceRef.MaxInstances = reader.ReadInt32(); + } + else + { + instanceRef.MaxInstances = reader.ReadInt32(); + reader.ReadInt32(); + instanceOffset = reader.ReadInt32(); + numInstance = reader.ReadInt32(); + instanceRef.Distant = reader.ReadInt32(); + + if (numInstance <= 0) + { + continue; + } + + var pos = reader.BaseStream.Position; + reader.Seek(instanceOffset, SeekOrigin.Begin); + for (var i = 0; i < numInstance; ++i) + { + var instance = new TrackTreesInstance(); + instance.ReferenceId = r; + instance.InstanceId = file.Instances.Count; + file.Instances.Add(instance); + instance.Transform = new Matrix4x4( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 1); + instance.Color = new Vector4( + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue); + if (type is TrackTreesFileType.Dirt2) + { + instance.ShadowFactor = reader.ReadSingle(); + instance.Dynamic = reader.ReadInt32(); + instance.Landmark = reader.ReadInt32(); + } + + instance.InstanceTag = instance.InstanceId; + } + + reader.BaseStream.Position = pos; + } + } + + if (numInstance <= 0 || + type is not TrackTreesFileType.Dirt3 and not TrackTreesFileType.GridAutosport) + { + return file; + } + + reader.Seek(instanceOffset, SeekOrigin.Begin); + for (var i = 0; i < numInstance; ++i) + { + var instance = new TrackTreesInstance(); + file.Instances.Add(instance); + instance.ReferenceId = reader.ReadInt32(); + instance.InstanceId = reader.ReadInt32(); + instance.Transform = new Matrix4x4( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 1); + instance.Color = new Vector4( + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue, + reader.ReadByte() / (float)byte.MaxValue); + instance.ShadowFactor = reader.ReadSingle(); + instance.Dynamic = reader.ReadInt32(); + instance.Landmark = reader.ReadInt32(); + instance.InstanceTag = reader.ReadInt32(); + if (type is TrackTreesFileType.GridAutosport) + { + var todSpecificOffset = reader.ReadInt32(); + if (todSpecificOffset < 0) + { + continue; + } + + instance.TodSpecific = ReadStringAtOffset(todSpecificOffset); + } + } + + return file; + + string ReadStringAtOffset(int offset) + { + var pos = reader.BaseStream.Position; + reader.Seek(offset, SeekOrigin.Begin); + var str = reader.ReadNullTerminatedString(); + reader.BaseStream.Position = pos; + return str; + } + } + + public void WriteBinary(EndianBinaryWriter writer) + { + int instanceRefOffset; + int instanceOffset; + int bytesPerInstance; + switch (Type) + { + case TrackTreesFileType.RaceDriverGrid: + instanceRefOffset = 36; + instanceOffset = instanceRefOffset + 48 * InstanceRefs.Count; + bytesPerInstance = 52; + break; + case TrackTreesFileType.Dirt2: + instanceRefOffset = 40; + instanceOffset = instanceRefOffset + 48 * InstanceRefs.Count; + bytesPerInstance = 64; + break; + case TrackTreesFileType.Dirt3: + instanceRefOffset = 64; + instanceOffset = instanceRefOffset + 40 * InstanceRefs.Count; + bytesPerInstance = 76; + break; + case TrackTreesFileType.GridAutosport: + instanceRefOffset = 64; + instanceOffset = instanceRefOffset + 40 * InstanceRefs.Count; + bytesPerInstance = 80; + break; + default: + throw new ArgumentOutOfRangeException(nameof(Type), Type, null); + } + + var strings = InstanceRefs.Select(x => x.Filename) + .Concat(Instances.Select(x => x.TodSpecific).Where(x => !string.IsNullOrEmpty(x))); + var stringBuffer = writer.Encoding.BuildStringByteBuffer(strings); + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(0); + writer.Write(12); + writer.Write(1); + } + + writer.Write(BoundsMin.X); writer.Write(BoundsMin.Y); writer.Write(BoundsMin.Z); + writer.Write(BoundsMax.X); writer.Write(BoundsMax.Y); writer.Write(BoundsMax.Z); + + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(InstanceRefs.Count); + } + + writer.Write(Instances.Count); + + if (Type is TrackTreesFileType.Dirt2 or TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + // F1 2012 - 2014 set total landmarks to 0 no matter what. Ignoring that for now + writer.Write(Instances.Count(i => i.Landmark != 0)); + } + + writer.Write(instanceRefOffset); + writer.Write(InstanceRefs.Count); + + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instanceOffset); + writer.Write(Instances.Count); + } + + int instanceStartIndex = 0; + int stringsOffset = instanceOffset + bytesPerInstance * Instances.Count; + foreach (var instanceRef in InstanceRefs) + { + writer.Write(stringsOffset + stringBuffer.StringOffsetMap[instanceRef.Filename]); + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instanceRef.ReferenceId); + } + + writer.Write(instanceRef.BoundsMin.X); writer.Write(instanceRef.BoundsMin.Y); + writer.Write(instanceRef.BoundsMin.Z); + writer.Write(instanceRef.BoundsMax.X); writer.Write(instanceRef.BoundsMax.Y); + writer.Write(instanceRef.BoundsMax.Z); + + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instanceRef.Distant); + writer.Write(instanceRef.MaxInstances); + } + else + { + writer.Write(instanceRef.MaxInstances); + writer.Write(instanceStartIndex); + writer.Write(instanceOffset + instanceStartIndex * bytesPerInstance); + + var numInstances = Instances.Count(i => i.ReferenceId == instanceRef.ReferenceId); + instanceStartIndex += numInstances; + writer.Write(numInstances); + writer.Write(instanceRef.Distant); + } + } + + foreach (var instance in Instances) + { + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instance.ReferenceId); + writer.Write(instance.InstanceId); + } + + for (var r = 0; r < 4; ++r) + { + for (var c = 0; c < 3; ++c) + { + writer.Write(instance.Transform[r, c]); + } + } + + var color = instance.Color * byte.MaxValue + new Vector4(0.5f); + writer.Write((byte)color.X); + writer.Write((byte)color.Y); + writer.Write((byte)color.Z); + writer.Write((byte)color.W); + + if (Type is TrackTreesFileType.Dirt2 or TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instance.ShadowFactor); + writer.Write(instance.Dynamic); + writer.Write(instance.Landmark); + } + + if (Type is TrackTreesFileType.Dirt3 or TrackTreesFileType.GridAutosport) + { + writer.Write(instance.InstanceTag); + } + + if (Type is TrackTreesFileType.GridAutosport) + { + if (stringBuffer.StringOffsetMap.TryGetValue(instance.TodSpecific, out var todSpecificOffset)) + { + writer.Write(stringsOffset + todSpecificOffset); + } + else + { + writer.Write(-1); + } + } + } + + writer.Write(stringBuffer.Buffer, 0, stringBuffer.BufferSize); + } +} \ No newline at end of file diff --git a/src/EgoEngineLibrary/Track/TrackTreesFileType.cs b/src/EgoEngineLibrary/Track/TrackTreesFileType.cs new file mode 100644 index 0000000..bdacc2e --- /dev/null +++ b/src/EgoEngineLibrary/Track/TrackTreesFileType.cs @@ -0,0 +1,18 @@ +namespace EgoEngineLibrary.Track; + +public enum TrackTreesFileType +{ + RaceDriverGrid, + /// + /// Dirt 2, F1 2010 - F1 2014 + /// + Dirt2, + /// + /// Dirt 3, Dirt Showdown, Grid 2 + /// + Dirt3, + /// + /// Grid Autosport, Dirt Rally + /// + GridAutosport +} \ No newline at end of file diff --git a/src/EgoEngineLibrary/Track/TrackTreesInstance.cs b/src/EgoEngineLibrary/Track/TrackTreesInstance.cs new file mode 100644 index 0000000..464eb89 --- /dev/null +++ b/src/EgoEngineLibrary/Track/TrackTreesInstance.cs @@ -0,0 +1,16 @@ +using System.Numerics; + +namespace EgoEngineLibrary.Track; + +public class TrackTreesInstance +{ + public int ReferenceId { get; set; } + public int InstanceId { get; set; } + public Matrix4x4 Transform { get; set; } + public Vector4 Color { get; set; } + public float ShadowFactor { get; set; } + public int Dynamic { get; set; } + public int Landmark { get; set; } + public int InstanceTag { get; set; } + public string TodSpecific { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/EgoEngineLibrary/Track/TrackTreesInstanceRef.cs b/src/EgoEngineLibrary/Track/TrackTreesInstanceRef.cs new file mode 100644 index 0000000..aa7c78e --- /dev/null +++ b/src/EgoEngineLibrary/Track/TrackTreesInstanceRef.cs @@ -0,0 +1,20 @@ +using System.Numerics; + +namespace EgoEngineLibrary.Track; + +public class TrackTreesInstanceRef +{ + public string Filename { get; set; } + public int ReferenceId { get; set; } + + public Vector3 BoundsMin { get; set; } + public Vector3 BoundsMax { get; set; } + + public int Distant { get; set; } + public int MaxInstances { get; set; } + + public TrackTreesInstanceRef(string filename) + { + Filename = filename; + } +} \ No newline at end of file diff --git a/src/Sandbox/Program.cs b/src/Sandbox/Program.cs index 7761ea1..be03886 100644 --- a/src/Sandbox/Program.cs +++ b/src/Sandbox/Program.cs @@ -6,7 +6,7 @@ internal class Program { static void Main(string[] args) { - GvdbSandbox.Run(args); + TrackTreesSandbox.Run(args); } private static void XmlSandbox() diff --git a/src/Sandbox/TrackTreesSandbox.cs b/src/Sandbox/TrackTreesSandbox.cs new file mode 100644 index 0000000..66fac51 --- /dev/null +++ b/src/Sandbox/TrackTreesSandbox.cs @@ -0,0 +1,65 @@ +using EgoEngineLibrary.Conversion; +using EgoEngineLibrary.IO; +using EgoEngineLibrary.Track; + +namespace Sandbox; + +public class TrackTreesSandbox +{ + public static void Run(string[] args) + { + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\Grid\tracks\", TrackTreesFileType.RaceDriverGrid); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\Dirt 2\tracks\", TrackTreesFileType.Dirt2); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\F1 2010\tracks\", TrackTreesFileType.Dirt2); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\F1 2011\tracks\", TrackTreesFileType.Dirt2); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\F1 2012\tracks\", TrackTreesFileType.Dirt2); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\f12013\tracks\", TrackTreesFileType.Dirt2); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\F1 2014\tracks\", TrackTreesFileType.Dirt2); + var (folder, type) = (@"C:\Games\Steam\steamapps\common\DiRT 3 Complete Edition\tracks\", TrackTreesFileType.Dirt3); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\DiRT Showdown\tracks\", TrackTreesFileType.Dirt3); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\grid 2\tracks\", TrackTreesFileType.Dirt3); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\GRID Autosport\tracks\", TrackTreesFileType.GridAutosport); + //var (folder, type) = (@"C:\Games\Steam\steamapps\common\DiRT Rally\tracks\", TrackTreesFileType.GridAutosport); + var files = Utils.GetFiles("trees.bin", folder); + foreach (var f in files) + { + try + { + using var fs = File.Open(f, FileMode.Open, FileAccess.Read, FileShare.Read); + using var ms = new MemoryStream(); + fs.CopyTo(ms); + + ms.Seek(0, SeekOrigin.Begin); + using var reader = new EndianBinaryReader(EndianBitConverter.Little, ms); + var trees = TrackTreesFile.ReadBinary(reader, type); + + using var ms2 = new MemoryStream(); + using var writer = new EndianBinaryWriter(EndianBitConverter.Little, ms2); + trees.WriteBinary(writer); + + if (!ms.ToArray().SequenceEqual(ms2.ToArray())) + { + Console.WriteLine(f); + } + } + catch (Exception e) + { + Console.WriteLine(f); + Console.WriteLine(e); + } + } + } + + private static void ConvertD2ToD3() + { + var f = @"C:\Games\Steam\steamapps\common\Dirt 2\tracks\croatia\croatia_rally\route_0\trees.bin"; + using var fs = File.Open(f, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new EndianBinaryReader(EndianBitConverter.Little, fs); + var trees = TrackTreesFile.ReadBinary(reader, TrackTreesFileType.Dirt2); + + using var fso = File.Open(f + ".bin", FileMode.Create, FileAccess.Write, FileShare.Read); + using var writer = new EndianBinaryWriter(EndianBitConverter.Little, fso); + trees.Type = TrackTreesFileType.Dirt3; + trees.WriteBinary(writer); + } +} \ No newline at end of file