diff --git a/MaterialLib/typedef.c b/MaterialLib/typedef.c new file mode 100644 index 0000000..e69de29 diff --git a/NResUI/App.cs b/NResUI/App.cs index 99ed89c..d7989f4 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -61,6 +61,7 @@ public class App serviceCollection.AddSingleton(new VarsetViewModel()); serviceCollection.AddSingleton(new CpDatSchemeViewModel()); serviceCollection.AddSingleton(new MaterialViewModel()); + serviceCollection.AddSingleton(new ResearchTreeViewModel()); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/NResUI/ImGuiUI/MainMenuBar.cs b/NResUI/ImGuiUI/MainMenuBar.cs index bd54813..7a1e83e 100644 --- a/NResUI/ImGuiUI/MainMenuBar.cs +++ b/NResUI/ImGuiUI/MainMenuBar.cs @@ -7,6 +7,7 @@ using NativeFileDialogSharp; using NResLib; using NResUI.Abstractions; using NResUI.Models; +using ResTreeLib; using ScrLib; using TexmLib; using VarsetLib; @@ -20,6 +21,7 @@ namespace NResUI.ImGuiUI MissionTmaViewModel missionTmaViewModel, VarsetViewModel varsetViewModel, CpDatSchemeViewModel cpDatSchemeViewModel, + ResearchTreeViewModel researchTreeViewModel, MessageBoxModalPanel messageBox) : IImGuiPanel { @@ -138,6 +140,21 @@ namespace NResUI.ImGuiUI } } + if (ImGui.MenuItem("Open research .trf File")) + { + var result = Dialog.FileOpen("trf"); + + if (result.IsOk) + { + var path = result.Path; + var parseResult = ResTreeParser.Parse(path); + + researchTreeViewModel.SetParseResult(parseResult, path); + + Console.WriteLine("Read .trf"); + } + } + if (nResExplorerViewModel.HasFile) { if (ImGui.MenuItem("Экспортировать NRes")) diff --git a/NResUI/ImGuiUI/ResearchTreeExplorer.cs b/NResUI/ImGuiUI/ResearchTreeExplorer.cs new file mode 100644 index 0000000..4077380 --- /dev/null +++ b/NResUI/ImGuiUI/ResearchTreeExplorer.cs @@ -0,0 +1,101 @@ +using ImGuiNET; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI; + +public class ResearchTreeExplorer : IImGuiPanel +{ + private readonly ResearchTreeViewModel _viewModel; + + public ResearchTreeExplorer(ResearchTreeViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + if (ImGui.Begin("Research Tree Explorer (trf)")) + { + ImGui.Text("trf - это файл дерева исследований. Их можно найти в папке MISSIONS/SCRIPTS"); + ImGui.Separator(); + + var nodes = _viewModel.ResearchNodeDatas; + + if (_viewModel.HasFile && nodes is not null) + { + if (ImGui.TreeNodeEx("Узлы")) + { + for (var i = 0; i < nodes.Count; i++) + { + var node = nodes[i]; + if (ImGui.TreeNodeEx($"{i} - \"{node.LongName}\" (\"{node.ShortName}\")")) + { + ImGui.Text("Состояние: "); + ImGui.SameLine(); + ImGui.Text(node.State.ToString("G")); + + ImGui.Text("ShortName: "); + ImGui.SameLine(); + ImGui.Text(node.ShortName); + + ImGui.Text("LongName: "); + ImGui.SameLine(); + ImGui.Text(node.LongName); + + ImGui.Text("HelpText: "); + ImGui.SameLine(); + ImGui.Text(node.HelpText); + + ImGui.Text("Description: "); + ImGui.SameLine(); + ImGui.Text(node.Description); + + ImGui.Text("Тип турели: "); + ImGui.SameLine(); + ImGui.Text(node.Node.TurretType.ToString()); + + ImGui.Text("Основной тип: "); + ImGui.SameLine(); + ImGui.Text(node.Node.MainType.ToString()); + + ImGui.Text("Подтип: "); + ImGui.SameLine(); + ImGui.Text(node.Node.SubType.ToString()); + + ImGui.Text("Подтип строения: "); + ImGui.SameLine(); + ImGui.Text(node.Node.BuildSubSystem.ToString()); + + ImGui.Text("Размер типа: "); + ImGui.SameLine(); + ImGui.Text(node.Node.SizeOfType.ToString()); + + ImGui.Text("Уровень апгрейда: "); + ImGui.SameLine(); + ImGui.Text(node.Node.UpgradeLevel.ToString()); + + ImGui.Text("Условия: "); + ImGui.SameLine(); + ImGui.Text(string.Join(", ", node.PrerequisiteIds)); + + ImGui.Text("Открывает: "); + ImGui.SameLine(); + ImGui.Text(string.Join(", ", node.UnlockIds)); + + ImGui.TreePop(); + } + } + + ImGui.TreePop(); + } + } + else + { + ImGui.Text("trf не открыт"); + } + + ImGui.End(); + } + } +} diff --git a/NResUI/Models/ResearchTreeViewModel.cs b/NResUI/Models/ResearchTreeViewModel.cs new file mode 100644 index 0000000..a76aaa0 --- /dev/null +++ b/NResUI/Models/ResearchTreeViewModel.cs @@ -0,0 +1,20 @@ +using ResTreeLib; + +namespace NResUI.Models; + +public class ResearchTreeViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public List? ResearchNodeDatas { get; set; } + + public string? Path { get; set; } + + public void SetParseResult(List researchNodeDatas, string path) + { + ResearchNodeDatas = researchNodeDatas; + HasFile = true; + Path = path; + } +} diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj index 5728421..9df73e6 100644 --- a/NResUI/NResUI.csproj +++ b/NResUI/NResUI.csproj @@ -20,6 +20,7 @@ + diff --git a/ParkanPlayground.sln.DotSettings.user b/ParkanPlayground.sln.DotSettings.user new file mode 100644 index 0000000..428747f --- /dev/null +++ b/ParkanPlayground.sln.DotSettings.user @@ -0,0 +1,3 @@ + + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/ParkanPlayground.slnx b/ParkanPlayground.slnx index 0ffc6ee..c61ce41 100644 --- a/ParkanPlayground.slnx +++ b/ParkanPlayground.slnx @@ -3,6 +3,7 @@ + @@ -13,6 +14,7 @@ + diff --git a/ParkanPlayground/Msh0D.cs b/ParkanPlayground/Msh0D.cs index 9f64526..ae7e7d0 100644 --- a/ParkanPlayground/Msh0D.cs +++ b/ParkanPlayground/Msh0D.cs @@ -25,9 +25,8 @@ public static class Msh0D var elements = elementBytes.Select(x => new Msh0DElement() { - Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)), - Magic04 = x.AsSpan(4)[0], - Magic05 = x.AsSpan(5)[0], + Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)), + TriangleCount = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)), Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)), CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)), IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)), @@ -44,8 +43,7 @@ public static class Msh0D // Magic04 и Magic06 обрабатываются вместе - public byte Magic04 { get; set; } - public byte Magic05 { get; set; } + public ushort TriangleCount { get; set; } public ushort Magic06 { get; set; } public ushort CountOf06 { get; set; } public int IndexInto06 { get; set; } diff --git a/ParkanPlayground/ParkanPlayground.csproj b/ParkanPlayground/ParkanPlayground.csproj index 4eedae8..bf89f86 100644 --- a/ParkanPlayground/ParkanPlayground.csproj +++ b/ParkanPlayground/ParkanPlayground.csproj @@ -8,6 +8,7 @@ + diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index 9516d29..8254e96 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -1,332 +1,16 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using ParkanPlayground.Effects; -using static ParkanPlayground.Effects.FxidReader; +using NResLib; +using ResTreeLib; -Console.OutputEncoding = Encoding.UTF8; +System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); -if (args.Length == 0) +foreach (var trfFile in Directory.EnumerateFiles("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS", "*.trf")) { - Console.WriteLine("Usage: ParkanPlayground "); - return; -} - -var path = args[0]; -bool anyError = false; -var sizeByType = new Dictionary -{ - [1] = 0xE0, // 1: Billboard - [2] = 0x94, // 2: Sound - [3] = 0xC8, // 3: AnimParticle - [4] = 0xCC, // 4: AnimBillboard - [5] = 0x70, // 5: Trail - [6] = 0x04, // 6: Point - [7] = 0xD0, // 7: Plane - [8] = 0xF8, // 8: Model - [9] = 0xD0, // 9: AnimModel - [10] = 0xD0, // 10: Cube -}; - -// Check for --dump-headers flag -bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers"; - -if (Directory.Exists(path)) -{ - var files = Directory.EnumerateFiles(path, "*.bin").ToList(); - if (dumpHeaders) - { - // Collect all headers for analysis - var headers = new List<(string name, EffectHeader h)>(); - foreach (var file in files) - { - try - { - using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false); - if (fs.Length >= 60) - { - headers.Add((Path.GetFileName(file), ReadEffectHeader(br))); - } - } - catch { } - } - - // Analyze unique values - Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n"); - - var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList(); - Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}"); - - var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList(); - Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}"); - - var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList(); - Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}"); - - var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList(); - Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}"); - Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}"); - Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}"); - - // Check reserved bytes - var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList(); - Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}"); - - // Check scales - var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList(); - Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}"); - - Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ==="); - Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale"); - Console.WriteLine(new string('-', 100)); - - foreach (var (name, h) in headers.Take(30)) - { - Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})"); - } - } - else - { - foreach (var file in files) - { - if (!ValidateFxidFile(file)) - { - anyError = true; - } - } - - Console.WriteLine(anyError - ? "Validation finished with errors." - : "All FXID files parsed successfully."); - } -} -else if (File.Exists(path)) -{ - anyError = !ValidateFxidFile(path); - Console.WriteLine(anyError ? "Validation failed." : "Validation OK."); -} -else -{ - Console.WriteLine($"Path not found: {path}"); -} - -void DumpEffectHeader(string path) -{ - try - { - using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false); - - if (fs.Length < 60) - { - Console.WriteLine($"{Path.GetFileName(path)}: file too small"); - return; - } - - var h = ReadEffectHeader(br); - - // Format reserved bytes as hex (show first 8 bytes for brevity) - var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " "); - if (h.Reserved.Length > 8) reservedHex += "..."; - - // Check if reserved has any non-zero bytes - bool reservedAllZero = h.Reserved.All(b => b == 0); - - Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})"); - } - catch (Exception ex) - { - Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}"); - } -} - -bool ValidateFxidFile(string path) -{ - try - { - using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false); - - const int headerSize = 60; // sizeof(EffectHeader) on disk - if (fs.Length < headerSize) - { - Console.WriteLine($"{path}: file too small ({fs.Length} bytes)."); - return false; - } - - var header = ReadEffectHeader(br); - - var typeCounts = new Dictionary(); - - for (int i = 0; i < header.ComponentCount; i++) - { - long blockStart = fs.Position; - if (fs.Position + 4 > fs.Length) - { - Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X})."); - return false; - } - - uint typeAndFlags = br.ReadUInt32(); - byte type = (byte)(typeAndFlags & 0xFF); - - if (!typeCounts.TryGetValue(type, out var count)) - { - count = 0; - } - typeCounts[type] = count + 1; - - if (!sizeByType.TryGetValue(type, out int blockSize)) - { - Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8})."); - return false; - } - - int remaining = blockSize - 4; - if (fs.Position + remaining > fs.Length) - { - Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X})."); - return false; - } - - if (type == 1) - { - var def = ReadBillboardComponent(br, typeAndFlags); - - if (def.Reserved.Length != 0x50) - { - Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50."); - return false; - } - } - else if (type == 2) - { - var def = ReadSoundComponent(br, typeAndFlags); - - if (def.SoundNameAndReserved.Length != 0x40) - { - Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40."); - return false; - } - } - else if (type == 3) - { - var def = ReadAnimParticleComponent(br, typeAndFlags); - - if (def.Reserved.Length != 0x38) - { - Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38."); - return false; - } - } - else if (type == 4) - { - var def = ReadAnimBillboardComponent(br, typeAndFlags); - - if (def.Reserved.Length != 0x3C) - { - Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C."); - return false; - } - } - else if (type == 5) - { - var def = ReadTrailComponent(br, typeAndFlags); - - if (def.Unknown04To10.Length != 0x10) - { - Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10."); - return false; - } - - if (def.TextureNameAndReserved.Length != 0x40) - { - Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40."); - return false; - } - } - else if (type == 6) - { - // Point components have no extra bytes beyond the 4-byte typeAndFlags header. - var def = ReadPointComponent(typeAndFlags); - } - else if (type == 7) - { - var def = ReadPlaneComponent(br, typeAndFlags); - - if (def.Base.Reserved.Length != 0x38) - { - Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38."); - return false; - } - } - else if (type == 8) - { - var def = ReadModelComponent(br, typeAndFlags); - - if (def.TextureNameAndFlags.Length != 0x40) - { - Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40."); - return false; - } - } - else if (type == 9) - { - var def = ReadAnimModelComponent(br, typeAndFlags); - - if (def.TextureNameAndFlags.Length != 0x48) - { - Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48."); - return false; - } - } - else if (type == 10) - { - var def = ReadCubeComponent(br, typeAndFlags); - - if (def.Base.Reserved.Length != 0x3C) - { - Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C."); - return false; - } - } - else - { - // Skip the remaining bytes for other component types. - fs.Position += remaining; - } - } - - // Dump a compact per-file summary of component types and counts. - var sb = new StringBuilder(); - bool first = true; - foreach (var kv in typeCounts) - { - if (!first) - { - sb.Append(", "); - } - sb.Append(kv.Key); - sb.Append('x'); - sb.Append(kv.Value); - first = false; - } - Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]"); - - if (fs.Position != fs.Length) - { - Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes)."); - return false; - } - - return true; - } - catch (Exception ex) - { - Console.WriteLine($"{path}: exception while parsing: {ex.Message}"); - return false; - } -} + using var fs = new FileStream(trfFile, FileMode.Open, FileAccess.Read, FileShare.Read); + + var nres = NResParser.ReadFile(trfFile); + + var resTree = ResTreeParser.Parse(nres.Archive!, fs); + Console.WriteLine(trfFile); + _ = 5; +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..744c51e --- /dev/null +++ b/Program.cs @@ -0,0 +1,146 @@ +using System.Buffers.Binary; +using MaterialLib; +using NResLib; +using ParkanPlayground; + +// ========== ANALYZE MATERIALS 72 AND 88 FROM LANDSCAPE 0B ========== + +// 1. Load Material.lib +var materialLibPath = @"E:\ParkanUnpacked\Material.lib.nres"; +if (!File.Exists(materialLibPath)) +{ + // Try alternative path + materialLibPath = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\Material.lib"; +} + +Console.WriteLine($"Loading Material.lib from: {materialLibPath}"); +var matLibResult = NResParser.ReadFile(materialLibPath); + +if (matLibResult.Archive is null) +{ + Console.WriteLine($"ERROR loading Material.lib: {matLibResult.Error}"); + Console.WriteLine("Trying to list available .lib files..."); + + // List what's available + var dataPath = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA"; + if (Directory.Exists(dataPath)) + { + foreach (var f in Directory.GetFiles(dataPath, "*.lib")) + { + Console.WriteLine($" Found: {f}"); + } + } + return; +} + +Console.WriteLine($"Material.lib loaded: {matLibResult.Archive.Files.Count} files\n"); + +// 2. Find materials by index (72 and 88) +using var matFs = new FileStream(materialLibPath, FileMode.Open, FileAccess.Read, FileShare.Read); + +var targetIds = new[] { 72, 88 }; + +foreach (var targetId in targetIds) +{ + // Material files are stored with their ID in the archive + var matEntry = matLibResult.Archive.Files.FirstOrDefault(f => f.Index == targetId); + + if (matEntry == null) + { + Console.WriteLine($"=== Material {targetId}: NOT FOUND ===\n"); + continue; + } + + Console.WriteLine($"=== Material {targetId} ==="); + Console.WriteLine($" Index: {matEntry.Index}"); + Console.WriteLine($" Name: {matEntry.FileName}"); + Console.WriteLine($" ElementCount (Version): {matEntry.ElementCount}"); + Console.WriteLine($" ElementSize (Magic1): {matEntry.ElementSize}"); + Console.WriteLine($" Offset: {matEntry.OffsetInFile}"); + Console.WriteLine($" Data size: {matEntry.ElementCount * matEntry.ElementSize} bytes"); + + // Parse the material + matFs.Seek(matEntry.OffsetInFile, SeekOrigin.Begin); + try + { + var material = MaterialParser.ReadFromStream( + matFs, + matEntry.FileName ?? $"MAT_{targetId}", + matEntry.ElementCount, + matEntry.ElementSize); + + Console.WriteLine($"\n Parsed Material:"); + Console.WriteLine($" Rendering Type: {material.MaterialRenderingType}"); + Console.WriteLine($" Supports Bump: {material.SupportsBumpMapping}"); + Console.WriteLine($" Source Blend: {material.SourceBlendMode}"); + Console.WriteLine($" Dest Blend: {material.DestBlendMode}"); + Console.WriteLine($" Stages: {material.Stages.Count}"); + Console.WriteLine($" Animations: {material.Animations.Count}"); + + for (int i = 0; i < material.Stages.Count; i++) + { + var stage = material.Stages[i]; + Console.WriteLine($"\n Stage {i}:"); + Console.WriteLine($" Texture: \"{stage.TextureName}\""); + Console.WriteLine($" TextureStageIndex: {stage.TextureStageIndex}"); + Console.WriteLine($" Diffuse: ({stage.DiffuseR:F2}, {stage.DiffuseG:F2}, {stage.DiffuseB:F2}, {stage.DiffuseA:F2})"); + Console.WriteLine($" Ambient: ({stage.AmbientR:F2}, {stage.AmbientG:F2}, {stage.AmbientB:F2}, {stage.AmbientA:F2})"); + } + } + catch (Exception ex) + { + Console.WriteLine($" ERROR parsing: {ex.Message}"); + } + + Console.WriteLine(); +} + +// 3. Also show .wea file contents for context +Console.WriteLine("=== WEA FILES (landscape materials) ==="); +var weaPath1 = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land1.wea"; +var weaPath2 = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land2.wea"; + +if (File.Exists(weaPath1)) +{ + Console.WriteLine($"\nLand1.wea:"); + foreach (var line in File.ReadAllLines(weaPath1)) + Console.WriteLine($" {line}"); +} + +if (File.Exists(weaPath2)) +{ + Console.WriteLine($"\nLand2.wea:"); + foreach (var line in File.ReadAllLines(weaPath2)) + Console.WriteLine($" {line}"); +} + +// 4. Find materials referenced in .wea by name +Console.WriteLine("\n=== MATERIALS REFERENCED IN WEA ==="); +var weaMatNames = new[] { "B_S0", "L04", "L02", "L00", "DEFAULT", "L05", "L03", "L01" }; + +foreach (var name in weaMatNames) +{ + var searchName = $"MAT0_{name}"; + var found = matLibResult.Archive.Files + .FirstOrDefault(f => f.FileName?.Contains(searchName, StringComparison.OrdinalIgnoreCase) == true); + + if (found != null) + { + Console.WriteLine($" {name,-10} -> Index {found.Index,3}: {found.FileName}"); + } + else + { + Console.WriteLine($" {name,-10} -> NOT FOUND"); + } +} + +Console.WriteLine("\nDone!"); + + + + + + + + + diff --git a/ResTreeLib/ResTreeLib.csproj b/ResTreeLib/ResTreeLib.csproj new file mode 100644 index 0000000..568da04 --- /dev/null +++ b/ResTreeLib/ResTreeLib.csproj @@ -0,0 +1,14 @@ + + + + net9.0 + enable + enable + + + + + + + + diff --git a/ResTreeLib/ResTreeParser.cs b/ResTreeLib/ResTreeParser.cs new file mode 100644 index 0000000..2b1763f --- /dev/null +++ b/ResTreeLib/ResTreeParser.cs @@ -0,0 +1,158 @@ +using System.Text; +using Common; +using NResLib; + +namespace ResTreeLib; + +public class ResTreeParser +{ + private static readonly Encoding CyrillicEncoding = CodePagesEncodingProvider.Instance.GetEncoding(1251) + ?? Encoding.GetEncoding("windows-1251"); + + public static List Parse(string filePath) + { + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + + var nres = NResParser.ReadFile(fs); + + fs.Seek(0, SeekOrigin.Begin); + + return Parse(nres.Archive!, fs); + } + + public static List Parse(NResArchive archive, Stream stream) + { + // Register provider for Windows-1251 if not already done globally + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + if (archive.Files.Any(x => x.FileType == "TRFB")) + { + _ = 5; + } + + // 1. Locate the Master Nodes (TRF0) + var trf0 = archive.Files.FirstOrDefault(f => f.FileType == "TRF0"); + if (trf0 == null) return new List(); + + // 2. Load the Node States (TRF1) - 1 byte per node + byte[] initialStates = LoadRawBuffer(stream, archive, "TRF1"); + + var nodes = new List(); + stream.Position = trf0.OffsetInFile; + for (int i = 0; i < trf0.ElementCount; i++) + { + nodes.Add(ReadTrf0Element(stream)); + } + + // 3. Load String Pools + byte[] shortNamesPool_trf7 = LoadRawBuffer(stream, archive, "TRF7"); + byte[] longNamesPool_trf8 = LoadRawBuffer(stream, archive, "TRF8"); + byte[] helpTextPool_trf9 = LoadRawBuffer(stream, archive, "TRF9"); + byte[] descriptionsPool_trfa = LoadRawBuffer(stream, archive, "TRFA"); + + // 4. Load Relationship Maps + // TRF2/3 = Prerequisites + var prereqMap = LoadRelationMap(stream, archive, "TRF2", "TRF3"); + // TRF4/5 = Unlocks/Effects + var unlockMap = LoadRelationMap(stream, archive, "TRF4", "TRF5"); + // TRFB/6 = unknown + var aux_trf6 = LoadRelationMap(stream, archive, "TRFB", "TRF6"); + + // 5. Assemble + var result = new List(); + for (int i = 0; i < nodes.Count; i++) + { + var element = nodes[i]; + result.Add(new ResearchNodeData + { + Index = i, + Node = element, + // Cast the TRF1 byte to our NodeState Flags + State = (initialStates.Length > i) ? (NodeState)initialStates[i] : NodeState.Hidden, + ShortName = GetStringFromPool(shortNamesPool_trf7, element.OffsetShortName_TRF7), + LongName = GetStringFromPool(longNamesPool_trf8, element.OffsetLongName_TRF8), + HelpText = GetStringFromPool(helpTextPool_trf9, element.OffsetHelpText_TRF9), + Description = GetStringFromPool(descriptionsPool_trfa, element.OffsetDescription_TRFA), + PrerequisiteIds = prereqMap.TryGetValue(i, out var pValue) ? pValue : [], + UnlockIds = unlockMap.TryGetValue(i, out var uValue) ? uValue : [] + }); + } + + return result; + } + + private static Trf0Element ReadTrf0Element(Stream s) + { + // Total size must be 40 bytes (0x28) + return new Trf0Element + { + ResearchCost = s.ReadFloatLittleEndian(), + ResearchTime = s.ReadFloatLittleEndian(), + ViewPosX = s.ReadFloatLittleEndian(), + ViewPosY = s.ReadFloatLittleEndian(), + OffsetShortName_TRF7 = s.ReadUInt32LittleEndian(), + OffsetLongName_TRF8 = s.ReadUInt32LittleEndian(), + OffsetHelpText_TRF9 = s.ReadUInt32LittleEndian(), + OffsetDescription_TRFA = s.ReadUInt32LittleEndian(), + OffsetAux_TRFB = s.ReadUInt16LittleEndian(), + TurretType = (byte)s.ReadByte(), + MainType = (byte)s.ReadByte(), + SubType = (byte)s.ReadByte(), + BuildSubSystem = (byte)s.ReadByte(), + SizeOfType = (byte)s.ReadByte(), + UpgradeLevel = (byte)s.ReadByte() + }; + } + + // LoadRelationMap and LoadRawBuffer remain the same as your provided code + private static Dictionary LoadRelationMap(Stream s, NResArchive archive, string headerType, string dataType) + { + var header = archive.Files.FirstOrDefault(f => f.FileType == headerType); + var data = archive.Files.FirstOrDefault(f => f.FileType == dataType); + var map = new Dictionary(); + + if (header == null || data == null) return map; + + uint[] counts = new uint[header.ElementCount]; + s.Position = header.OffsetInFile; + for (int i = 0; i < header.ElementCount; i++) + { + counts[i] = s.ReadUInt32LittleEndian(); + } + + s.Position = data.OffsetInFile; + for (int i = 0; i < counts.Length; i++) + { + uint count = counts[i]; + uint[] ids = new uint[count]; + for (int j = 0; j < count; j++) + { + ids[j] = s.ReadUInt32LittleEndian(); + } + map[i] = ids; + } + + return map; + } + + private static byte[] LoadRawBuffer(Stream s, NResArchive archive, string type) + { + var item = archive.Files.FirstOrDefault(f => f.FileType == type); + if (item == null) return []; + + byte[] buffer = new byte[item.FileLength]; + s.Position = item.OffsetInFile; + s.ReadExactly(buffer); + return buffer; + } + + private static string GetStringFromPool(byte[] pool, uint offset) + { + if (pool.Length == 0 || offset >= pool.Length) return string.Empty; + + int end = Array.IndexOf(pool, (byte)0, (int)offset); + int length = (end == -1) ? pool.Length - (int)offset : end - (int)offset; + + return CyrillicEncoding.GetString(pool, (int)offset, length); + } +} \ No newline at end of file diff --git a/ResTreeLib/ResearchNodeData.cs b/ResTreeLib/ResearchNodeData.cs new file mode 100644 index 0000000..c0bd2c5 --- /dev/null +++ b/ResTreeLib/ResearchNodeData.cs @@ -0,0 +1,27 @@ +namespace ResTreeLib; + +public record ResearchNodeData +{ + public int Index { get; init; } + public Trf0Element Node { get; init; } = null!; + public NodeState State { get; init; } // From TRF1 + public string ShortName { get; set; } = ""; + public string LongName { get; init; } = ""; + public string HelpText { get; set; } = ""; + public string Description { get; init; } = ""; + + /// + /// These IDs define what technologies a player must complete before the current node becomes available for research. + /// These are derived from TRF2 (which stores the count of parents) and TRF3 (which contains the flat list of parent IDs). + /// + /// + /// If Flamethrower Mk II has PrerequisiteIds = [18], then the node with index 18 (Flamethrower Mk I) must be finished first. + /// + public uint[] PrerequisiteIds { get; init; } = []; // From TRF2/3 + + /// + /// These IDs define the immediate "downstream" effects or technologies that are triggered when the current research is finished. + /// These are derived from TRF4 (the count of unlocks) and TRF5 (the flat list of target IDs). + /// + public uint[] UnlockIds { get; init; } = []; // From TRF4/5 +} diff --git a/ResTreeLib/Trf0Element.cs b/ResTreeLib/Trf0Element.cs new file mode 100644 index 0000000..163ce3a --- /dev/null +++ b/ResTreeLib/Trf0Element.cs @@ -0,0 +1,31 @@ +namespace ResTreeLib; + +// Result DTO for easier use + +public record Trf0Element +{ + public float ResearchCost; // field_0 + public float ResearchTime; // field_4 + public float ViewPosX; // field_8 + public float ViewPosY; // field_12 + public uint OffsetShortName_TRF7; // offset_into_TRF7 + public uint OffsetLongName_TRF8; // offset_into_TRF8 + public uint OffsetHelpText_TRF9; // offset_into_TRF9 + public uint OffsetDescription_TRFA; // offset_into_TRFA + public ushort OffsetAux_TRFB; // offset_into_TRFB + public byte TurretType; + public byte MainType; + public byte SubType; + public byte BuildSubSystem; + public byte SizeOfType; + public byte UpgradeLevel; +} + +[Flags] +public enum NodeState : byte +{ + Hidden = 0, + Available = 1 << 0, // 0x01 + Researched = 1 << 1, // 0x02 + Active = 1 << 2 // 0x04 - If not set, node is disabled +} \ No newline at end of file diff --git a/ScrLib/MissionTmaParseResult.cs b/ScrLib/ScrParseResult.cs similarity index 100% rename from ScrLib/MissionTmaParseResult.cs rename to ScrLib/ScrParseResult.cs