From ee660615288b18373a0781f9503d15e7f9324a32 Mon Sep 17 00:00:00 2001 From: Robin Kahlow Date: Tue, 23 Nov 2021 01:21:50 +0000 Subject: [PATCH] add rrtex converter, make chunky reader more general (#1) * add rrtex converter, make chunky reader more general * fix rgd viewer and json converter * use imagesharp file writer --- .../ChunkyHeaderReaderCLI.csproj | 14 ++ ChunkyHeaderReaderCLI/Program.cs | 36 +++ RGDJSONConverter/Program.cs | 112 ++++------ RGDReader.sln | 14 +- RGDReader/ChunkHeader.cs | 2 +- RGDReader/ChunkyFileReader.cs | 50 +++-- RGDReader/ChunkyList.cs | 15 ++ RGDReader/ChunkyUtil.cs | 58 +++-- RGDReader/FolderChunk.cs | 3 + RGDReader/KeyValueDataChunk.cs | 3 +- RGDReader/RGDNode.cs | 13 ++ RGDReaderCLI/Program.cs | 73 ++---- RGDViewer/MainViewModel.cs | 49 +---- RGDViewer/RGDViewModel.cs | 37 +--- RRTexConverter/Program.cs | 207 ++++++++++++++++++ RRTexConverter/ProgressBar.cs | 101 +++++++++ RRTexConverter/RRTexConverter.csproj | 20 ++ 17 files changed, 586 insertions(+), 221 deletions(-) create mode 100644 ChunkyHeaderReaderCLI/ChunkyHeaderReaderCLI.csproj create mode 100644 ChunkyHeaderReaderCLI/Program.cs create mode 100644 RGDReader/ChunkyList.cs create mode 100644 RGDReader/FolderChunk.cs create mode 100644 RGDReader/RGDNode.cs create mode 100644 RRTexConverter/Program.cs create mode 100644 RRTexConverter/ProgressBar.cs create mode 100644 RRTexConverter/RRTexConverter.csproj diff --git a/ChunkyHeaderReaderCLI/ChunkyHeaderReaderCLI.csproj b/ChunkyHeaderReaderCLI/ChunkyHeaderReaderCLI.csproj new file mode 100644 index 0000000..72fde9a --- /dev/null +++ b/ChunkyHeaderReaderCLI/ChunkyHeaderReaderCLI.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/ChunkyHeaderReaderCLI/Program.cs b/ChunkyHeaderReaderCLI/Program.cs new file mode 100644 index 0000000..9bd9014 --- /dev/null +++ b/ChunkyHeaderReaderCLI/Program.cs @@ -0,0 +1,36 @@ +using RGDReader; +using System.Text; + +if (args.Length != 1) +{ + Console.WriteLine("Usage: {0} ", AppDomain.CurrentDomain.FriendlyName); + return; +} + +string rgdPath = args[0]; + +var reader = new ChunkyFileReader(File.Open(rgdPath, FileMode.Open), Encoding.ASCII); +var fileHeader = reader.ReadChunkyFileHeader(); +Console.WriteLine("File header version: {0}", fileHeader.Version); + +void PrintHeaders(long position, long length, int depth = 0) +{ + reader.BaseStream.Position = position; + foreach (var header in reader.ReadChunkHeaders(length)) + { + for (int i = 0; i < depth; i++) + { + Console.Write("\t"); + } + + Console.WriteLine("Chunk Type: {0}, Name: {1}, Data length: {2}, Data position: {3}, Version: {4}, Path: {5}", + header.Type, header.Name, header.Length, header.DataPosition, header.Version, header.Path); + + if (header.Type == "FOLD") + { + PrintHeaders(header.DataPosition, header.Length, depth + 1); + } + } +} + +PrintHeaders(reader.BaseStream.Position, reader.BaseStream.Length - reader.BaseStream.Position); \ No newline at end of file diff --git a/RGDJSONConverter/Program.cs b/RGDJSONConverter/Program.cs index 9682833..e4d1bf7 100644 --- a/RGDJSONConverter/Program.cs +++ b/RGDJSONConverter/Program.cs @@ -26,104 +26,80 @@ void ConvertRGD(string rgdPath) var outRelativePath = Path.ChangeExtension(relativePath, "json"); var outJsonPath = Path.Join(outPath, outRelativePath); - using var reader = new ChunkyFileReader(File.Open(rgdPath, FileMode.Open), Encoding.ASCII); + var nodes = ChunkyUtil.ReadRGD(rgdPath); - var fileHeader = reader.ReadChunkyFileHeader(); - - KeyValueDataChunk? kvs = null; - KeysDataChunk? keys = null; - - while (reader.BaseStream.Position < reader.BaseStream.Length) + StringBuilder stringBuilder = new StringBuilder(); + void printIndent(int indent) { - var chunkHeader = reader.ReadChunkHeader(); - if (chunkHeader.Type == "DATA") + for (int i = 0; i < indent; i++) { - if (chunkHeader.Name == "AEGD") - { - kvs = reader.ReadKeyValueDataChunk(chunkHeader.Length); - } - - if (chunkHeader.Name == "KEYS") - { - keys = reader.ReadKeysDataChunk(); - break; - } + stringBuilder.Append(" "); } } - - if (kvs != null && keys != null) + void printValue(RGDNode node, int depth) { - var keysInv = ChunkyUtil.ReverseReadOnlyDictionary(keys.StringKeys); + printIndent(depth); + stringBuilder.Append("{\n"); + printIndent(depth + 1); + stringBuilder.AppendFormat("\"key\": \"{0}\",\n", node.Key); + printIndent(depth + 1); + stringBuilder.Append("\"value\": "); - StringBuilder stringBuilder = new StringBuilder(); - void printIndent(int indent) - { - for (int i = 0; i < indent; i++) - { - stringBuilder.Append(" "); - } - } - - void printTable(IList<(ulong Key, int Type, object Value)> table, int indent) + if (node.Value is IList childNodes) { stringBuilder.Append("[\n"); - for (int i = 0; i < table.Count; i++) + for (int i = 0; i < childNodes.Count; i++) { - var (childKey, childType, childValue) = table[i]; - - printIndent(indent); - stringBuilder.Append("{\n"); - - printIndent(indent + 1); - stringBuilder.AppendFormat("\"key\": \"{0}\",\n", keysInv[childKey]); - printIndent(indent + 1); - stringBuilder.Append("\"value\": "); - printValue(childType, childValue, indent + 1); + printValue(childNodes[i], depth + 2); + var childNode = childNodes[i]; - printIndent(indent); - stringBuilder.Append("}"); - if (i != table.Count - 1) + if (i != childNodes.Count - 1) { stringBuilder.Append(","); } + stringBuilder.Append("\n"); } - printIndent(indent - 1); + printIndent(depth + 1); stringBuilder.Append("]"); stringBuilder.Append("\n"); } - - void printValue(int type, object value, int indent) + else { - if (value is IList<(ulong Key, int Type, object Value)> table) - { - printTable(table, indent + 1); - } - else + stringBuilder.Append(node.Value switch { - stringBuilder.AppendFormat("{0}", type switch - { - 2 => (bool)value ? "true" : "false", - 3 => $"\"{((string)value).Replace("\\", "\\\\")}\"", - _ => value, - }); + bool b => b ? "true" : "false", + string s => $"\"{s.Replace("\\", "\\\\")}\"", + _ => node.Value, + }); - stringBuilder.Append("\n"); - } + stringBuilder.Append("\n"); } - stringBuilder.Append("{\n"); - printIndent(1); - stringBuilder.Append("\"data\": "); - printTable(kvs.KeyValues, 2); + printIndent(depth); stringBuilder.Append("}"); + } - Directory.CreateDirectory(Path.GetDirectoryName(outJsonPath)); - File.WriteAllText(outJsonPath, stringBuilder.ToString()); + stringBuilder.Append("{\n"); + printIndent(1); + stringBuilder.Append("\"data\": [\n"); + for (int i = 0; i < nodes.Count; i++) + { + printValue(nodes[i], 2); + if (i != nodes.Count - 1) + { + stringBuilder.Append(","); + } + stringBuilder.Append("\n"); } + printIndent(1); + stringBuilder.Append("]\n}"); + + Directory.CreateDirectory(Path.GetDirectoryName(outJsonPath)); + File.WriteAllText(outJsonPath, stringBuilder.ToString()); } using (var progress = new ProgressBar()) diff --git a/RGDReader.sln b/RGDReader.sln index 64bdaa2..cf850fc 100644 --- a/RGDReader.sln +++ b/RGDReader.sln @@ -14,7 +14,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RGDJSONConverter", "RGDJSONConverter\RGDJSONConverter.csproj", "{6CE167A1-D33F-44D8-88EB-2B2E978969B5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RGDJSONConverter", "RGDJSONConverter\RGDJSONConverter.csproj", "{6CE167A1-D33F-44D8-88EB-2B2E978969B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyHeaderReaderCLI", "ChunkyHeaderReaderCLI\ChunkyHeaderReaderCLI.csproj", "{CB3717F1-52D3-4A22-B815-CA3B846DD041}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RRTexConverter", "RRTexConverter\RRTexConverter.csproj", "{8F120438-7BE3-4890-99BB-57B7E5CED883}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +42,14 @@ Global {6CE167A1-D33F-44D8-88EB-2B2E978969B5}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CE167A1-D33F-44D8-88EB-2B2E978969B5}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CE167A1-D33F-44D8-88EB-2B2E978969B5}.Release|Any CPU.Build.0 = Release|Any CPU + {CB3717F1-52D3-4A22-B815-CA3B846DD041}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB3717F1-52D3-4A22-B815-CA3B846DD041}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB3717F1-52D3-4A22-B815-CA3B846DD041}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB3717F1-52D3-4A22-B815-CA3B846DD041}.Release|Any CPU.Build.0 = Release|Any CPU + {8F120438-7BE3-4890-99BB-57B7E5CED883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F120438-7BE3-4890-99BB-57B7E5CED883}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F120438-7BE3-4890-99BB-57B7E5CED883}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F120438-7BE3-4890-99BB-57B7E5CED883}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RGDReader/ChunkHeader.cs b/RGDReader/ChunkHeader.cs index 3aae05e..bfe92c6 100644 --- a/RGDReader/ChunkHeader.cs +++ b/RGDReader/ChunkHeader.cs @@ -1,3 +1,3 @@ namespace RGDReader; -public record class ChunkHeader(string Type, string Name, int Version, int Length, int MinVersion); \ No newline at end of file +public record class ChunkHeader(string Type, string Name, int Version, int Length, string Path, long DataPosition); \ No newline at end of file diff --git a/RGDReader/ChunkyFileReader.cs b/RGDReader/ChunkyFileReader.cs index f170625..4ae922b 100644 --- a/RGDReader/ChunkyFileReader.cs +++ b/RGDReader/ChunkyFileReader.cs @@ -15,6 +15,24 @@ public ChunkyFileReader(Stream input, Encoding encoding, bool leaveOpen) : base( { } + public IEnumerable ReadChunkHeaders(long length) + { + long startPosition = BaseStream.Position; + while (BaseStream.Position < startPosition + length) + { + var chunkHeader = ReadChunkHeader(); + + yield return chunkHeader; + + BaseStream.Position = chunkHeader.DataPosition + chunkHeader.Length; + } + } + + public IEnumerable ReadChunkHeaders() + { + return ReadChunkHeaders(BaseStream.Length - BaseStream.Position); + } + public ChunkHeader ReadChunkHeader() { return new ChunkHeader( @@ -22,7 +40,8 @@ public ChunkHeader ReadChunkHeader() new string(ReadChars(4)), ReadInt32(), ReadInt32(), - ReadInt32() + Encoding.ASCII.GetString(ReadBytes(ReadInt32())), + BaseStream.Position ); } @@ -62,54 +81,53 @@ private object ReadType(int type) 1 => ReadInt32(), 2 => ReadBoolean(), 3 => ReadCString(), - 100 => ReadTable(), - 101 => ReadTable(), + 100 => ReadChunkyList(), + 101 => ReadChunkyList(), _ => throw new Exception($"Unknown type {type}") }; } - private List<(ulong Key, int Type, object Value)> ReadTable() + private ChunkyList ReadChunkyList() { int length = ReadInt32(); // Read table index - List<(ulong key, int Type, int index)> keyTypeAndDataIndex = new(); + var keyTypeAndDataIndex = new (ulong key, int Type, int index)[length]; for (int i = 0; i < length; i++) { ulong key = ReadUInt64(); int type = ReadInt32(); int index = ReadInt32(); - keyTypeAndDataIndex.Add((key, type, index)); + keyTypeAndDataIndex[i] = (key, type, index); } // Read table row data long dataPosition = BaseStream.Position; - List<(ulong Key, int Type, object Value)> kvs = new(); + ChunkyList kvs = new(); foreach (var (key, type, index) in keyTypeAndDataIndex) { BaseStream.Position = dataPosition + index; - kvs.Add((key, type, ReadType(type))); + kvs.Add(new KeyValueEntry(key, ReadType(type))); } return kvs; } - public KeyValueDataChunk ReadKeyValueDataChunk(int length) + public KeyValueDataChunk ReadKeyValueDataChunk(ChunkHeader header) { - long startPosition = BaseStream.Position; - - int unknown2 = ReadInt32(); + BaseStream.Position = header.DataPosition; - var table = ReadTable(); - - BaseStream.Position = startPosition + length; + int unknown = ReadInt32(); + var table = ReadChunkyList(); return new KeyValueDataChunk(table); } - public KeysDataChunk ReadKeysDataChunk() + public KeysDataChunk ReadKeysDataChunk(ChunkHeader header) { + BaseStream.Position = header.DataPosition; + Dictionary stringKeys = new(); int count = ReadInt32(); diff --git a/RGDReader/ChunkyList.cs b/RGDReader/ChunkyList.cs new file mode 100644 index 0000000..deb15a5 --- /dev/null +++ b/RGDReader/ChunkyList.cs @@ -0,0 +1,15 @@ +namespace RGDReader; +public class ChunkyList : List +{ + public ChunkyList() + { + } + + public ChunkyList(IEnumerable collection) : base(collection) + { + } + + public ChunkyList(int capacity) : base(capacity) + { + } +} \ No newline at end of file diff --git a/RGDReader/ChunkyUtil.cs b/RGDReader/ChunkyUtil.cs index c44a7cb..802886e 100644 --- a/RGDReader/ChunkyUtil.cs +++ b/RGDReader/ChunkyUtil.cs @@ -1,4 +1,6 @@ -namespace RGDReader; +using System.Text; + +namespace RGDReader; public static class ChunkyUtil { @@ -13,23 +15,53 @@ public static IReadOnlyDictionary ReverseReadOnlyDictionary ReadRGD(string rgdPath) { - if (kvs.KeyValues.TryGetValue(key, out var result)) + using var reader = new ChunkyFileReader(File.Open(rgdPath, FileMode.Open), Encoding.ASCII); + + reader.ReadChunkyFileHeader(); + var chunkHeaders = reader.ReadChunkHeaders().ToArray(); + + ChunkHeader[] keysHeaders = chunkHeaders.Where(header => header.Type == "DATA" && header.Name == "KEYS").ToArray(); + ChunkHeader[] kvsHeaders = chunkHeaders.Where(header => header.Type == "DATA" && header.Name == "AEGD").ToArray(); + + if (keysHeaders.Length == 0) { - return result; + throw new Exception("No DATA KEYS chunk present"); } - return null; - } + if (keysHeaders.Length > 1) + { + throw new Exception("More than one DATA KEYS chunk present"); + } - public static IDictionary ResolveKeyValues(KeysDataChunk keys, KeyValueDataChunk kvs) - { - Dictionary resolved = new(); - foreach (var (stringKey, key) in keys.StringKeys) + if (kvsHeaders.Length == 0) + { + throw new Exception("No DATA AEGD chunk present"); + } + + if (kvsHeaders.Length > 1) + { + throw new Exception("More than one DATA AEGD chunk present"); + } + + var keys = reader.ReadKeysDataChunk(keysHeaders[0]); + var kvs = reader.ReadKeyValueDataChunk(kvsHeaders[0]); + + var keysInv = ReverseReadOnlyDictionary(keys.StringKeys); + + static RGDNode makeNode(ulong key, object value, IReadOnlyDictionary keysInv) { - resolved[stringKey] = ResolveKey(key, kvs); + string keyStr = keysInv[key]; + + if (value is ChunkyList table) + { + return new RGDNode(keyStr, table.Select(listItem => makeNode(listItem.Key, listItem.Value, keysInv)).ToArray()); + } + + return new RGDNode(keyStr, value); } - return resolved; - }*/ + + return kvs.KeyValues.Select(kv => makeNode(kv.Key, kv.Value, keysInv)).ToArray(); + } } \ No newline at end of file diff --git a/RGDReader/FolderChunk.cs b/RGDReader/FolderChunk.cs new file mode 100644 index 0000000..b00dbe2 --- /dev/null +++ b/RGDReader/FolderChunk.cs @@ -0,0 +1,3 @@ +namespace RGDReader; + +public record class FolderChunk(IList chunks); \ No newline at end of file diff --git a/RGDReader/KeyValueDataChunk.cs b/RGDReader/KeyValueDataChunk.cs index aa78427..9f0d8cd 100644 --- a/RGDReader/KeyValueDataChunk.cs +++ b/RGDReader/KeyValueDataChunk.cs @@ -1,3 +1,4 @@ namespace RGDReader; -public record class KeyValueDataChunk(IList<(ulong Key, int Type, object Value)> KeyValues); \ No newline at end of file +public record KeyValueEntry(ulong Key, object Value); +public record KeyValueDataChunk(IList KeyValues); \ No newline at end of file diff --git a/RGDReader/RGDNode.cs b/RGDReader/RGDNode.cs new file mode 100644 index 0000000..c2af504 --- /dev/null +++ b/RGDReader/RGDNode.cs @@ -0,0 +1,13 @@ +namespace RGDReader; + +public class RGDNode +{ + public string Key { get; init; } + public object Value { get; init; } + + public RGDNode(string key, object value) + { + Key = key; + Value = value; + } +} \ No newline at end of file diff --git a/RGDReaderCLI/Program.cs b/RGDReaderCLI/Program.cs index 8014317..01967d4 100644 --- a/RGDReaderCLI/Program.cs +++ b/RGDReaderCLI/Program.cs @@ -7,72 +7,33 @@ return; } -IReadOnlyDictionary typeDisplayName = new Dictionary -{ - [0] = "Float", - [1] = "Integer", - [2] = "Boolean", - [3] = "String", - [100] = "Table", - [101] = "List", -}; - string rgdPath = args[0]; -using var reader = new ChunkyFileReader(File.Open(rgdPath, FileMode.Open), Encoding.ASCII); - -Console.WriteLine("Reading {0}", args[0]); +var nodes = ChunkyUtil.ReadRGD(rgdPath); -var fileHeader = reader.ReadChunkyFileHeader(); - -KeyValueDataChunk? kvs = null; -KeysDataChunk? keys = null; - -while (reader.BaseStream.Position < reader.BaseStream.Length) +static void PrintNode(RGDNode node, int depth = 0) { - var chunkHeader = reader.ReadChunkHeader(); - if (chunkHeader.Type == "DATA") + for (int i = 0; i < depth; i++) { - if (chunkHeader.Name == "AEGD") - { - kvs = reader.ReadKeyValueDataChunk(chunkHeader.Length); - } - - if (chunkHeader.Name == "KEYS") - { - keys = reader.ReadKeysDataChunk(); - break; - } + Console.Write("\t"); } -} - -if (kvs != null && keys != null) -{ - var keysInv = ChunkyUtil.ReverseReadOnlyDictionary(keys.StringKeys); - Console.WriteLine("All key-values"); - - void printTable(IList<(ulong Key, int Type, object Value)> table, int indent = 0) + Console.Write(node.Key); + if (node.Value is IList children) { - foreach (var (childKey, childType, childValue) in table) + Console.WriteLine(); + foreach (var child in children) { - printValue(childKey, childType, childValue, indent + 1); + PrintNode(child, depth + 1); } } - - void printValue(ulong key, int type, object value, int indent = 0) + else { - Console.Write(string.Join("", Enumerable.Range(0, indent).Select(_ => " "))); - Console.Write(keysInv.GetValueOrDefault(key, "")); - if (value is IList<(ulong Key, int Type, object Value)> table) - { - Console.WriteLine(" [Table]"); - printTable(table, indent + 1); - } - else - { - Console.WriteLine(" [{0}] <{1}>", typeDisplayName[type], value); - } + Console.Write(": "); + Console.WriteLine(node.Value); } +} - printTable(kvs.KeyValues); -} \ No newline at end of file +foreach (var node in nodes) +{ + PrintNode(node); +} diff --git a/RGDViewer/MainViewModel.cs b/RGDViewer/MainViewModel.cs index 5cfe41d..fc5b5ae 100644 --- a/RGDViewer/MainViewModel.cs +++ b/RGDViewer/MainViewModel.cs @@ -49,52 +49,25 @@ private void OnRGDPathChanged(object? sender, PropertyChangedEventArgs e) } else { - using var reader = new ChunkyFileReader(File.Open(RGDPath, FileMode.Open), Encoding.ASCII); + var nodes = ChunkyUtil.ReadRGD(RGDPath); - var fileHeader = reader.ReadChunkyFileHeader(); - - KeyValueDataChunk? kvs = null; - KeysDataChunk? keys = null; - - while (reader.BaseStream.Position < reader.BaseStream.Length) + RGDViewModel nodeToViewModel(RGDNode node) { - var chunkHeader = reader.ReadChunkHeader(); - if (chunkHeader.Type == "DATA") - { - if (chunkHeader.Name == "AEGD") - { - kvs = reader.ReadKeyValueDataChunk(chunkHeader.Length); - } + IList childViewModels; - if (chunkHeader.Name == "KEYS") - { - keys = reader.ReadKeysDataChunk(); - break; - } + if (node.Value is IList nodeChildren) + { + childViewModels = nodeChildren.Select(node => nodeToViewModel(node)).ToArray(); } - } - - if (kvs != null && keys != null) - { - var keysInv = ChunkyUtil.ReverseReadOnlyDictionary(keys.StringKeys); - - RGDViewModel nodeToViewModel((ulong Hash, int Type, object Value) node) + else { - var childViewModels = new List(); - - if (node.Value is IList<(ulong Key, int Type, object Value)> nodeChildren) - { - foreach (var child in nodeChildren) - { - childViewModels.Add(nodeToViewModel((child.Key, child.Type, child.Value))); - } - } - - return new RGDViewModel(keysInv[node.Hash], node.Hash, node.Type, node.Value, childViewModels); + childViewModels = new RGDViewModel[0]; } - rootChildren = kvs.KeyValues.Select(kv => nodeToViewModel((kv.Key, kv.Type, kv.Value))).ToArray(); + return new RGDViewModel(node.Key, node.Value, childViewModels); } + + rootChildren = nodes.Select(nodeToViewModel).ToArray(); } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RootChildren))); diff --git a/RGDViewer/RGDViewModel.cs b/RGDViewer/RGDViewModel.cs index 6078175..31dd131 100644 --- a/RGDViewer/RGDViewModel.cs +++ b/RGDViewer/RGDViewModel.cs @@ -1,22 +1,19 @@ -using System; +using RGDReader; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace RGDViewer { public class RGDViewModel { - private static readonly IReadOnlyDictionary typeDisplayName = new Dictionary + private static IReadOnlyDictionary TypeName = new Dictionary() { - [0] = "Float", - [1] = "Integer", - [2] = "Boolean", - [3] = "String", - [100] = "Table", - [101] = "List", + [typeof(int)] = "Integer", + [typeof(float)] = "Float", + [typeof(string)] = "String", + [typeof(bool)] = "Boolean", + [typeof(RGDNode[])] = "List", }; public string Key @@ -25,27 +22,15 @@ public string Key set; } - public ulong Hash - { - get; - set; - } - public object Value { get; set; } - public int Type - { - get; - set; - } - public string DisplayValue { - get => string.Format("{0} <0x{1:X8}>: [{2}] {3}", Key, Hash, typeDisplayName[Type], Value is IList<(ulong Key, int Type, object Value)> table ? $"Count: {table.Count}" : Value); + get => string.Format("{0}: {1} ({2})", Key, Value is IList ? $"{{{Children.Count}}}" : Value, TypeName[Value.GetType()]); } public ObservableCollection Children @@ -54,11 +39,9 @@ public ObservableCollection Children set; } - public RGDViewModel(string key, ulong hash, int type, object value, IList children) + public RGDViewModel(string key, object value, IList children) { Key = key; - Hash = hash; - Type = type; Value = value; Children = new ObservableCollection(children); } diff --git a/RRTexConverter/Program.cs b/RRTexConverter/Program.cs new file mode 100644 index 0000000..9ca8d9e --- /dev/null +++ b/RRTexConverter/Program.cs @@ -0,0 +1,207 @@ +using RGDReader; +using System.Text; +using System.IO.Compression; +using BCnEncoder.Decoder; +using BCnEncoder.Shared; +using Microsoft.Extensions.FileSystemGlobbing; +using SixLabors.ImageSharp; +using BCnEncoder.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +if (args.Length != 2) +{ + Console.WriteLine("Usage: {0} ", AppDomain.CurrentDomain.FriendlyName); + return; +} + +string path = args[0]; +string outPath = args[1]; + +Console.WriteLine("Output directory: {0}", outPath); +Console.WriteLine("Searching for rgd files in {0}", path); + +Matcher matcher = new(); +matcher.AddInclude("**/*.rrtex"); + +var rgdPaths = matcher.GetResultsInFullPath(path).ToArray(); +Console.WriteLine("Found {0} rrtex files", rgdPaths.Length); + +List unhandledTextureMessages = new(); + +void ConvertRRTex(string rrtexPath) +{ + var reader = new ChunkyFileReader(File.Open(rrtexPath, FileMode.Open), Encoding.ASCII); + var fileHeader = reader.ReadChunkyFileHeader(); + + int mipCount = -1; + int width = 0; + int height = 0; + int[] mipTextureCounts = null; + List sizeCompressed = null; + List sizeUncompressed = null; + int textureCompression = -1; + + void PrintHeaders(long position, long length) + { + var relativePath = Path.GetRelativePath(path, rrtexPath); + var outRelativePath = Path.ChangeExtension(relativePath, null); + + reader.BaseStream.Position = position; + foreach (var header in reader.ReadChunkHeaders(length)) + { + long pos = reader.BaseStream.Position; + reader.BaseStream.Position = header.DataPosition; + + if (header.Name == "TMAN") + { + int unknown1 = reader.ReadInt32(); + width = reader.ReadInt32(); + height = reader.ReadInt32(); + int unknown2 = reader.ReadInt32(); + int unknown3 = reader.ReadInt32(); + textureCompression = reader.ReadInt32(); + + mipCount = reader.ReadInt32(); + int unknown5 = reader.ReadInt32(); + + mipTextureCounts = new int[mipCount]; + for (int i = 0; i < mipCount; i++) + { + mipTextureCounts[i] = reader.ReadInt32(); + } + + sizeCompressed = new(); + sizeUncompressed = new(); + + for (int i = 0; i < mipCount; i++) + { + for (int j = 0; j < mipTextureCounts[i]; j++) + { + sizeUncompressed.Add(reader.ReadInt32()); + sizeCompressed.Add(reader.ReadInt32()); + } + } + } + + if (header.Name == "TDAT") + { + int count = 0; + int unk = reader.ReadInt32(); + for (int i = 0; i < mipCount; i++) + { + List data = new(); + int w = -1; + int h = -1; + + for (int j = 0; j < mipTextureCounts[i]; j++) + { + long prePos = reader.BaseStream.Position; + + if (sizeCompressed[count] != sizeUncompressed[count]) + { + byte[] zlibHeader = reader.ReadBytes(2); + + DeflateStream deflateStream = new DeflateStream(reader.BaseStream, CompressionMode.Decompress, true); + MemoryStream inflatedStream = new MemoryStream(); + deflateStream.CopyTo(inflatedStream); + int length2 = (int)inflatedStream.Length; + + BinaryReader dataReader = new BinaryReader(inflatedStream); + dataReader.BaseStream.Position = 0; + + if (j == 0) + { + int mipLevel = dataReader.ReadInt32(); + int widthx = dataReader.ReadInt32(); + int heightx = dataReader.ReadInt32(); + w = Math.Max(widthx, 4); + h = Math.Max(heightx, 4); + int numPhysicalTexels = dataReader.ReadInt32(); + data.AddRange(dataReader.ReadBytes(length2 - 16)); + } + else + { + data.AddRange(dataReader.ReadBytes(length2)); + } + } + else + { + int length2 = sizeUncompressed[count]; + + if (j == 0) + { + int mipLevel = reader.ReadInt32(); + int widthx = reader.ReadInt32(); + int heightx = reader.ReadInt32(); + w = Math.Max(widthx, 4); + h = Math.Max(heightx, 4); + int numPhysicalTexels = reader.ReadInt32(); + data.AddRange(reader.ReadBytes(length2 - 16)); + } + else + { + data.AddRange(reader.ReadBytes(length2)); + } + } + + reader.BaseStream.Position = prePos + sizeCompressed[count]; + + count++; + } + + var decoder = new BcDecoder(); + + var format = textureCompression switch + { + 2 => CompressionFormat.R, + 18 => CompressionFormat.Bc1WithAlpha, + 19 => CompressionFormat.Bc1, + 22 => CompressionFormat.Bc3, + _ => CompressionFormat.Unknown + }; + + if (format == CompressionFormat.Unknown) + { + unhandledTextureMessages.Add(string.Format( + "Unknown texture compression method {0} for {1}, w={2} h={3} comp={4} uncomp={5}", + textureCompression, rrtexPath, w, h, sizeCompressed[count - 1], sizeUncompressed[count - 1] + )); + continue; + } + + using Image image = decoder.DecodeRawToImageRgba32(data.ToArray(), w, h, format); + + var outImagePath = Path.Join(outPath, $"{outRelativePath}_mip{i}.png"); + Directory.CreateDirectory(Path.GetDirectoryName(outImagePath)); + image.SaveAsPng(outImagePath); + } + } + + reader.BaseStream.Position = pos; + + if (header.Type == "FOLD") + { + PrintHeaders(header.DataPosition, header.Length); + } + } + } + + PrintHeaders(reader.BaseStream.Position, reader.BaseStream.Length - reader.BaseStream.Position); +} + +using (var progress = new ProgressBar()) +{ + int processed = 0; + foreach (var rgdPath in rgdPaths) + { + ConvertRRTex(rgdPath); + processed++; + progress.Report((double)processed / rgdPaths.Length); + } +} + +Console.WriteLine("Done, {0} unhandled textures:", unhandledTextureMessages.Count); +foreach (var unhandledTextureMessage in unhandledTextureMessages) +{ + Console.WriteLine(unhandledTextureMessage); +} \ No newline at end of file diff --git a/RRTexConverter/ProgressBar.cs b/RRTexConverter/ProgressBar.cs new file mode 100644 index 0000000..9c32fb6 --- /dev/null +++ b/RRTexConverter/ProgressBar.cs @@ -0,0 +1,101 @@ +// MIT License by Daniel S Wolf, https://gist.github.com/DanielSWolf/0ab6a96899cc5377bf54 +using System.Text; + +/// +/// An ASCII progress bar +/// +public class ProgressBar : IDisposable, IProgress +{ + private const int blockCount = 10; + private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8); + private const string animation = @"|/-\"; + + private readonly Timer timer; + + private double currentProgress = 0; + private string currentText = string.Empty; + private bool disposed = false; + private int animationIndex = 0; + + public ProgressBar() + { + timer = new Timer(TimerHandler); + + // A progress bar is only for temporary display in a console window. + // If the console output is redirected to a file, draw nothing. + // Otherwise, we'll end up with a lot of garbage in the target file. + if (!Console.IsOutputRedirected) + { + ResetTimer(); + } + } + + public void Report(double value) + { + // Make sure value is in [0..1] range + value = Math.Max(0, Math.Min(1, value)); + Interlocked.Exchange(ref currentProgress, value); + } + + private void TimerHandler(object state) + { + lock (timer) + { + if (disposed) return; + + int progressBlockCount = (int)(currentProgress * blockCount); + int percent = (int)(currentProgress * 100); + string text = string.Format("[{0}{1}] {2,3}% {3}", + new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount), + percent, + animation[animationIndex++ % animation.Length]); + UpdateText(text); + + ResetTimer(); + } + } + + private void UpdateText(string text) + { + // Get length of common portion + int commonPrefixLength = 0; + int commonLength = Math.Min(currentText.Length, text.Length); + while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength]) + { + commonPrefixLength++; + } + + // Backtrack to the first differing character + StringBuilder outputBuilder = new StringBuilder(); + outputBuilder.Append('\b', currentText.Length - commonPrefixLength); + + // Output new suffix + outputBuilder.Append(text.Substring(commonPrefixLength)); + + // If the new text is shorter than the old one: delete overlapping characters + int overlapCount = currentText.Length - text.Length; + if (overlapCount > 0) + { + outputBuilder.Append(' ', overlapCount); + outputBuilder.Append('\b', overlapCount); + } + + Console.Write(outputBuilder); + currentText = text; + } + + private void ResetTimer() + { + timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1)); + } + + public void Dispose() + { + lock (timer) + { + disposed = true; + UpdateText(string.Empty); + } + } + +} diff --git a/RRTexConverter/RRTexConverter.csproj b/RRTexConverter/RRTexConverter.csproj new file mode 100644 index 0000000..7c07509 --- /dev/null +++ b/RRTexConverter/RRTexConverter.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + +