From d7eb23e9e06cac3bc9234c0b02d204a96ddf2d96 Mon Sep 17 00:00:00 2001 From: bird_egop Date: Wed, 26 Feb 2025 04:27:16 +0300 Subject: [PATCH] Implement SCR UI --- MissionTmaLib/ClanInfo.cs | 2 +- MissionTmaLib/Parsing/MissionTmaParser.cs | 2 +- NResUI/App.cs | 2 + NResUI/ImGuiUI/BinaryExplorerPanel.cs | 262 ++++++++++++++++++++++ NResUI/ImGuiUI/MainMenuBar.cs | 71 +++--- NResUI/ImGuiUI/MissionTmaExplorer.cs | 6 +- NResUI/ImGuiUI/ScrExplorer.cs | 98 ++++++++ NResUI/Models/BinaryExplorerViewModel.cs | 27 +++ NResUI/Models/ScrViewModel.cs | 20 ++ NResUI/NResUI.csproj | 1 + ParkanPlayground.sln | 6 + ParkanPlayground/Program.cs | 19 +- ScrLib/Extensions.cs | 39 ++++ ScrLib/MissionTmaParseResult.cs | 3 + ScrLib/ScrFile.cs | 43 ++++ ScrLib/ScrLib.csproj | 9 + ScrLib/ScrParser.cs | 55 +++++ 17 files changed, 616 insertions(+), 49 deletions(-) create mode 100644 NResUI/ImGuiUI/BinaryExplorerPanel.cs create mode 100644 NResUI/ImGuiUI/ScrExplorer.cs create mode 100644 NResUI/Models/BinaryExplorerViewModel.cs create mode 100644 NResUI/Models/ScrViewModel.cs create mode 100644 ScrLib/Extensions.cs create mode 100644 ScrLib/MissionTmaParseResult.cs create mode 100644 ScrLib/ScrFile.cs create mode 100644 ScrLib/ScrLib.csproj create mode 100644 ScrLib/ScrParser.cs diff --git a/MissionTmaLib/ClanInfo.cs b/MissionTmaLib/ClanInfo.cs index fe1ec8a..b53955c 100644 --- a/MissionTmaLib/ClanInfo.cs +++ b/MissionTmaLib/ClanInfo.cs @@ -12,7 +12,7 @@ public class ClanInfo /// public ClanType ClanType { get; set; } - public string UnkString2 { get; set; } + public string ScriptsString { get; set; } public int UnknownClanPartCount { get; set; } public List UnknownParts { get; set; } diff --git a/MissionTmaLib/Parsing/MissionTmaParser.cs b/MissionTmaLib/Parsing/MissionTmaParser.cs index fef65c7..03abcd3 100644 --- a/MissionTmaLib/Parsing/MissionTmaParser.cs +++ b/MissionTmaLib/Parsing/MissionTmaParser.cs @@ -78,7 +78,7 @@ public class MissionTmaParser // MISSIONS\SCRIPTS\default // MISSIONS\SCRIPTS\tut1_pl // MISSIONS\SCRIPTS\tut1_en - clanTreeInfo.UnkString2 = fileStream.ReadLengthPrefixedString(); + clanTreeInfo.ScriptsString = fileStream.ReadLengthPrefixedString(); } if (2 < clanFeatureSet) diff --git a/NResUI/App.cs b/NResUI/App.cs index 8cdc248..2e413f0 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -53,6 +53,8 @@ public class App serviceCollection.AddSingleton(new NResExplorerViewModel()); serviceCollection.AddSingleton(new TexmExplorerViewModel()); serviceCollection.AddSingleton(new MissionTmaViewModel()); + serviceCollection.AddSingleton(new BinaryExplorerViewModel()); + serviceCollection.AddSingleton(new ScrViewModel()); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/NResUI/ImGuiUI/BinaryExplorerPanel.cs b/NResUI/ImGuiUI/BinaryExplorerPanel.cs new file mode 100644 index 0000000..799544a --- /dev/null +++ b/NResUI/ImGuiUI/BinaryExplorerPanel.cs @@ -0,0 +1,262 @@ +using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Text; +using System.Text.Json; +using ImGuiNET; +using NativeFileDialogSharp; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI; + +public class BinaryExplorerPanel : IImGuiPanel +{ + private readonly BinaryExplorerViewModel _viewModel; + + public BinaryExplorerPanel(BinaryExplorerViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + return; + if (ImGui.Begin("Binary Explorer")) + { + if (ImGui.Button("Open File")) + { + OpenFile(); + } + + if (_viewModel.HasFile) + { + ImGui.SameLine(); + ImGui.Text(_viewModel.Path); + + if (ImGui.Button("Сохранить регионы")) + { + File.WriteAllText("preset.json", JsonSerializer.Serialize(_viewModel.Regions)); + } + + ImGui.SameLine(); + if (ImGui.Button("Загрузить регионы")) + { + _viewModel.Regions = JsonSerializer.Deserialize>(File.ReadAllText("preset.json"))!; + } + + const int bytesPerRow = 16; + if (ImGui.BeginTable("HexTable", bytesPerRow + 1, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.WidthFixed); + for (var i = 0; i < bytesPerRow; i++) + { + ImGui.TableSetupColumn(i.ToString()); + } + + ImGui.TableHeadersRow(); + for (int i = 0; i < _viewModel.Data.Length; i += bytesPerRow) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{i:x8} "); + + for (int j = 0; j < 16; j++) + { + var index = i + j; + ImGui.TableNextColumn(); + if (index < _viewModel.Data.Length) + { + uint? regionColor = GetRegionColor(i + j); + + if (regionColor is not null) + { + ImGui.PushStyleColor(ImGuiCol.Header, regionColor.Value); + } + + if (ImGui.Selectable($"{_viewModel.Data[i + j]}##sel{i + j}", regionColor.HasValue)) + { + HandleRegionSelect(i + j); + } + + if (regionColor is not null) + { + ImGui.PopStyleColor(); + } + } + else + { + ImGui.Text(" "); + } + } + } + + ImGui.EndTable(); + } + + ImGui.SameLine(); + + ImGui.SetNextItemWidth(200f); + if (ImGui.ColorPicker4("NextColor", ref _viewModel.NextColor, ImGuiColorEditFlags.Float)) + { + } + + if (ImGui.BeginTable("Регионы", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Номер"); + ImGui.TableSetupColumn("Старт"); + ImGui.TableSetupColumn("Длина"); + ImGui.TableSetupColumn("Значение"); + ImGui.TableSetupColumn("Действия"); + + ImGui.TableHeadersRow(); + + for (int k = 0; k < _viewModel.Regions.Count; k++) + { + var region = _viewModel.Regions[k]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(k.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(region.Begin.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(region.Length.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(region.Value ?? "unknown"); + + ImGui.TableNextColumn(); + if (ImGui.Button($"float##f{k}")) + { + region.Value = BinaryPrimitives.ReadSingleLittleEndian(_viewModel.Data.AsSpan()[region.Begin..(region.Begin + region.Length)]) + .ToString("F2"); + } + + ImGui.SameLine(); + if (ImGui.Button($"int##i{k}")) + { + region.Value = BinaryPrimitives.ReadInt32LittleEndian(_viewModel.Data.AsSpan()[region.Begin..(region.Begin + region.Length)]) + .ToString(); + } + + ImGui.SameLine(); + if (ImGui.Button($"ASCII##a{k}")) + { + region.Value = Encoding.ASCII.GetString(_viewModel.Data.AsSpan()[region.Begin..(region.Begin + region.Length)]); + } + + ImGui.SameLine(); + if (ImGui.Button($"raw##r{k}")) + { + region.Value = string.Join( + "", + _viewModel.Data[region.Begin..(region.Begin + region.Length)] + .Select(x => x.ToString("x2")) + ); + } + } + + ImGui.EndTable(); + } + } + + ImGui.End(); + } + } + + private uint? GetRegionColor(int index) + { + Region? inRegion = _viewModel.Regions.Find(x => x.Begin <= index && x.Begin + x.Length > index); + + return inRegion?.Color; + } + + private void HandleRegionSelect(int index) + { + Region? inRegion = _viewModel.Regions.FirstOrDefault(x => x.Begin <= index && index < x.Begin + x.Length); + + if (inRegion is null) + { + // not in region + Region? prependRegion; + Region? appendRegion; + + if ((prependRegion = _viewModel.Regions.Find(x => x.Begin + x.Length == index)) is not null) + { + if (prependRegion.Color == GetImGuiColor(_viewModel.NextColor)) + { + prependRegion.Length += 1; + return; + } + } + + if ((appendRegion = _viewModel.Regions.Find(x => x.Begin - 1 == index)) is not null) + { + if (appendRegion.Color == GetImGuiColor(_viewModel.NextColor)) + { + appendRegion.Begin--; + appendRegion.Length += 1; + return; + } + } + + var color = ImGui.ColorConvertFloat4ToU32(_viewModel.NextColor); + + color = unchecked((uint) (0xFF << 24)) | color; + _viewModel.Regions.Add( + new Region() + { + Begin = index, + Length = 1, + Color = color + } + ); + } + else + { + if (inRegion.Length == 1) + { + _viewModel.Regions.Remove(inRegion); + } + else + { + if (inRegion.Begin == index) + { + inRegion.Begin++; + inRegion.Length--; + } + + if (inRegion.Begin + inRegion.Length - 1 == index) + { + inRegion.Length--; + } + } + } + } + + private static uint GetImGuiColor(Vector4 vec) + { + var color = ImGui.ColorConvertFloat4ToU32(vec); + + color = unchecked((uint) (0xFF << 24)) | color; + + return color; + } + + private bool OpenFile() + { + var result = Dialog.FileOpen("*"); + + if (result.IsOk) + { + var path = result.Path; + + var bytes = File.ReadAllBytes(path); + _viewModel.HasFile = true; + _viewModel.Data = bytes; + _viewModel.Path = path; + } + + return false; + } +} \ No newline at end of file diff --git a/NResUI/ImGuiUI/MainMenuBar.cs b/NResUI/ImGuiUI/MainMenuBar.cs index 3025482..000eb05 100644 --- a/NResUI/ImGuiUI/MainMenuBar.cs +++ b/NResUI/ImGuiUI/MainMenuBar.cs @@ -6,25 +6,19 @@ using NativeFileDialogSharp; using NResLib; using NResUI.Abstractions; using NResUI.Models; +using ScrLib; using TexmLib; namespace NResUI.ImGuiUI { - public class MainMenuBar : IImGuiPanel + public class MainMenuBar( + NResExplorerViewModel nResExplorerViewModel, + TexmExplorerViewModel texmExplorerViewModel, + ScrViewModel scrViewModel, + MissionTmaViewModel missionTmaViewModel, + MessageBoxModalPanel messageBox) + : IImGuiPanel { - private readonly NResExplorerViewModel _nResExplorerViewModel; - private readonly TexmExplorerViewModel _texmExplorerViewModel; - private readonly MissionTmaViewModel _missionTmaViewModel; - - private readonly MessageBoxModalPanel _messageBox; - public MainMenuBar(NResExplorerViewModel nResExplorerViewModel, TexmExplorerViewModel texmExplorerViewModel, MessageBoxModalPanel messageBox, MissionTmaViewModel missionTmaViewModel) - { - _nResExplorerViewModel = nResExplorerViewModel; - _texmExplorerViewModel = texmExplorerViewModel; - _messageBox = messageBox; - _missionTmaViewModel = missionTmaViewModel; - } - public void OnImGuiRender() { if (ImGui.BeginMenuBar()) @@ -41,7 +35,7 @@ namespace NResUI.ImGuiUI var parseResult = NResParser.ReadFile(path); - _nResExplorerViewModel.SetParseResult(parseResult, path); + nResExplorerViewModel.SetParseResult(parseResult, path); Console.WriteLine("Read NRES"); } } @@ -55,10 +49,10 @@ namespace NResUI.ImGuiUI var path = result.Path; using var fs = new FileStream(path, FileMode.Open); - + var parseResult = TexmParser.ReadFromStream(fs, path); - _texmExplorerViewModel.SetParseResult(parseResult, path); + texmExplorerViewModel.SetParseResult(parseResult, path); Console.WriteLine("Read TEXM"); } } @@ -74,11 +68,11 @@ namespace NResUI.ImGuiUI using var fs = new FileStream(path, FileMode.Open); fs.Seek(4116, SeekOrigin.Begin); - + var parseResult = TexmParser.ReadFromStream(fs, path); - _texmExplorerViewModel.SetParseResult(parseResult, path); - Console.WriteLine("Read TEXM"); + texmExplorerViewModel.SetParseResult(parseResult, path); + Console.WriteLine("Read TFNT TEXM"); } } @@ -90,21 +84,27 @@ namespace NResUI.ImGuiUI { var path = result.Path; var parseResult = MissionTmaParser.ReadFile(path); - - _missionTmaViewModel.SetParseResult(parseResult, path); - var orderedBuildings = parseResult.Mission.GameObjectsData.GameObjectInfos.Where(x => x.Type == GameObjectType.Building) - .OrderBy(x => x.Order) - .ToList(); - - foreach (var gameObjectInfo in orderedBuildings) - { - Console.WriteLine($"{gameObjectInfo.Order} : {gameObjectInfo.DatString}"); - } + missionTmaViewModel.SetParseResult(parseResult, path); + Console.WriteLine("Read TMA"); } } - if (_nResExplorerViewModel.HasFile) + if (ImGui.MenuItem("Open SCR Scripts File")) + { + var result = Dialog.FileOpen("scr"); + + if (result.IsOk) + { + var path = result.Path; + var parseResult = ScrParser.ReadFile(path); + + scrViewModel.SetParseResult(parseResult, path); + Console.WriteLine("Read SCR"); + } + } + + if (nResExplorerViewModel.HasFile) { if (ImGui.MenuItem("Экспортировать NRes")) { @@ -113,10 +113,10 @@ namespace NResUI.ImGuiUI if (result.IsOk) { var path = result.Path; - - NResExporter.Export(_nResExplorerViewModel.Archive!, path, _nResExplorerViewModel.Path!); - - _messageBox.Show("Успешно экспортировано"); + + NResExporter.Export(nResExplorerViewModel.Archive!, path, nResExplorerViewModel.Path!); + + messageBox.Show("Успешно экспортировано"); } } } @@ -127,6 +127,5 @@ namespace NResUI.ImGuiUI ImGui.EndMenuBar(); } } - } } \ No newline at end of file diff --git a/NResUI/ImGuiUI/MissionTmaExplorer.cs b/NResUI/ImGuiUI/MissionTmaExplorer.cs index f4848ec..5257e41 100644 --- a/NResUI/ImGuiUI/MissionTmaExplorer.cs +++ b/NResUI/ImGuiUI/MissionTmaExplorer.cs @@ -132,10 +132,10 @@ public class MissionTmaExplorer : IImGuiPanel ImGui.SameLine(); ImGui.Text(clanInfo.ClanType.ToReadableString()); - ImGui.Text("Неизвестная строка 1: "); - Utils.ShowHint("Кажется это путь к файлу поведения (Behavior), но пока не понятно. Обычно пути соответствуют 2 файла."); + ImGui.Text("Скрипты поведения: "); + Utils.ShowHint("Пути к файлам .scr и .fml описывающих настройку объектов и поведение AI"); ImGui.SameLine(); - ImGui.Text(clanInfo.UnkString2); + ImGui.Text(clanInfo.ScriptsString); if (clanInfo.UnknownParts.Count > 0) { diff --git a/NResUI/ImGuiUI/ScrExplorer.cs b/NResUI/ImGuiUI/ScrExplorer.cs new file mode 100644 index 0000000..f064f8b --- /dev/null +++ b/NResUI/ImGuiUI/ScrExplorer.cs @@ -0,0 +1,98 @@ +using ImGuiNET; +using NResUI.Abstractions; +using NResUI.Models; + +namespace NResUI.ImGuiUI; + +public class ScrExplorer : IImGuiPanel +{ + private readonly ScrViewModel _viewModel; + + public ScrExplorer(ScrViewModel viewModel) + { + _viewModel = viewModel; + } + + public void OnImGuiRender() + { + if (ImGui.Begin("SCR Explorer")) + { + var scr = _viewModel.Scr; + if (_viewModel.HasFile && scr is not null) + { + ImGui.Text("Магия: "); + Utils.ShowHint("тут всегда число 59 (0x3b) - это число известных игре скриптов"); + ImGui.SameLine(); + ImGui.Text(scr.Magic.ToString()); + + ImGui.Text("Кол-во секций: "); + ImGui.SameLine(); + ImGui.Text(scr.EntryCount.ToString()); + + if (ImGui.TreeNodeEx("Секции")) + { + for (var i = 0; i < scr.Entries.Count; i++) + { + var entry = scr.Entries[i]; + if (ImGui.TreeNodeEx($"Секция {i} - \"{entry.Title}\"")) + { + ImGui.Text("Индекс: "); + ImGui.SameLine(); + ImGui.Text(entry.Index.ToString()); + + ImGui.Text("Кол-во элементов: "); + ImGui.SameLine(); + ImGui.Text(entry.InnerCount.ToString()); + + if (ImGui.BeginTable($"Элементы##{i:0000}", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + { + ImGui.TableSetupColumn("Индекс скрипта"); + ImGui.TableSetupColumn("UnkInner2"); + ImGui.TableSetupColumn("UnkInner3"); + ImGui.TableSetupColumn("UnkInner4"); + ImGui.TableSetupColumn("UnkInner5"); + ImGui.TableSetupColumn("Кол-во аргументов"); + ImGui.TableSetupColumn("Аргументы"); + ImGui.TableSetupColumn("UnkInner7"); + ImGui.TableHeadersRow(); + + for (int j = 0; j < entry.Inners.Count; j++) + { + var inner = entry.Inners[j]; + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text(inner.ScriptIndex.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(inner.UnkInner2.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(inner.UnkInner3.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(inner.UnkInner4.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(inner.UnkInner5.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(inner.ArgumentsCount.ToString()); + ImGui.TableNextColumn(); + ImGui.Text(string.Join(", ", inner.Arguments)); + ImGui.TableNextColumn(); + ImGui.Text(inner.UnkInner7.ToString()); + } + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + } + + ImGui.TreePop(); + } + } + else + { + ImGui.Text("SCR не открыт"); + } + + ImGui.End(); + } + } +} \ No newline at end of file diff --git a/NResUI/Models/BinaryExplorerViewModel.cs b/NResUI/Models/BinaryExplorerViewModel.cs new file mode 100644 index 0000000..22fe32c --- /dev/null +++ b/NResUI/Models/BinaryExplorerViewModel.cs @@ -0,0 +1,27 @@ +using System.Numerics; + +namespace NResUI.Models; + +public class BinaryExplorerViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public string Path { get; set; } = ""; + public byte[] Data { get; set; } = []; + + public List Regions { get; set; } = []; + + public Vector4 NextColor; +} + +public class Region +{ + public int Begin { get; set; } + + public int Length { get; set; } + + public uint Color { get; set; } + + public string? Value; +} \ No newline at end of file diff --git a/NResUI/Models/ScrViewModel.cs b/NResUI/Models/ScrViewModel.cs new file mode 100644 index 0000000..ab5e4c5 --- /dev/null +++ b/NResUI/Models/ScrViewModel.cs @@ -0,0 +1,20 @@ +using ScrLib; + +namespace NResUI.Models; + +public class ScrViewModel +{ + public bool HasFile { get; set; } + public string? Error { get; set; } + + public ScrFile? Scr { get; set; } + + public string? Path { get; set; } + + public void SetParseResult(ScrFile scrFile, string path) + { + Scr = scrFile; + HasFile = true; + Path = path; + } +} \ No newline at end of file diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj index f441061..1656b84 100644 --- a/NResUI/NResUI.csproj +++ b/NResUI/NResUI.csproj @@ -21,6 +21,7 @@ + diff --git a/ParkanPlayground.sln b/ParkanPlayground.sln index 1116823..1a55c8f 100644 --- a/ParkanPlayground.sln +++ b/ParkanPlayground.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{BAF212FE-A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionTmaLib", "MissionTmaLib\MissionTmaLib.csproj", "{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScrLib", "ScrLib\ScrLib.csproj", "{C445359B-97D4-4432-9331-708B5A14887A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -65,5 +67,9 @@ Global {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU {773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Release|Any CPU.Build.0 = Release|Any CPU + {C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index f2d1bef..78a4b2f 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -3,7 +3,10 @@ using System.Text; using NResLib; using ParkanPlayground; -var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\11p.scr"; +// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr"; +// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scr_pl_1.scr"; +// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream.scr"; +var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream1.scr"; using var fs = new FileStream(path, FileMode.Open); @@ -32,25 +35,25 @@ for (var i = 0; i < entryCount; i++) Console.WriteLine($"\tInnerCount: {innerCount}"); for (var i1 = 0; i1 < innerCount; i1++) { - var unkInner1 = fs.ReadInt32LittleEndian(); + var scriptIndex = fs.ReadInt32LittleEndian(); var unkInner2 = fs.ReadInt32LittleEndian(); var unkInner3 = fs.ReadInt32LittleEndian(); var unkInner4 = fs.ReadInt32LittleEndian(); var unkInner5 = fs.ReadInt32LittleEndian(); - Console.WriteLine($"\t\tUnkInner1: {unkInner1}"); + Console.WriteLine($"\t\tScriptIndex: {scriptIndex}"); Console.WriteLine($"\t\tUnkInner2: {unkInner2}"); Console.WriteLine($"\t\tUnkInner3: {unkInner3}"); Console.WriteLine($"\t\tUnkInner4: {unkInner4}"); Console.WriteLine($"\t\tUnkInner5: {unkInner5}"); - var innerInnerCount = fs.ReadInt32LittleEndian(); - Console.WriteLine($"\t\tInnerInnerCount: {innerInnerCount}"); + var scriptArgumentsCount = fs.ReadInt32LittleEndian(); + Console.WriteLine($"\t\tScript Arguments Count: {scriptArgumentsCount}"); - for (var i2 = 0; i2 < innerInnerCount; i2++) + for (var i2 = 0; i2 < scriptArgumentsCount; i2++) { - var innerInner = fs.ReadInt32LittleEndian(); - Console.WriteLine($"\t\t\t{innerInner}"); + var scriptArgument = fs.ReadInt32LittleEndian(); + Console.WriteLine($"\t\t\t{scriptArgument}"); } var unkInner7 = fs.ReadInt32LittleEndian(); diff --git a/ScrLib/Extensions.cs b/ScrLib/Extensions.cs new file mode 100644 index 0000000..6880051 --- /dev/null +++ b/ScrLib/Extensions.cs @@ -0,0 +1,39 @@ +using System.Buffers.Binary; +using System.Text; + +namespace ScrLib; + +public static class Extensions +{ + public static int ReadInt32LittleEndian(this FileStream fs) + { + Span buf = stackalloc byte[4]; + fs.ReadExactly(buf); + + return BinaryPrimitives.ReadInt32LittleEndian(buf); + } + + public static float ReadFloatLittleEndian(this FileStream fs) + { + Span buf = stackalloc byte[4]; + fs.ReadExactly(buf); + + return BinaryPrimitives.ReadSingleLittleEndian(buf); + } + + public static string ReadLengthPrefixedString(this FileStream fs) + { + var len = fs.ReadInt32LittleEndian(); + + if (len == 0) + { + return ""; + } + + var buffer = new byte[len]; + + fs.ReadExactly(buffer, 0, len); + + return Encoding.ASCII.GetString(buffer, 0, len); + } +} \ No newline at end of file diff --git a/ScrLib/MissionTmaParseResult.cs b/ScrLib/MissionTmaParseResult.cs new file mode 100644 index 0000000..4c045c7 --- /dev/null +++ b/ScrLib/MissionTmaParseResult.cs @@ -0,0 +1,3 @@ +namespace ScrLib; + +public record ScrParseResult(ScrFile? Scr, string? Error); \ No newline at end of file diff --git a/ScrLib/ScrFile.cs b/ScrLib/ScrFile.cs new file mode 100644 index 0000000..1ff618d --- /dev/null +++ b/ScrLib/ScrFile.cs @@ -0,0 +1,43 @@ +namespace ScrLib; + +public class ScrFile +{ + /// + /// тут всегда число 59 (0x3b) - это число известных игре скриптов + /// + public int Magic { get; set; } + + public int EntryCount { get; set; } + + public List Entries { get; set; } +} + +public class ScrEntry +{ + public string Title { get; set; } + + public int Index { get; set; } + + public int InnerCount { get; set; } + + public List Inners { get; set; } +} + +public class ScrEntryInner +{ + /// + /// Номер скрипта в игре (это тех, которых 0x3b) + /// + public int ScriptIndex { get; set; } + + public int UnkInner2 { get; set; } + public int UnkInner3 { get; set; } + public int UnkInner4 { get; set; } + public int UnkInner5 { get; set; } + + public int ArgumentsCount { get; set; } + + public List Arguments { get; set; } + + public int UnkInner7 { get; set; } +} \ No newline at end of file diff --git a/ScrLib/ScrLib.csproj b/ScrLib/ScrLib.csproj new file mode 100644 index 0000000..3a63532 --- /dev/null +++ b/ScrLib/ScrLib.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/ScrLib/ScrParser.cs b/ScrLib/ScrParser.cs new file mode 100644 index 0000000..c8b0b0a --- /dev/null +++ b/ScrLib/ScrParser.cs @@ -0,0 +1,55 @@ +namespace ScrLib; + +public class ScrParser +{ + public static ScrFile ReadFile(string filePath) + { + var fs = new FileStream(filePath, FileMode.Open); + + var scrFile = new ScrFile(); + + scrFile.Magic = fs.ReadInt32LittleEndian(); + + scrFile.EntryCount = fs.ReadInt32LittleEndian(); + scrFile.Entries = []; + + for (var i = 0; i < scrFile.EntryCount; i++) + { + var entry = new ScrEntry(); + entry.Title = fs.ReadLengthPrefixedString(); + + // тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char* + fs.ReadByte(); + + entry.Index = fs.ReadInt32LittleEndian(); + entry.InnerCount = fs.ReadInt32LittleEndian(); + entry.Inners = []; + for (var i1 = 0; i1 < entry.InnerCount; i1++) + { + var entryInner = new ScrEntryInner(); + entryInner.ScriptIndex = fs.ReadInt32LittleEndian(); + + entryInner.UnkInner2 = fs.ReadInt32LittleEndian(); + entryInner.UnkInner3 = fs.ReadInt32LittleEndian(); + entryInner.UnkInner4 = fs.ReadInt32LittleEndian(); + entryInner.UnkInner5 = fs.ReadInt32LittleEndian(); + + entryInner.ArgumentsCount = fs.ReadInt32LittleEndian(); + + entryInner.Arguments = []; + + for (var i2 = 0; i2 < entryInner.ArgumentsCount; i2++) + { + entryInner.Arguments.Add(fs.ReadInt32LittleEndian()); + } + + entryInner.UnkInner7 = fs.ReadInt32LittleEndian(); + entry.Inners.Add(entryInner); + } + + scrFile.Entries.Add(entry); + } + + return scrFile; + } +} \ No newline at end of file