0
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-09-13 18:20:30 +03:00

25 Commits

Author SHA1 Message Date
bird_egop
be60d8d72f Examples and fixes 2025-09-04 03:13:46 +03:00
bird_egop
f2bed4b141 Allow to view cp .dat in UI 2025-09-04 02:45:26 +03:00
bird_egop
7f0246f996 update docs on wea. correctly parse msh 2025-09-03 01:30:54 +03:00
bird_egop
055694a4b4 Hack .msh 2025-08-31 02:20:44 +03:00
bird_egop
fca052365f add docs 2025-08-28 03:30:17 +03:00
bird_egop
5c52ab2b2b msh and cp converters. Mesh broken. 2025-08-26 04:29:30 +03:00
bird_egop
77e7f7652c update documentation 2025-08-26 04:21:48 +03:00
bird_egop
35af4da326 read msh 0A file 2025-08-23 19:03:03 +03:00
bird_egop
4b1c4bf3aa update readme 2025-08-23 03:26:04 +03:00
bird_egop
4ea756a1a4 Update readme 2025-08-23 03:21:03 +03:00
bird_egop
b9e15541c5 Parse cp .dat files. Object schemes.
Test parsing of .msh
2025-08-23 03:00:30 +03:00
Bird Egop
67c9020b96 Update README.md 2025-08-20 15:38:44 +03:00
bird_egop
476017e9c1 upgrade to net9 2025-08-18 22:05:17 +03:00
bird_egop
ee77738713 remove leftovers 2025-08-18 21:57:18 +03:00
Bird Egop
8e31f43abf Update README.md 2025-08-18 21:09:31 +03:00
bird_egop
f5bacc018c test 2025-04-12 16:42:44 +03:00
bird_egop
a6057bf072 unfuck 565 and 4444 textures 2025-03-11 04:36:05 +03:00
bird_egop
a419be1fce update NRES file with element count and element size, seen in ResTree .trf 2025-03-09 22:56:59 +03:00
bird_egop
8c4fc8f096 комментарии и дополнительные изыскания 2025-03-05 18:15:48 +03:00
bird_egop
135777a4c6 add varset view 2025-03-01 23:03:13 +03:00
bird_egop
76ef68635e scr reversed type 2025-03-01 22:45:15 +03:00
bird_egop
d7eb23e9e0 Implement SCR UI 2025-02-26 04:27:16 +03:00
bird_egop
b47a9aff5d Implement scr parsing 2025-02-25 01:51:28 +03:00
bird_egop
ba7c2afe2a unknown fixes 2025-02-24 23:35:55 +03:00
bird_egop
c50512ea52 add gameobjects view 2024-11-28 05:07:17 +03:00
74 changed files with 2837 additions and 316 deletions

3
Common/Common.csproj Normal file
View File

@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>

View File

@@ -1,7 +1,7 @@
using System.Buffers.Binary;
using System.Text;
namespace MissionTmaLib.Parsing;
namespace Common;
public static class Extensions
{
@@ -13,6 +13,14 @@ public static class Extensions
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static uint ReadUInt32LittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
@@ -21,6 +29,24 @@ public static class Extensions
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadNullTerminatedString(this FileStream fs)
{
var sb = new StringBuilder();
while (true)
{
var b = fs.ReadByte();
if (b == 0)
{
break;
}
sb.Append((char)b);
}
return sb.ToString();
}
public static string ReadLengthPrefixedString(this FileStream fs)
{
var len = fs.ReadInt32LittleEndian();

3
Common/IndexedEdge.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace Common;
public record IndexedEdge(ushort Index1, ushort Index2);

View File

@@ -1,7 +1,7 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace MissionTmaLib;
namespace Common;
[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")]
public class IntFloatValue(Span<byte> span)

3
Common/Vector3.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace Common;
public record Vector3(float X, float Y, float Z);

12
CpDatLib/CpDatEntry.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CpDatLib;
public record CpDatEntry(
string ArchiveFile,
string ArchiveEntryName,
int Magic1,
int Magic2,
string Description,
int Magic3,
int ChildCount, // игра не хранит это число в объекте, но оно есть в файле
List<CpDatEntry> Children
);

5
CpDatLib/CpDatLib.csproj Normal file
View File

@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
namespace CpDatLib;
public record CpDatParseResult(CpDatScheme? Scheme, string? Error);

68
CpDatLib/CpDatParser.cs Normal file
View File

@@ -0,0 +1,68 @@
using Common;
namespace CpDatLib;
public class CpDatParser
{
public static CpDatParseResult Parse(string filePath)
{
Span<byte> f0f1 = stackalloc byte[4];
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
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 байта - кол-во вложенных объектов
if ((fs.Length - 8) % entryLength != 0)
{
return new CpDatParseResult(null, "File size is not valid according to expected entry length.");
}
CpDatEntry root = ReadEntryRecursive(fs);
var scheme = new CpDatScheme(schemeType, root);
return new CpDatParseResult(scheme, null);
}
private static CpDatEntry ReadEntryRecursive(FileStream fs)
{
var str1 = fs.ReadNullTerminatedString();
fs.Seek(32 - str1.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var str2 = fs.ReadNullTerminatedString();
fs.Seek(32 - str2.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var magic1 = fs.ReadInt32LittleEndian();
var magic2 = fs.ReadInt32LittleEndian();
var descriptionString = fs.ReadNullTerminatedString();
fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var magic3 = fs.ReadInt32LittleEndian();
// игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов
var childCount = fs.ReadInt32LittleEndian();
List<CpDatEntry> children = new List<CpDatEntry>(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);
}
}

3
CpDatLib/CpDatScheme.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace CpDatLib;
public record CpDatScheme(SchemeType Type, CpDatEntry Root);

29
CpDatLib/CpEntryType.cs Normal file
View File

@@ -0,0 +1,29 @@
namespace CpDatLib;
public enum SchemeType : uint
{
ClassBuilding = 0x80000000,
ClassRobot = 0x01000000,
ClassAnimal = 0x20000000,
BunkerSmall = 0x80010000,
BunkerMedium = 0x80020000,
BunkerLarge = 0x80040000,
Generator = 0x80000002,
Mine = 0x80000004,
Storage = 0x80000008,
Plant = 0x80000010,
Hangar = 0x80000040,
TowerMedium = 0x80100000,
TowerLarge = 0x80200000,
MainTeleport = 0x80000200,
Institute = 0x80000400,
Bridge = 0x80001000,
Ruine = 0x80002000,
RobotTransport = 0x01002000,
RobotBuilder = 0x01004000,
RobotBattleunit = 0x01008000,
RobotHq = 0x01010000,
RobotHero = 0x01020000,
}

19
Directory.Build.props Normal file
View File

@@ -0,0 +1,19 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Enable Central Package Management -->
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
<!-- Enforce package version consistency -->
<EnablePackageVersionOverride>false</EnablePackageVersionOverride>
<!-- Suppress package version warnings -->
<NoWarn>$(NoWarn);NU1507;CS1591</NoWarn>
</PropertyGroup>
</Project>

16
Directory.Packages.props Normal file
View File

@@ -0,0 +1,16 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<!-- Package versions used across the solution -->
<ItemGroup>
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageVersion Include="Silk.NET" Version="2.22.0" />
<PackageVersion Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
</Project>

View File

@@ -2,9 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -2,9 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,3 +1,5 @@
namespace MissionTmaLib;
using Common;
namespace MissionTmaLib;
public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords);

View File

@@ -12,7 +12,7 @@ public class ClanInfo
/// </summary>
public ClanType ClanType { get; set; }
public string UnkString2 { get; set; }
public string ScriptsString { get; set; }
public int UnknownClanPartCount { get; set; }
public List<UnknownClanTreeInfoPart> UnknownParts { get; set; }
@@ -20,7 +20,7 @@ public class ClanInfo
/// Игра называет этот путь TreeName
/// </summary>
public string ResearchNResPath { get; set; }
public int UnkInt3 { get; set; }
public int Brains { get; set; }
public int AlliesMapCount { get; set; }
/// <summary>

View File

@@ -6,19 +6,4 @@ public enum ClanType
Player = 1,
AI = 2,
Neutral = 3
}
public static class Extensions
{
public static string ToReadableString(this ClanType clanType)
{
return clanType switch
{
ClanType.Environment => $"Окружение ({clanType:D})",
ClanType.Player => $"Игрок ({clanType:D})",
ClanType.AI => $"AI ({clanType:D})",
ClanType.Neutral => $"Нейтральный ({clanType:D})",
_ => $"Неизвестный ({clanType:D})"
};
}
}

View File

@@ -0,0 +1,28 @@
namespace MissionTmaLib;
public static class Extensions
{
public static string ToReadableString(this ClanType clanType)
{
return clanType switch
{
ClanType.Environment => $"Окружение ({clanType:D})",
ClanType.Player => $"Игрок ({clanType:D})",
ClanType.AI => $"AI ({clanType:D})",
ClanType.Neutral => $"Нейтральный ({clanType:D})",
_ => $"Неизвестный ({clanType:D})"
};
}
public static string ToReadableString(this GameObjectType type)
{
return type switch
{
GameObjectType.Building => $"Строение {type:D}",
GameObjectType.Warbot => $"Варбот {type:D}",
GameObjectType.Tree => $"Дерево {type:D}",
GameObjectType.Stone => $"Камень {type:D}",
_ => $"Неизвестный ({type:D})"
};
}
}

View File

@@ -1,4 +1,6 @@
namespace MissionTmaLib;
using Common;
namespace MissionTmaLib;
public class GameObjectInfo
{
@@ -22,7 +24,7 @@ public class GameObjectInfo
/// </remarks>
public int OwningClanIndex { get; set; }
public int UnknownInt3 { get; set; }
public int Order { get; set; }
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; }

View File

@@ -1,3 +1,5 @@
namespace MissionTmaLib;
using Common;
namespace MissionTmaLib;
public record GameObjectSetting(int SettingType, IntFloatValue Unk1, IntFloatValue Unk2, IntFloatValue Unk3, string Name);

View File

@@ -1,3 +1,5 @@
namespace MissionTmaLib;
using Common;
public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownInt2, float UnknownFloat, int UnknownInt3);
namespace MissionTmaLib;
public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3);

View File

@@ -1,9 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,3 @@
namespace MissionTmaLib.Parsing;
public record MissionTma(ArealsFileData ArealData, ClansFileData ClansData, GameObjectsFileData GameObjectsData);

View File

@@ -1,10 +1,12 @@
namespace MissionTmaLib.Parsing;
using Common;
namespace MissionTmaLib.Parsing;
public class MissionTmaParser
{
public static MissionTmaParseResult ReadFile(string filePath)
{
var fs = new FileStream(filePath, FileMode.Open);
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var arealData = LoadAreals(fs);
@@ -78,7 +80,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)
@@ -118,7 +120,7 @@ public class MissionTmaParser
if (4 < clanFeatureSet)
{
clanTreeInfo.UnkInt3 = fileStream.ReadInt32LittleEndian();
clanTreeInfo.Brains = fileStream.ReadInt32LittleEndian();
}
if (5 < clanFeatureSet)
@@ -185,7 +187,11 @@ public class MissionTmaParser
if (3 < gameObjectsFeatureSet)
{
gameObjectInfo.UnknownInt3 = fileStream.ReadInt32LittleEndian();
gameObjectInfo.Order = fileStream.ReadInt32LittleEndian();
if (gameObjectInfo.Type == GameObjectType.Building)
{
gameObjectInfo.Order += int.MaxValue;
}
}
// читает 12 байт
@@ -378,6 +384,4 @@ public class MissionTmaParser
lodeData
);
}
}
public record MissionTma(ArealsFileData ArealData, ClansFileData ClansData, GameObjectsFileData GameObjectsData);
}

View File

@@ -1,3 +1,5 @@
namespace MissionTmaLib;
using Common;
namespace MissionTmaLib;
public record UnknownClanTreeInfoPart(int UnkInt1, Vector3 UnkVector, float UnkInt2, float UnkInt3);

View File

@@ -1,3 +0,0 @@
namespace MissionTmaLib;
public record Vector3(float X, float Y, float Z);

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Encoding.CodePages" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,40 @@
using System.Buffers.Binary;
using System.Buffers.Binary;
using System.Text;
var fileBytes = File.ReadAllBytes("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\gamefont.rlb");
var fileBytes = File.ReadAllBytes("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\gamefont-1.rlb");
var header = fileBytes.AsSpan().Slice(0, 32);
var nlHeaderBytes = header.Slice(0, 2);
var mustBeZero = header[2];
var mustBeOne = header[3];
var numberOfEntriesBytes = header.Slice(4, 2);
var sortingFlagBytes = header.Slice(14, 2);
var decryptionKeyBytes = header.Slice(20, 2);
var numberOfEntries = BinaryPrimitives.ReadInt16LittleEndian(numberOfEntriesBytes);
var sortingFlag = BinaryPrimitives.ReadInt16LittleEndian(sortingFlagBytes);
var decryptionKey = BinaryPrimitives.ReadInt16LittleEndian(decryptionKeyBytes);
var headerSize = numberOfEntries * 32;
var decryptedHeader = new byte[headerSize];
var keyLow = decryptionKeyBytes[0];
var keyHigh = decryptionKeyBytes[1];
for (var i = 0; i < headerSize; i++)
{
byte tmp = (byte)((keyLow << 1) ^ keyHigh);
keyLow = tmp;
keyHigh = (byte)((keyHigh >> 1) ^ tmp);
decryptedHeader[i] = (byte)(fileBytes[32 + i] ^ tmp);
}
var decryptedHeaderString = Encoding.ASCII.GetString(decryptedHeader, 0, headerSize);
var entries = decryptedHeader.Chunk(32).ToArray();
var entriesStrings = entries.Select(x => Encoding.ASCII.GetString(x, 0, x.Length)).ToArray();
File.WriteAllBytes("export.nl", decryptedHeader);
var fileCount = BinaryPrimitives.ReadInt16LittleEndian(fileBytes.AsSpan().Slice(4, 2));
var decodedHeader = new byte[fileCount * 32];

View File

@@ -19,10 +19,11 @@ public record NResArchiveHeader(string NRes, int Version, int FileCount, int Tot
/// каждый элемент это 64 байта,
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64)
/// </summary>
/// <param name="FileType">[0..8] ASCII описание типа файла, например TEXM или MAT0</param>
/// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param>
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) </param>
/// <param name="Magic1">[8..12] Неизвестное число</param>
/// <param name="FileLength">[12..16] Длина файла в байтах</param>
/// <param name="Magic2">[16..20] Неизвестное число</param>
/// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param>
/// <param name="FileName">[20..40] ASCII имя файла</param>
/// <param name="Magic3">[40..44] Неизвестное число</param>
/// <param name="Magic4">[44..48] Неизвестное число</param>
@@ -32,9 +33,10 @@ public record NResArchiveHeader(string NRes, int Version, int FileCount, int Tot
/// <param name="Index">[60..64] Индекс в файле (от 0, не больше чем кол-во файлов)</param>
public record ListMetadataItem(
string FileType,
uint ElementCount,
int Magic1,
int FileLength,
int Magic2,
int ElementSize,
string FileName,
int Magic3,
int Magic4,

View File

@@ -30,7 +30,7 @@ public class NResExporter
extension = ".bin";
}
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.Index}_{fileName}{extension}");
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}");
File.WriteAllBytes(targetFilePath, buffer);
}

View File

@@ -1,9 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -7,7 +7,7 @@ public static class NResParser
{
public static NResParseResult ReadFile(string path)
{
using FileStream nResFs = new FileStream(path, FileMode.Open);
using FileStream nResFs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (nResFs.Length < 16)
{
@@ -48,13 +48,32 @@ public static class NResParser
for (int i = 0; i < header.FileCount; i++)
{
nResFs.ReadExactly(metaDataBuffer);
var type = "";
for (int j = 0; j < 4; j++)
{
if (!char.IsLetterOrDigit((char)metaDataBuffer[j]))
{
type += metaDataBuffer[j]
.ToString("X2") + " ";
}
else
{
type += (char)metaDataBuffer[j];
}
}
var type2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer.Slice(4));
type = type.Trim();
elements.Add(
new ListMetadataItem(
FileType: Encoding.ASCII.GetString(metaDataBuffer[..8]).TrimEnd('\0'),
FileType: type,
ElementCount: type2,
Magic1: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[8..12]),
FileLength: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]),
Magic2: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[16..20]),
ElementSize: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[16..20]),
FileName: Encoding.ASCII.GetString(metaDataBuffer[20..40]).TrimEnd('\0'),
Magic3: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[40..44]),
Magic4: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[44..48]),

View File

@@ -32,10 +32,11 @@
3. В конце файла есть метаданные.
Поскольку NRes это по сути архив, длина метаданных у каждого файла разная и считается как `Количество файлов * 64`, каждый элемент метаданных - 64 байта.
+ [0..8] ASCII описание типа файла, например TEXM или MAT0
+ [0..4] ASCII описание типа файла, например TEXM или MAT0
+ [4..8] Количество элементов в файле (если файл составной, например .trf)
+ [8..12] Неизвестное число
+ [12..16] Длина файла в байтах
+ [16..20] Неизвестное число
+ [16..20] Размер элемента в файле (если файл составной, например .trf)
+ [20..40] ASCII имя файла
+ [40..44] Неизвестное число
+ [44..48] Неизвестное число

View File

@@ -53,6 +53,10 @@ public class App
serviceCollection.AddSingleton(new NResExplorerViewModel());
serviceCollection.AddSingleton(new TexmExplorerViewModel());
serviceCollection.AddSingleton(new MissionTmaViewModel());
serviceCollection.AddSingleton(new BinaryExplorerViewModel());
serviceCollection.AddSingleton(new ScrViewModel());
serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider();

View File

@@ -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<List<Region>>(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;
}
}

View File

@@ -0,0 +1,112 @@
using CpDatLib;
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class CpDatSchemeExplorer : IImGuiPanel
{
private readonly CpDatSchemeViewModel _viewModel;
public CpDatSchemeExplorer(CpDatSchemeViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("cp .dat Scheme Explorer"))
{
ImGui.Text("cp .dat - это файл схема здания или робота. Их можно найти в папке UNITS");
ImGui.Separator();
var cpDat = _viewModel.CpDatScheme;
if (_viewModel.HasFile && cpDat is not null)
{
ImGui.Text("Тип объекта в схеме: ");
ImGui.SameLine();
ImGui.Text(cpDat.Type.ToString("G"));
var root = cpDat.Root;
DrawEntry(root, 0);
ImGui.Separator();
if (ImGui.BeginTable("content", 7, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Уровень вложенности");
ImGui.TableSetupColumn("Архив");
ImGui.TableSetupColumn("Элемент");
ImGui.TableSetupColumn("Magic1");
ImGui.TableSetupColumn("Magic2");
ImGui.TableSetupColumn("Описание");
ImGui.TableSetupColumn("Magic3");
ImGui.TableHeadersRow();
for (int i = 0; i < _viewModel.FlatList.Count; i++)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Level.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveEntryName);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic1.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic2.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Description);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic3.ToString());
}
ImGui.EndTable();
}
void DrawEntry(CpDatEntry entry, int index)
{
if (ImGui.TreeNodeEx($"Элемент: \"{entry.ArchiveFile}/{entry.ArchiveEntryName}\" - {entry.Description}##entry_{index}"))
{
ImGui.Text("Magic1: ");
ImGui.SameLine();
ImGui.Text(entry.Magic1.ToString());
ImGui.Text("Magic2: ");
ImGui.SameLine();
ImGui.Text(entry.Magic2.ToString());
ImGui.Text("Magic3: ");
ImGui.SameLine();
ImGui.Text(entry.Magic3.ToString());
ImGui.Text("Кол-во дочерних элементов: ");
ImGui.SameLine();
ImGui.Text(entry.ChildCount.ToString());
foreach (var child in entry.Children)
{
DrawEntry(child, ++index);
}
ImGui.TreePop();
}
}
}
else if (_viewModel.Error is not null)
{
ImGui.Text(_viewModel.Error);
}
else
{
ImGui.Text("cp .dat не открыт");
}
}
ImGui.End();
}
}

View File

@@ -1,29 +1,28 @@
using System.Numerics;
using System.Numerics;
using CpDatLib;
using ImGuiNET;
using MissionTmaLib;
using MissionTmaLib.Parsing;
using NativeFileDialogSharp;
using NResLib;
using NResUI.Abstractions;
using NResUI.Models;
using ScrLib;
using TexmLib;
using VarsetLib;
namespace NResUI.ImGuiUI
{
public class MainMenuBar : IImGuiPanel
public class MainMenuBar(
NResExplorerViewModel nResExplorerViewModel,
TexmExplorerViewModel texmExplorerViewModel,
ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
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())
@@ -40,7 +39,7 @@ namespace NResUI.ImGuiUI
var parseResult = NResParser.ReadFile(path);
_nResExplorerViewModel.SetParseResult(parseResult, path);
nResExplorerViewModel.SetParseResult(parseResult, path);
Console.WriteLine("Read NRES");
}
}
@@ -54,10 +53,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");
}
}
@@ -73,11 +72,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");
}
}
@@ -89,12 +88,57 @@ namespace NResUI.ImGuiUI
{
var path = result.Path;
var parseResult = MissionTmaParser.ReadFile(path);
_missionTmaViewModel.SetParseResult(parseResult, path);
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 (ImGui.MenuItem("Open Varset File"))
{
var result = Dialog.FileOpen("var");
if (result.IsOk)
{
var path = result.Path;
var parseResult = VarsetParser.Parse(path);
varsetViewModel.Items = parseResult;
Console.WriteLine("Read VARSET");
}
}
if (ImGui.MenuItem("Open cp .dat Scheme File"))
{
var result = Dialog.FileOpen("dat");
if (result.IsOk)
{
var path = result.Path;
var parseResult = CpDatParser.Parse(path);
cpDatSchemeViewModel.SetParseResult(parseResult, path);
Console.WriteLine("Read cp .dat");
}
}
if (nResExplorerViewModel.HasFile)
{
if (ImGui.MenuItem("Экспортировать NRes"))
{
@@ -103,10 +147,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("Успешно экспортировано");
}
}
}
@@ -117,6 +161,5 @@ namespace NResUI.ImGuiUI
ImGui.EndMenuBar();
}
}
}
}

View File

@@ -18,9 +18,14 @@ public class MissionTmaExplorer : IImGuiPanel
{
if (ImGui.Begin("Mission TMA Explorer"))
{
ImGui.Text("data.tma - это файл миссии. Его можно найти в папке MISSIONS");
ImGui.Separator();
var mission = _viewModel.Mission;
if (_viewModel.HasFile && mission is not null)
{
ImGui.Columns(2);
ImGui.Text("Путь к файлу: ");
ImGui.SameLine();
ImGui.Text(_viewModel.Path);
@@ -65,32 +70,44 @@ public class MissionTmaExplorer : IImGuiPanel
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(arealInfo.Coords[k].X.ToString("F2"));
ImGui.Text(
arealInfo.Coords[k]
.X.ToString("F2")
);
ImGui.TableNextColumn();
ImGui.Text(arealInfo.Coords[k].Y.ToString("F2"));
ImGui.Text(
arealInfo.Coords[k]
.Y.ToString("F2")
);
ImGui.TableNextColumn();
ImGui.Text(arealInfo.Coords[k].Z.ToString("F2"));
ImGui.Text(
arealInfo.Coords[k]
.Z.ToString("F2")
);
}
ImGui.EndTable();
}
ImGui.TreePop();
}
}
ImGui.TreePop();
}
ImGui.TreePop();
}
if (ImGui.TreeNodeEx("Кланы"))
{
var (clanFeatureSet, clanCount, clanInfos) = mission.ClansData;
ImGui.Text("Фиче-сет: ");
Utils.ShowHint("Магическое число из файла, на основе которого игра читает разные секции о клане");
ImGui.SameLine();
ImGui.Text(clanFeatureSet.ToString());
ImGui.Text("Количество кланов: ");
ImGui.SameLine();
ImGui.Text(clanCount.ToString());
@@ -113,15 +130,15 @@ public class MissionTmaExplorer : IImGuiPanel
ImGui.Text(" Y: ");
ImGui.SameLine();
ImGui.Text(clanInfo.Y.ToString());
ImGui.Text("Тип клана: ");
ImGui.SameLine();
ImGui.Text(clanInfo.ClanType.ToReadableString());
ImGui.Text("Неизвестная строка 1: ");
Utils.ShowHint("Кажется это путь к файлу поведения (Behavior), но пока не понятно. Обычно пути соответствуют 2 файла.");
ImGui.Text("Скрипты поведения (Mission Scripts): ");
Utils.ShowHint("Пути к файлам .scr и .fml описывающих настройку объектов и поведение AI");
ImGui.SameLine();
ImGui.Text(clanInfo.UnkString2);
ImGui.Text(clanInfo.ScriptsString);
if (clanInfo.UnknownParts.Count > 0)
{
@@ -162,14 +179,14 @@ public class MissionTmaExplorer : IImGuiPanel
ImGui.Text("Отсутствует неизвестная часть");
}
ImGui.Text("Путь к файлу .trf: ");
Utils.ShowHint("Не до конца понятно, что означает, вероятно это NRes с деревом исследований");
ImGui.Text("Дерево исследований: ");
Utils.ShowHint("NRes с деревом исследований");
ImGui.SameLine();
ImGui.Text(clanInfo.ResearchNResPath);
ImGui.Text("Неизвестное число 3: ");
ImGui.Text("Количество мозгов (Brains))): ");
ImGui.SameLine();
ImGui.Text(clanInfo.UnkInt3.ToString());
ImGui.Text(clanInfo.Brains.ToString());
ImGui.Text("Матрица союзников");
Utils.ShowHint("Если 1, то кланы - союзники, и не нападают друг на друга");
@@ -178,20 +195,23 @@ public class MissionTmaExplorer : IImGuiPanel
ImGui.TableSetupColumn("Клан");
ImGui.TableSetupColumn("Союзник?");
ImGui.TableHeadersRow();
foreach (var alliesMapKey in clanInfo.AlliesMap.Keys)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(alliesMapKey);
ImGui.TableNextColumn();
ImGui.Text(clanInfo.AlliesMap[alliesMapKey].ToString());
ImGui.Text(
clanInfo.AlliesMap[alliesMapKey]
.ToString()
);
}
ImGui.EndTable();
}
ImGui.TreePop();
}
}
@@ -204,8 +224,189 @@ public class MissionTmaExplorer : IImGuiPanel
if (ImGui.TreeNodeEx("Объекты"))
{
var gameObjectsData = mission.GameObjectsData;
ImGui.Text("Фиче-сет: ");
Utils.ShowHint("Магическое число из файла, на основе которого игра читает разные секции об объекте");
ImGui.SameLine();
ImGui.Text(gameObjectsData.GameObjectsFeatureSet.ToString());
ImGui.Text("Кол-во объектов: ");
ImGui.SameLine();
ImGui.Text(gameObjectsData.GameObjectsCount.ToString());
for (var i = 0; i < gameObjectsData.GameObjectInfos.Count; i++)
{
var gameObjectInfo = gameObjectsData.GameObjectInfos[i];
if (ImGui.TreeNodeEx($"Объект {i} - {gameObjectInfo.DatString}"))
{
ImGui.Text("Тип объекта: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.Type.ToReadableString());
ImGui.Text("Неизвестные флаги: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.UnknownFlags.ToString("X8"));
ImGui.Text("Путь к файлу .dat: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.DatString);
ImGui.Text("Индекс владеющего клана: ");
Utils.ShowHint("-1 если объект никому не принадлежит");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.OwningClanIndex.ToString());
ImGui.Text("Порядковый номер: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.Order.ToString());
ImGui.Text("Вектор позиции: ");
ImGui.SameLine();
ImGui.Text($"{gameObjectInfo.Position.X} : {gameObjectInfo.Position.Y} : {gameObjectInfo.Position.Z}");
ImGui.Text("Вектор поворота: ");
ImGui.SameLine();
ImGui.Text($"{gameObjectInfo.Rotation.X} : {gameObjectInfo.Rotation.Y} : {gameObjectInfo.Rotation.Z}");
ImGui.Text("Вектор масштаба: ");
ImGui.SameLine();
ImGui.Text($"{gameObjectInfo.Scale.X} : {gameObjectInfo.Scale.Y} : {gameObjectInfo.Scale.Z}");
ImGui.Text("Неизвестная строка 2: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.UnknownString2);
ImGui.Text("Неизвестное число 4: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.UnknownInt4.ToString());
ImGui.Text("Неизвестное число 5: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.UnknownInt5.ToString());
ImGui.Text("Неизвестное число 6: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.UnknownInt6.ToString());
if (ImGui.TreeNodeEx("Настройки"))
{
ImGui.Text("Неиспользуемый заголовок: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.Settings.Unused.ToString());
ImGui.Text("Кол-во настроек: ");
ImGui.SameLine();
ImGui.Text(gameObjectInfo.Settings.SettingsCount.ToString());
ImGui.Text("0 - дробное число, 1 - целое число");
if (ImGui.BeginTable("Настройки", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Тип");
ImGui.TableSetupColumn("Число 1");
ImGui.TableSetupColumn("Число 2");
ImGui.TableSetupColumn("Число 3");
ImGui.TableSetupColumn("Название");
ImGui.TableHeadersRow();
for (var i1 = 0; i1 < gameObjectInfo.Settings.Settings.Count; i1++)
{
var setting = gameObjectInfo.Settings.Settings[i1];
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(setting.SettingType.ToString());
ImGui.TableNextColumn();
ImGui.Text(setting.SettingType == 0 ? setting.Unk1.AsFloat.ToString() : setting.Unk1.AsInt.ToString());
ImGui.TableNextColumn();
ImGui.Text(setting.SettingType == 0 ? setting.Unk1.AsFloat.ToString() : setting.Unk1.AsInt.ToString());
ImGui.TableNextColumn();
ImGui.Text(setting.SettingType == 0 ? setting.Unk1.AsFloat.ToString() : setting.Unk1.AsInt.ToString());
ImGui.TableNextColumn();
ImGui.Text(setting.Name);
}
ImGui.EndTable();
}
ImGui.TreePop();
}
ImGui.TreePop();
}
}
ImGui.Text("LAND строка: ");
Utils.ShowHint("Видимо это путь к настройкам поверхности");
ImGui.SameLine();
ImGui.Text(gameObjectsData.LandString);
ImGui.Text("Неизвестное число: ");
ImGui.SameLine();
ImGui.Text(gameObjectsData.UnknownInt.ToString());
ImGui.Text("Техническое описание: ");
ImGui.SameLine();
ImGui.Text(gameObjectsData.MissionTechDescription?.Replace((char)0xcd, '.') ?? "Отсутствует");
var lodeData = gameObjectsData.LodeData;
if (lodeData is not null)
{
ImGui.Text("Информация о LOD-ах");
ImGui.Text("Неиспользуемый заголовок: ");
ImGui.SameLine();
ImGui.Text(lodeData.Unused.ToString());
ImGui.Text("Кол-во LOD-ов: ");
ImGui.SameLine();
ImGui.Text(lodeData.LodeCount.ToString());
if (ImGui.BeginTable("Информация о лодах", 7, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("X");
ImGui.TableSetupColumn("Y");
ImGui.TableSetupColumn("Z");
ImGui.TableSetupColumn("Число 1");
ImGui.TableSetupColumn("Флаги 2");
ImGui.TableSetupColumn("Число 3");
ImGui.TableSetupColumn("Число 4");
ImGui.TableHeadersRow();
for (var i1 = 0; i1 < lodeData.Lodes.Count; i1++)
{
var lode = lodeData.Lodes[i1];
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownVector.X.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownVector.Y.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownVector.Z.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownInt1.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownFlags2.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownFloat.ToString());
ImGui.TableNextColumn();
ImGui.Text(lode.UnknownInt3.ToString());
}
ImGui.EndTable();
}
}
else
{
ImGui.Text("Информаия о LOD-ах отсутствует");
}
ImGui.TreePop();
}
ImGui.NextColumn();
ImGui.Text("Тут хочу сделать карту");
}
else
{

View File

@@ -17,6 +17,9 @@ public class NResExplorerPanel : IImGuiPanel
{
if (ImGui.Begin("NRes Explorer"))
{
ImGui.Text("NRes - это файл-архив. Они имеют разные расширения. Примеры - Textures.lib, weapon.rlb, object.dlb, behpsp.res");
ImGui.Separator();
if (!_viewModel.HasFile)
{
ImGui.Text("No NRes is opened");
@@ -46,12 +49,13 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString());
if (ImGui.BeginTable("content", 11))
if (ImGui.BeginTable("content", 12, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Тип файла");
ImGui.TableSetupColumn("Кол-во элементов");
ImGui.TableSetupColumn("Magic1");
ImGui.TableSetupColumn("Длина файла в байтах");
ImGui.TableSetupColumn("Magic2");
ImGui.TableSetupColumn("Размер элемента");
ImGui.TableSetupColumn("Имя файла");
ImGui.TableSetupColumn("Magic3");
ImGui.TableSetupColumn("Magic4");
@@ -68,6 +72,8 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileType);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].ElementCount.ToString());
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic1.ToString()
@@ -80,7 +86,7 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic2.ToString()
.ElementSize.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileName);

View File

@@ -0,0 +1,147 @@
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
using ScrLib;
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"))
{
ImGui.Text("scr - это файл AI скриптов. Их можно найти в папке MISSIONS/SCRIPTS");
ImGui.Separator();
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("Тип действия");
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());
if (inner.ScriptIndex == 2)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
if (inner.ScriptIndex == 4)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
if (inner.ScriptIndex == 8)
{
Utils.ShowHint("Установить dCurrentProblem стейт (VARSET:arg0)");
}
if (inner.ScriptIndex == 20)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner2.ToString());
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner3.ToString());
ImGui.TableNextColumn();
ImGui.Text($"{(int) inner.Type}: {inner.Type:G}");
if (inner.Type == ScrEntryInnerType._0)
{
Utils.ShowHint("0 обязан иметь аргументы");
}
if (inner.Type == ScrEntryInnerType.CheckInternalState)
{
Utils.ShowHint("Для 5 вообще не нужны данные, игра проверяет внутренний стейт");
}
if (inner.Type == ScrEntryInnerType.SetVarsetValue)
{
Utils.ShowHint("В случае 6, игра берёт UnkInner2 (индекс в Varset) и устанавливает ему значение UnkInner3");
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner5.ToString());
ImGui.TableNextColumn();
ImGui.Text(inner.ArgumentsCount.ToString());
ImGui.TableNextColumn();
foreach (var argument in inner.Arguments)
{
if (ImGui.Button(argument.ToString()))
{
}
ImGui.SameLine();
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner7.ToString());
}
ImGui.EndTable();
}
ImGui.TreePop();
}
}
ImGui.TreePop();
}
}
else
{
ImGui.Text("SCR не открыт");
}
ImGui.End();
}
}
}

View File

@@ -21,6 +21,9 @@ public class TexmExplorer : IImGuiPanel
{
if (ImGui.Begin("TEXM Explorer"))
{
ImGui.Text("TEXM - это файл текстуры. Их можно найти внутри NRes архивов, например Textures.lib");
ImGui.Separator();
if (!_viewModel.HasFile)
{
ImGui.Text("No TEXM opened");

View File

@@ -0,0 +1,60 @@
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class VarsetExplorerPanel : IImGuiPanel
{
private readonly VarsetViewModel _viewModel;
public VarsetExplorerPanel(VarsetViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("VARSET Explorer"))
{
ImGui.Text(".var - это файл динамических настроек. Можно найти в MISSIONS/SCRIPTS/varset.var, а также внутри behpsp.res");
ImGui.Separator();
if (_viewModel.Items.Count == 0)
{
ImGui.Text("VARSET не загружен");
}
else
{
if (ImGui.BeginTable($"varset", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Индекс");
ImGui.TableSetupColumn("Тип");
ImGui.TableSetupColumn("Имя");
ImGui.TableSetupColumn("Значение");
ImGui.TableHeadersRow();
for (int j = 0; j < _viewModel.Items.Count; j++)
{
var item = _viewModel.Items[j];
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(j.ToString());
ImGui.TableNextColumn();
ImGui.Text(item.Type);
ImGui.TableNextColumn();
ImGui.Text(item.Name);
ImGui.TableNextColumn();
ImGui.Text(item.Value);
}
ImGui.EndTable();
}
ImGui.TreePop();
}
ImGui.End();
}
}
}

View File

@@ -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<Region> 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;
}

View File

@@ -0,0 +1,40 @@
using CpDatLib;
using ScrLib;
namespace NResUI.Models;
public class CpDatSchemeViewModel
{
public bool HasFile { get; set; }
public string? Error { get; set; }
public CpDatScheme? CpDatScheme { get; set; }
public List<(int Level, CpDatEntry Entry)> FlatList { get; set; }
public string? Path { get; set; }
public void SetParseResult(CpDatParseResult parseResult, string path)
{
CpDatScheme = parseResult.Scheme;
Error = parseResult.Error;
HasFile = true;
Path = path;
if (CpDatScheme is not null)
{
FlatList = [];
CollectEntries(CpDatScheme.Root, 0);
void CollectEntries(CpDatEntry entry, int level)
{
FlatList.Add((level, entry));
foreach (var child in entry.Children)
{
CollectEntries(child, level + 1);
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,8 @@
using VarsetLib;
namespace NResUI.Models;
public class VarsetViewModel
{
public List<VarsetItem> Items { get; set; } = [];
}

View File

@@ -2,9 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -12,16 +9,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageReference Include="Silk.NET" Version="2.22.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="NativeFileDialogSharp" />
<PackageReference Include="Silk.NET" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -137,9 +137,9 @@ namespace NResUI
public void SetLod(int @base, int min, int max)
{
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, @base);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, min);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, max);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureLodBias, in @base);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMinLod, in min);
_gl.TexParameterI(GLEnum.Texture2D, TextureParameterName.TextureMaxLod, in max);
}
public void SetWrap(TextureCoordinate coord, TextureWrapMode mode)

View File

@@ -1,69 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParkanPlayground", "ParkanPlayground\ParkanPlayground.csproj", "{7DB19000-6F41-4BAE-A904-D34EFCA065E9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TextureDecoder", "TextureDecoder\TextureDecoder.csproj", "{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NLUnpacker", "NLUnpacker\NLUnpacker.csproj", "{50C83E6C-23ED-4A8E-B948-89686A742CF0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResUI", "NResUI\NResUI.csproj", "{7456A089-0701-416C-8668-1F740BF4B72C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NResLib", "NResLib\NResLib.csproj", "{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeshUnpacker", "MeshUnpacker\MeshUnpacker.csproj", "{F1465FFE-0D66-4A3C-90D7-153A14E226E6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TexmLib", "TexmLib\TexmLib.csproj", "{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionDataUnpacker", "MissionDataUnpacker\MissionDataUnpacker.csproj", "{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "meta", "meta", "{BAF212FE-A0FD-41A2-A1A9-B406FDDFBAF3}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MissionTmaLib", "MissionTmaLib\MissionTmaLib.csproj", "{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DB19000-6F41-4BAE-A904-D34EFCA065E9}.Release|Any CPU.Build.0 = Release|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15D1C9ED-1080-417D-A4D1-CFF80BE6A218}.Release|Any CPU.Build.0 = Release|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50C83E6C-23ED-4A8E-B948-89686A742CF0}.Release|Any CPU.Build.0 = Release|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7456A089-0701-416C-8668-1F740BF4B72C}.Release|Any CPU.Build.0 = Release|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9429AEAE-80A6-4EE7-AB66-9161CC4C3A3D}.Release|Any CPU.Build.0 = Release|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1465FFE-0D66-4A3C-90D7-153A14E226E6}.Release|Any CPU.Build.0 = Release|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{40097CB1-B4B8-4D3E-A874-7D46F5C81DB3}.Release|Any CPU.Build.0 = Release|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7BF5C860-9194-4AF2-B5DA-216F98B03DBE}.Release|Any CPU.Build.0 = Release|Any CPU
{773D8EEA-6005-4127-9CB4-5F9F1A028B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
EndGlobal

21
ParkanPlayground.slnx Normal file
View File

@@ -0,0 +1,21 @@
<Solution>
<Folder Name="/meta/">
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="README.md" />
</Folder>
<Project Path="Common\Common.csproj" Type="Classic C#" />
<Project Path="CpDatLib\CpDatLib.csproj" Type="Classic C#" />
<Project Path="MeshUnpacker/MeshUnpacker.csproj" />
<Project Path="MissionDataUnpacker/MissionDataUnpacker.csproj" />
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
<Project Path="NLUnpacker/NLUnpacker.csproj" />
<Project Path="NResLib/NResLib.csproj" />
<Project Path="NResUI/NResUI.csproj" />
<Project Path="ParkanPlayground/ParkanPlayground.csproj" />
<Project Path="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" />
<Project Path="TextureDecoder/TextureDecoder.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" />
<Project Path="Visualisator/Visualisator.csproj" />
</Solution>

77
ParkanPlayground/Msh01.cs Normal file
View File

@@ -0,0 +1,77 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh01
{
public static Msh01Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var headerFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
if (headerFileEntry is null)
{
throw new Exception("Archive doesn't contain header file (01)");
}
var data = new byte[headerFileEntry.ElementCount * headerFileEntry.ElementSize];
mshFs.Seek(headerFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var dataSpan = data.AsSpan();
var elements = new List<SubMesh>((int)headerFileEntry.ElementCount);
for (var i = 0; i < headerFileEntry.ElementCount; i++)
{
var element = new SubMesh()
{
Type1 = dataSpan[i * headerFileEntry.ElementSize + 0],
Type2 = dataSpan[i * headerFileEntry.ElementSize + 1],
ParentIndex =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 2)),
OffsetIntoFile13 =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 4)),
IndexInFile08 =
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 6))
};
element.Lod[0] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 8));
element.Lod[1] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 10));
element.Lod[2] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 12));
element.Lod[3] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 14));
element.Lod[4] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 16));
element.Lod[5] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 18));
element.Lod[6] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 20));
element.Lod[7] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 22));
element.Lod[8] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 24));
element.Lod[9] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 26));
element.Lod[10] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 28));
element.Lod[11] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 30));
element.Lod[12] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 32));
element.Lod[13] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 34));
element.Lod[14] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 36));
elements.Add(element);
}
return new Msh01Component()
{
Elements = elements
};
}
public class Msh01Component
{
public List<SubMesh> Elements { get; set; }
}
public class SubMesh
{
public byte Type1 { get; set; }
public byte Type2 { get; set; }
public short ParentIndex { get; set; }
public short OffsetIntoFile13 { get; set; }
public short IndexInFile08 { get; set; }
public ushort[] Lod { get; set; } = new ushort[15];
}
}

186
ParkanPlayground/Msh02.cs Normal file
View File

@@ -0,0 +1,186 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
public static class Msh02
{
public static Msh02Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
if (fileEntry is null)
{
throw new Exception("Archive doesn't contain 02 component");
}
var data = new byte[fileEntry.FileLength];
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var header = data.AsSpan(0, 0x8c); // 140 bytes header
var center = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x68))
);
var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c));
var bb = new BoundingBox();
bb.Vec1 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
);
bb.Vec2 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
);
bb.Vec3 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
);
bb.Vec4 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
);
bb.Vec5 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
);
bb.Vec6 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
);
bb.Vec7 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
);
bb.Vec8 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92))
);
var bottom = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(112)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(116)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(120))
);
var top = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(124)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(128)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(132))
);
var xyRadius = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(136));
List<Msh02Element> elements = new List<Msh02Element>();
var skippedHeader = data.AsSpan(0x8c); // skip header
for (var i = 0; i < fileEntry.ElementCount; i++)
{
var element = new Msh02Element();
element.StartIndexIn07 =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 0));
element.CountIn07 =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 2));
element.StartOffsetIn0d =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 4));
element.ByteLengthIn0D =
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 6));
element.LocalMinimum = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 8)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 12)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 16))
);
element.LocalMaximum = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 20)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 24)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 28))
);
element.Center = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 32)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 36)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 40))
);
element.Vector4 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 44)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 48)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 52))
);
element.Vector5 = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 56)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 60)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 64))
);
elements.Add(element);
_ = 5;
}
return new Msh02Component()
{
Header = new Msh02Header()
{
BoundingBox = bb,
Center = center,
CenterW = centerW,
Bottom = bottom,
Top = top,
XYRadius = xyRadius
},
Elements = elements
};
}
public class Msh02Component
{
public Msh02Header Header { get; set; }
public List<Msh02Element> Elements { get; set; }
}
public class Msh02Header
{
public BoundingBox BoundingBox { get; set; }
public Vector3 Center { get; set; }
public float CenterW { get; set; }
public Vector3 Bottom { get; set; }
public Vector3 Top { get; set; }
public float XYRadius { get; set; }
}
public class Msh02Element
{
public ushort StartIndexIn07 { get; set; }
public ushort CountIn07 { get; set; }
public ushort StartOffsetIn0d { get; set; }
public ushort ByteLengthIn0D { get; set; }
public Vector3 LocalMinimum { get; set; }
public Vector3 LocalMaximum { get; set; }
public Vector3 Center { get; set; }
public Vector3 Vector4 { get; set; }
public Vector3 Vector5 { get; set; }
}
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; }
}
}

35
ParkanPlayground/Msh03.cs Normal file
View File

@@ -0,0 +1,35 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
public class Msh03
{
public static List<Vector3> ReadComponent(FileStream mshFs, NResArchive mshNres)
{
var verticesFileEntry = mshNres.Files.FirstOrDefault(x => x.FileType == "03 00 00 00");
if (verticesFileEntry is null)
{
throw new Exception("Archive doesn't contain vertices file (03)");
}
if (verticesFileEntry.ElementSize != 12)
{
throw new Exception("Vertices file (03) element size is not 12");
}
var verticesFile = new byte[verticesFileEntry.ElementCount * verticesFileEntry.ElementSize];
mshFs.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(verticesFile, 0, verticesFile.Length);
var vertices = verticesFile.Chunk(12).Select(x => new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadSingleLittleEndian(x.AsSpan(8))
)
).ToList();
return vertices;
}
}

32
ParkanPlayground/Msh06.cs Normal file
View File

@@ -0,0 +1,32 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh06
{
public static List<ushort> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "06 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (06)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elements = new List<ushort>((int)entry.ElementCount);
for (var i = 0; i < entry.ElementCount; i++)
{
elements.Add(
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2))
);
}
return elements;
}
}

53
ParkanPlayground/Msh07.cs Normal file
View File

@@ -0,0 +1,53 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh07
{
public static List<Msh07Element> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (07)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(16);
var elements = elementBytes.Select(x => new Msh07Element()
{
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
Magic02 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
Magic04 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
OffsetX = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)),
OffsetY = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)),
OffsetZ = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)),
Magic14 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)),
}).ToList();
return elements;
}
public class Msh07Element
{
public ushort Flags { get; set; }
public ushort Magic02 { get; set; }
public ushort Magic04 { get; set; }
public ushort Magic06 { get; set; }
// normalized vector X, need to divide by 32767 to get float in range -1..1
public short OffsetX { get; set; }
// normalized vector Y, need to divide by 32767 to get float in range -1..1
public short OffsetY { get; set; }
// normalized vector Z, need to divide by 32767 to get float in range -1..1
public short OffsetZ { get; set; }
public ushort Magic14 { get; set; }
}
}

49
ParkanPlayground/Msh0A.cs Normal file
View File

@@ -0,0 +1,49 @@
using System.Buffers.Binary;
using System.Text;
using NResLib;
namespace ParkanPlayground;
public class Msh0A
{
public static List<string> ReadComponent(FileStream mshFs, NResArchive archive)
{
var aFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "0A 00 00 00");
if (aFileEntry is null)
{
throw new Exception("Archive doesn't contain 0A component");
}
var data = new byte[aFileEntry.FileLength];
mshFs.Seek(aFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
int pos = 0;
var strings = new List<string>();
while (pos < data.Length)
{
var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos));
if (len == 0)
{
pos += 4; // empty entry, no string attached
strings.Add(""); // add empty string
}
else
{
// len is not 0, we need to read it
var strBytes = data.AsSpan(pos + 4, len);
var str = Encoding.UTF8.GetString(strBytes);
strings.Add(str);
pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated
}
}
if (strings.Count != aFileEntry.ElementCount)
{
throw new Exception("String count mismatch in 0A component");
}
return strings;
}
}

55
ParkanPlayground/Msh0D.cs Normal file
View File

@@ -0,0 +1,55 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh0D
{
public const int ElementSize = 20;
public static List<Msh0DElement> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (0D)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(ElementSize);
var elements = elementBytes.Select(x => new Msh0DElement()
{
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
Magic04 = x.AsSpan(4)[0],
Magic05 = x.AsSpan(5)[0],
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)),
CountOf03 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
IndexInto03 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0x10)),
}).ToList();
return elements;
}
public class Msh0DElement
{
public uint Flags { get; set; }
// Magic04 и Magic06 обрабатываются вместе
public byte Magic04 { get; set; }
public byte Magic05 { get; set; }
public ushort Magic06 { get; set; }
public ushort CountOf06 { get; set; }
public int IndexInto06 { get; set; }
public ushort CountOf03 { get; set; }
public int IndexInto03 { get; set; }
}
}

View File

@@ -0,0 +1,229 @@
using System.Text;
using Common;
using NResLib;
namespace ParkanPlayground;
public class MshConverter
{
public void Convert(string mshPath)
{
var mshNresResult = NResParser.ReadFile(mshPath);
var mshNres = mshNresResult.Archive!;
using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var component01 = Msh01.ReadComponent(mshFs, mshNres);
var component02 = Msh02.ReadComponent(mshFs, mshNres);
var component0A = Msh0A.ReadComponent(mshFs, mshNres);
var component07 = Msh07.ReadComponent(mshFs, mshNres);
var component0D = Msh0D.ReadComponent(mshFs, mshNres);
// Triangle Vertex Indices
var component06 = Msh06.ReadComponent(mshFs, mshNres);
// vertices
var component03 = Msh03.ReadComponent(mshFs, mshNres);
_ = 5;
// --- Write OBJ ---
using var sw = new StreamWriter("test.obj", false, new UTF8Encoding(false));
foreach (var v in component03)
sw.WriteLine($"v {v.X:F8} {v.Y:F8} {v.Z:F8}");
var vertices = new List<Vector3>();
var faces = new List<(int, int, int)>(); // store indices into vertices list
// 01 - это части меша (Piece)
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
{
Console.WriteLine($"Piece {pieceIndex}");
var piece01 = component01.Elements[pieceIndex];
// var state = (piece.State00 == 0xffff) ? 0 : piece.State00;
for (var lodIndex = 0; lodIndex < piece01.Lod.Length; lodIndex++)
{
var lod = piece01.Lod[lodIndex];
if (lod == 0xffff)
{
// Console.WriteLine($"Piece {pieceIndex} has lod -1 at {lodIndex}. Skipping");
continue;
}
sw.WriteLine($"o piece_{pieceIndex}_lod_{lodIndex}");
// 02 - Submesh
var part02 = component02.Elements[lod];
int indexInto07 = part02.StartIndexIn07;
var comp07 = component07[indexInto07];
Console.WriteLine($"Lod {lodIndex}");
Console.WriteLine($"Comp07: {comp07.OffsetX}, {comp07.OffsetY}, {comp07.OffsetZ}");
var element0Dstart = part02.StartOffsetIn0d;
var element0Dcount = part02.ByteLengthIn0D;
// Console.WriteLine($"Started piece {pieceIndex}. LOD={lod}. 0D start={element0Dstart}, count={element0Dcount}");
for (var comp0Dindex = 0; comp0Dindex < element0Dcount; comp0Dindex++)
{
var element0D = component0D[element0Dstart + comp0Dindex];
var indexInto03 = element0D.IndexInto03;
var indexInto06 = element0D.IndexInto06; // indices
uint maxIndex = element0D.CountOf03;
uint indicesCount = element0D.CountOf06;
// Convert IndexInto06 to ushort array index (3 ushorts per triangle)
// Console.WriteLine($"Processing 0D element[{element0Dstart + comp0Dindex}]. IndexInto03={indexInto03}, IndexInto06={indexInto06}. Number of triangles={indicesCount}");
if (indicesCount != 0)
{
// sw.WriteLine($"o piece_{pieceIndex}_of_mesh_{comp0Dindex}");
for (int ind = 0; ind < indicesCount; ind += 3)
{
// Each triangle uses 3 consecutive ushorts in component06
// sw.WriteLine($"o piece_{pieceIndex}_of_mesh_{comp0Dindex}_tri_{ind}");
var i1 = indexInto03 + component06[indexInto06];
var i2 = indexInto03 + component06[indexInto06 + 1];
var i3 = indexInto03 + component06[indexInto06 + 2];
var v1 = component03[i1];
var v2 = component03[i2];
var v3 = component03[i3];
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
// push vertices to global list
vertices.Add(v1);
vertices.Add(v2);
vertices.Add(v3);
int baseIndex = vertices.Count;
// record face (OBJ is 1-based indexing!)
faces.Add((baseIndex - 2, baseIndex - 1, baseIndex));
indexInto07++;
indexInto06 += 3; // step by 3 since each triangle uses 3 ushorts
}
_ = 5;
}
}
}
}
}
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
public static void ExportCube(string filePath, Vector3[] points)
{
if (points.Length != 8)
throw new ArgumentException("Cube must have exactly 8 points.");
using (StreamWriter writer = new StreamWriter(filePath))
{
// Write vertices
foreach (var p in points)
{
writer.WriteLine($"v {p.X} {p.Y} {p.Z}");
}
// Write faces (each face defined by 4 vertices, using 1-based indices)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
foreach (var f in faces)
{
writer.WriteLine($"f {f[0]} {f[1]} {f[2]} {f[3]}");
}
}
}
public static void ExportCubesAtPositions(string filePath, List<Vector3> centers, float size = 2f)
{
float half = size / 2f;
using (StreamWriter writer = new StreamWriter(filePath))
{
int vertexOffset = 0;
foreach (var c in centers)
{
// Generate 8 vertices for this cube
Vector3[] vertices = new Vector3[]
{
new Vector3(c.X - half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z + half)
};
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
}
// Define faces (1-based indices, counter-clockwise)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
// Write faces with offset
foreach (var f in faces)
{
writer.WriteLine(
$"f {f[0] + vertexOffset} {f[1] + vertexOffset} {f[2] + vertexOffset} {f[3] + vertexOffset}");
}
vertexOffset += 8;
}
}
}
void Export(string filePath, IEnumerable<Vector3> vertices, List<IndexedEdge> edges)
{
using (var writer = new StreamWriter(filePath))
{
writer.WriteLine("# Exported OBJ file");
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
}
// Write edges as lines ("l" elements in .obj format)
foreach (var e in edges)
{
// OBJ uses 1-based indexing
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
}
}
}
}

View File

@@ -2,13 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,41 +1,17 @@
using System.Buffers.Binary;
using System.Text;
using System.Buffers.Binary;
using Common;
using MissionTmaLib.Parsing;
using NResLib;
using ParkanPlayground;
var libFile = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\ui\\ui_back.lib";
// var cpDatEntryConverter = new CpDatEntryConverter();
// cpDatEntryConverter.Convert();
var parseResult = NResParser.ReadFile(libFile);
var converter = new MshConverter();
if (parseResult.Error != null)
{
Console.WriteLine(parseResult.Error);
return;
}
// var libFileName = new FileInfo(libFile).Name;
//
// if (Directory.Exists(libFileName))
// {
// Directory.Delete(libFileName, true);
// }
//
// var dir = Directory.CreateDirectory(libFileName);
//
// byte[] copyBuffer = new byte[8192];
//
// foreach (var element in elements)
// {
// nResFs.Seek(element.OffsetInFile, SeekOrigin.Begin);
// using var destFs = new FileStream(Path.Combine(libFileName, element.FileName), FileMode.CreateNew);
//
// var totalCopiedBytes = 0;
// while (totalCopiedBytes < element.ItemLength)
// {
// var needReadBytes = Math.Min(element.ItemLength - totalCopiedBytes, copyBuffer.Length);
// var readBytes = nResFs.Read(copyBuffer, 0, needReadBytes);
//
// destFs.Write(copyBuffer, 0, readBytes);
//
// totalCopiedBytes += readBytes;
// }
// }
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");
// converter.Convert("E:\\ParkanUnpacked\\bases.rlb\\25_MESH_R_H_02.msh");

388
README.md
View File

@@ -2,11 +2,13 @@
<div align="center">
<img width="300" height="300" src="https://github.com/user-attachments/assets/dcd9ac8f-7d30-491c-ae6c-537267beb7dc" alt="x86 Registers" />
<img width="817" height="376" alt="Image" src="https://github.com/user-attachments/assets/c4959106-9da4-4c78-a2b7-6c94e360a89e" />
</div>
## Сборка проекта
Проект написан на C# под `.NET 8`
Проект написан на C# под `.NET 9`
Вам должно хватить `dotnet build` для сборки всех проектов отдельно.
@@ -14,13 +16,13 @@
### Состояние проекта
- Распаковка всех `NRes` файлов
- Распаковка всех `TEXM` текстур
+ формат 565 работает некорректно
+ не понятно назначение двух магических чисел в заголовке
- Распаковка данных миссии `.tma`. Пока работает чтение ареалов и кланов.
- Распаковка файла NL. Есть только декодирование заголовка. Формат кажется не используется игрой, а реверс бинарника игры то ещё занятие.
- Распаковка текстуры шрифта формата TFNT. Встроен прямо в UI. По сути шрифт это 4116 байт заголовка и текстура TEXM сразу после.
- Поддержка всех `NRes` файлов - звуки, музыка, текстуры, карты и другие файлы. Есть документация.
- Поддержка всех `TEXM` текстур. Есть документация.
- Поддержка файлов миссий `.tma`.
- Поддержка шрифтов TFNT.
- Поддержка файлов скриптов `.scr`.
- Поддержка файлов параметров `.var`.
- Поддержка файлов схем объектов `.dat`.
### Структура проекта
@@ -34,47 +36,6 @@
Я конечно стараюсь, но ничего не обещаю.
#### NResUI
UI приложение на OpenGL + ImGui.
Туда постепенно добавляю логику.
#### NResLib
Библиотека распаковки формата NRes и всех файлов, которые им запакованы.
Есть логика импорта и экспорта. Работа не завершена, но уже сейчас можно читать любые архивы такого формата.
#### TexmLib
Библиотека распаковки текстур TEXM.
Есть логика импорта и экспорта, хотя к UI последняя не подключена.
#### NLUnpacker
Приложение распаковки NL.
Работа приостановлена, т.к. кажется игра не использует эти файлы.
#### MissionDataUnpacker
Приложение распаковки миссий `.tma`.
Готово чтение ареалов и кланов. Пока в процессе.
#### ParkanPlayground
Пустой проект, использую для локальных тестов.
#### TextureDecoder
Приложение для экспорта текстур TEXM.
Изначально тут игрался с текстурами.
## Для Reverse Engineering-а использую Ghidra
### Наблюдения
@@ -86,6 +47,335 @@ UI приложение на OpenGL + ImGui.
- Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки.
- Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`.
## Как быстро найти текст среди всех файлов игры
```shell
grep -rl --include="*" "s_tree_05" .
```
## Как быстро найти байты среди всех файлов игры
```shell
grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' .
```
## Как работает игра
Главное меню:
Игра сканирует хардкод папку `missions` на наличие файлов миссий. (буквально 01, 02, 03 и т.д.)
Сначала игра читает название миссии из файла `descr` - тут название для меню.
- Одиночные игры - `missions/single.{index}/descr`
- Тренировочные миссии - `missions/tutorial.{index}/descr`
- Кампания - `missions/campaign/campaign.{index1}/descr`
* Далее используются подпапки - `missions/campaign/campaign.{index1}/mission.{index2}/descr`
Как только игра не находит файл `descr`, заканчивается итерация по папкам (понял, т.к. пробуется файл 05 - он не существует).
Загрузка миссии:
Читается файл `ui/game_resources.cfg`
Из этого файла загружаются ресурсы
- `library = "ui\\ui.lib"` - загружается файл `ui.lib`
- `library = "ui\\font.lib"` - загружается файл `font.lib`
- `library = "sounds.lib"` - загружается файл `sounds.lib`
- `library = "voices.lib"` - загружается файл `voices.lib`
Затем игра читает `save/saveslots.cfg` - тут слоты сохранения
Затем `Comp.ini` - тут системные функции, которые используются для загрузки объектов.
```
IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
```
- `Host.url` - этого файла нет
- `palettes.lib` - тут палитры, но этот NRes пустой
- `system.rlb` - не понятно что
- `Textures.lib` - тут текстуры
- `Material.lib` - тут какие-то материалы - не понятно
- `LightMap.lib` - видимо это карты освещения - не понятно
- `sys.lib` - не понятно
- `ScanCode.dsc` - текстовый файл с мапом клавиш
- `command.dsc` - текстовый файл с мапом клавиш
Тут видимо идёт конфигурация ввода
- `table_1.man` - текстовый файл
- `table_2.man` - текстовый файл
- `hero.man` - текстовый файл
- `addition.man` - текстовый файл
- Снова `table_1.man`
- Снова `table_1.man`
- `M1.tbl` - текстовый файл
- Снова `table_2.man`
- Снова `table_2.man`
- `M2.tbl` - текстовый файл
- Снова `hero.man`
- Снова `hero.man`
- `HERO.TBL`
- Снова `addition.man`
- `ui/hq.cfg`
- Снова `ui/hq.cfg`
Дальше непосредственно читается миссия
- `mission.cfg` - метадата миссии
- `units\\units\\prebld\\scr_pre1.dat` из метаданных `object prebuild` - `cp` файл (грузятся подряд все)
- Опять `ui/hq.cfg`
- `mistips.mis` - описание для игрока (экран F1)
- `scancode.dsc` - хз
- `command.dsc` - хз
- `ui_hero.man` - хз
- `ui_bots.man` - хз
- `ui_hq.man` - хз
- `ui_other.man` - хз
- Цикл чтения курсоров
* `ui/cursor.cfg` - тут настройки курсора.
* `ui/{name}` - курсор
- Снова `mission.cfg` - метадата миссии
- `descr` - название
- `data/textres.cfg` - конфиг текстов
- Снова `mission.cfg` - метадата миссии
- Ещё раз `mission.cfg` - метадата миссии
- `ui/minimap.lib` - NRes с текстурами миникарты.
- `messages.cfg` - Tutorial messages
УРА НАКОНЕЦ-ТО `data.tma`
- Из `.tma` берётся LAND строка (я её так назвал)
- `DATA\\MAPS\\SC_3\\land1.wea`
- `DATA\\MAPS\\SC_3\\land2.wea`
- `BuildDat.lst` - Behaviour will use these schemes to Build Fortification
- `DATA\\MAPS\\SC_3\\land.map`
- `DATA\\MAPS\\SC_3\\land.msh`
- `effects.rlb`
Цикл по кланам из `.tma`
- `MISSIONS\\SCRIPTS\\screampl.scr`
- `varset.var`
- `MISSIONS\\SCRIPTS\\varset.var`
- `MISSIONS\\SCRIPTS\\screampl.fml`
- `missions/single.01/sky.ske`
- `missions/single.01/sky.wea`
Дальше начинаются объекты игры
- `"UNITS\\BUILDS\\BUNKER\\mbunk01.dat"` - cp файл
## Загрузка `cp` файлов
`cp` файл - схема. Он содержит дерево частей объекта.
`cp` файл читается в `ArealMap.dll/CreateObjectFromScheme`
В зависимости от типа объекта внутри схемы (байты 4..8) выбирается функция, с помощью которой загружается схема.
Функция выбирается на основе файла `Comp.ini`.
- Для ClassBuilding (0x80000000) - вызывается функция c классом 3 (по таблице ниже Building).
- Для всех остальных - функция с классом 4 (по таблице ниже Agent).
На основе файла `Comp.ini` и первом вызове внутри функции `World3D.dll/CreateObject` ремаппинг id:
| Class ID | ClassName | Function |
|:----------:|:-------------:|--------------------------------|
| 1 | Landscape | `terrain.dll LoadLandscape` |
| 2 | Agent | `animesh.dll LoadAgent` |
| 3 | Building | `terrain.dll LoadBuilding` |
| 4 | Agent | `animesh.dll LoadAgent` |
| 5 | Camera | `terrain.dll LoadCamera` |
| 7 | Atmospehere | `terrain.dll CreateAtmosphere` |
| 9 | Agent | `animesh.dll LoadAgent` |
| 10 | Agent | `animesh.dll LoadAgent` |
| 11 | Research | `misload.dll LoadResearch` |
| 12 | Agent | `animesh.dll LoadAgent` |
Будет дополняться по мере реверса.
Всем этим функциям передаётся `nres_file_name, nres_entry_name, 0, player_id`
## `fr FORT` файл
Всегда 0x80 байт
Содержит 2 ссылки на файлы:
- `.bas`
- `.ctl` - вызывается `LoadAgent`
## `.msh`
### Описание ниже валидно только для моделей роботов и зданий.
##### Land.msh использует другой формат, хотя 03 файл это всё ещё точки.
Загружается в `AniMesh.dll/LoadAniMesh`
- Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD
```
нулевому элементу добавляется флаг 0x1000000
Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08)
Если интерполируется анимация -0.5s короче чем magic1 у файла 13
И у файла есть OffsetIntoFile13
И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда)
Тогда вместо IndexInFile08 используется значение из файла 13 по этому оффсету (второй байт)
```
- Тип 02 - описание одного LOD Submesh
```
Вначале идёт заголовок 0x8C (140) байт
В заголовке:
8 Vector3 (x,y,z) - bounding box
1 Vector4 - center
1 Vector3 - bottom
1 Vector3 - top
1 float - xy_radius
Далее инфа про куски меша
```
- Тип 03 - это вершины (vertex)
- Тип 06 - индексы треугольников в файле 03
- Тип 04 - скорее всего какие-то цвета RGBA или типа того
- Тип 08 - меш-анимации (см файл 01)
```
Индексируется по IndexInFile08 из файла 01 либо по файлу 13 через OffsetIntoFile13
Структура:
Vector3 position;
float time; // содержит только целые секунды
short rotation_x; // делится на 32767
short rotation_y; // делится на 32767
short rotation_z; // делится на 32767
short rotation_w; // делится на 32767
---
Игра интерполирует анимацию между текущим стейтом и следующим по time.
Если время интерполяции совпадает с исходным time, жёстко берётся первый стейт из 0x13.
Если время интерполяции совпадает с конечным time, жёстко берётся второй стейт из 0x13.
Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time)
```
- Тип 12 - microtexture mapping
- Тип 13 - короткие меш-анимации (почему я это не дописал?)
```
Буквально (hex)
00 01 01 02 ...
```
- Тип 0A - ссылка на части меша, не упакованные в текущий меш (например у бункера 4 и 5 части хранятся в parts.rlb)
```
Не имеет фиксированной длины. Хранит строки в следующем формате.
Игра обращается по индексу, пропуская суммарную длину и пропуская 4 байта на каждую строку (длина).
т.е. буквально файл выглядит так
00 00 00 00 - пустая строка
03 00 00 00 - длина строки 1
73 74 72 00 - строка "str" + null terminator
.. и повторяется до конца файла
Кол-во элементов из файла 01 должно быть равно кол-ву строк в этом файле, хотя игра это не проверяет.
Если у элемента эта строка равна "central", ему выставляется флаг (flag |= 1)
```
## `.wea`
Загружается в `World3D.dll/LoadMatManager`
По сути это текстовый файл состоящий из 2 частей:
- Материалы
```
{count}
{id} {name}
```
- Карты освещения
```
LIGHTMAPS
{count}
{id} {name}
```
Может как-то анимироваться. Как - пока не понятно.
# Внутренняя система ID
- `1` -
- `4` - IShader
- `5` - ITerrain
- `6` - IGameObject (0x138)
- `7` - IShadeConfig (у меня в папке с игрой его не оказалось)
- `8` - ICamera
- `9` - IQueue
- `10` - IControl
- `0xb` - IAnimation
- `0xd` - IMatManager
- `0xe` - ILightManager
- `0x10` - IBehavior
- `0x11` - IBasement
- `0x12` - ICamera2 или IBufferingCamera
- `0x13` - IEffectManager
- `0x14` - IPosition
- `0x15` - IAgent
- `0x16` - ILifeSystem
- `0x17` - IBuilding - точно он, т.к. ArealMap.CreateObject на него проверяет
- `0x18` - IMesh2
- `0x19` - IManManager
- `0x20` - IJointMesh
- `0x21` - IShade
- `0x23` - IGameSettings
- `0x24` - IGameObject2
- `0x25` - unknown (implemented by AniMesh)
- `0x26` - unknown (implemented by AniMesh)
- `0x28` - ICollObject
- `0x101` - 3DRender
- `0x105` - NResFile
- `0x106` - NResFileMetadata
- `0x201` - IWizard
- `0x202` - IItemManager
- `0x203` - ICollManager
- `0x301` - IArealMap
- `0x302` - ISystemArealMap
- `0x303` - IHallway
- `0x304` - Distributor
- `0x401` - ISuperAI
- `0x501` - MissionData
- `0x502` - ResTree
- `0x700` - NetWatcher
- `0x701` - INetworkInterface
- `0x10d` - CreateVertexBufferData
## Опции
World3D.dll содержит функцию CreateGameSettings.
Она создаёт объект настроек и далее вызывает методы в соседних библиотеках.
- Terrain.dll - InitializeSettings
- Effect.dll - InitializeSettings
- Control.dll - InitializeSettings
Остальные наверное не трогают настройки.
| Resource ID | wOptionID | Name | Default | Description |
|:-----------:|:---------------:|:--------------------------:|:-------:|--------------------|
| 1 | 100 (0x64) | "Texture detail" | | |
| 2 | 101 (0x65) | "3D Sound" | | |
| 3 | 102 (0x66) | "Mouse sensitivity" | | |
| 4 | 103 (0x67) | "Joystick sensitivity" | | |
| 5 | !not a setting! | "Illegal wOptionID" | | |
| 6 | 104 (0x68) | "Wait for retrace" | | |
| 7 | 105 (0x69) | "Inverse mouse X" | | |
| 8 | 106 (0x6a) | "Inverse mouse Y" | | |
| 9 | 107 (0x6b) | "Inverse joystick X" | | |
| 10 | 108 (0x6c) | "Inverse joystick Y" | | |
| 11 | 109 (0x6d) | "Use BumpMapping" | | |
| 12 | 110 (0x6e) | "3D Sound quality" | | |
| 13 | 90 (0x5a) | "Reverse sound" | | |
| 14 | 91 (0x5b) | "Sound buffer frequency" | | |
| 15 | 92 (0x5c) | "Play sound buffer always" | | |
| 16 | 93 (0x5d) | "Select best sound device" | | |
| ---- | 30 (0x1e) | ShadeConfig | | из файла shade.cfg |
| ---- | (0x8001e) | | | добавляет AniMesh |
## Контакты
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).

View File

@@ -0,0 +1,3 @@
namespace ScrLib;
public record ScrParseResult(ScrFile? Scr, string? Error);

60
ScrLib/ScrFile.cs Normal file
View File

@@ -0,0 +1,60 @@
namespace ScrLib;
public class ScrFile
{
/// <summary>
/// тут всегда число 59 (0x3b) - это число известных игре скриптов
/// </summary>
public int Magic { get; set; }
public int EntryCount { get; set; }
public List<ScrEntry> Entries { get; set; }
}
public class ScrEntry
{
public string Title { get; set; }
public int Index { get; set; }
public int InnerCount { get; set; }
public List<ScrEntryInner> Inners { get; set; }
}
public class ScrEntryInner
{
/// <summary>
/// Номер скрипта в игре (это тех, которых 0x3b)
/// </summary>
public int ScriptIndex { get; set; }
public int UnkInner2 { get; set; }
public int UnkInner3 { get; set; }
public ScrEntryInnerType Type { get; set; }
public int UnkInner5 { get; set; }
public int ArgumentsCount { get; set; }
public List<int> Arguments { get; set; }
public int UnkInner7 { get; set; }
}
public enum ScrEntryInnerType
{
Unspecified = -1,
_0 = 0,
_1 = 1,
_2 = 2,
_3 = 3,
_4 = 4,
CheckInternalState = 5,
/// <summary>
/// В случае 6, игра берёт UnkInner2 (индекс в Varset) и устанавливает ему значение UnkInner3
/// </summary>
SetVarsetValue = 6,
}

5
ScrLib/ScrLib.csproj Normal file
View File

@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

57
ScrLib/ScrParser.cs Normal file
View File

@@ -0,0 +1,57 @@
using Common;
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.Type = (ScrEntryInnerType)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;
}
}

View File

@@ -180,14 +180,20 @@ public class TexmFile
{
var rawPixel = span.Slice(i, 2);
var g = (byte)(((rawPixel[0] >> 3) & 0b11111) * 255 / 31);
var b = (byte)((((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111)) * 255 / 63);
var r = (byte)((rawPixel[1] & 0b11111) * 255 / 31);
// swap endianess
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = r;
var r = (byte)(((rawPixel[0] >> 3) & 0b11111) * 255 / 31);
var g = (byte)((((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111)) * 255 / 63);
var b = (byte)((rawPixel[1] & 0b11111) * 255 / 31);
result[i / 2 * 4 + 0] = (byte)(0xff - r);
result[i / 2 * 4 + 1] = (byte)(0xff - g);
result[i / 2 * 4 + 2] = (byte)(0xff - b);
result[i / 2 * 4 + 3] = 0xff;
// swap endianess back
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
}
return result;
@@ -202,6 +208,9 @@ public class TexmFile
{
var rawPixel = span.Slice(i, 2);
// swap endianess
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
var a = (byte)(((rawPixel[0] >> 4) & 0b1111) * 17);
var b = (byte)(((rawPixel[0] >> 0) & 0b1111) * 17);
var g = (byte)(((rawPixel[1] >> 4) & 0b1111) * 17);
@@ -211,6 +220,9 @@ public class TexmFile
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = a;
// swap endianess back
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
}
return result;
@@ -247,16 +259,21 @@ public class TexmFile
for (var i = 0; i < span.Length; i += 4)
{
var rawPixel = span.Slice(i, 4);
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
var b = rawPixel[0];
var r = rawPixel[0];
var g = rawPixel[1];
var r = rawPixel[2];
var b = rawPixel[2];
var a = rawPixel[3];
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
result[i + 3] = a;
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
}
return result;

View File

@@ -1,13 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
</Project>

View File

@@ -2,17 +2,10 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
<ItemGroup>

3
VarsetLib/VarsetItem.cs Normal file
View File

@@ -0,0 +1,3 @@
namespace VarsetLib;
public record VarsetItem(string Type, string Name, string Value);

View File

@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>

68
VarsetLib/VarsetParser.cs Normal file
View File

@@ -0,0 +1,68 @@
namespace VarsetLib;
public class VarsetParser
{
public static List<VarsetItem> Parse(string path)
{
FileStream fs = new FileStream(path, FileMode.Open);
var reader = new StreamReader(fs);
List<VarsetItem> 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;
}
}

180
Visualisator/Program.cs Normal file
View File

@@ -0,0 +1,180 @@
// Configure window options
using System.Buffers.Binary;
using System.Numerics;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
public static class Program
{
private static string vertexShaderSource = @"
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uMVP;
void main()
{
gl_Position = uMVP * vec4(aPos, 1.0);
gl_PointSize = 8.0;
}
";
private static string fragmentShaderSource = @"
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 1.0, 1.0, 1.0); // White points
}
";
private static IWindow? window;
private static GL? gl = null;
private static uint shaderProgram = uint.MaxValue;
private static uint vao = uint.MaxValue;
private static uint vbo = uint.MaxValue;
private static Matrix4x4 mvp = new Matrix4x4();
private static float[] points = [];
public static void Main(string[] args)
{
var path = "C:\\ParkanUnpacked\\Land.msh\\2_03 00 00 00_Land.bin";
var bytes = File.ReadAllBytes(path);
points = new float[bytes.Length / 4];
for (int i = 0; i < bytes.Length / 4; i++)
{
points[i] = BinaryPrimitives.ReadSingleBigEndian(bytes.AsSpan()[(i * 4)..]);
}
var options = WindowOptions.Default;
options.API = new GraphicsAPI(ContextAPI.OpenGL, new APIVersion(3, 3));
options.Title = "3D Points with Silk.NET";
window = Window.Create(options);
window.Load += OnLoad;
window.Render += OnRender;
window.Run();
}
unsafe static void OnLoad()
{
gl = window.CreateOpenGL();
// Compile shaders
uint vertexShader = gl.CreateShader(ShaderType.VertexShader);
gl.ShaderSource(vertexShader, vertexShaderSource);
gl.CompileShader(vertexShader);
CheckShaderCompile(vertexShader);
uint fragmentShader = gl.CreateShader(ShaderType.FragmentShader);
gl.ShaderSource(fragmentShader, fragmentShaderSource);
gl.CompileShader(fragmentShader);
CheckShaderCompile(fragmentShader);
// Create shader program
shaderProgram = gl.CreateProgram();
gl.AttachShader(shaderProgram, vertexShader);
gl.AttachShader(shaderProgram, fragmentShader);
gl.LinkProgram(shaderProgram);
CheckProgramLink(shaderProgram);
gl.DeleteShader(vertexShader);
gl.DeleteShader(fragmentShader);
// Create VAO and VBO
vao = gl.GenVertexArray();
gl.BindVertexArray(vao);
vbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo);
unsafe
{
fixed (float* ptr = points)
{
gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint) (points.Length * sizeof(float)),
ptr,
BufferUsageARB.StaticDraw
);
}
}
gl.VertexAttribPointer(
0,
3,
VertexAttribPointerType.Float,
false,
3 * sizeof(float),
(void*) 0
);
gl.EnableVertexAttribArray(0);
gl.BindVertexArray(0); // Unbind VAO
gl.Enable(EnableCap.DepthTest);
}
unsafe static void OnRender(double dt)
{
gl.ClearColor(
0.1f,
0.1f,
0.1f,
1.0f
);
gl.Clear((uint) (ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit));
// Set up MVP matrix
Matrix4x4 view = Matrix4x4.CreateLookAt(
new Vector3(100, 100, 40), // Camera position
Vector3.Zero, // Look at origin
Vector3.UnitY
); // Up direction
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
(float) Math.PI / 4f, // 45 degrees
(float) window.Size.X / window.Size.Y,
0.1f,
100f
);
mvp = view * projection;
gl.UseProgram(shaderProgram);
// Set MVP matrix (transpose=true for column-major format)
int mvpLocation = gl.GetUniformLocation(shaderProgram, "uMVP");
fixed (Matrix4x4* ptr = &mvp)
{
gl.UniformMatrix4(
mvpLocation,
1,
true,
(float*) ptr
);
}
gl.BindVertexArray(vao);
gl.DrawArrays(PrimitiveType.Points, 0, (uint) (points.Length / 3));
}
// Error checking methods
static void CheckShaderCompile(uint shader)
{
gl.GetShader(shader, ShaderParameterName.CompileStatus, out int success);
if (success == 0)
Console.WriteLine(gl.GetShaderInfoLog(shader));
}
static void CheckProgramLink(uint program)
{
gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int success);
if (success == 0)
Console.WriteLine(gl.GetProgramInfoLog(program));
}
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="NativeFileDialogSharp" />
<PackageReference Include="Silk.NET" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" />
</ItemGroup>
</Project>