From a774db37a65001abe431bb7a633b879754500ddd Mon Sep 17 00:00:00 2001 From: bird_egop Date: Sun, 5 Oct 2025 18:17:18 +0300 Subject: [PATCH] improvements --- Common/Extensions.cs | 28 +++- CpDatLib/CpDatEntry.cs | 23 ++- CpDatLib/CpDatParser.cs | 28 ++-- MissionTmaLib/Parsing/MissionTmaParser.cs | 11 +- NResLib/NResParser.cs | 5 + NResUI/App.cs | 3 + NResUI/ImGuiUI/CpDatSchemeExplorer.cs | 48 ++++++- NResUI/ImGuiUI/NResExplorerPanel.cs | 167 +++++++++++++++++++++- NResUI/Models/CpDatSchemeViewModel.cs | 30 ++-- NResUI/NResUI.csproj | 5 +- ParkanPlayground/Msh02.cs | 39 ++--- ParkanPlayground/Msh15.cs | 55 +++++++ ParkanPlayground/MshConverter.cs | 2 + ParkanPlayground/Program.cs | 4 +- README.md | 3 +- ScrLib/ScrParser.cs | 17 ++- VarsetLib/VarsetParser.cs | 132 +++++++++-------- 17 files changed, 472 insertions(+), 128 deletions(-) create mode 100644 ParkanPlayground/Msh15.cs diff --git a/Common/Extensions.cs b/Common/Extensions.cs index 4ab4f00..0785ded 100644 --- a/Common/Extensions.cs +++ b/Common/Extensions.cs @@ -5,7 +5,7 @@ namespace Common; public static class Extensions { - public static int ReadInt32LittleEndian(this FileStream fs) + public static int ReadInt32LittleEndian(this Stream fs) { Span buf = stackalloc byte[4]; fs.ReadExactly(buf); @@ -13,7 +13,7 @@ public static class Extensions return BinaryPrimitives.ReadInt32LittleEndian(buf); } - public static uint ReadUInt32LittleEndian(this FileStream fs) + public static uint ReadUInt32LittleEndian(this Stream fs) { Span buf = stackalloc byte[4]; fs.ReadExactly(buf); @@ -21,7 +21,7 @@ public static class Extensions return BinaryPrimitives.ReadUInt32LittleEndian(buf); } - public static float ReadFloatLittleEndian(this FileStream fs) + public static float ReadFloatLittleEndian(this Stream fs) { Span buf = stackalloc byte[4]; fs.ReadExactly(buf); @@ -29,7 +29,7 @@ public static class Extensions return BinaryPrimitives.ReadSingleLittleEndian(buf); } - public static string ReadNullTerminatedString(this FileStream fs) + public static string ReadNullTerminatedString(this Stream fs) { var sb = new StringBuilder(); @@ -47,7 +47,25 @@ public static class Extensions return sb.ToString(); } - public static string ReadLengthPrefixedString(this FileStream fs) + public static string ReadNullTerminated1251String(this Stream fs) + { + var sb = new StringBuilder(); + + while (true) + { + var b = (byte)fs.ReadByte(); + if (b == 0) + { + break; + } + + sb.Append(Encoding.GetEncoding("windows-1251").GetString([b])); + } + + return sb.ToString(); + } + + public static string ReadLengthPrefixedString(this Stream fs) { var len = fs.ReadInt32LittleEndian(); diff --git a/CpDatLib/CpDatEntry.cs b/CpDatLib/CpDatEntry.cs index 435f881..6bfd255 100644 --- a/CpDatLib/CpDatEntry.cs +++ b/CpDatLib/CpDatEntry.cs @@ -6,7 +6,26 @@ public record CpDatEntry( int Magic1, int Magic2, string Description, - int Magic3, + DatEntryType Type, int ChildCount, // игра не хранит это число в объекте, но оно есть в файле List Children -); \ No newline at end of file +); + +// Magic3 seems to be a type +// 0 - chassis +// 1 - turret (у зданий почему-то дефлектор тоже 1), может быть потому, что дефлектор вращается так же как башня у юнитов +// 2 - armour +// 3 - part +// 4 - cannon +// 5 - ammo + +public enum DatEntryType +{ + Unspecified = -1, + Chassis = 0, + Turret = 1, + Armour = 2, + Part = 3, + Cannon = 4, + Ammo = 5, +} \ No newline at end of file diff --git a/CpDatLib/CpDatParser.cs b/CpDatLib/CpDatParser.cs index 547beda..27c2dba 100644 --- a/CpDatLib/CpDatParser.cs +++ b/CpDatLib/CpDatParser.cs @@ -6,23 +6,29 @@ public class CpDatParser { public static CpDatParseResult Parse(string filePath) { - Span f0f1 = stackalloc byte[4]; - using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return Parse(fs); + } + + public static CpDatParseResult Parse(Stream fs) + { + Span f0f1 = stackalloc byte[4]; + if (fs.Length < 8) return new CpDatParseResult(null, "File too small to be a valid \"cp\" .dat file."); fs.ReadExactly(f0f1); - + if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0) { return new CpDatParseResult(null, "File does not start with expected header bytes f1_f0"); } - + var schemeType = (SchemeType)fs.ReadInt32LittleEndian(); - var entryLength = 0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов + var entryLength = + 0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов if ((fs.Length - 8) % entryLength != 0) { return new CpDatParseResult(null, "File size is not valid according to expected entry length."); @@ -35,7 +41,7 @@ public class CpDatParser return new CpDatParseResult(scheme, null); } - private static CpDatEntry ReadEntryRecursive(FileStream fs) + private static CpDatEntry ReadEntryRecursive(Stream fs) { var str1 = fs.ReadNullTerminatedString(); @@ -47,22 +53,22 @@ public class CpDatParser var magic1 = fs.ReadInt32LittleEndian(); var magic2 = fs.ReadInt32LittleEndian(); - var descriptionString = fs.ReadNullTerminatedString(); + var descriptionString = fs.ReadNullTerminated1251String(); fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator - var magic3 = fs.ReadInt32LittleEndian(); + var type = (DatEntryType)fs.ReadInt32LittleEndian(); // игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов var childCount = fs.ReadInt32LittleEndian(); - + List children = new List(childCount); - + for (var i = 0; i < childCount; i++) { var child = ReadEntryRecursive(fs); children.Add(child); } - return new CpDatEntry(str1, str2, magic1, magic2, descriptionString, magic3, childCount, Children: children); + return new CpDatEntry(str1, str2, magic1, magic2, descriptionString, type, childCount, Children: children); } } \ No newline at end of file diff --git a/MissionTmaLib/Parsing/MissionTmaParser.cs b/MissionTmaLib/Parsing/MissionTmaParser.cs index 3463df8..c05ee22 100644 --- a/MissionTmaLib/Parsing/MissionTmaParser.cs +++ b/MissionTmaLib/Parsing/MissionTmaParser.cs @@ -8,6 +8,11 @@ public class MissionTmaParser { using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return ReadFile(fs); + } + + public static MissionTmaParseResult ReadFile(Stream fs) + { var arealData = LoadAreals(fs); var clansData = LoadClans(fs); @@ -20,7 +25,7 @@ public class MissionTmaParser return new MissionTmaParseResult(missionDat, null); } - private static ArealsFileData LoadAreals(FileStream fileStream) + private static ArealsFileData LoadAreals(Stream fileStream) { var unusedHeader = fileStream.ReadInt32LittleEndian(); var arealCount = fileStream.ReadInt32LittleEndian(); @@ -56,7 +61,7 @@ public class MissionTmaParser return new ArealsFileData(unusedHeader, arealCount, infos); } - private static ClansFileData? LoadClans(FileStream fileStream) + private static ClansFileData? LoadClans(Stream fileStream) { var clanFeatureSet = fileStream.ReadInt32LittleEndian(); @@ -158,7 +163,7 @@ public class MissionTmaParser return clanInfo; } - private static GameObjectsFileData LoadGameObjects(FileStream fileStream) + private static GameObjectsFileData LoadGameObjects(Stream fileStream) { var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian(); diff --git a/NResLib/NResParser.cs b/NResLib/NResParser.cs index 1da8854..e15baac 100644 --- a/NResLib/NResParser.cs +++ b/NResLib/NResParser.cs @@ -9,6 +9,11 @@ public static class NResParser { using FileStream nResFs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + return ReadFile(nResFs); + } + + public static NResParseResult ReadFile(Stream nResFs) + { if (nResFs.Length < 16) { return new NResParseResult(null, "Файл не может быть NRes, менее 16 байт"); diff --git a/NResUI/App.cs b/NResUI/App.cs index dd327ea..bda3894 100644 --- a/NResUI/App.cs +++ b/NResUI/App.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Text; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using NResUI.Abstractions; @@ -33,6 +34,8 @@ public class App public void Init(IWindow window, GL openGl, ImFontPtr openSansFont) { + // Call this once at program startup + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); ImGui.StyleColorsLight(); IServiceCollection serviceCollection = new ServiceCollection(); diff --git a/NResUI/ImGuiUI/CpDatSchemeExplorer.cs b/NResUI/ImGuiUI/CpDatSchemeExplorer.cs index 6c13c7e..e56a814 100644 --- a/NResUI/ImGuiUI/CpDatSchemeExplorer.cs +++ b/NResUI/ImGuiUI/CpDatSchemeExplorer.cs @@ -34,22 +34,60 @@ public class CpDatSchemeExplorer : IImGuiPanel ImGui.Separator(); - if (ImGui.BeginTable("content", 7, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + if (ImGui.BeginTable("content", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.Sortable)) { + ImGui.TableSetupColumn("Индекс"); ImGui.TableSetupColumn("Уровень вложенности"); ImGui.TableSetupColumn("Архив"); ImGui.TableSetupColumn("Элемент"); ImGui.TableSetupColumn("Magic1"); ImGui.TableSetupColumn("Magic2"); ImGui.TableSetupColumn("Описание"); - ImGui.TableSetupColumn("Magic3"); + ImGui.TableSetupColumn("Тип"); ImGui.TableHeadersRow(); + // Handle sorting + ImGuiTableSortSpecsPtr sortSpecs = ImGui.TableGetSortSpecs(); + if (sortSpecs.SpecsDirty) + { + // Only handle the first sort spec for simplicity + var sortSpec = sortSpecs.Specs; + + if (sortSpec.ColumnIndex == 0) + { + _viewModel.RebuildFlatList(); + } + else + { + + _viewModel.FlatList.Sort((a, b) => + { + int result = 0; + switch (sortSpec.ColumnIndex) + { + case 1: result = a.Level.CompareTo(b.Level); break; + case 2: result = string.Compare(a.Entry.ArchiveFile, b.Entry.ArchiveFile, StringComparison.Ordinal); break; + case 3: result = string.Compare(a.Entry.ArchiveEntryName, b.Entry.ArchiveEntryName, StringComparison.Ordinal); break; + case 4: result = a.Entry.Magic1.CompareTo(b.Entry.Magic1); break; + case 5: result = a.Entry.Magic2.CompareTo(b.Entry.Magic2); break; + case 6: result = string.Compare(a.Entry.Description, b.Entry.Description, StringComparison.Ordinal); break; + case 7: result = a.Entry.Type.CompareTo(b.Entry.Type); break; + } + + return sortSpec.SortDirection == ImGuiSortDirection.Descending ? -result : result; + }); + } + + sortSpecs.SpecsDirty = false; + } + for (int i = 0; i < _viewModel.FlatList.Count; i++) { ImGui.TableNextRow(); ImGui.TableNextColumn(); + ImGui.Text(i.ToString()); + ImGui.TableNextColumn(); ImGui.Text(_viewModel.FlatList[i].Level.ToString()); ImGui.TableNextColumn(); ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile); @@ -62,7 +100,7 @@ public class CpDatSchemeExplorer : IImGuiPanel ImGui.TableNextColumn(); ImGui.Text(_viewModel.FlatList[i].Entry.Description); ImGui.TableNextColumn(); - ImGui.Text(_viewModel.FlatList[i].Entry.Magic3.ToString()); + ImGui.Text(_viewModel.FlatList[i].Entry.Type.ToString("G")); } ImGui.EndTable(); @@ -80,9 +118,9 @@ public class CpDatSchemeExplorer : IImGuiPanel ImGui.SameLine(); ImGui.Text(entry.Magic2.ToString()); - ImGui.Text("Magic3: "); + ImGui.Text("Тип: "); ImGui.SameLine(); - ImGui.Text(entry.Magic3.ToString()); + ImGui.Text(entry.Type.ToString()); ImGui.Text("Кол-во дочерних элементов: "); ImGui.SameLine(); diff --git a/NResUI/ImGuiUI/NResExplorerPanel.cs b/NResUI/ImGuiUI/NResExplorerPanel.cs index 21fe34a..d526860 100644 --- a/NResUI/ImGuiUI/NResExplorerPanel.cs +++ b/NResUI/ImGuiUI/NResExplorerPanel.cs @@ -1,23 +1,43 @@ -using ImGuiNET; +using CpDatLib; +using ImGuiNET; +using MissionTmaLib.Parsing; +using NResLib; using NResUI.Abstractions; using NResUI.Models; +using ScrLib; +using TexmLib; +using VarsetLib; namespace NResUI.ImGuiUI; public class NResExplorerPanel : IImGuiPanel { private readonly NResExplorerViewModel _viewModel; + private readonly TexmExplorerViewModel _texmExplorerViewModel; + private readonly VarsetViewModel _varsetViewModel; + private readonly CpDatSchemeViewModel _cpDatSchemeViewModel; + private readonly MissionTmaViewModel _missionTmaViewModel; + private readonly ScrViewModel _scrViewModel; - public NResExplorerPanel(NResExplorerViewModel viewModel) + public NResExplorerPanel(NResExplorerViewModel viewModel, TexmExplorerViewModel texmExplorerViewModel, + VarsetViewModel varsetViewModel, CpDatSchemeViewModel cpDatSchemeViewModel, MissionTmaViewModel missionTmaViewModel, ScrViewModel scrViewModel) { _viewModel = viewModel; + _texmExplorerViewModel = texmExplorerViewModel; + _varsetViewModel = varsetViewModel; + _cpDatSchemeViewModel = cpDatSchemeViewModel; + _missionTmaViewModel = missionTmaViewModel; + _scrViewModel = scrViewModel; } + int contextMenuRow = -1; + public void OnImGuiRender() { if (ImGui.Begin("NRes Explorer")) { - ImGui.Text("NRes - это файл-архив. Они имеют разные расширения. Примеры - Textures.lib, weapon.rlb, object.dlb, behpsp.res"); + ImGui.Text( + "NRes - это файл-архив. Они имеют разные расширения. Примеры - Textures.lib, weapon.rlb, object.dlb, behpsp.res"); ImGui.Separator(); if (!_viewModel.HasFile) @@ -34,7 +54,7 @@ public class NResExplorerPanel : IImGuiPanel if (_viewModel.Archive is not null) { ImGui.Text(_viewModel.Path); - + ImGui.Text("Header: "); ImGui.SameLine(); ImGui.Text(_viewModel.Archive.Header.NRes); @@ -48,8 +68,8 @@ public class NResExplorerPanel : IImGuiPanel ImGui.SameLine(); ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString()); - - if (ImGui.BeginTable("content", 12, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) + if (ImGui.BeginTable("content", 12, + ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) { ImGui.TableSetupColumn("Тип файла"); ImGui.TableSetupColumn("Кол-во элементов"); @@ -70,6 +90,17 @@ public class NResExplorerPanel : IImGuiPanel { ImGui.TableNextRow(); ImGui.TableNextColumn(); + + ImGui.Selectable("##row_select" + i, false, ImGuiSelectableFlags.SpanAllColumns); + if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + Console.WriteLine("Context menu for row " + i); + contextMenuRow = i; + ImGui.OpenPopup("row_context_menu"); + } + + ImGui.SameLine(); + ImGui.Text(_viewModel.Archive.Files[i].FileType); ImGui.TableNextColumn(); ImGui.Text(_viewModel.Archive.Files[i].ElementCount.ToString()); @@ -122,6 +153,130 @@ public class NResExplorerPanel : IImGuiPanel ); } + if (ImGui.BeginPopup("row_context_menu")) + { + if (contextMenuRow == -1 || contextMenuRow > _viewModel.Archive.Files.Count) + { + ImGui.Text("Broken context menu :(. Reopen"); + } + else + { + var file = _viewModel.Archive.Files[contextMenuRow]; + ImGui.Text("Actions for file " + file.FileName); + ImGui.TextDisabled("Program has no understading of file format("); + ImGui.Separator(); + if (ImGui.MenuItem("Open as Texture TEXM")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = TexmParser.ReadFromStream(ms, file.FileName); + + _texmExplorerViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName)); + Console.WriteLine("Read TEXM from context menu"); + } + + if (ImGui.MenuItem("Open as Archive NRes")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = NResParser.ReadFile(ms); + + _viewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName)); + Console.WriteLine("Read NRes from context menu"); + } + + if (ImGui.MenuItem("Open as Varset .var")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = VarsetParser.Parse(ms); + + _varsetViewModel.Items = parseResult; + Console.WriteLine("Read Varset from context menu"); + } + + if (ImGui.MenuItem("Open as Scheme cp.dat")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = CpDatParser.Parse(ms); + + _cpDatSchemeViewModel.SetParseResult(parseResult, file.FileName); + Console.WriteLine("Read cp.dat from context menu"); + } + + if (ImGui.MenuItem("Open as Mission .tma")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = MissionTmaParser.ReadFile(ms); + + _missionTmaViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName)); + Console.WriteLine("Read .tma from context menu"); + } + + if (ImGui.MenuItem("Open as Scripts .scr")) + { + using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, + FileShare.Read); + fs.Seek(file.OffsetInFile, SeekOrigin.Begin); + + var buffer = new byte[file.FileLength]; + + fs.ReadExactly(buffer, 0, file.FileLength); + + using var ms = new MemoryStream(buffer); + + var parseResult = ScrParser.ReadFile(ms); + + _scrViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName)); + Console.WriteLine("Read .scr from context menu"); + } + } + + ImGui.EndPopup(); + } + ImGui.EndTable(); } } diff --git a/NResUI/Models/CpDatSchemeViewModel.cs b/NResUI/Models/CpDatSchemeViewModel.cs index 7018c1c..95e60ed 100644 --- a/NResUI/Models/CpDatSchemeViewModel.cs +++ b/NResUI/Models/CpDatSchemeViewModel.cs @@ -9,8 +9,8 @@ public class CpDatSchemeViewModel public string? Error { get; set; } public CpDatScheme? CpDatScheme { get; set; } - - public List<(int Level, CpDatEntry Entry)> FlatList { get; set; } + + public List<(int Level, CpDatEntry Entry)> FlatList { get; set; } = []; public string? Path { get; set; } @@ -23,17 +23,27 @@ public class CpDatSchemeViewModel if (CpDatScheme is not null) { - FlatList = []; + RebuildFlatList(); + } + } + + public void RebuildFlatList() + { + FlatList = []; + + if (CpDatScheme is null) + { + return; + } - CollectEntries(CpDatScheme.Root, 0); + CollectEntries(CpDatScheme.Root, 0); - void CollectEntries(CpDatEntry entry, int level) + void CollectEntries(CpDatEntry entry, int level) + { + FlatList.Add((level, entry)); + foreach (var child in entry.Children) { - FlatList.Add((level, entry)); - foreach (var child in entry.Children) - { - CollectEntries(child, level + 1); - } + CollectEntries(child, level + 1); } } } diff --git a/NResUI/NResUI.csproj b/NResUI/NResUI.csproj index b9bf4ee..09340c6 100644 --- a/NResUI/NResUI.csproj +++ b/NResUI/NResUI.csproj @@ -1,7 +1,8 @@ - + - Exe + WinExe + Exe diff --git a/ParkanPlayground/Msh02.cs b/ParkanPlayground/Msh02.cs index 92659f5..a68b58e 100644 --- a/ParkanPlayground/Msh02.cs +++ b/ParkanPlayground/Msh02.cs @@ -29,42 +29,42 @@ public static class Msh02 var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c)); var bb = new BoundingBox(); - bb.Vec1 = new Vector3( + bb.BottomFrontLeft = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8)) ); - bb.Vec2 = new Vector3( + bb.BottomFrontRight = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20)) ); - bb.Vec3 = new Vector3( + bb.BottomBackRight = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32)) ); - bb.Vec4 = new Vector3( + bb.BottomBackLeft = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44)) ); - bb.Vec5 = new Vector3( + bb.TopFrontLeft = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56)) ); - bb.Vec6 = new Vector3( + bb.TopFrontRight = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68)) ); - bb.Vec7 = new Vector3( + bb.TopBackRight = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80)) ); - bb.Vec8 = new Vector3( + bb.TopBackLeft = new Vector3( BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)), BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92)) @@ -149,6 +149,9 @@ public static class Msh02 public List Elements { get; set; } } + /// + /// 140 байт в начале файла + /// public class Msh02Header { public BoundingBox BoundingBox { get; set; } @@ -172,15 +175,19 @@ public static class Msh02 public Vector3 Vector5 { get; set; } } + /// + /// 96 bytes - bounding box (8 points each 3 float = 96 bytes) + /// 0x60 bytes or 0x18 by 4 bytes + /// public class BoundingBox { - public Vector3 Vec1 { get; set; } - public Vector3 Vec2 { get; set; } - public Vector3 Vec3 { get; set; } - public Vector3 Vec4 { get; set; } - public Vector3 Vec5 { get; set; } - public Vector3 Vec6 { get; set; } - public Vector3 Vec7 { get; set; } - public Vector3 Vec8 { get; set; } + public Vector3 BottomFrontLeft { get; set; } + public Vector3 BottomFrontRight { get; set; } + public Vector3 BottomBackRight { get; set; } + public Vector3 BottomBackLeft { get; set; } + public Vector3 TopFrontLeft { get; set; } + public Vector3 TopFrontRight { get; set; } + public Vector3 TopBackRight { get; set; } + public Vector3 TopBackLeft { get; set; } } } \ No newline at end of file diff --git a/ParkanPlayground/Msh15.cs b/ParkanPlayground/Msh15.cs new file mode 100644 index 0000000..5197c4c --- /dev/null +++ b/ParkanPlayground/Msh15.cs @@ -0,0 +1,55 @@ +using System.Buffers.Binary; +using NResLib; + +namespace ParkanPlayground; + +public static class Msh15 +{ + public static List ReadComponent( + FileStream mshFs, NResArchive archive) + { + var entry = archive.Files.FirstOrDefault(x => x.FileType == "15 00 00 00"); + + if (entry is null) + { + throw new Exception("Archive doesn't contain file (15)"); + } + + var data = new byte[entry.ElementCount * entry.ElementSize]; + mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin); + mshFs.ReadExactly(data, 0, data.Length); + + var elementBytes = data.Chunk(28); + + var elements = elementBytes.Select(x => new Msh15Element() + { + Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)), + Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)), + Vertex1Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)), + Vertex2Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)), + Vertex3Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)), + Magic0E = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)), + Magic12 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)), + Magic16 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)), + Magic1A = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)), + + }).ToList(); + + return elements; + } + + public class Msh15Element + { + public uint Flags { get; set; } + + public uint Magic04 { get; set; } + public ushort Vertex1Index { get; set; } + public ushort Vertex2Index { get; set; } + public ushort Vertex3Index { get; set; } + + public uint Magic0E { get; set; } + public uint Magic12 { get; set; } + public uint Magic16 { get; set; } + public ushort Magic1A { get; set; } + } +} \ No newline at end of file diff --git a/ParkanPlayground/MshConverter.cs b/ParkanPlayground/MshConverter.cs index 7fb63d5..5d57b0f 100644 --- a/ParkanPlayground/MshConverter.cs +++ b/ParkanPlayground/MshConverter.cs @@ -15,6 +15,8 @@ public class MshConverter var component01 = Msh01.ReadComponent(mshFs, mshNres); var component02 = Msh02.ReadComponent(mshFs, mshNres); + var component15 = Msh15.ReadComponent(mshFs, mshNres); + var component0A = Msh0A.ReadComponent(mshFs, mshNres); var component07 = Msh07.ReadComponent(mshFs, mshNres); var component0D = Msh0D.ReadComponent(mshFs, mshNres); diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index 691e96b..40db8df 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -9,8 +9,8 @@ using ParkanPlayground; var converter = new MshConverter(); -converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\133_fr_m_bunker.msh"); -// converter.Convert("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\DATA\\MAPS\\SC_1\\Land.msh"); +// converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\133_fr_m_bunker.msh"); +converter.Convert("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\DATA\\MAPS\\SC_1\\Land.msh"); // converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\73_fr_m_brige.msh"); // converter.Convert("E:\\ParkanUnpacked\\intsys.rlb\\277_MESH_o_pws_l_01.msh"); // converter.Convert("E:\\ParkanUnpacked\\static.rlb\\2_MESH_s_stn_0_01.msh"); diff --git a/README.md b/README.md index 471ef5b..44ac408 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4) - `4` - IShader - `5` - ITerrain - `6` - IGameObject (0x138) -- `7` - IShadeConfig (у меня в папке с игрой его не оказалось) +- `7` - IAtmosphereObject - `8` - ICamera - `9` - IQueue - `10` - IControl @@ -329,6 +329,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4) - `0x101` - 3DRender - `0x105` - NResFile - `0x106` - NResFileMetadata +- `0x107` - 3DSound - `0x201` - IWizard - `0x202` - IItemManager - `0x203` - ICollManager diff --git a/ScrLib/ScrParser.cs b/ScrLib/ScrParser.cs index dbd84f7..21821f7 100644 --- a/ScrLib/ScrParser.cs +++ b/ScrLib/ScrParser.cs @@ -6,10 +6,15 @@ public class ScrParser { public static ScrFile ReadFile(string filePath) { - var fs = new FileStream(filePath, FileMode.Open); + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return ReadFile(fs); + } + + public static ScrFile ReadFile(Stream fs) + { var scrFile = new ScrFile(); - + scrFile.Magic = fs.ReadInt32LittleEndian(); scrFile.EntryCount = fs.ReadInt32LittleEndian(); @@ -19,7 +24,7 @@ public class ScrParser { var entry = new ScrEntry(); entry.Title = fs.ReadLengthPrefixedString(); - + // тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char* fs.ReadByte(); @@ -30,12 +35,12 @@ public class ScrParser { var entryInner = new ScrEntryInner(); entryInner.ScriptIndex = fs.ReadInt32LittleEndian(); - + entryInner.UnkInner2 = fs.ReadInt32LittleEndian(); entryInner.UnkInner3 = fs.ReadInt32LittleEndian(); entryInner.Type = (ScrEntryInnerType)fs.ReadInt32LittleEndian(); entryInner.UnkInner5 = fs.ReadInt32LittleEndian(); - + entryInner.ArgumentsCount = fs.ReadInt32LittleEndian(); entryInner.Arguments = []; @@ -48,7 +53,7 @@ public class ScrParser entryInner.UnkInner7 = fs.ReadInt32LittleEndian(); entry.Inners.Add(entryInner); } - + scrFile.Entries.Add(entry); } diff --git a/VarsetLib/VarsetParser.cs b/VarsetLib/VarsetParser.cs index 019b4eb..cf36b33 100644 --- a/VarsetLib/VarsetParser.cs +++ b/VarsetLib/VarsetParser.cs @@ -4,65 +4,79 @@ public class VarsetParser { public static List Parse(string path) { - FileStream fs = new FileStream(path, FileMode.Open); - - var reader = new StreamReader(fs); - - List varsetItems = []; - - var lineIndex = 1; - while (!reader.EndOfStream) - { - var line = reader.ReadLine()!; - if (line.Length == 0) - { - lineIndex++; - continue; - } - - if (line.StartsWith("//") || line.Trim().StartsWith("//")) - { - lineIndex++; - continue; - } - - if (!line.StartsWith("VAR")) - { - Console.WriteLine($"Error on line: {lineIndex}! Not starting with VAR"); - lineIndex++; - continue; - } - - var openParenthesisIndex = line.IndexOf("("); - var closeParenthesisIndex = line.IndexOf(")"); - - if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 || closeParenthesisIndex <= openParenthesisIndex) - { - Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid"); - lineIndex++; - continue; - } - - var arguments = line.Substring(openParenthesisIndex + 1, closeParenthesisIndex - openParenthesisIndex - 1); - - var parts = arguments.Trim() - .Split(','); - - var type = parts[0] - .Trim(); - - var name = parts[1] - .Trim(); - - var value = parts[2] - .Trim(); - - var item = new VarsetItem(type, name, value); - varsetItems.Add(item); - - lineIndex++; - } + using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - return varsetItems; + return Parse(fs); + } + + public static List Parse(Stream fs) + { + try + { + var reader = new StreamReader(fs); + + List varsetItems = []; + + var lineIndex = 1; + while (!reader.EndOfStream) + { + var line = reader.ReadLine()!; + if (line.Length == 0) + { + lineIndex++; + continue; + } + + if (line.StartsWith("//") || line.Trim().StartsWith("//")) + { + lineIndex++; + continue; + } + + if (!line.StartsWith("VAR")) + { + Console.WriteLine($"Error on line: {lineIndex}! Not starting with VAR"); + lineIndex++; + continue; + } + + var openParenthesisIndex = line.IndexOf("("); + var closeParenthesisIndex = line.IndexOf(")"); + + if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 || + closeParenthesisIndex <= openParenthesisIndex) + { + Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid"); + lineIndex++; + continue; + } + + var arguments = line.Substring(openParenthesisIndex + 1, + closeParenthesisIndex - openParenthesisIndex - 1); + + var parts = arguments.Trim() + .Split(','); + + var type = parts[0] + .Trim(); + + var name = parts[1] + .Trim(); + + var value = parts[2] + .Trim(); + + var item = new VarsetItem(type, name, value); + varsetItems.Add(item); + + lineIndex++; + } + + return varsetItems; + } + catch + { + return []; + } } } \ No newline at end of file