diff --git a/Volatility/CLI/Commands/ExportResourceCommand.cs b/Volatility/CLI/Commands/ExportResourceCommand.cs index 8d3547e..04e1387 100644 --- a/Volatility/CLI/Commands/ExportResourceCommand.cs +++ b/Volatility/CLI/Commands/ExportResourceCommand.cs @@ -10,11 +10,13 @@ internal class ExportResourceCommand : ICommand { public static string CommandToken => "ExportResource"; public static string CommandDescription => "Exports information and relevant data from an imported/created resource into a platform's format."; - public static string CommandParameters => "--recurse --overwrite --type= --format= --respath= --outpath="; + public static string CommandParameters => "--recurse --overwrite --type= --format= --respath= --outpath= [--imports=] [--importsfile]"; public string? Format { get; set; } public string? ResourcePath { get; set; } public string? OutputPath { get; set; } + public string? Imports { get; set; } + public bool ImportsFile { get; set; } public bool Overwrite { get; set; } public bool Recursive { get; set; } @@ -57,6 +59,18 @@ public async Task Execute() throw new InvalidPlatformException("Error: Invalid file format specified!"); } + Unpacker? importUnpackerOverride = null; + if (!string.IsNullOrEmpty(Imports) && !string.Equals(Imports, "DEFAULT", StringComparison.OrdinalIgnoreCase)) + { + if (!TypeUtilities.TryParseEnum(Imports, out Unpacker parsedUnpacker)) + { + Console.WriteLine("Error: Invalid imports export mode specified!"); + return; + } + + importUnpackerOverride = parsedUnpacker; + } + var loadOperation = new LoadResourceOperation(); var exportOperation = new ExportResourceOperation(); @@ -104,7 +118,7 @@ public async Task Execute() return; } - await exportOperation.ExecuteAsync(resource, OutputPath, platform); + await exportOperation.ExecuteAsync(resource, OutputPath, platform, importUnpackerOverride, ImportsFile); Console.WriteLine($"Exported {Path.GetFileName(ResourcePath)} as {Path.GetFullPath(OutputPath)}."); })); @@ -117,6 +131,8 @@ public void SetArgs(Dictionary args) Format = (args.TryGetValue("format", out object? format) ? format as string : "")?.ToUpper(); ResourcePath = args.TryGetValue("respath", out object? respath) ? respath as string : ""; OutputPath = args.TryGetValue("outpath", out object? outpath) ? outpath as string : ""; + Imports = args.TryGetValue("imports", out object? imports) ? imports as string : ""; + ImportsFile = args.TryGetValue("importsfile", out var importsfile) && (bool)importsfile; Overwrite = args.TryGetValue("overwrite", out var ow) && (bool)ow; Recursive = args.TryGetValue("recurse", out var re) && (bool)re; } diff --git a/Volatility/Frontend.cs b/Volatility/Frontend.cs index f2f6a8c..063d5b0 100644 --- a/Volatility/Frontend.cs +++ b/Volatility/Frontend.cs @@ -38,8 +38,7 @@ static void Main(string[] args) { if (args.Length > 0) { - string fullCommand = string.Join(" ", args); - RunCommand(fullCommand); + RunCommandTokenized(args); } else { @@ -128,6 +127,31 @@ static void RunCommand(string input) } } + static void RunCommandTokenized(string[] input) + { + if (input.Length == 0) + { + return; + } + + try + { + var command = ParseCommandTokenized(input); + command.Execute().GetAwaiter().GetResult(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); +#if DEBUG + throw; +#endif + } + } + static ICommand ParseCommandTokenized(string[] input) // Split command { var commandName = input[0].ToLower(); diff --git a/Volatility/Operations/Resources/ExportResourceOperation.cs b/Volatility/Operations/Resources/ExportResourceOperation.cs index 4d2df09..508df0c 100644 --- a/Volatility/Operations/Resources/ExportResourceOperation.cs +++ b/Volatility/Operations/Resources/ExportResourceOperation.cs @@ -1,11 +1,15 @@ using Volatility.Resources; using Volatility.Utilities; - namespace Volatility.Operations.Resources; internal class ExportResourceOperation { - public Task ExecuteAsync(Resource resource, string outputPath, Platform platform) + public Task ExecuteAsync( + Resource resource, + string outputPath, + Platform platform, + Unpacker? importUnpackerOverride = null, + bool writeImportsToSeparateFile = false) { string? directoryPath = Path.GetDirectoryName(outputPath); @@ -32,6 +36,14 @@ public Task ExecuteAsync(Resource resource, string outputPath, Platform platform break; } + WriteExternalImports( + resource, + outputPath, + writer, + endian, + ResolveExternalImportsUnpackerFormat(resource, importUnpackerOverride), + writeImportsToSeparateFile); + if (resource is ShaderBase shader) { var stages = shader.GetCompileStages(); @@ -70,6 +82,94 @@ public Task ExecuteAsync(Resource resource, string outputPath, Platform platform return Task.CompletedTask; } + private static Unpacker ResolveExternalImportsUnpackerFormat( + Resource resource, + Unpacker? importUnpackerOverride) + { + return importUnpackerOverride ?? resource.Unpacker; + } + + private static void WriteExternalImports( + Resource resource, + string outputPath, + ResourceBinaryWriter writer, + Endian endian, + Unpacker importUnpacker, + bool forceExternalImportsFile) + { + List> imports = resource.GetExternalImports().ToList(); + + string yamlImportsPath = ResourceImport.GetImportsPath(outputPath, Unpacker.YAP); + string datImportsPath = ResourceImport.GetImportsPath(outputPath, Unpacker.Raw); + + if (imports.Count == 0) + { + ResourceImport.DeleteImportsSidecarFiles(outputPath); + return; + } + + if (importUnpacker == Unpacker.YAP) + { + ResourceImport.DeleteImportsSidecarFiles(outputPath); + WriteExternalImportsYaml(imports, yamlImportsPath); + return; + } + + if (forceExternalImportsFile) + { + ResourceImport.DeleteImportsSidecarFiles(outputPath); + WriteExternalImportsDat(datImportsPath, endian, imports); + return; + } + + writer.BaseStream.Seek(0, SeekOrigin.End); + WriteBinaryImports(writer, imports); + ResourceImport.DeleteImportsSidecarFiles(outputPath); + } + + private static void WriteExternalImportsYaml( + List> imports, + string importsPath) + { + List lines = new(imports.Count); + foreach (KeyValuePair entry in imports) + { + ulong resourceId = ResourceUtilities.ResolveResourceID(entry.Value); + lines.Add($"- \"0x{entry.Key:x8}\": \"{resourceId:X8}\""); + } + + File.WriteAllLines(importsPath, lines); + } + + private static void WriteExternalImportsDat( + string importsPath, + Endian endianness, + List> imports) + { + using FileStream fs = new(importsPath, FileMode.Create, FileAccess.Write); + using EndianAwareBinaryWriter writer = new(fs, endianness); + WriteBinaryImports(writer, imports); + } + + private static void WriteBinaryImports( + EndianAwareBinaryWriter writer, + List> imports) + { + foreach (KeyValuePair entry in imports) + { + if (entry.Key < 0 || entry.Key > uint.MaxValue) + { + throw new InvalidDataException( + $"Import offset 0x{entry.Key:X} cannot be stored in a binary imports block."); + } + + // Probably overkill but I just want to make sure we always use the correct writer overloads + writer.Write((ulong)ResourceUtilities.ResolveResourceID(entry.Value)); + writer.Write((uint)entry.Key); + writer.Write(0x00000000); + } + } + private static string GetShaderProgramBufferPath( string shaderOutputPath, ShaderStageCompile stage, diff --git a/Volatility/Operations/Resources/ImportResourceOperation.cs b/Volatility/Operations/Resources/ImportResourceOperation.cs index 67af050..6d93b07 100644 --- a/Volatility/Operations/Resources/ImportResourceOperation.cs +++ b/Volatility/Operations/Resources/ImportResourceOperation.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Text.RegularExpressions; using Volatility.Resources; @@ -105,33 +104,10 @@ public async Task ExecuteAsync(ResourceType resourceType, if (!File.Exists(convertedSamplePathName) || overwrite) { - ProcessStartInfo start = new ProcessStartInfo - { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - using Process process = new Process(); - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - Console.WriteLine($"Converting extracted sample {sampleName}.snr to wave..."); - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); + ProcessUtilities.RunAndRelayOutput( + sxPath, + $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}\""); } else { diff --git a/Volatility/Operations/Resources/SaveResourceOperation.cs b/Volatility/Operations/Resources/SaveResourceOperation.cs index 8b8e48d..3fcc0c4 100644 --- a/Volatility/Operations/Resources/SaveResourceOperation.cs +++ b/Volatility/Operations/Resources/SaveResourceOperation.cs @@ -15,6 +15,7 @@ public SaveResourceOperation() .DisableAliases() .WithTypeInspector(inner => new IncludeFieldsTypeInspector(inner)) .WithTypeConverter(new ResourceYamlTypeConverter()) + .WithTypeConverter(new BitArrayYamlTypeConverter()) .WithTypeConverter(new StrongIDYamlTypeConverter()) .WithTypeConverter(new StringEnumYamlTypeConverter()) .Build(); diff --git a/Volatility/Resources/AptData/AptData.cs b/Volatility/Resources/AptData/AptData.cs index 90cabb8..319554c 100644 --- a/Volatility/Resources/AptData/AptData.cs +++ b/Volatility/Resources/AptData/AptData.cs @@ -2,11 +2,10 @@ namespace Volatility.Resources; +[ResourceDefinition(ResourceType.AptData)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class AptData : Resource { - public override ResourceType ResourceType => ResourceType.AptData; - public override Platform ResourcePlatform => Platform.Agnostic; - public string MovieName; public string BaseComponentName; public GuiGeometryObject GuiGeometry; @@ -146,4 +145,4 @@ public enum ETextureType : int Vector = 0, TexturedClamp = 1, TexturedWrap = 2 -} \ No newline at end of file +} diff --git a/Volatility/Resources/AttribSysVault/AttribSysVault.cs b/Volatility/Resources/AttribSysVault/AttribSysVault.cs index 60658df..4b77f04 100644 --- a/Volatility/Resources/AttribSysVault/AttribSysVault.cs +++ b/Volatility/Resources/AttribSysVault/AttribSysVault.cs @@ -9,11 +9,10 @@ namespace Volatility.Resources; // Learn More: // https://burnout.wiki/wiki/AttribSysVault +[ResourceDefinition(ResourceType.AttribSysVault)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class AttribSysVault : Resource { - public override ResourceType ResourceType => ResourceType.AttribSysVault; - public override Platform ResourcePlatform => Platform.Agnostic; - public ulong VltDataOffset { get; set; } public uint VltSizeInBytes { get; set; } public ulong BinDataOffset { get; set; } @@ -76,8 +75,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann PtrN = []; Data = string.Empty; - Arch arch = ResourceArch; - if (arch == Arch.x64) + if (ResourceArch == Arch.x64) { VltDataOffset = reader.ReadUInt64(); VltSizeInBytes = reader.ReadUInt32(); @@ -123,7 +121,8 @@ public override void WriteToStream(ResourceBinaryWriter writer, Endian endiannes public AttribSysVault() : base() { } - public AttribSysVault(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + public AttribSysVault(string path, Endian endianness = Endian.Agnostic) + : base(path, endianness) { } private void ParseVlt(EndianAwareBinaryReader reader, List pendingAttributes) { diff --git a/Volatility/Resources/BinaryResource.cs b/Volatility/Resources/BinaryResource.cs index 4cf68b6..8479db3 100644 --- a/Volatility/Resources/BinaryResource.cs +++ b/Volatility/Resources/BinaryResource.cs @@ -7,22 +7,31 @@ namespace Volatility.Resources; // Learn More: // https://burnout.wiki/wiki/Binary_File +[ResourceDefinition(ResourceType.BinaryFile)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class BinaryResource : Resource { - public override ResourceType ResourceType => ResourceType.BinaryFile; - public uint DataSize { get; set; } public uint DataOffset { get; set; } - public BinaryResource(uint dataOffset, uint dataSize) + public BinaryResource(uint dataOffset, uint dataSize) : this() { DataSize = dataSize; - DataOffset = dataOffset; + DataOffset = dataOffset == 0 ? 0x10u : dataOffset; } - public BinaryResource() : base() { } - - public BinaryResource(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + public BinaryResource() : base() + { + DataOffset = 0x10; + } + + public BinaryResource(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) + { + if (DataOffset == 0) + { + DataOffset = 0x10; + } + } public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { @@ -36,8 +45,16 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) { + base.WriteToStream(writer, endianness); + + if (DataOffset < 0x10) + { + DataOffset = 0x10; + } + writer.Write(DataSize); writer.Write(DataOffset); writer.Write(new byte[8]); + writer.BaseStream.Seek(DataOffset, SeekOrigin.Begin); } -} \ No newline at end of file +} diff --git a/Volatility/Resources/EnvironmentKeyframe/EnvironmentKeyframe.cs b/Volatility/Resources/EnvironmentKeyframe/EnvironmentKeyframe.cs index 1b8bf42..9a09998 100644 --- a/Volatility/Resources/EnvironmentKeyframe/EnvironmentKeyframe.cs +++ b/Volatility/Resources/EnvironmentKeyframe/EnvironmentKeyframe.cs @@ -7,10 +7,10 @@ namespace Volatility.Resources; // Learn More: // https://burnout.wiki/wiki/Environment_Keyframe +[ResourceDefinition(ResourceType.EnvironmentKeyframe)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class EnvironmentKeyframe : Resource { - public override ResourceType ResourceType => ResourceType.EnvironmentKeyframe; - public BloomData BloomSettings; public VignetteData VignetteSettings; public TintData TintSettings; @@ -293,4 +293,4 @@ public static void Write(ResourceBinaryWriter writer, CloudsData value) writer.Write(value.DirectionAngle); writer.Write(new byte[0x4]); } -} \ No newline at end of file +} diff --git a/Volatility/Resources/EnvironmentTimeLine/EnvironmentTimeLine.cs b/Volatility/Resources/EnvironmentTimeLine/EnvironmentTimeLine.cs index a7539e9..84aad07 100644 --- a/Volatility/Resources/EnvironmentTimeLine/EnvironmentTimeLine.cs +++ b/Volatility/Resources/EnvironmentTimeLine/EnvironmentTimeLine.cs @@ -1,79 +1,171 @@ -using YamlDotNet.Serialization; +using Volatility.Utilities; namespace Volatility.Resources; +[ResourceDefinition(ResourceType.EnvironmentTimeLine)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class EnvironmentTimeline : Resource { - public override ResourceType ResourceType => ResourceType.EnvironmentTimeLine; + private const int SectionAlignment = 0x10; + private const int KeyframeTimeSize = sizeof(float); + private const int KeyframeReferencePlaceholderSize = sizeof(uint); - public LocationData[] Locations; + public LocationData[] Locations = []; public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) { base.WriteToStream(writer, endianness); - writer.Write(0x1); // Version - writer.Write(Locations.Length); - writer.Write(0x10); // Locations Pointer - writer.Write(0x0); // Padding + LocationData[] locations = Locations ?? []; + + long currentOffset = 0x10; // Header size + ulong locationsOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + locations.Length, + GetLocationStructSize(ResourceArch), + SectionAlignment); + + ulong[] keyframeTimesOffsets = new ulong[locations.Length]; + ulong[] keyframeRefsOffsets = new ulong[locations.Length]; + + int totalImports = 0; + for (int i = 0; i < locations.Length; i++) + { + KeyframeReference[] keyframes = locations[i].Keyframes ?? []; + + keyframeTimesOffsets[i] = ResourceUtilities.GetSectionOffset( + ref currentOffset, + keyframes.Length, + KeyframeTimeSize, + SectionAlignment); + + keyframeRefsOffsets[i] = ResourceUtilities.GetSectionOffset( + ref currentOffset, + keyframes.Length, + KeyframeReferencePlaceholderSize, + SectionAlignment); + + totalImports += keyframes.Length; + } + + long importsOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + totalImports * ResourceImport.ImportEntrySize, + SectionAlignment); + + writer.Write(0x1); // Version + writer.Write(locations.Length); + writer.Write((uint)locationsOffset); // Locations Pointer + writer.Write(0x0); // Padding + + writer.WriteSection(locationsOffset, locations, (w, location, index) => + WriteLocationHeader(w, location, ResourceArch, keyframeTimesOffsets[index], keyframeRefsOffsets[index])); + + for (int i = 0; i < locations.Length; i++) + { + KeyframeReference[] keyframes = locations[i].Keyframes ?? []; + + writer.WriteSection(keyframeTimesOffsets[i], keyframes, static (w, keyframe) => w.Write(keyframe.KeyframeTime)); + writer.WriteSection(keyframeRefsOffsets[i], keyframes, static (w, _) => w.Write(0u)); + } + + if (importsOffset != 0) + { + writer.BaseStream.Position = importsOffset; + for (int i = 0; i < locations.Length; i++) + { + KeyframeReference[] keyframes = locations[i].Keyframes ?? []; + for (int j = 0; j < keyframes.Length; j++) + { + writer.Write(keyframes[j].ResourceReference.ReferenceID); + writer.Write((uint)(keyframeRefsOffsets[i] + ((ulong)j * KeyframeReferencePlaceholderSize))); + writer.Write(0u); + } + } + } } public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); - Arch arch = ResourceArch; - int version = reader.ReadInt32(); if (version != 1) { throw new InvalidDataException($"Version mismatch! Version should be 1. (Found version {version})"); } - uint locationCount = reader.ReadUInt32(); - Locations = new LocationData[locationCount]; + int locationCount = reader.ReadInt32(); + ulong locationsPtr = reader.ReadPointer(ResourceArch); + reader.BaseStream.Seek(0x10, SeekOrigin.Begin); + + Locations = reader.ParseSection(locationsPtr, locationCount, r => ReadLocation(r, ResourceArch)).ToArray(); + } - uint locationsPtr = reader.ReadUInt32(); + public EnvironmentTimeline() : base() { } - for (int i = 0; i < locationCount; i++) - { - reader.BaseStream.Seek(locationsPtr + ((arch == Arch.x64 ? 0x18 : 0xC) * i), SeekOrigin.Begin); - // This is the same kind of 32/64 count value that StreamedDeformationSpec uses - may move that boilerplate and change this? - uint keyframeCount = (uint)(arch == Arch.x64 ? reader.ReadUInt64() : reader.ReadUInt32()); - ulong keyframeTimesPtr = reader.ReadPointer(ResourceArch); - ulong keyframeRefsPtr = reader.ReadPointer(ResourceArch); + public EnvironmentTimeline(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } - Locations[i].Keyframes = new KeyframeReference[keyframeCount]; + private static int GetLocationStructSize(Arch arch) + { + return arch == Arch.x64 ? 0x18 : 0x0C; + } - long maxLength = (long)new[] - { - keyframeTimesPtr + (keyframeCount * sizeof(uint)), - keyframeRefsPtr + (keyframeCount * sizeof(uint)), - }.Max(); + private static LocationData ReadLocation(ResourceBinaryReader reader, Arch arch) + { + uint keyframeCount = reader.ReadArchDependUInt(arch); + ulong keyframeTimesPtr = reader.ReadPointer(arch); + ulong keyframeRefsPtr = reader.ReadPointer(arch); - for (int j = 0; j < keyframeCount; j++) - { - reader.BaseStream.Seek((long)keyframeTimesPtr + (0x4 * j), SeekOrigin.Begin); + KeyframeReference[] keyframes = new KeyframeReference[keyframeCount]; + if (keyframeCount == 0) + { + return new LocationData { Keyframes = keyframes }; + } - Locations[i].Keyframes[j].KeyframeTime = reader.ReadSingle(); + List keyframeTimes = reader.ParseSection(keyframeTimesPtr, (int)keyframeCount, r => r.ReadSingle()); - reader.BaseStream.Seek((long)keyframeRefsPtr + (0x4 * j), SeekOrigin.Begin); + long importBlockOffset = Math.Max( + (long)keyframeTimesPtr + (keyframeCount * KeyframeTimeSize), + (long)keyframeRefsPtr + (keyframeCount * KeyframeReferencePlaceholderSize)); - ResourceImport.ReadExternalImport(fileOffset: reader.BaseStream.Position, reader, maxLength, out Locations[i].Keyframes[j].ResourceReference); - } + for (int i = 0; i < keyframeCount; i++) + { + long fileOffset = (long)keyframeRefsPtr + (i * KeyframeReferencePlaceholderSize); + ResourceImport.ReadExternalImport(fileOffset, reader, importBlockOffset, out ResourceImport resourceReference); + + keyframes[i] = new KeyframeReference + { + KeyframeTime = i < keyframeTimes.Count ? keyframeTimes[i] : 0f, + ResourceReference = resourceReference + }; } - } - public EnvironmentTimeline() : base() { } + return new LocationData + { + Keyframes = keyframes + }; + } - public EnvironmentTimeline(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + private static void WriteLocationHeader( + ResourceBinaryWriter writer, + LocationData location, + Arch arch, + ulong keyframeTimesOffset, + ulong keyframeRefsOffset) + { + uint keyframeCount = (uint)(location.Keyframes?.Length ?? 0); + writer.WriteArchDependUInt(keyframeCount, arch); + writer.WritePointer(keyframeTimesOffset, arch); + writer.WritePointer(keyframeRefsOffset, arch); + } - public struct LocationData + public struct LocationData { public KeyframeReference[] Keyframes; } - public struct KeyframeReference + public struct KeyframeReference { public float KeyframeTime; public ResourceImport ResourceReference; diff --git a/Volatility/Resources/GuiPopup/GuiPopup.cs b/Volatility/Resources/GuiPopup/GuiPopup.cs index 2c9e886..7360721 100644 --- a/Volatility/Resources/GuiPopup/GuiPopup.cs +++ b/Volatility/Resources/GuiPopup/GuiPopup.cs @@ -1,110 +1,129 @@ -using System.Text; +using Volatility.Utilities; namespace Volatility.Resources; +[ResourceDefinition(ResourceType.GuiPopup)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class GuiPopup : Resource { - public List Popups { get; } = new(); + private const int PopupStructSize = 0xC0; - const int PopupStructSize = 0xC0; - - public override ResourceType ResourceType => ResourceType.GuiPopup; - public override Platform ResourcePlatform => Platform.Agnostic; + public List Popups { get; set; } = []; public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness) { - ushort size = (ushort)(Popups.Count * PopupStructSize); base.WriteToStream(writer, endianness); - long start = writer.BaseStream.Position; - writer.Write((uint)0x8); - writer.Write((short)Popups.Count); - writer.Write((short)PopupStructSize); - foreach (var p in Popups) - WriteOne(writer, p); + + const int PopupOffsetsStart = 0x40; + + Arch arch = ResourceArch; + int popupCount = Popups.Count; + int popupOffsetEntrySize = ResourceUtilities.GetPointerSize(arch); + long firstPopupOffset = ResourceUtilities.AlignOffset( + PopupOffsetsStart + ((long)popupCount * popupOffsetEntrySize), + 0x10); + long totalSize = firstPopupOffset + ((long)popupCount * PopupStructSize); + + if (popupCount > short.MaxValue) + { + throw new InvalidDataException($"GuiPopup count {popupCount} exceeds int16_t storage."); + } + + if (totalSize > short.MaxValue) + { + throw new InvalidDataException($"GuiPopup size 0x{totalSize:X} exceeds int16_t storage."); + } + + writer.WritePointer(PopupOffsetsStart, arch); + writer.Write((short)popupCount); + writer.Write((short)totalSize); + writer.WriteFixedBytes(null, PopupOffsetsStart - (int)writer.BaseStream.Position); + + writer.BaseStream.Position = PopupOffsetsStart; + for (int i = 0; i < popupCount; i++) + { + writer.WritePointer((ulong)(firstPopupOffset + ((long)i * PopupStructSize)), arch); + } + + writer.WriteFixedBytes(null, (int)(firstPopupOffset - writer.BaseStream.Position)); + writer.WriteSection((ulong)firstPopupOffset, Popups, Popup.Write); } public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness) { base.ParseFromStream(reader, endianness); + Popups.Clear(); - long start = reader.BaseStream.Position; - uint dataPtr = reader.ReadUInt32(); - short count = reader.ReadInt16(); - short elemSize = reader.ReadInt16(); - long ret = reader.BaseStream.Position; - reader.BaseStream.Position = start + dataPtr; - for (int i = 0; i < count; i++) - Popups.Add(ReadOne(reader)); - reader.BaseStream.Position = ret; - } - static Popup ReadOne(ResourceBinaryReader r) - { - Popup p = new Popup(); - p.NameId = r.ReadUInt64(); - p.Name = ReadFixedString(r, 13); - r.BaseStream.Position += 3; - p.Style = (PopupStyle)r.ReadInt32(); - p.Icon = (PopupIcons)r.ReadInt32(); - p.TitleId = ReadFixedString(r, 32); - p.MessageId = ReadFixedString(r, 32); - p.MessageParam0 = (PopupParamTypes)r.ReadInt32(); - p.MessageParam1 = (PopupParamTypes)r.ReadInt32(); - p.MessageParamsUsed = r.ReadInt32(); - p.Button1Id = ReadFixedString(r, 32); - p.Button1Param = (PopupParamTypes)r.ReadInt32(); - p.Button1ParamUsed = r.ReadByte() != 0; - p.Button2Id = ReadFixedString(r, 32); - r.BaseStream.Position += 3; - p.Button2Param = (PopupParamTypes)r.ReadInt32(); - p.Button2ParamUsed = r.ReadByte() != 0; - r.BaseStream.Position += 7; - return p; - } + Arch arch = ResourceArch; + int popupOffsetEntrySize = ResourceUtilities.GetPointerSize(arch); - static void WriteOne(ResourceBinaryWriter w, Popup p) - { - w.Write(p.NameId); - WriteFixedString(w, p.Name, 13); - w.Write(new byte[3]); - w.Write((int)p.Style); - w.Write((int)p.Icon); - WriteFixedString(w, p.TitleId, 32); - WriteFixedString(w, p.MessageId, 32); - w.Write((int)p.MessageParam0); - w.Write((int)p.MessageParam1); - w.Write(p.MessageParamsUsed); - WriteFixedString(w, p.Button1Id, 32); - w.Write((int)p.Button1Param); - w.Write((byte)(p.Button1ParamUsed ? 1 : 0)); - WriteFixedString(w, p.Button2Id, 32); - w.Write(new byte[3]); - w.Write((int)p.Button2Param); - w.Write((byte)(p.Button2ParamUsed ? 1 : 0)); - w.Write(new byte[7]); - } + ulong dataPtr = reader.ReadPointer(arch); + short countRaw = reader.ReadInt16(); + short totalSize = reader.ReadInt16(); + if (arch == Arch.x64) + { + reader.BaseStream.Seek(sizeof(uint), SeekOrigin.Current); + } - static string ReadFixedString(ResourceBinaryReader r, int len) - { - var bytes = r.ReadBytes(len); - int n = Array.IndexOf(bytes, 0); - if (n >= 0) return Encoding.ASCII.GetString(bytes, 0, n); - return Encoding.ASCII.GetString(bytes); - } + if (countRaw < 0) + { + throw new InvalidDataException($"GuiPopup popup count cannot be negative. Found {countRaw}."); + } - static void WriteFixedString(ResourceBinaryWriter w, string? s, int len) - { - var bytes = Encoding.ASCII.GetBytes(s ?? string.Empty); - if (bytes.Length > len) w.Write(bytes, 0, len); - else + int count = countRaw; + + if (count > 0 && dataPtr == 0) + { + throw new InvalidDataException( + "GuiPopup pointer table cannot be null when popup count is greater than zero."); + } + + if (dataPtr != 0 && dataPtr < (ulong)reader.BaseStream.Position) + { + throw new InvalidDataException( + $"GuiPopup data pointer mismatch! Expected >= 0x{reader.BaseStream.Position:X}, found 0x{dataPtr:X}."); + } + + long expectedMinimumSize = (long)dataPtr + ((long)count * popupOffsetEntrySize); + if (count > 0 && reader.BaseStream.Length < expectedMinimumSize) { - w.Write(bytes); - if (bytes.Length < len) w.Write(new byte[len - bytes.Length]); + throw new InvalidDataException( + $"GuiPopup offset table exceeds file length. Needed 0x{expectedMinimumSize:X}, found 0x{reader.BaseStream.Length:X}."); + } + + List popupOffsets = count > 0 + ? reader.ParseSection(dataPtr, count, r => r.ReadPointer(arch)) + : []; + + for (int i = 0; i < popupOffsets.Count; i++) + { + ulong popupOffset = popupOffsets[i]; + if (popupOffset == 0) + { + continue; + } + + if (popupOffset + PopupStructSize > (ulong)reader.BaseStream.Length) + { + throw new InvalidDataException( + $"GuiPopup entry {i} at 0x{popupOffset:X} exceeds file length 0x{reader.BaseStream.Length:X}."); + } + + reader.ParseSection(popupOffset, Popup.Read, out Popup popup); + Popups.Add(popup); + } + + if (totalSize > 0 && totalSize != reader.BaseStream.Length) + { + Console.WriteLine($"WARNING: GuiPopup reported size 0x{totalSize:X}, actual size 0x{reader.BaseStream.Length:X}."); } } public GuiPopup() : base() { } - public GuiPopup(string path, Endian endianness) : base(path, endianness) { } + + public GuiPopup(string path, Endian endianness) + : base(path, endianness) { } public enum PopupStyle : int { @@ -159,5 +178,57 @@ public struct Popup public string Button2Id; public PopupParamTypes Button2Param; public bool Button2ParamUsed; + + public static Popup Read(ResourceBinaryReader reader) + { + Popup popup = new() + { + NameId = reader.ReadUInt64(), + Name = ResourceUtilities.ReadFixedString(reader, 13) + }; + + reader.BaseStream.Seek(0x3, SeekOrigin.Current); + + popup.Style = (PopupStyle)reader.ReadInt32(); + popup.Icon = (PopupIcons)reader.ReadInt32(); + popup.TitleId = ResourceUtilities.ReadFixedString(reader, 32); + popup.MessageId = ResourceUtilities.ReadFixedString(reader, 32); + popup.MessageParam0 = (PopupParamTypes)reader.ReadInt32(); + popup.MessageParam1 = (PopupParamTypes)reader.ReadInt32(); + popup.MessageParamsUsed = reader.ReadInt32(); + popup.Button1Id = ResourceUtilities.ReadFixedString(reader, 32); + popup.Button1Param = (PopupParamTypes)reader.ReadInt32(); + popup.Button1ParamUsed = reader.ReadByte() != 0; + popup.Button2Id = ResourceUtilities.ReadFixedString(reader, 32); + reader.BaseStream.Seek(0x3, SeekOrigin.Current); + + popup.Button2Param = (PopupParamTypes)reader.ReadInt32(); + popup.Button2ParamUsed = reader.ReadByte() != 0; + reader.BaseStream.Seek(0x7, SeekOrigin.Current); + + return popup; + } + + public static void Write(ResourceBinaryWriter writer, Popup popup) + { + writer.Write(popup.NameId); + ResourceUtilities.WriteFixedString(writer, popup.Name, 13); + writer.WriteFixedBytes(null, 0x3); + writer.Write((int)popup.Style); + writer.Write((int)popup.Icon); + ResourceUtilities.WriteFixedString(writer, popup.TitleId, 32); + ResourceUtilities.WriteFixedString(writer, popup.MessageId, 32); + writer.Write((int)popup.MessageParam0); + writer.Write((int)popup.MessageParam1); + writer.Write(popup.MessageParamsUsed); + ResourceUtilities.WriteFixedString(writer, popup.Button1Id, 32); + writer.Write((int)popup.Button1Param); + writer.Write((byte)(popup.Button1ParamUsed ? 1 : 0)); + ResourceUtilities.WriteFixedString(writer, popup.Button2Id, 32); + writer.WriteFixedBytes(null, 0x3); + writer.Write((int)popup.Button2Param); + writer.Write((byte)(popup.Button2ParamUsed ? 1 : 0)); + writer.WriteFixedBytes(null, 0x7); + } } -} \ No newline at end of file +} diff --git a/Volatility/Resources/InstanceList/InstanceList.cs b/Volatility/Resources/InstanceList/InstanceList.cs index 0718d66..95f4928 100644 --- a/Volatility/Resources/InstanceList/InstanceList.cs +++ b/Volatility/Resources/InstanceList/InstanceList.cs @@ -1,101 +1,215 @@ -using static Volatility.Utilities.MatrixUtilities; +using Volatility.Utilities; + +using static Volatility.Utilities.MatrixUtilities; namespace Volatility.Resources; -// The Instance List resource type contains lists of models along with their -// respective locations in the game world. It serves as one of the top-level +// The Instance List resource type contains lists of models along with their +// respective locations in the game world. It serves as one of the top-level // resource types for track unit loading. - +// // Learn More: // https://burnout.wiki/wiki/Instance_List +[ResourceDefinition(ResourceType.InstanceList)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class InstanceList : Resource { - public override ResourceType ResourceType => ResourceType.InstanceList; - + private const int SectionAlignment = 0x10; + [EditorLabel("Number of instances"), EditorCategory("Instance List"), EditorReadOnly, EditorTooltip("The amount of instances that have a model assigned, but NOT the size of the entire instance array.")] public uint NumInstances; [EditorLabel("Instances"), EditorCategory("Instance List"), EditorTooltip("The list of instances in this list.")] public List Instances = []; - //uint ArraySize, VersionNumber; - public InstanceList() : base() { } public InstanceList(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) + { + base.WriteToStream(writer, endianness); + + int instanceBlockSize = GetInstanceBlockSize(ResourceArch); + uint entryCount = (uint)Instances.Count; + if (NumInstances > entryCount) + { + throw new InvalidDataException( + $"NumInstances ({NumInstances}) cannot exceed the total array size ({entryCount})."); + } + + long currentOffset = GetHeaderSize(ResourceArch); + long instanceListOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + checked((int)(entryCount * instanceBlockSize)), + SectionAlignment); + + writer.WritePointer((ulong)instanceListOffset, ResourceArch); + writer.Write(entryCount); + writer.Write(NumInstances); + writer.Write(1u); + + writer.WriteSection(instanceListOffset, Instances, (w, instance) => WriteInstanceBlock(w, instance, ResourceArch)); + } + public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); - // Absolute pointers (not relative to any specific point in the file) - long instanceListPtr = reader.ReadInt32(); - + ulong instanceListPtr = reader.ReadPointer(ResourceArch); uint entries = reader.ReadUInt32(); NumInstances = reader.ReadUInt32(); - // Version - if (reader.ReadUInt32() != 1) + uint version = reader.ReadUInt32(); + if (version != 1) { - throw new Exception("Version mismatch!"); + throw new InvalidDataException($"Version mismatch! Version should be 1. (Found version {version})"); } - reader.BaseStream.Seek(instanceListPtr, SeekOrigin.Begin); + Instances.Clear(); - long instanceBlockSize = ResourceArch == Arch.x64 ? 0x60 : 0x50; + long instanceBlockSize = GetInstanceBlockSize(ResourceArch); + if (entries > 0 && instanceListPtr == 0) + { + throw new InvalidDataException("Instance list pointer is null, but the resource declares instance entries."); + } - for (int i = 0; i < entries; i++) + if (NumInstances > entries) { - reader.BaseStream.Seek(instanceListPtr + (instanceBlockSize * i), SeekOrigin.Begin); + throw new InvalidDataException( + $"Invalid InstanceList header: NumInstances ({NumInstances}) cannot exceed array size ({entries})."); + } - ResourceImport.ReadExternalImport(fileOffset: reader.BaseStream.Position, reader, instanceListPtr + (instanceBlockSize * entries), out ResourceImport model); - short backdropZoneID = reader.ReadInt16(); + if (instanceListPtr != 0 && instanceListPtr < (ulong)GetHeaderSize(ResourceArch)) + { + throw new InvalidDataException( + $"Invalid InstanceList pointer 0x{instanceListPtr:X}. Instance data overlaps the resource header."); + } - //ushort _padding1 = reader.ReadUInt16(); - //uint _padding2 = reader.ReadUInt32(); + long instanceDataLength = checked(instanceBlockSize * (long)entries); + if (instanceListPtr != 0 && ((long)instanceListPtr + instanceDataLength) > reader.BaseStream.Length) + { + throw new InvalidDataException( + $"Instance data range 0x{instanceListPtr:X}-0x{((long)instanceListPtr + instanceDataLength):X} exceeds stream length 0x{reader.BaseStream.Length:X}."); + } - reader.BaseStream.Seek(0x6, SeekOrigin.Current); + long importBlockOffset = (long)instanceListPtr + instanceDataLength; - float maxVisibleDistanceSquared = reader.ReadSingle(); + for (int i = 0; i < entries; i++) + { + long instanceOffset = (long)instanceListPtr + (instanceBlockSize * i); - Transform transform = Matrix44AffineToTransform(ReadMatrix44Affine(reader)); + reader.ParseSection(instanceOffset, r => ReadInstance(r, ResourceArch, importBlockOffset), out Instance instance); + Instances.Add(instance); + } + } - reader.BaseStream.Seek(instanceListPtr + instanceBlockSize * entries + 0x10 * i, SeekOrigin.Begin); + private static int GetInstanceBlockSize(Arch arch) + { + return arch == Arch.x64 ? 0x60 : 0x50; + } + + private static int GetHeaderSize(Arch arch) + { + return ResourceUtilities.GetPointerSize(arch) + (sizeof(uint) * 3); + } + + private static Instance ReadInstance( + ResourceBinaryReader reader, + Arch arch, + long importBlockOffset) + { + long blockStart = reader.BaseStream.Position; - Instances.Add(new Instance + ResourceImport.ReadExternalImport(blockStart, reader, importBlockOffset, out ResourceImport modelReference); + reader.BaseStream.Seek(blockStart + ResourceUtilities.GetPointerSize(arch), SeekOrigin.Begin); + + short backdropZoneId = reader.ReadInt16(); + reader.BaseStream.Seek(0x6, SeekOrigin.Current); + float maxVisibleDistanceSquared = reader.ReadSingle(); + if (arch == Arch.x64) + { + reader.BaseStream.Seek(0xC, SeekOrigin.Current); + } + + Matrix44Affine transformMatrix = ReadMatrix44Affine(reader); + Transform transform = Matrix44AffineToTransform(transformMatrix); + + return new Instance + { + ModelReference = modelReference, + BackdropZoneID = backdropZoneId, + MaxVisibleDistanceSquared = maxVisibleDistanceSquared, + Transform = transform, + TransformMatrix = transformMatrix, + }; + } + + private static void WriteInstanceBlock(ResourceBinaryWriter writer, Instance instance, Arch arch) + { + long blockStart = writer.BaseStream.Position; + + writer.WritePointer(0, arch); + writer.Write(instance.BackdropZoneID); + writer.WriteFixedBytes(null, 0x6); + writer.Write(instance.MaxVisibleDistanceSquared); + if (arch == Arch.x64) + { + writer.WriteFixedBytes(null, 0xC); + } + + Matrix44Affine transformMatrix = instance.TransformMatrix != default + ? instance.TransformMatrix + : TransformToMatrix44Affine(instance.Transform); + WriteMatrix44Affine(writer, transformMatrix); + + int remaining = GetInstanceBlockSize(arch) - (int)(writer.BaseStream.Position - blockStart); + if (remaining < 0) + { + throw new InvalidDataException( + $"Instance block overflow. Wrote 0x{writer.BaseStream.Position - blockStart:X} bytes into a 0x{GetInstanceBlockSize(arch):X} byte block."); + } + + if (remaining > 0) + { + writer.Write(new byte[remaining]); + } + } + + public override IEnumerable> GetExternalImports() + { + int instanceBlockSize = GetInstanceBlockSize(ResourceArch); + long instanceListOffset = ResourceUtilities.AlignOffset(GetHeaderSize(ResourceArch), SectionAlignment); + for (int i = 0; i < Instances.Count; i++) + { + ResourceImport modelReference = Instances[i].ModelReference; + if (!modelReference.ExternalImport) { - ModelReference = model, - BackdropZoneID = backdropZoneID, - // Padding1 = _padding1, Padding2 = _padding2, - MaxVisibleDistanceSquared = maxVisibleDistanceSquared, - Transform = transform, - ResourceId = new ResourceImport - { - ReferenceID = reader.ReadUInt32(), - ExternalImport = false - }, - }); + continue; + } + + yield return new KeyValuePair( + instanceListOffset + (i * instanceBlockSize), + modelReference); } } } public struct Instance { - [EditorLabel("Resource ID"), EditorCategory("InstanceList/Instances"), EditorTooltip("The reference to the resource placed by this instance.")] - public ResourceImport ResourceId; + [EditorLabel("Model Reference"), EditorCategory("InstanceList/Instances"), EditorTooltip("The external model import referenced by this instance.")] + public ResourceImport ModelReference; [EditorLabel("Transform"), EditorCategory("InstanceList/Instances"), EditorTooltip("The location, rotation, and scale of this instance.")] public Transform Transform; + [EditorHidden] + public Matrix44Affine TransformMatrix; + [EditorLabel("Transform"), EditorCategory("InstanceList/Instances"), EditorTooltip("If this is a backdrop, the PVS Zone ID that this backdrop represents.")] public short BackdropZoneID; - - // public ushort Padding1; public uint Padding2; - - [EditorLabel("Max Visible Distance Squared"), EditorCategory("InstanceList/Instances"), EditorTooltip("The maximum distance that this instance can be seen (in meters), squared.")] - public float MaxVisibleDistanceSquared; // Unused? [EditorHidden] - public ResourceImport ModelReference; -} \ No newline at end of file + public float MaxVisibleDistanceSquared; +} diff --git a/Volatility/Resources/Model/Model.cs b/Volatility/Resources/Model/Model.cs index ab2af14..cab8551 100644 --- a/Volatility/Resources/Model/Model.cs +++ b/Volatility/Resources/Model/Model.cs @@ -1,140 +1,172 @@ -namespace Volatility.Resources; +using Volatility.Utilities; + +namespace Volatility.Resources; // The Model resource type links top-level resources (like InstanceList) // to Renderable resources that contain the 3D geometry, while including // Level of Detail (LOD) information. - +// // Learn More: // https://burnout.wiki/wiki/Model +[ResourceDefinition(ResourceType.Model)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class Model : Resource { + private const int StateSize = sizeof(byte); + private const int LodDistanceSize = sizeof(float); + + [EditorHidden] + public uint HeaderMetadata; + [EditorCategory("Model Container"), EditorLabel("Flags")] public byte Flags; [EditorCategory("Model Container"), EditorLabel("Models")] public List ModelDatas = []; - public override ResourceType ResourceType => ResourceType.Model; - public override Platform ResourcePlatform => Platform.Agnostic; - public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) { base.WriteToStream(writer, endianness); - int models = ModelDatas.Count; - - uint renderablesPtr = 0x14; // Writing length of header - uint statesPtr = (uint)(renderablesPtr + (0x4 * models)); - uint lodDistancesPtr = (uint)(statesPtr + (0x1 * models)); - - writer.Write(renderablesPtr); - writer.Write(statesPtr); - writer.Write(lodDistancesPtr); - writer.Write(-1); // Game explorer index, leaving our mark for now - writer.Write((byte)ModelDatas.Count); // Dangerous. We need to limit number of models - writer.Write(Flags); - writer.Write((byte)ModelDatas.Count); // Number of states. Same as number of renderables - writer.BaseStream.WriteByte(0x02); - - writer.BaseStream.Seek(renderablesPtr, SeekOrigin.Begin); - - // Renderable Ptrs - for (int i = 0; i < models; i++) + Arch arch = ResourceArch; + int modelCount = ModelDatas.Count; + if (modelCount > byte.MaxValue) { - writer.Write((uint)(i * 0x4)); + throw new InvalidDataException("Model resources cannot store more than 255 renderables."); } - // States (Writing as uint?? A single renderable has a 0x4-long state apparently.) - for (int i = 0; i < models; i++) - { - writer.Write((uint)ModelDatas[i].State); - } - - // LOD Distances - for (int i = 0; i < models; i++) - { - writer.Write(ModelDatas[i].LODDistance); - } + int renderablePointerSize = ResourceUtilities.GetPointerSize(arch); + long currentOffset = GetHeaderSize(arch); + long renderablesOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + modelCount * renderablePointerSize, + 1); + long statesOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + modelCount * StateSize, + 1); + long lodDistancesOffset = ResourceUtilities.GetSectionOffset( + ref currentOffset, + modelCount * LodDistanceSize, + sizeof(uint)); + + writer.WritePointer((ulong)renderablesOffset, arch); + writer.WritePointer((ulong)statesOffset, arch); + writer.WritePointer((ulong)lodDistancesOffset, arch); + writer.Write(HeaderMetadata); + writer.Write((byte)modelCount); + writer.Write(Flags); + writer.Write((byte)modelCount); + writer.Write((byte)0x02); - // Resource ID References - for (int i = 0; i < models; i++) - { - writer.Write(ModelDatas[i].ResourceReference.ReferenceID); - writer.Write(renderablesPtr + (i * 0x4)); - writer.Write((uint)0x0); // Unknown. Always 0 in BPR, not always 0 on X360 - } + writer.WriteSection(renderablesOffset, ModelDatas, (w, _, index) => w.WritePointer((ulong)(index * ResourceImport.ImportEntrySize), arch)); + writer.WriteSection(statesOffset, ModelDatas, (w, modelData) => w.Write((byte)modelData.State)); + writer.WriteSection(lodDistancesOffset, ModelDatas, (w, modelData) => w.Write(modelData.LODDistance)); } + public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); - // Get the version check out of the way before we begin. - reader.BaseStream.Seek(0x13, SeekOrigin.Begin); - if (reader.ReadByte() != 0x2) - { - throw new Exception("Version mismatch!"); - } - - reader.BaseStream.Seek(0x0, SeekOrigin.Begin); + Arch arch = ResourceArch; - // Absolute pointers (not relative to any specific point in the file) - uint renderablesPtr = reader.ReadUInt32(); - uint renderableStatesPtr = reader.ReadUInt32(); - uint lodDistancesPtr = reader.ReadUInt32(); + ulong renderablesPtr = reader.ReadPointer(arch); + ulong renderableStatesPtr = reader.ReadPointer(arch); + ulong lodDistancesPtr = reader.ReadPointer(arch); - // Null for imported resources. - // TODO: Reconstruct game explorer or get from ResourceDB - int gameExplorerIndex = reader.ReadInt32(); + HeaderMetadata = reader.ReadUInt32(); byte numRenderables = reader.ReadByte(); + Flags = reader.ReadByte(); + byte numStates = reader.ReadByte(); + byte version = reader.ReadByte(); + + if (version != 0x2) + { + throw new InvalidDataException($"Version mismatch! Version should be 2. (Found version {version})"); + } if (numRenderables == 0) { Console.WriteLine("WARNING: Found no renderables in this model!"); } - Flags = reader.ReadByte(); - - long maxLength = new[] + if (numStates != numRenderables) { - lodDistancesPtr + numRenderables * sizeof(uint), - renderablesPtr + numRenderables * sizeof(uint), - renderableStatesPtr + numRenderables - }.Max(); - - // This currently does a lot of seeking. - // It may improve performance if we separate this. - for (int i = 0; i < numRenderables; i++) + throw new InvalidDataException( + $"Unsupported model header: numStates ({numStates}) does not match numRenderables ({numRenderables})."); + } + + int renderablePointerSize = ResourceUtilities.GetPointerSize(arch); + long importsOffset = Math.Max( + (long)lodDistancesPtr + (numStates * LodDistanceSize), + Math.Max( + (long)renderablesPtr + (numRenderables * renderablePointerSize), + (long)renderableStatesPtr + (numStates * StateSize))); + + ModelDatas.Clear(); + for (int i = 0; i < numStates; i++) { - ModelData modelData = new ModelData(); - - reader.BaseStream.Seek(renderablesPtr + (i * 0x4), SeekOrigin.Begin); + ModelDatas.Add(ReadModelData( + reader, + arch, + i, + renderablesPtr, + renderableStatesPtr, + lodDistancesPtr, + importsOffset)); + } + } - uint idRelativePtr = reader.ReadUInt32(); + public Model() : base() { } - reader.BaseStream.Seek(renderableStatesPtr + i, SeekOrigin.Begin); - modelData.State = (State)reader.ReadByte(); + public Model(string path, Endian endianness = Endian.Agnostic) + : base(path, endianness) { } + + private static ModelData ReadModelData( + ResourceBinaryReader reader, + Arch arch, + int index, + ulong renderablesPtr, + ulong renderableStatesPtr, + ulong lodDistancesPtr, + long importsOffset) + { + ModelData modelData = new(); + int renderablePointerSize = ResourceUtilities.GetPointerSize(arch); - reader.BaseStream.Seek(lodDistancesPtr + (i * 0x4), SeekOrigin.Begin); - modelData.LODDistance = reader.ReadSingle(); + reader.ParseSection(renderablesPtr + ((ulong)index * (ulong)renderablePointerSize), r => r.ReadPointer(arch), out _); + reader.ParseSection(renderableStatesPtr + (ulong)index, r => (State)r.ReadByte(), out modelData.State); + reader.ParseSection(lodDistancesPtr + ((ulong)index * LodDistanceSize), r => r.ReadSingle(), out modelData.LODDistance); - reader.BaseStream.Seek( - idRelativePtr + - renderablesPtr + - (numRenderables * (0x4 + 0x1 + 0x4)) + - (reader.Endianness == Endian.BE ? 0x4 : 0x0), SeekOrigin.Begin - ); + ResourceImport.ReadExternalImport(index, reader, importsOffset, out modelData.ResourceReference); + return modelData; + } - ResourceImport.ReadExternalImport(i, reader, maxLength, out modelData.ResourceReference); + public override IEnumerable> GetExternalImports() + { + int renderablePointerSize = ResourceUtilities.GetPointerSize(ResourceArch); + long renderablesOffset = GetHeaderSize(ResourceArch); - ModelDatas.Add(modelData); + for (int i = 0; i < ModelDatas.Count; i++) + { + ResourceImport resourceReference = ModelDatas[i].ResourceReference; + if (!resourceReference.ExternalImport) + { + continue; + } + + yield return new KeyValuePair( + renderablesOffset + (i * renderablePointerSize), + resourceReference); } } - public Model() : base() { } - - public Model(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + private static int GetHeaderSize(Arch arch) + { + return (ResourceUtilities.GetPointerSize(arch) * 3) + sizeof(uint) + 0x4; + } public struct ModelData { diff --git a/Volatility/Resources/Renderable/RenderableBPR.cs b/Volatility/Resources/Renderable/RenderableBPR.cs index f41addc..e02542c 100644 --- a/Volatility/Resources/Renderable/RenderableBPR.cs +++ b/Volatility/Resources/Renderable/RenderableBPR.cs @@ -1,5 +1,6 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.BPR)] public class RenderableBPR : RenderableBase { public override Endian ResourceEndian => Endian.LE; diff --git a/Volatility/Resources/Renderable/RenderableBase.cs b/Volatility/Resources/Renderable/RenderableBase.cs index 43ef5be..c1185a3 100644 --- a/Volatility/Resources/Renderable/RenderableBase.cs +++ b/Volatility/Resources/Renderable/RenderableBase.cs @@ -11,6 +11,7 @@ namespace Volatility.Resources; // Learn More: // https://burnout.wiki/wiki/Renderable +[ResourceDefinition(ResourceType.Renderable)] public abstract class RenderableBase : Resource { public Vector3Plus BoundingSphere; @@ -21,8 +22,6 @@ public abstract class RenderableBase : Resource public uint IndexBuffer; // Only on PC platforms public uint VertexBuffer; // Only on PC platforms - public override ResourceType ResourceType => ResourceType.Renderable; - public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); @@ -41,7 +40,9 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann // TODO: Parse RenderableMeshes } - public RenderableBase(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + protected RenderableBase() : base() { } + + protected RenderableBase(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } } diff --git a/Volatility/Resources/Renderable/RenderablePC.cs b/Volatility/Resources/Renderable/RenderablePC.cs index 5ef382a..2614008 100644 --- a/Volatility/Resources/Renderable/RenderablePC.cs +++ b/Volatility/Resources/Renderable/RenderablePC.cs @@ -1,5 +1,6 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.TUB)] public class RenderablePC : RenderableBase { public override Endian ResourceEndian => Endian.LE; diff --git a/Volatility/Resources/Renderable/RenderablePS3.cs b/Volatility/Resources/Renderable/RenderablePS3.cs index 575295c..a8b9bda 100644 --- a/Volatility/Resources/Renderable/RenderablePS3.cs +++ b/Volatility/Resources/Renderable/RenderablePS3.cs @@ -1,5 +1,6 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.PS3)] public class RenderablePS3 : RenderableBase { public override Endian ResourceEndian => Endian.BE; diff --git a/Volatility/Resources/Renderable/RenderableX360.cs b/Volatility/Resources/Renderable/RenderableX360.cs index 3335b2d..e83c6fd 100644 --- a/Volatility/Resources/Renderable/RenderableX360.cs +++ b/Volatility/Resources/Renderable/RenderableX360.cs @@ -1,5 +1,6 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.X360)] public class RenderableX360 : RenderableBase { public override Endian ResourceEndian => Endian.BE; diff --git a/Volatility/Resources/Resource.cs b/Volatility/Resources/Resource.cs index 5cf1c01..68d2335 100644 --- a/Volatility/Resources/Resource.cs +++ b/Volatility/Resources/Resource.cs @@ -19,11 +19,12 @@ public abstract class Resource [EditorCategory("Import Data"), EditorLabel("Unpacker"), EditorTooltip("The tool used to extract this resource from a bundle.")] public Unpacker Unpacker = Unpacker.Raw; - public virtual ResourceType ResourceType => ResourceType.Invalid; + public ResourceType ResourceType => ResourceMetadata.GetResourceType(GetType()); public virtual Endian ResourceEndian => Endian.Agnostic; // Forced endianness for platform-specific resources (e.g. Textures) public virtual Platform ResourcePlatform => Platform.Agnostic; public virtual Arch ResourceArch => Arch; public virtual void SetResourceArch(Arch newArch) { Arch = newArch; } + public virtual IEnumerable> GetExternalImports() { yield break; } public virtual void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) { @@ -45,6 +46,11 @@ public virtual void ParseFromStream(ResourceBinaryReader reader, Endian endianne public Resource() { } public Resource(string path, Endian endianness = Endian.Agnostic) + { + InitializeFromPath(path, endianness); + } + + protected void InitializeFromPath(string path, Endian endianness = Endian.Agnostic) { if (string.IsNullOrEmpty(path)) return; @@ -279,4 +285,4 @@ public enum Arch x32 = 0, [EditorLabel("64 bit (Console BPR)")] x64 = 1 -} \ No newline at end of file +} diff --git a/Volatility/Resources/ResourceFactory.cs b/Volatility/Resources/ResourceFactory.cs index 119db48..7efc30d 100644 --- a/Volatility/Resources/ResourceFactory.cs +++ b/Volatility/Resources/ResourceFactory.cs @@ -1,96 +1,41 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace Volatility.Resources; public static class ResourceFactory { - private static readonly Dictionary<(ResourceType, Platform), Func> resourceCreators = new() + private static readonly Dictionary<(ResourceType, Platform), Func> resourceCreators = CreateResourceCreators(); + + private static Dictionary<(ResourceType, Platform), Func> CreateResourceCreators() { - // Texture resources - { (ResourceType.Texture, Platform.BPR), path => { - var resource = new TextureBPR(path); - resource.PullAll(); - return resource; - } }, - { (ResourceType.Texture, Platform.TUB), path => { - var resource = new TexturePC(path); - resource.PullAll(); - return resource; - } }, - { (ResourceType.Texture, Platform.X360), path => { - var resource = new TextureX360(path); - resource.PullAll(); - return resource; - } }, - { (ResourceType.Texture, Platform.PS3), path => { - var resource = new TexturePS3(path); - resource.PullAll(); - return resource; - } }, - - // Splicer resources - { (ResourceType.Splicer, Platform.BPR), path => new Splicer(path, Endian.LE) }, - { (ResourceType.Splicer, Platform.TUB), path => new Splicer(path, Endian.LE) }, - { (ResourceType.Splicer, Platform.X360), path => new Splicer(path, Endian.BE) }, - { (ResourceType.Splicer, Platform.PS3), path => new Splicer(path, Endian.BE) }, - - // Renderable resources - { (ResourceType.Renderable, Platform.BPR), path => new RenderableBPR(path) }, - { (ResourceType.Renderable, Platform.TUB), path => new RenderablePC(path) }, - { (ResourceType.Renderable, Platform.X360), path => new RenderableX360(path) }, - { (ResourceType.Renderable, Platform.PS3), path => new RenderablePS3(path) }, - - // InstanceList resources - { (ResourceType.InstanceList, Platform.BPR), path => new InstanceList(path, Endian.LE) }, - { (ResourceType.InstanceList, Platform.TUB), path => new InstanceList(path, Endian.LE) }, - { (ResourceType.InstanceList, Platform.X360), path => new InstanceList(path, Endian.BE) }, - { (ResourceType.InstanceList, Platform.PS3), path => new InstanceList(path, Endian.BE) }, - - // Model resources - { (ResourceType.Model, Platform.BPR), path => new Model(path, Endian.LE) }, - { (ResourceType.Model, Platform.TUB), path => new Model(path, Endian.LE) }, - { (ResourceType.Model, Platform.X360), path => new Model(path, Endian.BE) }, - { (ResourceType.Model, Platform.PS3), path => new Model(path, Endian.BE) }, - - // EnvironmentKeyframe resources - { (ResourceType.EnvironmentKeyframe, Platform.BPR), path => new EnvironmentKeyframe(path, Endian.LE) }, - { (ResourceType.EnvironmentKeyframe, Platform.TUB), path => new EnvironmentKeyframe(path, Endian.LE) }, - { (ResourceType.EnvironmentKeyframe, Platform.X360), path => new EnvironmentKeyframe(path, Endian.BE) }, - { (ResourceType.EnvironmentKeyframe, Platform.PS3), path => new EnvironmentKeyframe(path, Endian.BE) }, - - // EnvironmentTimeline resources - { (ResourceType.EnvironmentTimeLine, Platform.BPR), path => new EnvironmentTimeline(path, Endian.LE) }, - { (ResourceType.EnvironmentTimeLine, Platform.TUB), path => new EnvironmentTimeline(path, Endian.LE) }, - { (ResourceType.EnvironmentTimeLine, Platform.X360), path => new EnvironmentTimeline(path, Endian.BE) }, - { (ResourceType.EnvironmentTimeLine, Platform.PS3), path => new EnvironmentTimeline(path, Endian.BE) }, - - // SnapshotData resources - { (ResourceType.SnapshotData, Platform.BPR), path => new SnapshotData(path, Endian.LE) }, - { (ResourceType.SnapshotData, Platform.TUB), path => new SnapshotData(path, Endian.LE) }, - { (ResourceType.SnapshotData, Platform.X360), path => new SnapshotData(path, Endian.BE) }, - { (ResourceType.SnapshotData, Platform.PS3), path => new SnapshotData(path, Endian.BE) }, - - // AttribSysVault resources - { (ResourceType.AttribSysVault, Platform.BPR), path => new AttribSysVault(path, Endian.LE) }, - { (ResourceType.AttribSysVault, Platform.TUB), path => new AttribSysVault(path, Endian.LE) }, - { (ResourceType.AttribSysVault, Platform.X360), path => new AttribSysVault(path, Endian.BE) }, - { (ResourceType.AttribSysVault, Platform.PS3), path => new AttribSysVault(path, Endian.BE) }, - - // StreamedDeformationSpec resources - { (ResourceType.StreamedDeformationSpec, Platform.BPR), path => new StreamedDeformationSpec(path, Endian.LE) }, - { (ResourceType.StreamedDeformationSpec, Platform.TUB), path => new StreamedDeformationSpec(path, Endian.LE) }, - { (ResourceType.StreamedDeformationSpec, Platform.X360), path => new StreamedDeformationSpec(path, Endian.BE) }, - { (ResourceType.StreamedDeformationSpec, Platform.PS3), path => new StreamedDeformationSpec(path, Endian.BE) }, - - // AptData resources - { (ResourceType.AptData, Platform.BPR), path => new AptData(path, Endian.LE) }, - { (ResourceType.AptData, Platform.TUB), path => new AptData(path, Endian.LE) }, - { (ResourceType.AptData, Platform.X360), path => new AptData(path, Endian.BE) }, - { (ResourceType.AptData, Platform.PS3), path => new AptData(path, Endian.BE) }, - - // Shader resources - { (ResourceType.Shader, Platform.Agnostic), path => new ShaderBase(path) }, - { (ResourceType.Shader, Platform.TUB), path => new ShaderPC(path) }, - }; + ResourceCreatorRegistry registry = new(); + + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + AddRegisteredResource(registry); + + return registry.Build(); + } public static Resource CreateResource(ResourceType resourceType, Platform platform, string filePath, bool x64 = false) { @@ -99,14 +44,159 @@ public static Resource CreateResource(ResourceType resourceType, Platform platfo var key = (resourceType, platform); if (resourceCreators.TryGetValue(key, out var creator)) { - var output = creator(filePath); + Resource output = creator(filePath); if (x64) output.SetResourceArch(Arch.x64); return output; } - else + + throw new InvalidPlatformException($"The '{resourceType}' type is not supported for the '{platform}' platform."); + } + + private static void AddRegisteredResource<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TResource>( + ResourceCreatorRegistry registry) + where TResource : Resource + { + registry.AddRegistrations(typeof(TResource)); + } + + private sealed class ResourceCreatorRegistry + { + private readonly Dictionary<(ResourceType, Platform), Func> _creators = new(); + + public void AddCreator(ResourceType resourceType, Platform platform, Func creator) { - throw new InvalidPlatformException($"The '{resourceType}' type is not supported for the '{platform}' platform."); + _creators.Add((resourceType, platform), creator); + } + + public void Add( + ResourceType resourceType, + Platform platform, + Func creator, + Action? afterCreate = null) + where TResource : Resource + { + AddCreator(resourceType, platform, path => + { + TResource resource = creator(path); + afterCreate?.Invoke(resource); + return resource; + }); + } + + public void AddRegistrations( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type resourceClass) + { + ResourceType resourceType = ResourceMetadata.GetResourceType(resourceClass); + ResourceRegistrationAttribute[] registrations = resourceClass + .GetCustomAttributes(inherit: false) + .ToArray(); + + foreach (ResourceRegistrationAttribute registration in registrations) + { + foreach (Platform platform in ExpandPlatforms(registration.Platforms)) + { + Func creator = registration.EndianMapped + ? CreateEndianMappedCreator(resourceClass, platform) + : CreatePathCreator(resourceClass); + + if (registration.PullAll) + { + creator = WrapWithPullAll(creator); + } + + AddCreator(resourceType, platform, creator); + } + } + } + + public Dictionary<(ResourceType, Platform), Func> Build() + { + return _creators; + } + + private static Func CreatePathCreator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type resourceClass) + { + ConstructorInfo? stringCtor = resourceClass.GetConstructor([typeof(string)]); + if (stringCtor != null) + { + return path => (Resource)stringCtor.Invoke([path]); + } + + ConstructorInfo? stringEndianCtor = resourceClass.GetConstructor([typeof(string), typeof(Endian)]); + if (stringEndianCtor != null) + { + return path => (Resource)stringEndianCtor.Invoke([path, Endian.Agnostic]); + } + + throw new InvalidOperationException( + $"Could not find a usable string constructor for resource class '{resourceClass.FullName}'."); + } + + private static Func CreateEndianMappedCreator( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type resourceClass, + Platform platform) + { + if (platform == Platform.Agnostic) + { + throw new InvalidOperationException( + $"Resource class '{resourceClass.FullName}' cannot use endian-mapped registration with Platform.Agnostic."); + } + + ConstructorInfo? constructor = resourceClass.GetConstructor([typeof(string), typeof(Endian)]); + if (constructor == null) + { + throw new InvalidOperationException( + $"Resource class '{resourceClass.FullName}' must expose a (string path, Endian endianness) constructor for endian-mapped registration."); + } + + Endian endianness = platform switch + { + Platform.BPR or Platform.TUB => Endian.LE, + Platform.X360 or Platform.PS3 => Endian.BE, + _ => throw new InvalidOperationException($"No default endianness mapping exists for platform '{platform}'."), + }; + + return path => (Resource)constructor.Invoke([path, endianness]); + } + + private static Func WrapWithPullAll(Func creator) + { + return path => + { + Resource resource = creator(path); + resource.PullAll(); + return resource; + }; + } + + private static IEnumerable ExpandPlatforms(RegistrationPlatforms platforms) + { + if ((platforms & RegistrationPlatforms.Agnostic) != 0) + { + yield return Platform.Agnostic; + } + + if ((platforms & RegistrationPlatforms.BPR) != 0) + { + yield return Platform.BPR; + } + + if ((platforms & RegistrationPlatforms.TUB) != 0) + { + yield return Platform.TUB; + } + + if ((platforms & RegistrationPlatforms.X360) != 0) + { + yield return Platform.X360; + } + + if ((platforms & RegistrationPlatforms.PS3) != 0) + { + yield return Platform.PS3; + } } } } diff --git a/Volatility/Resources/ResourceImport.cs b/Volatility/Resources/ResourceImport.cs index d946c21..621af6d 100644 --- a/Volatility/Resources/ResourceImport.cs +++ b/Volatility/Resources/ResourceImport.cs @@ -8,6 +8,8 @@ namespace Volatility.Resources; public struct ResourceImport { + public const int ImportEntrySize = 0x10; + // The idea here is that if the name is populated but // the ID is empty, the name will be calculated into an ID // on export. If both a name and ID exist, use the ID, as @@ -40,14 +42,33 @@ public ResourceImport(string name, bool externalImport = false) ExternalImport = externalImport; } + public static string GetImportsPath(string resourcePath, Unpacker unpacker) + { + string suffix = unpacker switch + { + Unpacker.YAP => "_imports.yaml", + _ => "_imports.dat", + }; + + return Path.Combine( + Path.GetDirectoryName(resourcePath) ?? string.Empty, + Path.GetFileNameWithoutExtension(resourcePath) + suffix); + } + + public static void DeleteImportsSidecarFiles(string resourcePath) + { + DeleteFileIfExists(GetImportsPath(resourcePath, Unpacker.YAP)); + DeleteFileIfExists(GetImportsPath(resourcePath, Unpacker.Raw)); + } + public static bool ReadExternalImport(int index, EndianAwareBinaryReader reader, long importBlockOffset, out ResourceImport resourceImport) { long originalPosition = reader.BaseStream.Position; // In-resource imports block - if (reader.BaseStream.Length >= importBlockOffset + (0x10 * index) + 0x10) + if (reader.BaseStream.Length >= importBlockOffset + ((long)ImportEntrySize * index) + ImportEntrySize) { - reader.BaseStream.Seek(importBlockOffset + (0x10 * index), SeekOrigin.Begin); + reader.BaseStream.Seek(importBlockOffset + ((long)ImportEntrySize * index), SeekOrigin.Begin); resourceImport = new ResourceImport(reader.ReadUInt64(), externalImport: true); @@ -58,16 +79,21 @@ public static bool ReadExternalImport(int index, EndianAwareBinaryReader reader, reader.BaseStream.Seek(originalPosition, SeekOrigin.Begin); - // YAP imports yaml if (reader.BaseStream is FileStream fs) { - string baseName = Path.GetFileNameWithoutExtension(fs.Name); - string directory = Path.GetDirectoryName(fs.Name); - string yamlPath = Path.Combine(directory, baseName + "_imports.yaml"); + if (TryReadBinaryImportAt(GetImportsPath(fs.Name, Unpacker.Raw), reader.Endianness, index, out ResourceID binaryImport)) + { + resourceImport = new ResourceImport(binaryImport, externalImport: true); + return true; + } - resourceImport = new ResourceImport(GetYAMLImportValueAt(yamlPath, index), externalImport: true); + string yamlPath = GetImportsPath(fs.Name, Unpacker.YAP); + if (File.Exists(yamlPath)) + { + resourceImport = new ResourceImport(GetYAMLImportValueAt(yamlPath, index), externalImport: true); - return true; + return true; + } } resourceImport = default; @@ -81,7 +107,7 @@ public static bool ReadExternalImport(long fileOffset, EndianAwareBinaryReader r reader.BaseStream.Seek(importBlockOffset, SeekOrigin.Begin); // In-resource imports block - while (reader.BaseStream.Position + 0x10 <= reader.BaseStream.Length) + while (reader.BaseStream.Position + ImportEntrySize <= reader.BaseStream.Length) { ulong resourceValue = reader.ReadUInt64(); long entryKey = reader.ReadUInt32(); @@ -95,13 +121,15 @@ public static bool ReadExternalImport(long fileOffset, EndianAwareBinaryReader r reader.BaseStream.Seek(originalPosition, SeekOrigin.Begin); - // YAP imports yaml if (reader.BaseStream is FileStream fs) { - string baseName = Path.GetFileNameWithoutExtension(fs.Name); - string directory = Path.GetDirectoryName(fs.Name); - string yamlPath = Path.Combine(directory, baseName + "_imports.yaml"); + if (TryReadBinaryImportByKey(GetImportsPath(fs.Name, Unpacker.Raw), reader.Endianness, fileOffset, out ResourceID binaryImport)) + { + resourceImport = new ResourceImport(binaryImport, externalImport: true); + return true; + } + string yamlPath = GetImportsPath(fs.Name, Unpacker.YAP); if (File.Exists(yamlPath)) { ulong? yamlValue = GetYAMLImportValueByKey(yamlPath, fileOffset); @@ -117,6 +145,73 @@ public static bool ReadExternalImport(long fileOffset, EndianAwareBinaryReader r return false; } + private static bool TryReadBinaryImportAt( + string binaryPath, + Endian endianness, + int index, + out ResourceID referenceId) + { + referenceId = ResourceID.Default; + + if (index < 0 || !File.Exists(binaryPath)) + { + return false; + } + + long entryOffset = (long)ImportEntrySize * index; + using EndianAwareBinaryReader importReader = new(new FileStream(binaryPath, FileMode.Open, FileAccess.Read, FileShare.Read), endianness); + + if (importReader.BaseStream.Length < entryOffset + ImportEntrySize) + { + return false; + } + + importReader.BaseStream.Seek(entryOffset, SeekOrigin.Begin); + referenceId = importReader.ReadUInt64(); + return true; + } + + private static bool TryReadBinaryImportByKey( + string binaryPath, + Endian endianness, + long fileOffset, + out ResourceID referenceId) + { + referenceId = ResourceID.Default; + + if (fileOffset < 0 || fileOffset > uint.MaxValue || !File.Exists(binaryPath)) + { + return false; + } + + using EndianAwareBinaryReader importReader = new(new FileStream(binaryPath, FileMode.Open, FileAccess.Read, FileShare.Read), endianness); + + while (importReader.BaseStream.Position + ImportEntrySize <= importReader.BaseStream.Length) + { + ulong resourceValue = importReader.ReadUInt64(); + uint entryKey = importReader.ReadUInt32(); + importReader.BaseStream.Seek(sizeof(uint), SeekOrigin.Current); + + if (entryKey != (uint)fileOffset) + { + continue; + } + + referenceId = resourceValue; + return true; + } + + return false; + } + + private static void DeleteFileIfExists(string path) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + public static ResourceID GetYAMLImportValueAt(string yamlPath, int index) { var yaml = File.ReadAllText(yamlPath); diff --git a/Volatility/Resources/ResourceMetadata.cs b/Volatility/Resources/ResourceMetadata.cs new file mode 100644 index 0000000..1f0af18 --- /dev/null +++ b/Volatility/Resources/ResourceMetadata.cs @@ -0,0 +1,60 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Volatility.Resources; + +[Flags] +public enum RegistrationPlatforms +{ + None = 0, + BPR = 1 << 0, + TUB = 1 << 1, + X360 = 1 << 2, + PS3 = 1 << 3, + Agnostic = 1 << 4, + All = BPR | TUB | X360 | PS3, +} + +[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] +public sealed class ResourceDefinitionAttribute : Attribute +{ + public ResourceDefinitionAttribute(ResourceType resourceType) + { + ResourceType = resourceType; + } + + public ResourceType ResourceType { get; } +} + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)] +public sealed class ResourceRegistrationAttribute : Attribute +{ + public ResourceRegistrationAttribute(RegistrationPlatforms platforms) + { + Platforms = platforms; + } + + public RegistrationPlatforms Platforms { get; } + public bool EndianMapped { get; init; } + public bool PullAll { get; init; } +} + +internal static class ResourceMetadata +{ + private static readonly ConcurrentDictionary ResourceTypes = new(); + + public static ResourceType GetResourceType(Type resourceClass) + { + return ResourceTypes.GetOrAdd(resourceClass, static type => + { + ResourceDefinitionAttribute? definition = type.GetCustomAttribute(inherit: true); + if (definition == null) + { + throw new InvalidOperationException( + $"Resource type metadata is missing for '{type.FullName}'. Add [ResourceDefinition(...)] to the resource or its base class."); + } + + return definition.ResourceType; + }); + } +} diff --git a/Volatility/Resources/Shader/ShaderBase.cs b/Volatility/Resources/Shader/ShaderBase.cs index e07dedd..45c230c 100644 --- a/Volatility/Resources/Shader/ShaderBase.cs +++ b/Volatility/Resources/Shader/ShaderBase.cs @@ -1,11 +1,9 @@ namespace Volatility.Resources; +[ResourceDefinition(ResourceType.Shader)] +[ResourceRegistration(RegistrationPlatforms.Agnostic)] public class ShaderBase : Resource { - public override ResourceType ResourceType => ResourceType.Shader; - public override Endian ResourceEndian => Endian.Agnostic; - public override Platform ResourcePlatform => Platform.Agnostic; - [EditorCategory("Shader/Source"), EditorLabel("Source File"), EditorTooltip("Relative path to the HLSL source file.")] public string? ShaderSourcePath { get; set; } diff --git a/Volatility/Resources/Shader/ShaderPC.cs b/Volatility/Resources/Shader/ShaderPC.cs index a7b485d..02b37a2 100644 --- a/Volatility/Resources/Shader/ShaderPC.cs +++ b/Volatility/Resources/Shader/ShaderPC.cs @@ -5,6 +5,7 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.TUB)] public class ShaderPC : ShaderBase { public override Endian ResourceEndian => Endian.LE; diff --git a/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBPR.cs b/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBPR.cs index eb1faff..4ed28ec 100644 --- a/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBPR.cs +++ b/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBPR.cs @@ -6,6 +6,7 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.BPR)] public class ShaderProgramBufferBPR : ShaderProgramBufferBase { public override Endian ResourceEndian => Endian.LE; diff --git a/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBase.cs b/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBase.cs index 3c3dd8a..1ec7ed2 100644 --- a/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBase.cs +++ b/Volatility/Resources/ShaderProgramBuffer/ShaderProgramBufferBase.cs @@ -1,11 +1,8 @@ namespace Volatility.Resources; +[ResourceDefinition(ResourceType.RwShaderProgramBuffer)] public class ShaderProgramBufferBase : Resource { - public override ResourceType ResourceType => ResourceType.RwShaderProgramBuffer; - public override Endian ResourceEndian => Endian.Agnostic; - public override Platform ResourcePlatform => Platform.Agnostic; - public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness) { base.WriteToStream(writer, endianness); diff --git a/Volatility/Resources/SnapshotData/SnapshotData.cs b/Volatility/Resources/SnapshotData/SnapshotData.cs index 5880532..088be84 100644 --- a/Volatility/Resources/SnapshotData/SnapshotData.cs +++ b/Volatility/Resources/SnapshotData/SnapshotData.cs @@ -1,85 +1,126 @@ -using System.Numerics; - namespace Volatility.Resources; +[ResourceDefinition(ResourceType.SnapshotData)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class SnapshotData : BinaryResource { - public override ResourceType ResourceType => ResourceType.SnapshotData; - public override Platform ResourcePlatform => Platform.Agnostic; + private const int SnapshotHeaderSize = 0x10; + private const int SnapshotChannelSize = 0xC; + private const int SnapshotStatusSize = 0x8; public List Channels = []; public List SnapshotStatuses = []; public override void WriteToStream(ResourceBinaryWriter writer, Endian endianness = Endian.Agnostic) { - DataSize = (uint)(0x10 + (Channels.Count * 0xC) + (SnapshotStatuses.Count * 0x8)); + int snapshotCount = GetSnapshotCountForWrite(); - base.WriteToStream(writer, endianness); + DataSize = (uint)( + SnapshotHeaderSize + + (Channels.Count * SnapshotChannelSize) + + (SnapshotStatuses.Count * SnapshotStatusSize)); - writer.Write(SnapshotStatuses.Count); - writer.Write(Channels.Count); + base.WriteToStream(writer, endianness); - foreach (SnapshotChannelData channelData in Channels) - { - writer.Write(channelData.Flags); - writer.Write(channelData.ChannelID); - writer.Write(0xFFFFFFFF); - } + long channelsOffset = writer.BaseStream.Position + SnapshotHeaderSize; + long statusesOffset = channelsOffset + (Channels.Count * SnapshotChannelSize); - foreach (SnapshotStatusData statusData in SnapshotStatuses) - { - writer.Write(statusData.Flags); - writer.Write(statusData.TimeRemaining); - } + writer.Write(snapshotCount); + writer.Write(Channels.Count); + writer.Write(1); // maiPad[0] (mixer state?) + writer.Write(0x12345678); // maiPad[1] + writer.WriteSection(channelsOffset, Channels, SnapshotChannelData.Write); + writer.WriteSection(statusesOffset, SnapshotStatuses, SnapshotStatusData.Write); } + public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { base.ParseFromStream(reader, endianness); - int numSnapshots = reader.ReadInt32(); // Snapshot data - int numChannels = reader.ReadInt32(); // Channel data - + int snapshotCount = reader.ReadInt32(); + int channelCount = reader.ReadInt32(); _ = reader.ReadUInt64(); - // Channel data - for (int i = 0; i < numSnapshots; i++) + long channelsOffset = reader.BaseStream.Position; + long statusesOffset = channelsOffset + (channelCount * SnapshotChannelSize); + + Channels = reader.ParseSection(channelsOffset, channelCount, SnapshotChannelData.Read); + SnapshotStatuses = reader.ParseSection(statusesOffset, snapshotCount * channelCount, SnapshotStatusData.Read); + } + + public SnapshotData() : base() { } + + public SnapshotData(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + + private int GetSnapshotCountForWrite() + { + if (Channels.Count == 0) { - Channels.Add(new SnapshotChannelData() - { - Flags = reader.ReadUInt32(), - ChannelID = reader.ReadUInt32() - }); - uint invalid = reader.ReadUInt32(); - if (invalid != 0xFFFFFFFF) + if (SnapshotStatuses.Count != 0) { - Console.Error.WriteLine($"Expected 0xFFFFFFFF at {reader.BaseStream.Position}, got {invalid}!"); + throw new InvalidDataException("Snapshot statuses cannot be written without at least one channel."); } + + return 0; } - // Snapshot data - for (int i = 0; i < (numSnapshots * numChannels); i++) + if (SnapshotStatuses.Count % Channels.Count != 0) { - SnapshotStatuses.Add(new SnapshotStatusData() - { - Flags = reader.ReadUInt32(), - TimeRemaining = reader.ReadSingle() - }); + throw new InvalidDataException( + $"Snapshot status count ({SnapshotStatuses.Count}) must be divisible by channel count ({Channels.Count})."); } - } - - public SnapshotData() : base() { } - public SnapshotData(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + return SnapshotStatuses.Count / Channels.Count; + } } public struct SnapshotChannelData { public uint Flags; public uint ChannelID; -} + + public static SnapshotChannelData Read(ResourceBinaryReader reader) + { + SnapshotChannelData channelData = new() + { + Flags = reader.ReadUInt32(), + ChannelID = reader.ReadUInt32() + }; + + uint terminator = reader.ReadUInt32(); + if (terminator != 0xFFFFFFFF) + { + Console.Error.WriteLine($"Expected 0xFFFFFFFF at {reader.BaseStream.Position}, got {terminator}!"); + } + + return channelData; + } + + public static void Write(ResourceBinaryWriter writer, SnapshotChannelData channelData) + { + writer.Write(channelData.Flags); + writer.Write(channelData.ChannelID); + writer.Write(0xFFFFFFFFu); + } +} public struct SnapshotStatusData { public float TimeRemaining; public uint Flags; -} \ No newline at end of file + + public static SnapshotStatusData Read(ResourceBinaryReader reader) + { + return new SnapshotStatusData + { + Flags = reader.ReadUInt32(), + TimeRemaining = reader.ReadSingle() + }; + } + + public static void Write(ResourceBinaryWriter writer, SnapshotStatusData statusData) + { + writer.Write(statusData.Flags); + writer.Write(statusData.TimeRemaining); + } +} diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index e452605..1004b2a 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -7,13 +7,17 @@ namespace Volatility.Resources; // The Splicer resource type contains multiple sound assets and presets for // how those sounds are played. They are typically triggered by in-game actions. // Splicers begin with a Binary File resource. - +// // Learn More: // https://burnout.wiki/wiki/Splicer +[ResourceDefinition(ResourceType.Splicer)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class Splicer : BinaryResource { - public override ResourceType ResourceType => ResourceType.Splicer; + private const int HeaderSize = 0xC; + private const int SpliceHeaderSize = 0x18; + private const int SampleRefSize = 0x2C; public List Splices = []; @@ -25,112 +29,51 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann { base.ParseFromStream(reader, endianness); - int version = reader.ReadInt32(); + int version = reader.ReadInt32(); if (version != 1) { throw new InvalidDataException("Version mismatch! Version should be 1."); } - + int sizedata = reader.ReadInt32(); - int numSplices = reader.ReadInt32(); if (numSplices <= 0) { throw new InvalidDataException("No splices in Splicer file!"); } - List spliceRefCounts = new(numSplices); - - // Read Splice Data - for (int i = 0; i < numSplices; i++) - { - uint nameHash = reader.ReadUInt32(); - ushort spliceIdx = reader.ReadUInt16(); - sbyte eType = reader.ReadSByte(); - byte numRefs = reader.ReadByte(); - float vol = reader.ReadSingle(); - float rpitch = reader.ReadSingle(); - float rvol = reader.ReadSingle(); - _ = reader.ReadInt32(); // pSampleRefList (null) - - spliceRefCounts.Add(numRefs); - Splices.Add(new SpliceData - { - NameHash = nameHash, - SpliceIndex = spliceIdx, - ESpliceType = eType, - Volume = vol, - RND_Pitch = rpitch, - RND_Vol = rvol, - SampleRefs = new List(numRefs) - }); - } - - long sampleRefsPtrOffset = reader.BaseStream.Position - DataOffset; - - reader.BaseStream.Seek(sizedata + 0xC + DataOffset, SeekOrigin.Begin); - - int numSamples = reader.ReadInt32(); + long spliceHeadersOffset = reader.BaseStream.Position; + List spliceHeaders = reader.ParseSection(spliceHeadersOffset, numSplices, SpliceHeader.Read); - List samplePtrs = new(numSamples); - for (int i = 0; i < numSamples; i++) + Splices = new(numSplices); + foreach (SpliceHeader header in spliceHeaders) { - samplePtrs.Add(reader.ReadInt32()); + Splices.Add(header.ToSpliceData()); } - long samplePtrOffset = reader.BaseStream.Position - DataOffset; + long sampleRefsOffset = spliceHeadersOffset + (numSplices * SpliceHeaderSize); + long sampleTableOffset = DataOffset + HeaderSize + sizedata; - for (int i = 0; i < numSamples; i++) - { - reader.BaseStream.Seek(samplePtrOffset + DataOffset + samplePtrs[i], SeekOrigin.Begin); - - int length = (int)((i == (numSamples - 1) ? reader.BaseStream.Length : samplePtrs[i + 1]) - samplePtrs[i]); - - byte[]? data = reader.ReadBytes(length); + reader.BaseStream.Seek(sampleTableOffset, SeekOrigin.Begin); - _samples.Add - ( - new SpliceSample - { - SampleID = SnrID.HashFromBytes(data), - Data = data, - } - ); - - Console.WriteLine($"Adding sample {i} as {_samples[i].SampleID}"); - } + int numSamples = reader.ReadInt32(); + long samplePointersOffset = reader.BaseStream.Position; + List samplePointers = reader.ParseSection(samplePointersOffset, numSamples, r => r.ReadInt32()); + long sampleDataOffset = samplePointersOffset + (numSamples * sizeof(int)); - reader.BaseStream.Seek(sampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); + _samples = ReadSamples(reader, samplePointers, sampleDataOffset); - // Read SampleRefs + long currentSampleRefOffset = sampleRefsOffset; for (int i = 0; i < Splices.Count; i++) { - int count = spliceRefCounts[i]; - List list = Splices[i].SampleRefs; - - for (int j = 0; j < count; j++) - { - ushort sampleIdx = reader.ReadUInt16(); - SpliceSampleRef sr = new() - { - Sample = _samples[sampleIdx].SampleID, - ESpliceType = reader.ReadSByte(), - Padding = reader.ReadByte(), - Volume = reader.ReadSingle(), - Pitch = reader.ReadSingle(), - Offset = reader.ReadSingle(), - Az = reader.ReadSingle(), - Duration = reader.ReadSingle(), - FadeIn = reader.ReadSingle(), - FadeOut = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), - RND_Pitch = reader.ReadSingle(), - Priority = reader.ReadByte(), - ERollOffType = reader.ReadByte(), - Padding2 = reader.ReadUInt16() - }; - list.Add(sr); - } + int count = spliceHeaders[i].SampleRefCount; + List sampleRefs = reader.ParseSection( + currentSampleRefOffset, + count, + r => ReadSampleRef(r, _samples)); + + Splices[i].SampleRefs.AddRange(sampleRefs); + currentSampleRefOffset += count * SampleRefSize; } } @@ -138,85 +81,63 @@ public override void WriteToStream(ResourceBinaryWriter writer, Endian endiannes { LoadDependentSamples(); - base.WriteToStream(writer, endianness); + int totalRefs = Splices.Sum(s => s.SampleRefs.Count); + int sizeOfSplices = Splices.Count * SpliceHeaderSize; + int sizeOfSampleRefs = totalRefs * SampleRefSize; - writer.BaseStream.Position = DataOffset; + DataSize = (uint)(HeaderSize + sizeOfSplices + sizeOfSampleRefs + sizeof(int)); - writer.Write(1); // version + base.WriteToStream(writer, endianness); + writer.BaseStream.Position = DataOffset; - int totalRefs = Splices.Sum(s => s.SampleRefs.Count); - int sizeOfSplices = Splices.Count * 0x18; // Size of Splice_Data - int sizeOfSampleRefs = totalRefs * 0x2C; // Size of Splice_SampleRef int sizedata = sizeOfSplices + sizeOfSampleRefs; + long spliceHeadersOffset = writer.BaseStream.Position + HeaderSize; + long sampleRefsOffset = spliceHeadersOffset + sizeOfSplices; + long sampleTableOffset = DataOffset + HeaderSize + sizedata; - writer.Write(sizedata); // sizedata/pSampleRefTOC + writer.Write(1); // version + writer.Write(sizedata); + writer.Write(Splices.Count); - writer.Write(Splices.Count); // NumSplices - - foreach (SpliceData splice in Splices) - { - writer.Write(splice.NameHash); - writer.Write(splice.SpliceIndex); - writer.Write(splice.ESpliceType); - writer.Write((byte)splice.SampleRefs.Count); - writer.Write(splice.Volume); - writer.Write(splice.RND_Pitch); - writer.Write(splice.RND_Vol); - - writer.Write(0); // pSampleRefList placeholder - } + writer.WriteSection(spliceHeadersOffset, Splices, WriteSpliceHeader); + writer.BaseStream.Position = sampleRefsOffset; foreach (SpliceData splice in Splices) { - foreach (SpliceSampleRef sr in splice.SampleRefs) + foreach (SpliceSampleRef sampleRef in splice.SampleRefs) { - int sampleIdx = _samples.FindIndex(x => x.SampleID == sr.Sample); - writer.Write((ushort)sampleIdx); - writer.Write(sr.ESpliceType); - writer.Write(sr.Padding); - writer.Write(sr.Volume); - writer.Write(sr.Pitch); - writer.Write(sr.Offset); - writer.Write(sr.Az); - writer.Write(sr.Duration); - writer.Write(sr.FadeIn); - writer.Write(sr.FadeOut); - writer.Write(sr.RND_Vol); - writer.Write(sr.RND_Pitch); - writer.Write(sr.Priority); - writer.Write(sr.ERollOffType); - writer.Write(sr.Padding2); + WriteSampleRef(writer, sampleRef, ResolveSampleIndex(sampleRef.Sample)); } } - writer.BaseStream.Position = DataOffset + 0xC + sizedata; // Header + sizedata - int numSamples = _samples.Count; + writer.BaseStream.Position = sampleTableOffset; + writer.Write(_samples.Count); - writer.Write(numSamples); - - // Reserve space for offsets - long offsetsStart = writer.BaseStream.Position; - for (int i = 0; i < numSamples; i++) writer.Write(0); + long samplePointersStart = writer.BaseStream.Position; + for (int i = 0; i < _samples.Count; i++) + { + writer.Write(0); + } - int running = 0; - for (int i = 0; i < numSamples; i++) + int runningOffset = 0; + for (int i = 0; i < _samples.Count; i++) { byte[] data = _samples[i].Data; writer.Write(data); - // backfill this sample's offset - long save = writer.BaseStream.Position; - writer.Seek((int)(offsetsStart + i * 4), SeekOrigin.Begin); - writer.Write(running); - writer.Seek((int)save, SeekOrigin.Begin); - running += data.Length; + + long savePosition = writer.BaseStream.Position; + writer.BaseStream.Position = samplePointersStart + (i * sizeof(int)); + writer.Write(runningOffset); + writer.BaseStream.Position = savePosition; + + runningOffset += data.Length; } - // Update DataSize DataSize = (uint)(writer.BaseStream.Length - DataOffset); - long pos = writer.BaseStream.Position; - writer.BaseStream.Seek(0, SeekOrigin.Begin); + long endPosition = writer.BaseStream.Position; + writer.BaseStream.Position = 0; writer.Write(DataSize); - writer.BaseStream.Seek(pos, SeekOrigin.Begin); + writer.BaseStream.Position = endPosition; } public void LoadDependentSamples(bool recurse = false) @@ -225,10 +146,10 @@ public void LoadDependentSamples(bool recurse = false) .SelectMany(s => s.SampleRefs.Select(sr => sr.Sample)) .Distinct() .ToList(); - + string dir = Path.Combine ( - GetEnvironmentDirectory(EnvironmentDirectory.Splicer), + GetEnvironmentDirectory(EnvironmentDirectory.Splicer), "Samples" ); @@ -240,12 +161,16 @@ public void LoadDependentSamples(bool recurse = false) byte[] data = File.ReadAllBytes(f); SnrID id = SnrID.HashFromBytes(data); if (!map.ContainsKey(id) && needed.Contains(id)) + { map[id] = data; + } } foreach (SnrID id in needed.Where(id => !map.ContainsKey(id))) + { throw new FileNotFoundException($"Missing sample for {id}"); - + } + _samples = needed.Select(id => new SpliceSample { SampleID = id, Data = map[id] }).ToList(); } @@ -256,8 +181,140 @@ public List GetLoadedSamples() public Splicer() : base() { } - public Splicer(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } - + public Splicer(string path, Endian endianness = Endian.Agnostic) + : base(path, endianness) { } + + private static List ReadSamples( + ResourceBinaryReader reader, + List samplePointers, + long sampleDataOffset) + { + List samples = new(samplePointers.Count); + + for (int i = 0; i < samplePointers.Count; i++) + { + long start = sampleDataOffset + samplePointers[i]; + long end = i == samplePointers.Count - 1 + ? reader.BaseStream.Length + : sampleDataOffset + samplePointers[i + 1]; + + reader.BaseStream.Seek(start, SeekOrigin.Begin); + byte[] data = reader.ReadBytes((int)(end - start)); + + samples.Add(new SpliceSample + { + SampleID = SnrID.HashFromBytes(data), + Data = data, + }); + } + + return samples; + } + + private static SpliceSampleRef ReadSampleRef(ResourceBinaryReader reader, List samples) + { + ushort sampleIndex = reader.ReadUInt16(); + return new SpliceSampleRef + { + Sample = samples[sampleIndex].SampleID, + ESpliceType = reader.ReadSByte(), + Padding = reader.ReadByte(), + Volume = reader.ReadSingle(), + Pitch = reader.ReadSingle(), + Offset = reader.ReadSingle(), + Az = reader.ReadSingle(), + Duration = reader.ReadSingle(), + FadeIn = reader.ReadSingle(), + FadeOut = reader.ReadSingle(), + RND_Vol = reader.ReadSingle(), + RND_Pitch = reader.ReadSingle(), + Priority = reader.ReadByte(), + ERollOffType = reader.ReadByte(), + Padding2 = reader.ReadUInt16() + }; + } + + private static void WriteSpliceHeader(ResourceBinaryWriter writer, SpliceData splice) + { + writer.Write(splice.NameHash); + writer.Write(splice.SpliceIndex); + writer.Write(splice.ESpliceType); + writer.Write((byte)splice.SampleRefs.Count); + writer.Write(splice.Volume); + writer.Write(splice.RND_Pitch); + writer.Write(splice.RND_Vol); + writer.Write(0); + } + + private static void WriteSampleRef(ResourceBinaryWriter writer, SpliceSampleRef sampleRef, int sampleIndex) + { + writer.Write((ushort)sampleIndex); + writer.Write(sampleRef.ESpliceType); + writer.Write(sampleRef.Padding); + writer.Write(sampleRef.Volume); + writer.Write(sampleRef.Pitch); + writer.Write(sampleRef.Offset); + writer.Write(sampleRef.Az); + writer.Write(sampleRef.Duration); + writer.Write(sampleRef.FadeIn); + writer.Write(sampleRef.FadeOut); + writer.Write(sampleRef.RND_Vol); + writer.Write(sampleRef.RND_Pitch); + writer.Write(sampleRef.Priority); + writer.Write(sampleRef.ERollOffType); + writer.Write(sampleRef.Padding2); + } + + private int ResolveSampleIndex(SnrID sampleId) + { + int sampleIndex = _samples.FindIndex(x => x.SampleID == sampleId); + if (sampleIndex < 0) + { + throw new InvalidDataException($"Unable to resolve sample {sampleId} in splicer export."); + } + + return sampleIndex; + } + + private readonly record struct SpliceHeader( + uint NameHash, + ushort SpliceIndex, + sbyte ESpliceType, + byte SampleRefCount, + float Volume, + float RandomPitch, + float RandomVolume) + { + public static SpliceHeader Read(ResourceBinaryReader reader) + { + SpliceHeader header = new( + reader.ReadUInt32(), + reader.ReadUInt16(), + reader.ReadSByte(), + reader.ReadByte(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle()); + + _ = reader.ReadInt32(); + return header; + } + + public SpliceData ToSpliceData() + { + return new SpliceData + { + NameHash = NameHash, + SpliceIndex = SpliceIndex, + ESpliceType = ESpliceType, + Volume = Volume, + RND_Pitch = RandomPitch, + RND_Vol = RandomVolume, + SampleRefs = new List(SampleRefCount) + }; + } + } + [StructLayout(LayoutKind.Sequential, Pack = 1)] public class SpliceData { @@ -295,4 +352,4 @@ public struct SpliceSample public SnrID SampleID; public byte[] Data; } -} \ No newline at end of file +} diff --git a/Volatility/Resources/StreamedDeformationSpec/StreamedDeformationSpec.cs b/Volatility/Resources/StreamedDeformationSpec/StreamedDeformationSpec.cs index a595856..a4c0e15 100644 --- a/Volatility/Resources/StreamedDeformationSpec/StreamedDeformationSpec.cs +++ b/Volatility/Resources/StreamedDeformationSpec/StreamedDeformationSpec.cs @@ -312,14 +312,14 @@ public GlassPaneSpec(ResourceBinaryReader reader) } } +[ResourceDefinition(ResourceType.StreamedDeformationSpec)] +[ResourceRegistration(RegistrationPlatforms.All, EndianMapped = true)] public class StreamedDeformationSpec : Resource { public const int HeaderSize32 = 0x6B0; public const int HeaderSize64 = 0x6F0; private const int SectionAlignment = 0x10; - public override ResourceType ResourceType => ResourceType.StreamedDeformationSpec; - public int VersionNumber { get; set; } public ulong TagPointDataOffset { get; set; } public int NumberOfTagPoints { get; set; } @@ -392,7 +392,7 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann CameraTagsInfo = new (reader, arch); LightTagsInfo = new(reader, arch); - reader.BaseStream.Seek(arch == Arch.x64 ? 0x8 : 0x4, SeekOrigin.Current); + reader.BaseStream.Seek(ResourceUtilities.GetPointerSize(ResourceArch), SeekOrigin.Current); HandlingBodyDimensions = reader.ReadVector3(); diff --git a/Volatility/Resources/Texture/TextureBPR.cs b/Volatility/Resources/Texture/TextureBPR.cs index 3bf1a80..d47eab8 100644 --- a/Volatility/Resources/Texture/TextureBPR.cs +++ b/Volatility/Resources/Texture/TextureBPR.cs @@ -1,7 +1,8 @@ -using static Volatility.Utilities.DataUtilities; +using Volatility.Utilities; namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.BPR, PullAll = true)] public class TextureBPR : TextureBase { public override Endian ResourceEndian => Endian.LE; @@ -75,11 +76,13 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann SetResourceArch(reader.BaseStream.Length > 0x40 ? Arch.x64 : Arch.x32); - reader.BaseStream.Seek(ResourceArch == Arch.x64 ? 0x8 : 0x4, SeekOrigin.Begin); // Skip TextureInterfacePtr + int pointerSize = ResourceUtilities.GetPointerSize(ResourceArch); + + reader.BaseStream.Seek(pointerSize, SeekOrigin.Begin); // Skip TextureInterfacePtr Usage = (D3D11_USAGE)reader.ReadInt32(); Dimension = (DIMENSION)reader.ReadInt32(); - reader.BaseStream.Seek(ResourceArch == Arch.x64 ? 0x18 : 0xC, SeekOrigin.Current); // Skip pointers - reader.BaseStream.Seek(0x4, SeekOrigin.Current); // Skip Unknown0 + reader.BaseStream.Seek(pointerSize * 3, SeekOrigin.Current); // Skip pointers + reader.BaseStream.Seek(0x4, SeekOrigin.Current); // Skip Unknown0 Format = (DXGI_FORMAT)reader.ReadInt32(); Flags = (BPRTextureFlags)reader.ReadUInt32(); Width = reader.ReadUInt16(); @@ -88,11 +91,11 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann ArraySize = reader.ReadUInt16(); MostDetailedMip = reader.ReadByte(); MipmapLevels = reader.ReadByte(); - reader.BaseStream.Seek(sizeof(ushort), SeekOrigin.Current); // Skip Unknown1 - reader.BaseStream.Seek(ResourceArch == Arch.x64 ? 0x8 : 0x4, SeekOrigin.Current); // Unknown 2, 64 bit + reader.BaseStream.Seek(sizeof(ushort), SeekOrigin.Current); // Skip Unknown1 + reader.BaseStream.Seek(pointerSize, SeekOrigin.Current); // Unknown 2, 64 bit PlacedTileMode = (XG_TILE_MODE)reader.ReadInt32(); PlacedDataSize = (uint)reader.ReadInt32(); - reader.BaseStream.Seek(ResourceArch == Arch.x64 ? 0x8 : 0x4, SeekOrigin.Current); // TextureData, 64 bit + reader.BaseStream.Seek(pointerSize, SeekOrigin.Current); // TextureData, 64 bit } public override void PushInternalDimension() diff --git a/Volatility/Resources/Texture/TextureBase.cs b/Volatility/Resources/Texture/TextureBase.cs index 3eaa152..67a155a 100644 --- a/Volatility/Resources/Texture/TextureBase.cs +++ b/Volatility/Resources/Texture/TextureBase.cs @@ -6,12 +6,11 @@ // systematically throughout the game. // Learn More: -// https://burnout.wiki/wiki/Texture +// https://burnout.wiki/wiki/Texture/Burnout_Paradise +[ResourceDefinition(ResourceType.Texture)] public abstract class TextureBase : Resource { - public override ResourceType ResourceType => ResourceType.Texture; - [EditorCategory("Texture"), EditorLabel("Width"), EditorTooltip("The target width of the texture.")] public ushort Width { get; set; } @@ -94,9 +93,9 @@ public override void PushAll() PushInternalFlags(); } - public TextureBase() : base() => Depth = 1; + protected TextureBase() : base() => Depth = 1; - public TextureBase(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } + protected TextureBase(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } } // BPR formatted but converted for each platform public enum DIMENSION : int @@ -121,4 +120,4 @@ public enum TextureBaseUsageFlags PropTexture = 3, // GlobalProps AnyTexture = WorldTexture | GRTexture | PropTexture -} \ No newline at end of file +} diff --git a/Volatility/Resources/Texture/TexturePC.cs b/Volatility/Resources/Texture/TexturePC.cs index c9df887..0a193fc 100644 --- a/Volatility/Resources/Texture/TexturePC.cs +++ b/Volatility/Resources/Texture/TexturePC.cs @@ -2,6 +2,7 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.TUB, PullAll = true)] public class TexturePC : TextureBase { public override Endian ResourceEndian => Endian.LE; diff --git a/Volatility/Resources/Texture/TexturePS3.cs b/Volatility/Resources/Texture/TexturePS3.cs index 80c65f2..2ed42b6 100644 --- a/Volatility/Resources/Texture/TexturePS3.cs +++ b/Volatility/Resources/Texture/TexturePS3.cs @@ -2,6 +2,7 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.PS3, PullAll = true)] public class TexturePS3 : TextureBase { public override Endian ResourceEndian => Endian.BE; @@ -178,4 +179,4 @@ public enum StoreType : int TYPE_3D = 3, TYPE_CUBE = 0x10002, TYPE_FORCEENUMSIZEINT = 0x7FFFFFFF -} \ No newline at end of file +} diff --git a/Volatility/Resources/Texture/TextureX360.cs b/Volatility/Resources/Texture/TextureX360.cs index b11bb11..7eae86b 100644 --- a/Volatility/Resources/Texture/TextureX360.cs +++ b/Volatility/Resources/Texture/TextureX360.cs @@ -8,6 +8,7 @@ namespace Volatility.Resources; +[ResourceRegistration(RegistrationPlatforms.X360, PullAll = true)] public class TextureX360 : TextureBase { public override Endian ResourceEndian => Endian.BE; @@ -155,8 +156,7 @@ public override void WriteToStream(ResourceBinaryWriter writer, Endian endiannes writer.Write(Format.PackToBytes()); // Padding that's usually just garbage data. - writer.Write(Encoding.UTF8.GetBytes("Volatility")); - writer.Write(new byte[0x2]); + writer.WriteFixedBytes(Encoding.ASCII.GetBytes("Volatility"), 12); } public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) diff --git a/Volatility/Utilities/DxcShaderCompiler.cs b/Volatility/Utilities/DxcShaderCompiler.cs index ab232c2..8ed7fb2 100644 --- a/Volatility/Utilities/DxcShaderCompiler.cs +++ b/Volatility/Utilities/DxcShaderCompiler.cs @@ -1,7 +1,5 @@ using System.Diagnostics; using System.Runtime.InteropServices; -using System.Text; - using Volatility.Resources; using static Volatility.Utilities.EnvironmentUtilities; @@ -46,30 +44,7 @@ public static void CompileToCSO(ShaderBase shader, ShaderStageCompile stage, str string sourcePath = ResolveSourcePath(shader); ProcessStartInfo startInfo = BuildStartInfo(dxcPath, sourcePath, shader, stage, entryPoint, targetProfile, outputPath); - using Process process = new() { StartInfo = startInfo }; - - StringBuilder output = new(); - process.OutputDataReceived += (_, e) => - { - if (!string.IsNullOrWhiteSpace(e.Data)) - output.AppendLine(e.Data); - }; - process.ErrorDataReceived += (_, e) => - { - if (!string.IsNullOrWhiteSpace(e.Data)) - output.AppendLine(e.Data); - }; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - - if (process.ExitCode != 0) - { - string message = output.Length > 0 ? output.ToString() : "No compiler output."; - throw new InvalidOperationException($"dxc failed (exit {process.ExitCode}).\n{message}"); - } + ProcessUtilities.RunAndCapture(startInfo); } private static ProcessStartInfo BuildStartInfo( diff --git a/Volatility/Utilities/MatrixUtilities.cs b/Volatility/Utilities/MatrixUtilities.cs index a69907e..76b543b 100644 --- a/Volatility/Utilities/MatrixUtilities.cs +++ b/Volatility/Utilities/MatrixUtilities.cs @@ -36,6 +36,33 @@ public static Transform Matrix44AffineToTransform(Matrix44Affine matrix) return transform; } + public static Matrix44Affine TransformToMatrix44Affine(Transform transform) + { + Quaternion rotation = Quaternion.Normalize( + transform.Rotation == default ? Quaternion.Identity : transform.Rotation); + + Matrix44Affine matrix = Matrix4x4.CreateFromQuaternion(rotation); + + matrix.M11 *= transform.Scale.X; + matrix.M12 *= transform.Scale.X; + matrix.M13 *= transform.Scale.X; + + matrix.M21 *= transform.Scale.Y; + matrix.M22 *= transform.Scale.Y; + matrix.M23 *= transform.Scale.Y; + + matrix.M31 *= transform.Scale.Z; + matrix.M32 *= transform.Scale.Z; + matrix.M33 *= transform.Scale.Z; + + matrix.M41 = transform.Location.X; + matrix.M42 = transform.Location.Y; + matrix.M43 = transform.Location.Z; + matrix.M44 = 1.0f; + + return matrix; + } + public static Matrix44 ReadMatrix44(BinaryReader reader) { float m11 = reader.ReadSingle(); diff --git a/Volatility/Utilities/PS3TextureUtilities.cs b/Volatility/Utilities/PS3TextureUtilities.cs index 0f225c4..b7db1fb 100644 --- a/Volatility/Utilities/PS3TextureUtilities.cs +++ b/Volatility/Utilities/PS3TextureUtilities.cs @@ -165,38 +165,12 @@ public static void PS3GTFToDDS(byte[] ps3Header, string sourceBitmapPath, string throw new FileNotFoundException("Unable to find external tool gtf2dds.exe!"); } - ProcessStartInfo start = new ProcessStartInfo - { - FileName = gtf2ddsPath, - Arguments = $"-o \"{destinationBitmapPath}.dds\" \"{destinationBitmapPath}.gtf\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - if (verbose) Console.WriteLine($"Running: {gtf2ddsPath} -o \"{destinationBitmapPath}.dds\" \"{destinationBitmapPath}.gtf\""); + if (verbose) Console.WriteLine("Converting PS3 GTF texture to DDS..."); - using (Process process = new Process()) - { - if (verbose) Console.WriteLine("Converting PS3 GTF texture to DDS..."); - - process.StartInfo = start; - process.OutputDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.ErrorDataReceived += (sender, e) => - { - if (!string.IsNullOrEmpty(e.Data)) Console.WriteLine(e.Data); - }; - - process.Start(); - process.BeginOutputReadLine(); - process.BeginErrorReadLine(); - process.WaitForExit(); - } + ProcessUtilities.RunAndRelayOutput( + gtf2ddsPath, + $"-o \"{destinationBitmapPath}.dds\" \"{destinationBitmapPath}.gtf\""); fileBytes = File.ReadAllBytes($"{destinationBitmapPath}.dds"); diff --git a/Volatility/Utilities/ProcessUtilities.cs b/Volatility/Utilities/ProcessUtilities.cs new file mode 100644 index 0000000..824d3ea --- /dev/null +++ b/Volatility/Utilities/ProcessUtilities.cs @@ -0,0 +1,114 @@ +using System.Diagnostics; +using System.Text; + +namespace Volatility.Utilities; + +internal static class ProcessUtilities +{ + public static string RunAndCapture(string fileName, string arguments, string? workingDirectory = null) + { + ProcessStartInfo startInfo = CreateStartInfo(fileName, arguments, workingDirectory); + return RunAndCapture(startInfo); + } + + public static string RunAndCapture(ProcessStartInfo startInfo) + { + using Process process = new() { StartInfo = startInfo }; + StringBuilder output = new(); + + process.Start(); + output.Append(process.StandardOutput.ReadToEnd()); + output.Append(process.StandardError.ReadToEnd()); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException( + $"Process '{GetProcessDisplayName(startInfo)}' failed with exit code {process.ExitCode}.{Environment.NewLine}{output}"); + } + + return output.ToString(); + } + + public static void RunAndRelayOutput( + string fileName, + string arguments, + string? workingDirectory = null, + Action? stdoutHandler = null, + Action? stderrHandler = null) + { + ProcessStartInfo startInfo = CreateStartInfo(fileName, arguments, workingDirectory); + RunAndRelayOutput(startInfo, stdoutHandler, stderrHandler); + } + + public static void RunAndRelayOutput( + ProcessStartInfo startInfo, + Action? stdoutHandler = null, + Action? stderrHandler = null) + { + using Process process = new() { StartInfo = startInfo }; + StringBuilder output = new(); + + process.OutputDataReceived += (_, e) => + { + if (string.IsNullOrEmpty(e.Data)) + { + return; + } + + output.AppendLine(e.Data); + (stdoutHandler ?? Console.WriteLine)(e.Data); + }; + + process.ErrorDataReceived += (_, e) => + { + if (string.IsNullOrEmpty(e.Data)) + { + return; + } + + output.AppendLine(e.Data); + (stderrHandler ?? Console.WriteLine)(e.Data); + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + + if (process.ExitCode != 0) + { + throw new InvalidOperationException( + $"Process '{GetProcessDisplayName(startInfo)}' failed with exit code {process.ExitCode}.{Environment.NewLine}{output}"); + } + } + + private static ProcessStartInfo CreateStartInfo(string fileName, string arguments, string? workingDirectory) + { + return new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? Directory.GetCurrentDirectory() : workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + } + + private static string GetProcessDisplayName(ProcessStartInfo startInfo) + { + if (!string.IsNullOrWhiteSpace(startInfo.Arguments)) + { + return $"{startInfo.FileName} {startInfo.Arguments}"; + } + + if (startInfo.ArgumentList.Count > 0) + { + return $"{startInfo.FileName} {string.Join(' ', startInfo.ArgumentList)}"; + } + + return startInfo.FileName; + } +} diff --git a/Volatility/Utilities/ResourceBinaryReader.cs b/Volatility/Utilities/ResourceBinaryReader.cs index 10257a1..c663ce0 100644 --- a/Volatility/Utilities/ResourceBinaryReader.cs +++ b/Volatility/Utilities/ResourceBinaryReader.cs @@ -76,6 +76,13 @@ public void ParseSection(ulong offset, int count, Func ParseSection(ulong offset, int count, Func parser) + { + List destination = new(Math.Max(count, 0)); + ParseSection(offset, count, parser, destination); + return destination; + } + public void ParseSection(long offset, int count, Func parser, List destination) { if (count <= 0 || offset == 0) @@ -94,6 +101,13 @@ public void ParseSection(long offset, int count, Func ParseSection(long offset, int count, Func parser) + { + List destination = new(Math.Max(count, 0)); + ParseSection(offset, count, parser, destination); + return destination; + } + public void ParseSection(ulong offset, Func parser, out T destination) { if (offset == 0) @@ -157,4 +171,4 @@ public uint ReadArchDependUInt(Arch arch) return value; } -} \ No newline at end of file +} diff --git a/Volatility/Utilities/ResourceBinaryWriter.cs b/Volatility/Utilities/ResourceBinaryWriter.cs index 3e6a368..e75263c 100644 --- a/Volatility/Utilities/ResourceBinaryWriter.cs +++ b/Volatility/Utilities/ResourceBinaryWriter.cs @@ -80,6 +80,20 @@ public void WriteSection(long offset, List data, Action(long offset, T[] data, Action writeItem) + { + if (offset == 0 || data.Length == 0) + { + return; + } + + BaseStream.Position = offset; + for (int i = 0; i < data.Length; i++) + { + writeItem(this, data[i]); + } + } + public void WriteSection(long offset, List data, Action writeItem) { if (offset == 0 || data.Count == 0) @@ -94,6 +108,20 @@ public void WriteSection(long offset, List data, Action(long offset, T[] data, Action writeItem) + { + if (offset == 0 || data.Length == 0) + { + return; + } + + BaseStream.Position = offset; + for (int i = 0; i < data.Length; i++) + { + writeItem(this, data[i], i); + } + } + public void WriteSection(ulong offset, T data, Action writeItem) { if (offset == 0) @@ -130,6 +158,20 @@ public void WriteSection(ulong offset, List data, Action(ulong offset, T[] data, Action writeItem) + { + if (offset == 0 || data.Length == 0) + { + return; + } + + BaseStream.Position = (long)offset; + for (int i = 0; i < data.Length; i++) + { + writeItem(this, data[i]); + } + } + public void WriteSection(ulong offset, List data, Action writeItem) { if (offset == 0 || data.Count == 0) @@ -144,6 +186,20 @@ public void WriteSection(ulong offset, List data, Action(ulong offset, T[] data, Action writeItem) + { + if (offset == 0 || data.Length == 0) + { + return; + } + + BaseStream.Position = (long)offset; + for (int i = 0; i < data.Length; i++) + { + writeItem(this, data[i], i); + } + } + public void WriteFixedBytes(byte[]? data, int count) { byte[] output = new byte[count]; diff --git a/Volatility/Utilities/ResourceUtilities.cs b/Volatility/Utilities/ResourceUtilities.cs index 3c9e624..7a0f8cc 100644 --- a/Volatility/Utilities/ResourceUtilities.cs +++ b/Volatility/Utilities/ResourceUtilities.cs @@ -1,7 +1,16 @@ -namespace Volatility.Utilities; +using System.Text; + +using Volatility.Resources; + +namespace Volatility.Utilities; public class ResourceUtilities { + public static int GetPointerSize(Arch arch) + { + return arch == Arch.x64 ? sizeof(ulong) : sizeof(uint); + } + public static List GetFixedSizeList(List source, int size) { List output = new(size); @@ -30,4 +39,50 @@ public static ulong GetSectionOffset(ref long currentOffset, int count, int elem currentOffset += (long)count * elementSize; return offset; } + + public static long GetSectionOffset(ref long currentOffset, int length, int sectionAlignment) + { + if (length <= 0) + { + return 0; + } + + currentOffset = AlignOffset(currentOffset, sectionAlignment); + long offset = currentOffset; + currentOffset += length; + return offset; + } + + public static string ReadFixedString(BinaryReader reader, int length) + { + byte[] bytes = reader.ReadBytes(length); + int nullTerminator = Array.IndexOf(bytes, (byte)0); + int outputLength = nullTerminator >= 0 ? nullTerminator : bytes.Length; + return Encoding.ASCII.GetString(bytes, 0, outputLength); + } + + public static void WriteFixedString(BinaryWriter writer, string? value, int length) + { + byte[] bytes = Encoding.ASCII.GetBytes(value ?? string.Empty); + if (bytes.Length >= length) + { + writer.Write(bytes, 0, length); + return; + } + + writer.Write(bytes); + writer.Write(new byte[length - bytes.Length]); + } + + public static ResourceID ResolveResourceID(ResourceImport resourceImport) + { + if (resourceImport.ReferenceID != ResourceID.Default) + { + return resourceImport.ReferenceID; + } + + return string.IsNullOrWhiteSpace(resourceImport.Name) + ? ResourceID.Default + : ResourceID.HashFromString(resourceImport.Name); + } } diff --git a/Volatility/Utilities/WorkspaceUtilities.cs b/Volatility/Utilities/WorkspaceUtilities.cs new file mode 100644 index 0000000..cc12d46 --- /dev/null +++ b/Volatility/Utilities/WorkspaceUtilities.cs @@ -0,0 +1,29 @@ +namespace Volatility.Utilities; + +internal static class WorkspaceUtilities +{ + public static string FindRepositoryRoot() + { + foreach (string startPath in GetCandidateStartPaths()) + { + string? current = Path.GetFullPath(startPath); + while (!string.IsNullOrEmpty(current)) + { + if (File.Exists(Path.Combine(current, "Volatility.sln"))) + { + return current; + } + + current = Directory.GetParent(current)?.FullName; + } + } + + throw new DirectoryNotFoundException("Unable to locate the repository root containing Volatility.sln."); + } + + private static IEnumerable GetCandidateStartPaths() + { + yield return Directory.GetCurrentDirectory(); + yield return EnvironmentUtilities.GetExecutableDirectory(); + } +} diff --git a/Volatility/Utilities/YAML/BitArrayYamlTypeConverter.cs b/Volatility/Utilities/YAML/BitArrayYamlTypeConverter.cs new file mode 100644 index 0000000..2ce4f3e --- /dev/null +++ b/Volatility/Utilities/YAML/BitArrayYamlTypeConverter.cs @@ -0,0 +1,49 @@ +using System.Collections; +using System.Text; + +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Volatility.Utilities; + +public class BitArrayYamlTypeConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) => type == typeof(BitArray); + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer nestedObjectDeserializer) + { + string bits = parser.Consume().Value ?? string.Empty; + BitArray bitArray = new(bits.Length); + + for (int i = 0; i < bits.Length; i++) + { + bitArray[i] = bits[i] switch + { + '0' => false, + '1' => true, + _ => throw new YamlException($"Invalid BitArray value '{bits[i]}'. Expected only '0' or '1'."), + }; + } + + return bitArray; + } + + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer nestedObjectSerializer) + { + if (value is not BitArray bitArray) + { + emitter.Emit(new Scalar(string.Empty)); + return; + } + + StringBuilder bits = new(bitArray.Length); + + foreach (bool bit in bitArray) + { + bits.Append(bit ? '1' : '0'); + } + + emitter.Emit(new Scalar(bits.ToString())); + } +} diff --git a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs index 8e7a59d..0e23040 100644 --- a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs +++ b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs @@ -43,6 +43,7 @@ public static object DeserializeResource(Type resourceClass, string yaml) var finalDeserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() + .WithTypeConverter(new BitArrayYamlTypeConverter()) .WithTypeConverter(new StrongIDYamlTypeConverter()) .Build(); using (var reader = new StringReader(mergedYaml)) @@ -102,4 +103,4 @@ private static Dictionary RemovePropertiesKeys(Dictionary !kvp.Key.EndsWith(".Properties")) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } -} \ No newline at end of file +} diff --git a/tools/libbndl-extractor b/tools/libbndl-extractor new file mode 160000 index 0000000..7f8e9d3 --- /dev/null +++ b/tools/libbndl-extractor @@ -0,0 +1 @@ +Subproject commit 7f8e9d302a6a4878c73b77b15c3fedd786047dd4