diff --git a/Volatility/CLI/Commands/AutotestCommand.cs b/Volatility/CLI/Commands/AutotestCommand.cs index f6a8d78..a8888a9 100644 --- a/Volatility/CLI/Commands/AutotestCommand.cs +++ b/Volatility/CLI/Commands/AutotestCommand.cs @@ -48,7 +48,7 @@ public async Task Execute() TexturePC textureHeaderPC = new TexturePC { AssetName = "autotest_header_PC", - ResourceID = GetResourceIDFromName("autotest_header_PC"), + ResourceID = ResourceID.HashFromString("autotest_header_PC"), Format = D3DFORMAT.D3DFMT_DXT1, Width = 1024, Height = 512, @@ -62,7 +62,7 @@ public async Task Execute() TextureBPR textureHeaderBPR = new TextureBPR { AssetName = "autotest_header_BPR", - ResourceID = GetResourceIDFromName("autotest_header_BPR"), + ResourceID = ResourceID.HashFromString("autotest_header_BPR"), Format = DXGI_FORMAT.DXGI_FORMAT_BC1_UNORM, Width = 1024, Height = 512, @@ -77,7 +77,7 @@ public async Task Execute() textureHeaderBPR.SetResourceArch(Arch.x64); textureHeaderBPR.AssetName = "autotest_header_BPRx64"; - textureHeaderBPR.ResourceID = GetResourceIDFromName(textureHeaderBPR.AssetName); + textureHeaderBPR.ResourceID = ResourceID.HashFromString(textureHeaderBPR.AssetName); // Write 64 bit test BPR header TestHeaderRW("autotest_header_BPRx64.dat", textureHeaderBPR); @@ -86,7 +86,7 @@ public async Task Execute() TexturePS3 textureHeaderPS3 = new TexturePS3 { AssetName = "autotest_header_PS3", - ResourceID = GetResourceIDFromName("autotest_header_PS3"), + ResourceID = ResourceID.HashFromString("autotest_header_PS3"), Format = CELL_GCM_COLOR_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45, Width = 1024, Height = 512, @@ -100,7 +100,7 @@ public async Task Execute() TextureX360 textureHeaderX360 = new TextureX360 { AssetName = "autotest_header_X360", - ResourceID = GetResourceIDFromName("autotest_header_X360"), + ResourceID = ResourceID.HashFromString("autotest_header_X360"), Format = new GPUTEXTURE_FETCH_CONSTANT { Tiled = true, diff --git a/Volatility/CLI/Commands/ExportResourceCommand.cs b/Volatility/CLI/Commands/ExportResourceCommand.cs index a4708dd..a535bbe 100644 --- a/Volatility/CLI/Commands/ExportResourceCommand.cs +++ b/Volatility/CLI/Commands/ExportResourceCommand.cs @@ -116,10 +116,6 @@ public async Task Execute() case TextureBase texture: texture.PushAll(); goto default; - case Splicer splicer: - splicer.WriteToStream(writer); - splicer.SpliceSamples(writer, Path.GetDirectoryName(sourceFile)); - break; default: resource.WriteToStream(writer); break; diff --git a/Volatility/CLI/Commands/ImportResourceCommand.cs b/Volatility/CLI/Commands/ImportResourceCommand.cs index 58a04be..0a47620 100644 --- a/Volatility/CLI/Commands/ImportResourceCommand.cs +++ b/Volatility/CLI/Commands/ImportResourceCommand.cs @@ -1,8 +1,9 @@ +using System.Text; using System.Diagnostics; +using System.Security.Cryptography; using System.Text.RegularExpressions; using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; using Volatility.Resources; using Volatility.Utilities; @@ -73,7 +74,7 @@ public async Task Execute() .DisableAliases() .WithTypeInspector(inner => new IncludeFieldsTypeInspector(inner)) .WithTypeConverter(new ResourceYamlTypeConverter()) - .WithTypeConverter(new ResourceIDYamlTypeConverter()) + .WithTypeConverter(new StrongIDYamlTypeConverter()) .WithTypeConverter(new StringEnumYamlTypeConverter()) .Build(); @@ -157,62 +158,75 @@ public async Task Execute() Splicer? splicer = resource as Splicer; - byte[][]? samples = splicer?.GetLoadedSamples(); - - for (int i = 0; i < samples?.Length; i++) - { - string sampleName = $"{resource.AssetName}_{i}"; - - string sampleDirectory = Path.Combine(directoryPath, $"{resource.AssetName}_Samples"); + List? samples = splicer?.GetLoadedSamples(); + for (int i = 0; i < samples?.Count; i++) + { + string sampleDirectory = Path.Combine("data", "Splicer", "Samples"); Directory.CreateDirectory(sampleDirectory); - Console.WriteLine($"Writing extracted sample {sampleName}.snr"); - await File.WriteAllBytesAsync(Path.Combine(sampleDirectory, $"{sampleName}.snr"), samples[i]); + string sampleName = $"{samples[i].SampleID}"; - if (sxExists) + string samplePathName = Path.Combine(sampleDirectory, sampleName); + + if (!File.Exists($"{samplePathName}.snr") || Overwrite) + { + Console.WriteLine($"Writing extracted sample {sampleName}.snr"); + await File.WriteAllBytesAsync($"{samplePathName}.snr", samples[i].Data); + } + else { - string samplePathName = Path.Combine(sampleDirectory, sampleName); + Console.WriteLine($"Skipping extracted sample {sampleName}.snr"); + } + if (sxExists) + { string convertedSamplePathName = Path.Combine(sampleDirectory, "_extracted"); Directory.CreateDirectory(convertedSamplePathName); - convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName); - - ProcessStartInfo start = new ProcessStartInfo - { - FileName = sxPath, - Arguments = $"-wave -s16l_int -v0 \"{samplePathName}.snr\" -=\"{convertedSamplePathName}.wav\"", - 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(); - } - } - } - } - Console.WriteLine($"Imported {Path.GetFileName(ImportPath)} as {Path.GetFullPath(filePath)}."); + convertedSamplePathName = Path.Combine(convertedSamplePathName, sampleName + ".wav"); + + 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(); + } + } + else + { + Console.WriteLine($"Converted sample {Path.GetFileName(convertedSamplePathName)} already exists, skipping..."); + } + } + } + } + Console.WriteLine($"Imported {Path.GetFileName(sourceFile)} as {Path.GetFullPath(filePath)}."); })); } await Task.WhenAll(tasks); diff --git a/Volatility/Resources/Resource.cs b/Volatility/Resources/Resource.cs index c64af1b..9c907fe 100644 --- a/Volatility/Resources/Resource.cs +++ b/Volatility/Resources/Resource.cs @@ -94,7 +94,7 @@ public Resource(string path, Endian endianness = Endian.Agnostic) else { // TODO: Add new entry to ResourceDB - ResourceID = Convert.ToUInt64(GetResourceIDFromName(name, importEndianness), 16); + ResourceID = Convert.ToUInt64(ResourceID.FromIDString(name)); AssetName = name; } diff --git a/Volatility/Resources/Splicer/Splicer.cs b/Volatility/Resources/Splicer/Splicer.cs index 91864b1..b48a8eb 100644 --- a/Volatility/Resources/Splicer/Splicer.cs +++ b/Volatility/Resources/Splicer/Splicer.cs @@ -1,5 +1,6 @@ using System.Runtime.InteropServices; -using System.Text; + +using Volatility.Utilities; namespace Volatility.Resources; @@ -14,23 +15,11 @@ public class Splicer : BinaryResource { public override ResourceType GetResourceType() => ResourceType.Splicer; - public SPLICE_Data[] Splices; - public SPLICE_SampleRef[] SampleRefs; - public IntPtr[] SamplePtrs; - - // This only gets populated when parsing from a stream. - // Not sure whether this is a good idea to keep as-is. - private byte[][] _samples; - - // Used to make reading parsed files easier. - // May remove or keep as generated values - // to make reading & editing easier. - [EditorReadOnly] - public IntPtr SampleRefsPtrOffset; - - [EditorReadOnly] - public IntPtr SamplePtrOffset; + public List Splices = new(); + // Only gets populated when parsing from a stream, or when + // loading referenced sample IDs through LoadDependentSamples + private List _samples = new(); public override void ParseFromStream(ResourceBinaryReader reader, Endian endianness = Endian.Agnostic) { @@ -40,7 +29,6 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann if (version != 1) { throw new InvalidDataException("Version mismatch! Version should be 1."); - return; } int pSampleRefTOC = reader.ReadInt32(); @@ -49,184 +37,229 @@ public override void ParseFromStream(ResourceBinaryReader reader, Endian endiann if (numSplices <= 0) { throw new InvalidDataException("No splices in Splicer file!"); - return; } - int lowestSampleIndex = new int(); + var spliceRefCounts = new List(numSplices); + + if (Splices == null) + Splices = new List(numSplices); + // Read Splice Data - Splices = new SPLICE_Data[numSplices]; for (int i = 0; i < numSplices; i++) { - // NameHash (null) - _ = reader.ReadInt32(); - - Splices[i] = new SPLICE_Data() + 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 SPLICE_Data { - SpliceIndex = reader.ReadUInt16(), - ESpliceType = reader.ReadSByte(), - Num_SampleRefs = reader.ReadByte(), - Volume = reader.ReadSingle(), - RND_Pitch = reader.ReadSingle(), - RND_Vol = reader.ReadSingle(), - }; - - // pSampleRefList (null) - _ = reader.ReadInt32(); - - Splices[i].SampleListIndex = lowestSampleIndex; - lowestSampleIndex += Splices[i].Num_SampleRefs; + NameHash = nameHash, + SpliceIndex = spliceIdx, + ESpliceType = eType, + Volume = vol, + RND_Pitch = rpitch, + RND_Vol = rvol, + SampleRefs = new List(numRefs) + }); } - int numSampleRefs = Splices[numSplices - 1].SampleListIndex + Splices[numSplices - 1].Num_SampleRefs; - - SampleRefsPtrOffset = (nint)(reader.BaseStream.Position - DataOffset); + int numSampleRefs = spliceRefCounts.Sum(); - // Read SampleRefs - SampleRefs = new SPLICE_SampleRef[numSampleRefs]; - for (int i = 0; i < numSampleRefs; i++) - { - SampleRefs[i] = new SPLICE_SampleRef() - { - SampleIndex = reader.ReadUInt16(), - 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(), - }; - } + long _sampleRefsPtrOffset = reader.BaseStream.Position - DataOffset; reader.BaseStream.Seek(pSampleRefTOC + 0xC + DataOffset, SeekOrigin.Begin); int numSamples = reader.ReadInt32(); - SamplePtrs = new IntPtr[numSamples]; + List _samplePtrs = new List(numSamples); for (int i = 0; i < numSamples; i++) { - SamplePtrs[i] = reader.ReadInt32(); + _samplePtrs.Add(reader.ReadInt32()); } - SamplePtrOffset = (nint)(reader.BaseStream.Position - DataOffset); + long _samplePtrOffset = reader.BaseStream.Position - DataOffset; + + if (_samples == null) + _samples = new List(numSamples); - _samples = new byte[numSamples][]; 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]); + 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); - _samples[i] = reader.ReadBytes(length); + _samples.Add + ( + new Sample + { + SampleID = SnrID.HashFromBytes(data), + Data = data, + } + ); + + Console.WriteLine($"Adding sample {i} as {_samples[i].SampleID}"); + + data = null; + } + + reader.BaseStream.Seek(_sampleRefsPtrOffset + DataOffset, SeekOrigin.Begin); + + // Read SampleRefs + for (int i = 0; i < Splices.Count; i++) + { + int count = spliceRefCounts[i]; + var list = Splices[i].SampleRefs; + + for (int j = 0; j < count; j++) + { + ushort sampleIdx = reader.ReadUInt16(); + var sr = new SPLICE_SampleRef + { + 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); + } } } public override void WriteToStream(EndianAwareBinaryWriter writer, Endian endianness = Endian.Agnostic) { + LoadDependentSamples(); + base.WriteToStream(writer, endianness); writer.BaseStream.Position = DataOffset; - writer.Write((int)1); // version + writer.Write(1); // version + + 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; - int sampleRefTOCPosition = (int)writer.BaseStream.Position; // Saving this for later + writer.Write(sizedata); // sizedata/pSampleRefTOC - writer.Write((int)0); // pSampleRefTOC + writer.Write(Splices.Count); // NumSplices - writer.Write((int)Splices.Length); // NumSplices + var spliceStartIndices = new List(Splices.Count); + int runningIndex = 0; - // Write Splices - for (int i = 0; i < Splices.Length; i++) + foreach (SPLICE_Data splice in Splices) { - // NameHash, unused by game - writer.Write(Encoding.Default.GetBytes("sper")); - - writer.Write(Splices[i].SpliceIndex); - writer.Write(Splices[i].ESpliceType); - writer.Write(Splices[i].Num_SampleRefs); - writer.Write(Splices[i].Volume); - writer.Write(Splices[i].RND_Pitch); - writer.Write(Splices[i].RND_Vol); - - // pSampleRefList, filled at runtime - writer.Write(Encoding.Default.GetBytes("dunk")); + 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 } - // Write SampleRefs - for (int i = 0; i < SampleRefs.Length; i++) + long sampleRefsStart = writer.BaseStream.Position; + foreach (var splice in Splices) { - writer.Write(SampleRefs[i].SampleIndex); - writer.Write(SampleRefs[i].ESpliceType); - writer.Write(SampleRefs[i].Padding); - writer.Write(SampleRefs[i].Volume); - writer.Write(SampleRefs[i].Pitch); - writer.Write(SampleRefs[i].Offset); - writer.Write(SampleRefs[i].Az); - writer.Write(SampleRefs[i].Duration); - writer.Write(SampleRefs[i].FadeIn); - writer.Write(SampleRefs[i].FadeOut); - writer.Write(SampleRefs[i].RND_Vol); - writer.Write(SampleRefs[i].RND_Pitch); - writer.Write(SampleRefs[i].Priority); - writer.Write(SampleRefs[i].ERollOffType); - writer.Write(SampleRefs[i].Padding2); + foreach (var sr 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); + } } - int sampleRefTOC = ((int)writer.BaseStream.Position) - (int)DataOffset; // Saving this for later - - writer.Seek(sampleRefTOCPosition, SeekOrigin.Begin); - - writer.Write(sampleRefTOC); + writer.BaseStream.Position = DataOffset + 0xC + sizedata; // Header + sizedata + int numSamples = _samples.Count; + int pSampleRefTOC = (int)(writer.BaseStream.Position - DataOffset); - writer.Seek(sampleRefTOCPosition + (int)DataOffset, SeekOrigin.Begin); - } - - public void SpliceSamples(EndianAwareBinaryWriter writer, string samplesDir) - { - // Enumerate then write Samples - string samplesDirectory = Path.Combine(Path.GetDirectoryName(samplesDir), $"{AssetName}_Samples"); + writer.Write(numSamples); - string[] paths = Directory.GetFiles(samplesDirectory, "*.snr"); - byte[][] samples = Array.Empty(); - int[] lengths = Array.Empty(); + // Reserve space for offsets + long offsetsStart = writer.BaseStream.Position; + for (int i = 0; i < numSamples; i++) writer.Write(0); - int lowestIndex = 0; - - for (int i = 0; i < paths.Length; i++) + int running = 0; + for (int i = 0; i < numSamples; i++) { - samples[i] = File.ReadAllBytes(paths[i]); - lengths[i] = samples[i].Length; + 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; + } - // Write SamplePtrs - writer.Write(lowestIndex); + // Update DataSize + DataSize = (uint)(writer.BaseStream.Length - DataOffset); + long pos = writer.BaseStream.Position; + writer.BaseStream.Seek(0, SeekOrigin.Begin); + writer.Write(DataSize); + writer.BaseStream.Seek(pos, SeekOrigin.Begin); + } - lowestIndex += samples[i].Length; - } + public void LoadDependentSamples(bool recurse = false) + { + var needed = Splices + .SelectMany(s => s.SampleRefs.Select(sr => sr.Sample)) + .Distinct() + .ToList(); + + string dir = Path.Combine(AppContext.BaseDirectory, "data", "Splicer", "Samples"); + var files = Directory.GetFiles(dir, "*.snr", recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); - for (int i = 0; i < samples.Length; i++) + var map = new Dictionary(needed.Count); + foreach (var f in files) { - writer.Write(samples[i]); + var data = File.ReadAllBytes(f); + var id = SnrID.HashFromBytes(data); + if (!map.ContainsKey(id) && needed.Contains(id)) + map[id] = data; } - long tempOffset = writer.BaseStream.Position; - - // Handle the BinaryResource data size - // Not exactly a fan of how this is hardcoded. - DataSize = (uint)(writer.BaseStream.Length - 0x10); - writer.BaseStream.Seek(0, SeekOrigin.Begin); - writer.Write(DataSize); - - writer.BaseStream.Seek(tempOffset, SeekOrigin.Begin); + foreach (var id in needed) + if (!map.ContainsKey(id)) + throw new FileNotFoundException($"Missing sample for {id}"); + _samples = needed.Select(id => new Sample { SampleID = id, Data = map[id] }).ToList(); } - public byte[][] GetLoadedSamples() + public List GetLoadedSamples() { return _samples; } @@ -236,22 +269,21 @@ public Splicer() : base() { } public Splicer(string path, Endian endianness = Endian.Agnostic) : base(path, endianness) { } [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct SPLICE_Data + public class SPLICE_Data { public uint NameHash; public ushort SpliceIndex; public sbyte ESpliceType; - public byte Num_SampleRefs; public float Volume; public float RND_Pitch; public float RND_Vol; - public int SampleListIndex; + public List SampleRefs { get; set; } } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct SPLICE_SampleRef { - public ushort SampleIndex; + public SnrID Sample; public sbyte ESpliceType; public byte Padding; public float Volume; @@ -267,4 +299,10 @@ public struct SPLICE_SampleRef public byte ERollOffType; public ushort Padding2; } + + public struct Sample + { + public SnrID SampleID; + public byte[] Data; + } } \ No newline at end of file diff --git a/Volatility/StrongID.cs b/Volatility/StrongID.cs new file mode 100644 index 0000000..0edc06a --- /dev/null +++ b/Volatility/StrongID.cs @@ -0,0 +1,102 @@ + +global using ResourceID = StrongID; +global using SnrID = StrongID; + +using System.Text; + +public interface IStrongID +{ + ulong Value { get; } +} +public interface IIDTag +{ + static abstract ulong Hash(string text); + static abstract ulong Hash(ReadOnlySpan data); +} + +public readonly record struct StrongID(ulong Value) where TTag : IIDTag +{ + public static StrongID Default => new(0UL); + public static implicit operator ulong(StrongID id) => id.Value; + public static implicit operator StrongID(ulong v) => new(v); + public static implicit operator StrongID(string s) => HashFromString(s); + public static StrongID HashFromString(string s) => new(TTag.Hash(s)); // For creating IDs from names + public static StrongID HashFromBytes(byte[] b) => new(TTag.Hash(b)); // For creating IDs from data (SnrID) + public static StrongID FromIDString(string s) => new(Convert.ToUInt64(s)); // For strings that are already IDs + public override string ToString() => Value.ToString("X8"); +} + +public readonly struct ResourceTag : IIDTag +{ + public static ulong Hash(string text) => Crc32.Compute(text.ToLower(System.Globalization.CultureInfo.CurrentCulture)); + public static ulong Hash(ReadOnlySpan data) => throw new NotSupportedException("Generating ResourceIDs from direct bytes is not supported. Please convert your input to a string and use the Hash(string text) overload."); +} + +public readonly struct SnrTag : IIDTag +{ + public static ulong Hash(string text) => Crc64.Compute(text.ToLower(System.Globalization.CultureInfo.CurrentCulture)); + public static ulong Hash(ReadOnlySpan data) + { + ReadOnlySpan prefix = "Volatility_"u8; + Span buf = stackalloc byte[prefix.Length + data.Length]; + prefix.CopyTo(buf); + data.CopyTo(buf[prefix.Length..]); + return Crc64.Compute(buf); + } +} + +public static class Crc32 +{ + private static readonly uint[] Table = BuildTable(); + public static uint Compute(string s) + { + var bytes = Encoding.UTF8.GetBytes(s); + uint crc = 0xFFFFFFFFu; + foreach (var b in bytes) + crc = (crc >> 8) ^ Table[(crc ^ b) & 0xFF]; + return ~crc; + } + private static uint[] BuildTable() + { + const uint poly = 0xEDB88320u; + var table = new uint[256]; + for (uint i = 0; i < 256; i++) + { + uint c = i; + for (int j = 0; j < 8; j++) + c = (c & 1) != 0 ? (poly ^ (c >> 1)) : (c >> 1); + table[i] = c; + } + return table; + } +} + +public static class Crc64 +{ + private static readonly ulong[] Table = BuildTable(); + + public static ulong Compute(string s) => + Compute(Encoding.UTF8.GetBytes(s)); + + public static ulong Compute(ReadOnlySpan data) + { + ulong crc = 0xFFFFFFFFFFFFFFFFul; + foreach (var b in data) + crc = Table[(byte)(crc ^ b)] ^ (crc >> 8); + return ~crc; + } + + private static ulong[] BuildTable() + { + const ulong poly = 0xC96C5795D7870F42ul; + var table = new ulong[256]; + for (ulong i = 0; i < 256; i++) + { + ulong c = i; + for (int j = 0; j < 8; j++) + c = (c & 1) != 0 ? (poly ^ (c >> 1)) : (c >> 1); + table[i] = c; + } + return table; + } +} diff --git a/Volatility/Types.cs b/Volatility/Types.cs index 531ae8d..0e4e766 100644 --- a/Volatility/Types.cs +++ b/Volatility/Types.cs @@ -11,19 +11,10 @@ global using ColorRGBA = System.Numerics.Vector4; global using Vector2Literal = System.Numerics.Vector2; + public struct Transform { public Vector3 Location; public Quaternion Rotation; - public Vector3 Scale; -} - -public readonly struct ResourceID -{ - public readonly ulong Value; - public ResourceID(ulong v) => Value = v; - public static implicit operator ulong(ResourceID r) => r.Value; - public static implicit operator ResourceID(ulong v) => new ResourceID(v); - public static readonly ResourceID Default = new ResourceID(0UL); - public override string ToString() => $"{Value:X8}"; + public Vector3 Scale; } \ No newline at end of file diff --git a/Volatility/Utilities/ClassUtilities.cs b/Volatility/Utilities/ClassUtilities.cs index ba30ddf..5cecd7c 100644 --- a/Volatility/Utilities/ClassUtilities.cs +++ b/Volatility/Utilities/ClassUtilities.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.InteropServices; namespace Volatility.Utilities; @@ -26,4 +27,17 @@ public static Type[] GetDerivedTypes(Type baseType) { return member.GetCustomAttribute(); } + + public static Byte[] SerializeStructure(T msg) where T : struct + { + int objsize = Marshal.SizeOf(); + byte[] ret = new byte[objsize]; + IntPtr buff = Marshal.AllocHGlobal(objsize); + + Marshal.StructureToPtr(msg, buff, true); + Marshal.Copy(buff, ret, 0, objsize); + Marshal.FreeHGlobal(buff); + + return ret; + } } diff --git a/Volatility/Utilities/ResourceIDUtilities.cs b/Volatility/Utilities/ResourceIDUtilities.cs index f867432..e2b789e 100644 --- a/Volatility/Utilities/ResourceIDUtilities.cs +++ b/Volatility/Utilities/ResourceIDUtilities.cs @@ -133,21 +133,4 @@ public static string GetNameByResourceID(ResourceID id) return ""; } - - public static string GetResourceIDFromName(string name, Endian endian = Endian.LE) - { - byte[] hash = Crc32.Hash(Encoding.UTF8.GetBytes(name.ToLower())); - - if (endian == Endian.BE) - { - Array.Reverse(hash); - } - - return BitConverter.ToString(hash).Replace("-", "_").ToUpper(); - } - - public static ResourceID GetResourceIDFromName(string name) - { - return BinaryPrimitives.ReadUInt32BigEndian(Crc32.Hash(Encoding.UTF8.GetBytes(name.ToLower()))); - } } diff --git a/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs b/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs deleted file mode 100644 index 18edd7e..0000000 --- a/Volatility/Utilities/YAML/ResourceIDYamlTypeConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using YamlDotNet.Core; -using YamlDotNet.Core.Events; -using YamlDotNet.Serialization; - -namespace Volatility.Utilities; - -public class ResourceIDYamlTypeConverter : IYamlTypeConverter -{ - public bool Accepts(Type type) => type == typeof(ResourceID); - - public object ReadYaml(IParser parser, Type type, ObjectDeserializer nestedObjectDeserializer) - { - var scalar = parser.Consume().Value; - var hex = scalar.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? scalar.Substring(2) - : scalar; - var ul = Convert.ToUInt64(hex, 16); - return new ResourceID(ul); - } - - public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer nestedObjectSerializer) - { - var text = value.ToString(); - emitter.Emit(new Scalar( - anchor: null, - tag: null, - value: text, - style: ScalarStyle.Plain, - isPlainImplicit: true, - isQuotedImplicit: false - )); - } -} \ No newline at end of file diff --git a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs index 4c8a58b..8e7a59d 100644 --- a/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs +++ b/Volatility/Utilities/YAML/ResourceYamlDeserializer.cs @@ -43,7 +43,7 @@ public static object DeserializeResource(Type resourceClass, string yaml) var finalDeserializer = new DeserializerBuilder() .IgnoreUnmatchedProperties() - .WithTypeConverter(new ResourceIDYamlTypeConverter()) + .WithTypeConverter(new StrongIDYamlTypeConverter()) .Build(); using (var reader = new StringReader(mergedYaml)) { diff --git a/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs b/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs new file mode 100644 index 0000000..9e771b7 --- /dev/null +++ b/Volatility/Utilities/YAML/StrongIDYamlTypeConverter.cs @@ -0,0 +1,44 @@ +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace Volatility.Utilities +{ + public class StrongIDYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type.IsValueType + && type.IsGenericType + && type.GetGenericTypeDefinition() == typeof(StrongID<>); + } + + public object ReadYaml(IParser parser, Type type, ObjectDeserializer nestedObjectDeserializer) + { + var scalar = parser.Consume().Value!; + var hex = scalar.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? scalar.Substring(2) + : scalar; + var value = Convert.ToUInt64(hex, 16); + + var ctor = type.GetConstructor(new[] { typeof(ulong) }) + ?? throw new InvalidOperationException( + $"No ctor(ulong) found on {type.FullName}" + ); + return ctor.Invoke(new object[] { value })!; + } + + public void WriteYaml(IEmitter emitter, object value, Type type, ObjectSerializer nestedObjectSerializer) + { + var text = value.ToString()!; + emitter.Emit(new Scalar( + anchor: null, + tag: null, + value: text, + style: ScalarStyle.Plain, + isPlainImplicit: true, + isQuotedImplicit: false + )); + } + } +}