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

15 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
578 changed files with 1554 additions and 42165 deletions

View File

@@ -1,23 +0,0 @@
when creating or edditing code, adjust namespace declaration style to oneliner, e.g. "namespace MyNamespace;".
always separate usings, namespaces, type declarations, methods and properties with empty line.
always add comments to the code, when the code is not trivial.
always put classes into separate files.
always try to build the project you've edited.
always summarize the changes you've made.
always add changes to git with descriptive comment, but be concise.
never use terminal commands to edit code. In case of a failure, write it to user and stop execution.
never address compiler warnings yourself. If you see a warning, suggest to address it.
when working with RVA variables, always add that to variable name, e.g. "nameRVA".
always build only affected project, not full solution.
never introduce special cases in general solutions.

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 ScrLib;
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

@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NResLib\NResLib.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,332 +0,0 @@
using System.Buffers.Binary;
using NResLib;
using System.Numerics;
using System.Text;
namespace LandscapeExplorer;
public static class Program
{
private const string MapsDirectory = @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_3";
public static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("Parkan 1 Landscape Explorer\n");
// Get all .map and .msh files in the directory
var mapFiles = Directory.GetFiles(MapsDirectory, "*.map");
var mshFiles = Directory.GetFiles(MapsDirectory, "*.msh");
Console.WriteLine($"Found {mapFiles.Length} .map files and {mshFiles.Length} .msh files in {MapsDirectory}\n");
// Process .map files
Console.WriteLine("=== MAP Files Analysis ===\n");
foreach (var mapFile in mapFiles)
{
AnalyzeNResFile(mapFile);
}
// Process .msh files
Console.WriteLine("\n=== MSH Files Analysis ===\n");
foreach (var mshFile in mshFiles)
{
AnalyzeNResFile(mshFile);
// Perform detailed landscape analysis on MSH files
AnalyzeLandscapeMeshFile(mshFile);
}
Console.WriteLine("\nAnalysis complete.");
}
/// <summary>
/// Analyzes an NRes file and displays its structure
/// </summary>
/// <param name="filePath">Path to the NRes file</param>
private static void AnalyzeNResFile(string filePath)
{
Console.WriteLine($"Analyzing file: {Path.GetFileName(filePath)}");
var parseResult = NResParser.ReadFile(filePath);
if (parseResult.Error != null)
{
Console.WriteLine($" Error: {parseResult.Error}");
return;
}
var archive = parseResult.Archive!;
Console.WriteLine($" Header: {archive.Header.NRes}, Version: {archive.Header.Version:X}, Files: {archive.Header.FileCount}, Size: {archive.Header.TotalFileLengthBytes} bytes");
// Group files by type for better analysis
var filesByType = archive.Files.GroupBy(f => f.FileType);
foreach (var group in filesByType)
{
Console.WriteLine($" File Type: {group.Key}, Count: {group.Count()}");
// Display details of the first file of each type as an example
var example = group.First();
Console.WriteLine($" Example: {example.FileName}");
Console.WriteLine($" Elements: {example.ElementCount}, Element Size: {example.ElementSize} bytes");
Console.WriteLine($" File Length: {example.FileLength} bytes, Offset: {example.OffsetInFile}");
// If this is a landscape-related file, provide more detailed analysis
if (IsLandscapeRelatedType(group.Key))
{
AnalyzeLandscapeData(example, filePath);
}
}
Console.WriteLine();
}
/// <summary>
/// Determines if a file type is related to landscape data
/// </summary>
private static bool IsLandscapeRelatedType(string fileType)
{
// Based on the Landscape constructor analysis, these types might be related to landscape
return fileType == "LAND" || fileType == "TERR" || fileType == "MSH0" ||
fileType == "MESH" || fileType == "MATR" || fileType == "TEXT";
}
/// <summary>
/// Analyzes landscape-specific data in a file
/// </summary>
private static void AnalyzeLandscapeData(ListMetadataItem item, string filePath)
{
Console.WriteLine($" [Landscape Data Analysis]:");
// Read the file data for this specific item
using var fs = new FileStream(filePath, FileMode.Open);
fs.Seek(item.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[Math.Min(item.FileLength, 256)]; // Read at most 256 bytes for analysis
fs.Read(buffer, 0, buffer.Length);
// Display some basic statistics based on the file type
if (item.FileType == "LAND" || item.FileType == "TERR")
{
Console.WriteLine($" Terrain data with {item.ElementCount} elements");
// If element size is known, we can calculate grid dimensions
if (item.ElementCount > 0 && item.ElementSize > 0)
{
// Assuming square terrain, which is common in games from this era
var gridSize = Math.Sqrt(item.ElementCount);
if (Math.Abs(gridSize - Math.Round(gridSize)) < 0.001) // If it's close to a whole number
{
Console.WriteLine($" Terrain grid size: {Math.Round(gridSize)} x {Math.Round(gridSize)}");
}
}
}
else if (item.FileType == "MSH0" || item.FileType == "MESH")
{
// For mesh data, try to estimate vertex/face counts
Console.WriteLine($" Mesh data, possibly with vertices and faces");
// Common sizes: vertices are often 12 bytes (3 floats), faces are often 12 bytes (3 indices)
if (item.ElementSize == 12)
{
Console.WriteLine($" Possibly {item.ElementCount} vertices or faces");
}
}
// Display first few bytes as hex for debugging
var hexPreview = BitConverter.ToString(
buffer.Take(32)
.ToArray()
)
.Replace("-", " ");
Console.WriteLine($" Data preview (hex): {hexPreview}...");
}
/// <summary>
/// Performs a detailed analysis of a landscape mesh file
/// </summary>
/// <param name="filePath">Path to the MSH file</param>
private static void AnalyzeLandscapeMeshFile(string filePath)
{
Console.WriteLine($"\nDetailed Landscape Analysis for: {Path.GetFileName(filePath)}\n");
var parseResult = NResParser.ReadFile(filePath);
if (parseResult.Error != null || parseResult.Archive == null)
{
Console.WriteLine($" Error analyzing file: {parseResult.Error}");
return;
}
var archive = parseResult.Archive;
// Based on the Landscape constructor and the file analysis, we can identify specific sections
// File types in MSH files appear to be numeric values (01, 02, 03, etc.)
// First, let's extract all the different data sections
var sections = new Dictionary<string, (ListMetadataItem Meta, byte[] Data)>();
foreach (var item in archive.Files)
{
using var fs = new FileStream(filePath, FileMode.Open);
fs.Seek(item.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[item.FileLength];
fs.Read(buffer, 0, buffer.Length);
sections[item.FileType] = (item, buffer);
}
// Now analyze each section based on what we know from the Landscape constructor
Console.WriteLine(" Landscape Structure Analysis:");
// Type 01 appears to be basic landscape information (possibly header/metadata)
if (sections.TryGetValue("01 00 00 00", out var section01))
{
Console.WriteLine($" Section 01: Basic Landscape Info");
Console.WriteLine($" Elements: {section01.Meta.ElementCount}, Element Size: {section01.Meta.ElementSize} bytes");
Console.WriteLine($" Total Size: {section01.Meta.FileLength} bytes");
// Try to extract some basic info if the format is as expected
if (section01.Meta.ElementSize == 38 && section01.Data.Length >= 38)
{
// This is speculative based on common terrain formats
var width = BitConverter.ToInt32(section01.Data, 0);
var height = BitConverter.ToInt32(section01.Data, 4);
Console.WriteLine($" Possible Dimensions: {width} x {height}");
}
}
// Type 03 appears to be vertex data (based on element size of 12 bytes which is typical for 3D vertices)
if (sections.TryGetValue("03 00 00 00", out var section03))
{
Console.WriteLine($"\n Section 03: Vertex Data");
Console.WriteLine($" Vertex Count: {section03.Meta.ElementCount}");
Console.WriteLine($" Vertex Size: {section03.Meta.ElementSize} bytes");
// If we have vertex data in expected format (3 floats per vertex)
if (section03.Meta.ElementSize == 12 && section03.Data.Length >= 36)
{
// Display first 3 vertices as example
Console.WriteLine(" Sample Vertices:");
for (int i = 0; i < Math.Min(3, section03.Meta.ElementCount); i++)
{
var offset = i * 12;
var x = BitConverter.ToSingle(section03.Data, offset);
var y = BitConverter.ToSingle(section03.Data, offset + 4);
var z = BitConverter.ToSingle(section03.Data, offset + 8);
Console.WriteLine($" Vertex {i}: ({x}, {y}, {z})");
}
// Calculate terrain bounds
var minX = float.MaxValue;
var minY = float.MaxValue;
var minZ = float.MaxValue;
var maxX = float.MinValue;
var maxY = float.MinValue;
var maxZ = float.MinValue;
for (int i = 0; i < section03.Meta.ElementCount; i++)
{
var offset = i * 12;
if (offset + 12 <= section03.Data.Length)
{
var x = BitConverter.ToSingle(section03.Data, offset);
var y = BitConverter.ToSingle(section03.Data, offset + 4);
var z = BitConverter.ToSingle(section03.Data, offset + 8);
minX = Math.Min(minX, x);
minY = Math.Min(minY, y);
minZ = Math.Min(minZ, z);
maxX = Math.Max(maxX, x);
maxY = Math.Max(maxY, y);
maxZ = Math.Max(maxZ, z);
}
}
Console.WriteLine(" Terrain Bounds:");
Console.WriteLine($" Min: ({minX}, {minY}, {minZ})");
Console.WriteLine($" Max: ({maxX}, {maxY}, {maxZ})");
Console.WriteLine($" Dimensions: {maxX - minX} x {maxY - minY} x {maxZ - minZ}");
}
}
// Type 02 might be face/index data for the mesh
if (sections.TryGetValue("02 00 00 00", out var section02))
{
Console.WriteLine($"\n Section 02: Possible Face/Index Data");
Console.WriteLine($" Elements: {section02.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section02.Meta.ElementSize} bytes");
// If element size is divisible by 4 (common for index data)
if (section02.Meta.ElementSize % 4 == 0 && section02.Data.Length >= 12)
{
// Display first triangle as example (assuming 3 indices per triangle)
Console.WriteLine(" Sample Indices (if this is index data):");
var indicesPerElement = section02.Meta.ElementSize / 4;
for (int i = 0; i < Math.Min(1, section02.Meta.ElementCount); i++)
{
Console.Write($" Element {i}: ");
for (int j = 0; j < indicesPerElement; j++)
{
var offset = i * section02.Meta.ElementSize + j * 4;
if (offset + 4 <= section02.Data.Length)
{
var index = BitConverter.ToInt32(section02.Data, offset);
Console.Write($"{index} ");
}
}
Console.WriteLine();
}
}
}
// Types 04, 05, 12, 0E, 0B might be texture coordinates, normals, colors, etc.
var otherSections = new[] {"04 00 00 00", "05 00 00 00", "12 00 00 00", "0E 00 00 00", "0B 00 00 00"};
foreach (var sectionType in otherSections)
{
if (sections.TryGetValue(sectionType, out var section))
{
Console.WriteLine($"\n Section {sectionType.Substring(0, 2)}: Additional Mesh Data");
Console.WriteLine($" Elements: {section.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section.Meta.ElementSize} bytes");
// If element size is 4 bytes, it could be color data, texture indices, etc.
if (section.Meta.ElementSize == 4 && section.Data.Length >= 12)
{
Console.WriteLine(" Sample Data (as integers):");
for (int i = 0; i < Math.Min(3, section.Meta.ElementCount); i++)
{
var offset = i * 4;
var value = BitConverter.ToInt32(section.Data, offset);
Console.WriteLine($" Element {i}: {value}");
}
}
}
}
// Type 15 might be material or special data (Msh_15 in the decompiled code)
if (sections.TryGetValue("15 00 00 00", out var section15) && sections.TryGetValue("03 00 00 00", out var vertexSection))
{
Console.WriteLine($"\n Section 15: Special Data (Msh_15 type in decompiled code)");
Console.WriteLine($" Elements: {section15.Meta.ElementCount}");
Console.WriteLine($" Element Size: {section15.Meta.ElementSize} bytes");
int count = 0;
for (var i = 0; i < section15.Data.Length; i += 28)
{
var first = BinaryPrimitives.ReadUInt32LittleEndian(section15.Data.AsSpan(i));
if ((first & 0x20000) != 0)
{
Console.WriteLine($"Found {first}/0x{first:X8} 0x20000 at index {i / 28}. &0x20000={first&0x20000}/0x{first&0x20000:X8} offset: {i:X8}");
count++;
}
}
Console.WriteLine($"Total found: {count}");
}
}
}

View File

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

View File

@@ -2,9 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.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

@@ -1,4 +1,6 @@
namespace MissionTmaLib;
using Common;
namespace MissionTmaLib;
public class GameObjectInfo
{

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;
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>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,39 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace MissionTmaLib.Parsing;
public static class Extensions
{
public static int ReadInt32LittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this FileStream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadLengthPrefixedString(this FileStream fs)
{
var len = fs.ReadInt32LittleEndian();
if (len == 0)
{
return "";
}
var buffer = new byte[len];
fs.ReadExactly(buffer, 0, len);
return Encoding.ASCII.GetString(buffer, 0, len);
}
}

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);

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

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

View File

@@ -1,151 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace NResLib;
public class NResPacker
{
public static string Pack(NResArchive archive, string srcNresPath, string contentDirectoryPath, string targetFileDirectoryPath)
{
var diskFiles = Directory.GetFiles(contentDirectoryPath)
.Select(Path.GetFileName)
.ToList();
var fileOffset = 16; // 16 по умолчанию, т.к. есть заголовок в 16 байт.
var metadataItems = new List<ListMetadataItem>();
foreach (var archiveFile in archive.Files)
{
var extension = Path.GetExtension(archiveFile.FileName);
var fileName = Path.GetFileNameWithoutExtension(archiveFile.FileName);
if (extension == "")
{
extension = ".bin";
}
var targetFileName = $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}";
if (diskFiles.All(x => x != targetFileName))
{
return $"Не найдён файл {targetFileName}";
}
var filePath = Path.Combine(contentDirectoryPath, targetFileName);
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
throw new Exception();
}
var newFileLength = (int)fileInfo.Length;
var listItem = new ListMetadataItem(
archiveFile.FileType,
archiveFile.ElementCount,
archiveFile.Magic1,
newFileLength,
archiveFile.ElementSize,
archiveFile.FileName,
archiveFile.Magic3,
archiveFile.Magic4,
archiveFile.Magic5,
archiveFile.Magic6,
fileOffset,
archiveFile.Index
);
fileOffset += newFileLength;
metadataItems.Add(listItem);
}
var totalFileLength =
16 + // заголовок
metadataItems.Sum(x => x.FileLength) + // сумма длин всех файлов
metadataItems.Count * 64; // длина всех метаданных
var header = new NResArchiveHeader(archive.Header.NRes, archive.Header.Version, archive.Header.FileCount, totalFileLength);
var targetArchive = new NResArchive(header, metadataItems);
// имя архива = имени папки в которую архив распаковывали
string targetArchiveFileName = Path.GetFileName(srcNresPath)!;
var targetArchivePath = Path.Combine(targetFileDirectoryPath, targetArchiveFileName);
using var fs = new FileStream(targetArchivePath, FileMode.CreateNew);
Span<byte> span = stackalloc byte[4];
span.Clear();
Encoding.ASCII.GetBytes(header.NRes, span);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.Version);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.FileCount);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, header.TotalFileLengthBytes);
fs.Write(span);
foreach (var archiveFile in targetArchive.Files)
{
var extension = Path.GetExtension(archiveFile.FileName);
var fileName = Path.GetFileNameWithoutExtension(archiveFile.FileName);
if (extension == "")
{
extension = ".bin";
}
var targetFileName = $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}";
var filePath = Path.Combine(contentDirectoryPath, targetFileName);
using var srcFs = new FileStream(filePath, FileMode.Open);
srcFs.CopyTo(fs);
}
Span<byte> fileNameSpan = stackalloc byte[20];
foreach (var archiveFile in targetArchive.Files)
{
span.Clear();
Encoding.ASCII.GetBytes(archiveFile.FileType, span);
fs.Write(span);
BinaryPrimitives.WriteUInt32LittleEndian(span, archiveFile.ElementCount);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic1);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.FileLength);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.ElementSize);
fs.Write(span);
fileNameSpan.Clear();
Encoding.ASCII.GetBytes(archiveFile.FileName, fileNameSpan);
fs.Write(fileNameSpan);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic3);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic4);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic5);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Magic6);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.OffsetInFile);
fs.Write(span);
BinaryPrimitives.WriteInt32LittleEndian(span, archiveFile.Index);
fs.Write(span);
}
fs.Flush();
return "Запакован архив";
}
}

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)
{

View File

@@ -56,6 +56,7 @@ public class App
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,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,4 +1,5 @@
using System.Numerics;
using System.Numerics;
using CpDatLib;
using ImGuiNET;
using MissionTmaLib;
using MissionTmaLib.Parsing;
@@ -18,6 +19,7 @@ namespace NResUI.ImGuiUI
ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
MessageBoxModalPanel messageBox)
: IImGuiPanel
{
@@ -121,6 +123,21 @@ namespace NResUI.ImGuiUI
}
}
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"))
@@ -138,34 +155,6 @@ namespace NResUI.ImGuiUI
}
}
if (nResExplorerViewModel.HasFile)
{
if (ImGui.MenuItem("Запаковать NRes"))
{
messageBox.Show("Выберите папку с контентом NRES");
var contentDirectoryPicker = Dialog.FolderPicker();
if (contentDirectoryPicker.IsOk)
{
var contentDirectoryPath = contentDirectoryPicker.Path;
var targetFileDirectoryPicker = Dialog.FolderPicker();
if (targetFileDirectoryPicker.IsOk)
{
var targetFileDirectory = targetFileDirectoryPicker.Path;
var packResult = NResPacker.Pack(
nResExplorerViewModel.Archive!,
nResExplorerViewModel.Path!,
contentDirectoryPath, targetFileDirectory);
messageBox.Show(packResult);
}
}
}
}
ImGui.EndMenu();
}

View File

@@ -18,6 +18,9 @@ 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)
{

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");
@@ -82,8 +85,8 @@ public class NResExplorerPanel : IImGuiPanel
);
ImGui.TableNextColumn();
ImGui.Text(
"0x" + _viewModel.Archive.Files[i]
.ElementSize.ToString("X2")
_viewModel.Archive.Files[i]
.ElementSize.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileName);

View File

@@ -18,6 +18,9 @@ public class ScrExplorer : IImGuiPanel
{
if (ImGui.Begin("SCR Explorer"))
{
ImGui.Text("scr - это файл AI скриптов. Их можно найти в папке MISSIONS/SCRIPTS");
ImGui.Separator();
var scr = _viewModel.Scr;
if (_viewModel.HasFile && scr is not null)
{

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

@@ -17,6 +17,9 @@ public class VarsetExplorerPanel : IImGuiPanel
{
if (ImGui.Begin("VARSET Explorer"))
{
ImGui.Text(".var - это файл динамических настроек. Можно найти в MISSIONS/SCRIPTS/varset.var, а также внутри behpsp.res");
ImGui.Separator();
if (_viewModel.Items.Count == 0)
{
ImGui.Text("VARSET не загружен");

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

@@ -2,9 +2,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
@@ -12,13 +9,14 @@
</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" />

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,104 +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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScrLib", "ScrLib\ScrLib.csproj", "{C445359B-97D4-4432-9331-708B5A14887A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VarsetLib", "VarsetLib\VarsetLib.csproj", "{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Visualisator", "Visualisator\Visualisator.csproj", "{667A7E03-5CAA-4591-9980-F6C722911A35}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X86Disassembler", "X86Disassembler\X86Disassembler.csproj", "{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "X86DisassemblerTests", "X86DisassemblerTests\X86DisassemblerTests.csproj", "{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LandscapeExplorer", "LandscapeExplorer\LandscapeExplorer.csproj", "{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}"
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
{C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C445359B-97D4-4432-9331-708B5A14887A}.Release|Any CPU.Build.0 = Release|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0EC800E2-1444-40D5-9EDD-93276F4D1FF5}.Release|Any CPU.Build.0 = Release|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Debug|Any CPU.Build.0 = Debug|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Release|Any CPU.ActiveCfg = Release|Any CPU
{667A7E03-5CAA-4591-9980-F6C722911A35}.Release|Any CPU.Build.0 = Release|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5C2E94A-0F63-4E09-BC04-F2518E2CC1F0}.Release|Any CPU.Build.0 = Release|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6A1F5A9-0C7A-4F8F-B8C5-83E9D3F3A1D5}.Release|Any CPU.Build.0 = Release|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2700BD3F-DC67-4B58-8F73-F790AA68E4FE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,31 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssemblyCodeArray_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003Fa1_003Fbc9d4e81_003FAssemblyCodeArray_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssemblyCodeMemory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F6e_003F09b667c6_003FAssemblyCodeMemory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollectionAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F435f965090c5b89f0c5efb49ac3c5a72367d90599314191af25a832d0942f_003FCollectionAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACsvReader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ff7b87edd534764eebf2388a77d49e5cd9c6d49eb6788dca9b1c07d4545412715_003FCsvReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADefaultTypeConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F64b864a5d465bc24fc4b55e1026aba213beb1733ef631abeca5a9f25357eda_003FDefaultTypeConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADisassembler_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003Fd4_003Fad0818f9_003FDisassembler_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AEncoding_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F1675dc7b710feeeb3e0bc8728be8a947537155c199480fb23b776e81d459_003FEncoding_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFailAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F807c15a7d8383b1548dff1ae33270e637836659d9caecd676ea6f2c59f1c71a_003FFailAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGL_002Egen_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F54e6df16dd99323ba9b0682ce5d5dac3648ccd10aafd29d5f3fad52b62bf3f75_003FGL_002Egen_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAssemblyCode_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F8c_003F9fe9bac2_003FIAssemblyCode_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMatrix4x4_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fed6aa59cd75423c5b655901d6ec4fb4be48ab669fa6fb01b3a7a7f31be95_003FMatrix4x4_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMemory_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FLocal_003FSymbols_003Fsrc_003Fdotnet_003Fruntime_003F5535e31a712343a63f5d7d796cd874e563e5ac14_003Fsrc_003Flibraries_003FSystem_002EPrivate_002ECoreLib_003Fsrc_003FSystem_003FMemory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASafeFileHandle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa03380083db34a2faee436e29e06a72ae8e910_003Fb6_003F67cd826c_003FSafeFileHandle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ASingle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc99a63bcf3d2a18c20ee19e58ac875ab1edf2a147c8b92ffeed185ab8a44b4_003FSingle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringAsserts_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F999ae9cc4ab7b7cfbc5080803e994426e97fd9d87c5b1f44544a799bc114_003FStringAsserts_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003Aud_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F15_003F87bd9007_003Fud_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003Audis86_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FUsers_003FAdmin_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa494e0aa381c41ff9484df33e5edb42535e00_003F95_003F953bbb0f_003Fudis86_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=47ebaefe_002Da806_002D4565_002Dabe7_002D4f14ac675135/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &amp;lt;X86DisassemblerTests&amp;gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;Project Location="C:\Projects\CSharp\ParkanPlayground\X86DisassemblerTests" Presentation="&amp;lt;X86DisassemblerTests&amp;gt;" /&gt;&#xD;
&lt;/SessionState&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=87a33e46_002D2816_002D434f_002D972a_002D703eb7a78476/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;And&gt;&#xD;
&lt;Namespace&gt;X86DisassemblerTests&lt;/Namespace&gt;&#xD;
&lt;Project Location="C:\Projects\CSharp\ParkanPlayground\X86DisassemblerTests" Presentation="&amp;lt;X86DisassemblerTests&amp;gt;" /&gt;&#xD;
&lt;/And&gt;&#xD;
&lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

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,19 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.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>
<ItemGroup>
<PackageReference Include="SharpDisasm" Version="1.1.11" />
</ItemGroup>
</Project>

View File

@@ -1,116 +1,17 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Text.Json;
using ScrLib;
using SharpDisasm;
using VarsetLib;
using System.Buffers.Binary;
using Common;
using MissionTmaLib.Parsing;
using NResLib;
using ParkanPlayground;
// var cpDatEntryConverter = new CpDatEntryConverter();
// cpDatEntryConverter.Convert();
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scr_pl_1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\varset.var";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\preload.lda";
//
// var fs = new FileStream(path, FileMode.Open);
//
// var count = fs.ReadInt32LittleEndian();
//
// Span<byte> data = stackalloc byte[0x124];
//
// for (var i = 0; i < count; i++)
// {
// fs.ReadExactly(data);
// }
//
// Console.WriteLine(
// fs.Position == fs.Length
// );
var converter = new MshConverter();
// var items = VarsetParser.Parse(path);
// Console.WriteLine(items.Count);
// Span<byte> flt = stackalloc byte[4];
// flt[0] = 0x7f;
// flt[1] = 0x7f;
// flt[2] = 0xff;
// flt[3] = 0xff;
// var f = BinaryPrimitives.ReadSingleBigEndian(flt);
//
// Console.WriteLine(f);
// return;
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MisLoad.dll";
var path = "C:\\ParkanUnpacked\\Land.msh\\2_03 00 00 00_Land.bin";
var fs = new FileStream(path, FileMode.Open);
var outputFs = new FileStream("Land.obj", FileMode.Create);
var sw = new StreamWriter(outputFs);
List<Vector3D> points = [];
var count = 0;
while (fs.Position < fs.Length)
{
var x = fs.ReadFloatLittleEndian();
var y = fs.ReadFloatLittleEndian();
var z = fs.ReadFloatLittleEndian();
var vertex = new Vector3D(x, y, z);
sw.WriteLine($"v {x} {y} {z}");
var seenIndex = points.FindIndex(vec => vec == vertex);
if (seenIndex != -1)
{
vertex.Duplicates = seenIndex;
}
points.Add(vertex);
count++;
}
File.WriteAllText("human-readable.json", JsonSerializer.Serialize(points, new JsonSerializerOptions()
{
WriteIndented = true
}));
Console.WriteLine($"Total vertices: {count}");
// for (int i = 0; i < count / 4; i++)
public record Vector3D(float X, float Y, float Z)
{
public int Duplicates { get; set; }
}
// var indices = string.Join(" ", Enumerable.Range(1, count));
//
// sw.WriteLine($"l {indices}");
//
// fs.Seek(0x1000, SeekOrigin.Begin);
//
// byte[] buf = new byte[34];
// fs.ReadExactly(buf);
//
// var disassembler = new SharpDisasm.Disassembler(buf, ArchitectureMode.x86_32);
// foreach (var instruction in disassembler.Disassemble())
// {
// Console.WriteLine($"{instruction.PC - instruction.Offset}: {instruction}");
//
// new Instruction()
// {
// Action = instruction.Mnemonic.ToString(),
// Arguments = {instruction.Operands[0].ToString()}
// };
// }
public class Instruction
{
public string Action { get; set; } = "";
public List<string> Arguments { get; set; } = [];
}
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

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

View File

@@ -1,4 +1,6 @@
namespace ScrLib;
using Common;
namespace ScrLib;
public class ScrParser
{

View File

@@ -1,44 +0,0 @@
using X86Disassembler.X86;
namespace TestDisassembler;
public class Program
{
public static void Main(string[] args)
{
// Test the specific byte sequence that's causing issues
byte[] codeBytes = HexStringToByteArray("816B1078563412");
// Create a disassembler with the code
Disassembler disassembler = new Disassembler(codeBytes, 0x1000);
// Disassemble the code
var instructions = disassembler.Disassemble();
// Print the number of instructions
Console.WriteLine($"Number of instructions: {instructions.Count}");
// Print each instruction
for (int i = 0; i < instructions.Count; i++)
{
Console.WriteLine($"Instruction {i+1}: {instructions[i].Mnemonic} {instructions[i].Operands}");
}
}
private static byte[] HexStringToByteArray(string hex)
{
// Remove any non-hex characters
hex = hex.Replace(" ", "").Replace("-", "");
// Create a byte array
byte[] bytes = new byte[hex.Length / 2];
// Convert each pair of hex characters to a byte
for (int i = 0; i < hex.Length; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}

View File

@@ -1,44 +0,0 @@
using X86Disassembler.X86;
namespace TestDisassembler;
public class Program
{
public static void Main(string[] args)
{
// Test the specific byte sequence with segment override prefix that's causing issues
byte[] codeBytes = HexStringToByteArray("26FF7510");
// Create a disassembler with the code
Disassembler disassembler = new Disassembler(codeBytes, 0x1000);
// Disassemble the code
var instructions = disassembler.Disassemble();
// Print the number of instructions
Console.WriteLine($"Number of instructions: {instructions.Count}");
// Print each instruction
for (int i = 0; i < instructions.Count; i++)
{
Console.WriteLine($"Instruction {i+1}: {instructions[i].Mnemonic} {instructions[i].Operands}");
}
}
private static byte[] HexStringToByteArray(string hex)
{
// Remove any non-hex characters
hex = hex.Replace(" ", "").Replace("-", "");
// Create a byte array
byte[] bytes = new byte[hex.Length / 2];
// Convert each pair of hex characters to a byte
for (int i = 0; i < hex.Length; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
}
return bytes;
}
}

View File

@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\X86Disassembler\X86Disassembler.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,13 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.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>net9.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>

View File

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

View File

@@ -2,17 +2,14 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<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>
</Project>

View File

@@ -1,22 +0,0 @@
namespace X86Disassembler.Analysers;
/// <summary>
/// Represents a disassembled function with its control flow graph
/// </summary>
public class AsmFunction
{
/// <summary>
/// The starting address of the function
/// </summary>
public ulong Address { get; set; }
/// <summary>
/// The list of basic blocks that make up the function
/// </summary>
public List<InstructionBlock> Blocks { get; set; } = [];
public override string ToString()
{
return $"{Address:X8}\n{string.Join("\n", Blocks)}";
}
}

View File

@@ -1,554 +0,0 @@
using X86Disassembler.X86;
using X86Disassembler.X86.Operands;
namespace X86Disassembler.Analysers;
/// <summary>
/// Disassembles code into basic blocks by following control flow instructions.
/// A basic block is a sequence of instructions with a single entry point (the first instruction)
/// and a single exit point (the last instruction, typically a jump or return).
/// </summary>
public class BlockDisassembler
{
// The buffer containing the code to disassemble
private readonly byte[] _codeBuffer;
// The length of the buffer
private readonly int _length;
// The base address of the code
private readonly ulong _baseAddress;
/// <summary>
/// Initializes a new instance of the BlockDisassembler class
/// </summary>
/// <param name="codeBuffer">The raw code bytes to be disassembled</param>
/// <param name="baseAddress">The base RVA (Relative Virtual Address) of the code section</param>
public BlockDisassembler(byte[] codeBuffer, ulong baseAddress)
{
_codeBuffer = codeBuffer;
_length = codeBuffer.Length;
_baseAddress = baseAddress;
}
/// <summary>
/// Disassembles code starting from the specified RVA address by following control flow.
/// Creates blocks of instructions separated by jumps, branches, and returns.
/// </summary>
/// <param name="rvaAddress">The RVA (Relative Virtual Address) to start disassembly from</param>
/// <returns>A list of instruction blocks representing the control flow of the code</returns>
public AsmFunction DisassembleFromAddress(uint rvaAddress)
{
// Create instruction decoder for parsing the code buffer
InstructionDecoder decoder = new InstructionDecoder(_codeBuffer, _length);
// Track visited addresses to prevent infinite loops
HashSet<ulong> visitedAddresses = [];
// Queue of addresses to process (breadth-first approach)
Queue<ulong> addressQueue = [];
// Calculate the file offset from the RVA by subtracting the base address
// Store the file offset for processing, but we'll convert back to RVA when creating blocks
ulong fileOffset = rvaAddress - _baseAddress;
addressQueue.Enqueue(fileOffset);
// Keep track of the original entry point RVA for the function
ulong entryPointRVA = rvaAddress;
// List to store discovered basic blocks
List<InstructionBlock> blocks = [];
// Dictionary to track blocks by address for quick lookup
Dictionary<ulong, InstructionBlock> blocksByAddress = new Dictionary<ulong, InstructionBlock>();
while (addressQueue.Count > 0)
{
// Get the next address to process
var address = addressQueue.Dequeue();
// Skip if we've already visited this address
if (!visitedAddresses.Add(address))
{
// Skip addresses we've already processed
continue;
}
// Position the decoder at the current address
decoder.SetPosition((int) address);
// Collect instructions for this block
List<Instruction> instructions = [];
// Get the current block if it exists (for tracking predecessors)
InstructionBlock? currentBlock = null;
if (blocksByAddress.TryGetValue(address, out var existingBlock))
{
currentBlock = existingBlock;
}
// Process instructions until we hit a control flow change
while (true)
{
// Get the current position
ulong currentPosition = (ulong)decoder.GetPosition();
// If we've stepped onto an existing block, create a new block up to this point
// and stop processing this path (to avoid duplicating instructions)
if (blocksByAddress.TryGetValue(currentPosition, out var targetBlock) && currentPosition != address)
{
// We've stepped onto an existing block, create a new one up to this point
// Register this block and establish the relationship with the target block
var newBlock = RegisterBlock(blocks, address, instructions, null, false, false);
blocksByAddress[address] = newBlock;
// Add the target block as a successor to the new block
newBlock.Successors.Add(targetBlock);
// Add the new block as a predecessor to the target block
targetBlock.Predecessors.Add(newBlock);
break;
}
// Decode the next instruction
var instruction = decoder.DecodeInstruction();
// Handle decoding failures
if (instruction is null)
{
throw new InvalidOperationException($"Unexpectedly failed to decode instruction at {address}");
}
// Add the instruction to the current block
instructions.Add(instruction);
// Check for conditional jump (e.g., JZ, JNZ, JLE)
// For conditional jumps, we need to follow both the jump target and the fall-through path
if (instruction.Type.IsConditionalJump())
{
// Get the jump target address
uint jumpTargetAddress = instruction.StructuredOperands[0].GetValue();
// Get the fall-through address (next instruction after this jump)
uint fallThroughAddress = (uint)decoder.GetPosition();
// Register this block (it ends with a conditional jump)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
// Register the target block if it doesn't exist yet
InstructionBlock? jumpTargetBlock = null;
if (blocksByAddress.TryGetValue(jumpTargetAddress, out var existingTargetBlock))
{
jumpTargetBlock = existingTargetBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(jumpTargetAddress);
}
// Register the fall-through block if it doesn't exist yet
InstructionBlock? fallThroughBlock = null;
if (blocksByAddress.TryGetValue(fallThroughAddress, out var existingFallThroughBlock))
{
fallThroughBlock = existingFallThroughBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(fallThroughAddress);
}
// If the jump target block exists, add it as a successor to the current block
if (jumpTargetBlock != null)
{
newBlock.Successors.Add(jumpTargetBlock);
jumpTargetBlock.Predecessors.Add(newBlock);
}
// If the fall-through block exists, add it as a successor to the current block
if (fallThroughBlock != null)
{
newBlock.Successors.Add(fallThroughBlock);
fallThroughBlock.Predecessors.Add(newBlock);
}
break;
}
// Check for unconditional jump (e.g., JMP)
// For unconditional jumps, we only follow the jump target
if (instruction.Type.IsRegularJump())
{
// Get the jump target address
uint jumpTargetAddress = instruction.StructuredOperands[0].GetValue();
// Register this block (it ends with an unconditional jump)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
// Register the target block if it doesn't exist yet
InstructionBlock? jumpTargetBlock = null;
if (blocksByAddress.TryGetValue(jumpTargetAddress, out var existingTargetBlock))
{
jumpTargetBlock = existingTargetBlock;
}
else
{
// We'll create this block later when we process the queue
// For now, just queue it for processing
addressQueue.Enqueue(jumpTargetAddress);
}
// If the jump target block exists, add it as a successor to the current block
if (jumpTargetBlock != null)
{
newBlock.Successors.Add(jumpTargetBlock);
jumpTargetBlock.Predecessors.Add(newBlock);
}
break;
}
// Check for return instruction (e.g., RET, RETF)
// Returns end a block without any successors
if (instruction.Type.IsRet())
{
// Register this block (it ends with a return)
var newBlock = RegisterBlock(blocks, address, instructions, currentBlock, false, false);
blocksByAddress[address] = newBlock;
break;
}
}
}
// Since blocks aren't necessarily ordered (ASM can jump anywhere it likes)
// we need to sort the blocks ourselves
blocks.Sort((b1, b2) => b1.Address.CompareTo(b2.Address));
// First, establish the successor and predecessor relationships based on file offsets
// This is done by analyzing the last instruction of each block
foreach (var block in blocks)
{
if (block.Instructions.Count == 0) continue;
var lastInstruction = block.Instructions[^1];
// Check if the last instruction is a conditional jump
if (lastInstruction.Type.IsConditionalJump())
{
// Get the jump target address (file offset)
ulong targetAddress = 0;
if (lastInstruction.StructuredOperands.Count > 0 && lastInstruction.StructuredOperands[0] is RelativeOffsetOperand relOp)
{
targetAddress = relOp.TargetAddress;
}
// Find the target block
var targetBlock = blocks.FirstOrDefault(b => b.Address == targetAddress);
if (targetBlock != null)
{
// Add the target block as a successor to this block
if (!block.Successors.Contains(targetBlock))
{
block.Successors.Add(targetBlock);
}
// Add this block as a predecessor to the target block
if (!targetBlock.Predecessors.Contains(block))
{
targetBlock.Predecessors.Add(block);
}
// For conditional jumps, also add the fall-through block as a successor
// The fall-through block is the one that immediately follows this block in memory
// Find the next block in address order
var nextBlock = blocks.OrderBy(b => b.Address).FirstOrDefault(b => b.Address > block.Address);
if (nextBlock != null)
{
// The fall-through block is the one that immediately follows this block in memory
var fallThroughBlock = nextBlock;
// Add the fall-through block as a successor to this block
if (!block.Successors.Contains(fallThroughBlock))
{
block.Successors.Add(fallThroughBlock);
}
// Add this block as a predecessor to the fall-through block
if (!fallThroughBlock.Predecessors.Contains(block))
{
fallThroughBlock.Predecessors.Add(block);
}
}
}
}
// Check if the last instruction is an unconditional jump
else if (lastInstruction.Type == InstructionType.Jmp)
{
// Get the jump target address (file offset)
ulong targetAddress = 0;
if (lastInstruction.StructuredOperands.Count > 0 && lastInstruction.StructuredOperands[0] is RelativeOffsetOperand relOp)
{
targetAddress = relOp.TargetAddress;
}
// Find the target block
var targetBlock = blocks.FirstOrDefault(b => b.Address == targetAddress);
if (targetBlock != null)
{
// Add the target block as a successor to this block
if (!block.Successors.Contains(targetBlock))
{
block.Successors.Add(targetBlock);
}
// Add this block as a predecessor to the target block
if (!targetBlock.Predecessors.Contains(block))
{
targetBlock.Predecessors.Add(block);
}
}
}
// For non-jump instructions that don't end the function (like Ret), add the fall-through block
else if (!lastInstruction.Type.IsRet())
{
// The fall-through block is the one that immediately follows this block in memory
// Find the next block in address order
var nextBlock = blocks.OrderBy(b => b.Address).FirstOrDefault(b => b.Address > block.Address);
if (nextBlock != null)
{
// The fall-through block is the one that immediately follows this block in memory
var fallThroughBlock = nextBlock;
// Add the fall-through block as a successor to this block
if (!block.Successors.Contains(fallThroughBlock))
{
block.Successors.Add(fallThroughBlock);
}
// Add this block as a predecessor to the fall-through block
if (!fallThroughBlock.Predecessors.Contains(block))
{
fallThroughBlock.Predecessors.Add(block);
}
}
}
}
// Store the original file offset for each block in a dictionary
Dictionary<InstructionBlock, ulong> blockToFileOffset = new Dictionary<InstructionBlock, ulong>();
foreach (var block in blocks)
{
blockToFileOffset[block] = block.Address;
}
// Convert all block addresses from file offsets to RVA
// and update the block dictionary for quick lookup
Dictionary<ulong, InstructionBlock> rvaBlocksByAddress = new Dictionary<ulong, InstructionBlock>();
Dictionary<ulong, ulong> fileOffsetToRvaMap = new Dictionary<ulong, ulong>();
// First pass: create a mapping from file offset to RVA for each block
foreach (var block in blocks)
{
// Get the original file offset address
ulong blockFileOffset = block.Address;
// Calculate the RVA address
ulong blockRvaAddress = blockFileOffset + _baseAddress;
// Store the mapping
fileOffsetToRvaMap[blockFileOffset] = blockRvaAddress;
}
// Second pass: update all blocks to use RVA addresses
foreach (var block in blocks)
{
// Get the original file offset address
ulong blockFileOffset = block.Address;
// Update the block's address to RVA
ulong blockRvaAddress = fileOffsetToRvaMap[blockFileOffset];
block.Address = blockRvaAddress;
// Add to the dictionary for quick lookup
rvaBlocksByAddress[blockRvaAddress] = block;
}
// Now update all successors and predecessors to use the correct RVA addresses
foreach (var block in blocks)
{
// Create new lists for successors and predecessors with the correct RVA addresses
List<InstructionBlock> updatedSuccessors = new List<InstructionBlock>();
List<InstructionBlock> updatedPredecessors = new List<InstructionBlock>();
// Update successors
foreach (var successor in block.Successors)
{
// Get the original file offset of the successor
if (blockToFileOffset.TryGetValue(successor, out ulong successorFileOffset))
{
// Look up the RVA address in our mapping
if (fileOffsetToRvaMap.TryGetValue(successorFileOffset, out ulong successorRvaAddress))
{
// Find the block with this RVA address
if (rvaBlocksByAddress.TryGetValue(successorRvaAddress, out var rvaSuccessor))
{
updatedSuccessors.Add(rvaSuccessor);
}
}
}
}
// Update predecessors
foreach (var predecessor in block.Predecessors)
{
// Get the original file offset of the predecessor
if (blockToFileOffset.TryGetValue(predecessor, out ulong predecessorFileOffset))
{
// Look up the RVA address in our mapping
if (fileOffsetToRvaMap.TryGetValue(predecessorFileOffset, out ulong predecessorRvaAddress))
{
// Find the block with this RVA address
if (rvaBlocksByAddress.TryGetValue(predecessorRvaAddress, out var rvaPredecessor))
{
updatedPredecessors.Add(rvaPredecessor);
}
}
}
}
// Replace the old lists with the updated ones
block.Successors = updatedSuccessors;
block.Predecessors = updatedPredecessors;
}
// Create a new AsmFunction with the RVA address
var asmFunction = new AsmFunction()
{
Address = entryPointRVA,
Blocks = blocks,
};
// Verify that the entry block exists (no need to log this information)
return asmFunction;
}
/// <summary>
/// Creates and registers a new instruction block in the blocks collection
/// </summary>
/// <param name="blocks">The list of blocks to add to</param>
/// <param name="address">The starting address of the block</param>
/// <param name="instructions">The instructions contained in the block</param>
/// <param name="currentBlock">The current block being processed (null if this is the first block)</param>
/// <param name="isJumpTarget">Whether this block is a jump target</param>
/// <param name="isFallThrough">Whether this block is a fall-through from another block</param>
/// <returns>The newly created block</returns>
public InstructionBlock RegisterBlock(
List<InstructionBlock> blocks,
ulong address,
List<Instruction> instructions,
InstructionBlock? currentBlock = null,
bool isJumpTarget = false,
bool isFallThrough = false)
{
// Check if a block already exists at this address
var existingBlock = blocks.FirstOrDefault(b => b.Address == address);
if (existingBlock != null)
{
// If the current block is not null, update the relationships
if (currentBlock != null)
{
// Add the existing block as a successor to the current block if not already present
if (!currentBlock.Successors.Contains(existingBlock))
{
currentBlock.Successors.Add(existingBlock);
}
// Add the current block as a predecessor to the existing block if not already present
if (!existingBlock.Predecessors.Contains(currentBlock))
{
existingBlock.Predecessors.Add(currentBlock);
}
}
return existingBlock;
}
// Create a new block with the provided address and instructions
var block = new InstructionBlock()
{
Address = address,
Instructions = new List<Instruction>(instructions) // Create a copy of the instructions list
};
// Add the block to the collection
blocks.Add(block);
// If the current block is not null, update the relationships
if (currentBlock != null)
{
// Add the new block as a successor to the current block
currentBlock.Successors.Add(block);
// Add the current block as a predecessor to the new block
block.Predecessors.Add(currentBlock);
}
return block;
}
}
/// <summary>
/// Represents a basic block of instructions with a single entry and exit point
/// </summary>
public class InstructionBlock
{
/// <summary>
/// The starting address of the block
/// </summary>
public ulong Address { get; set; }
/// <summary>
/// The list of instructions contained in this block
/// </summary>
public List<Instruction> Instructions { get; set; } = [];
/// <summary>
/// The blocks that can transfer control to this block
/// </summary>
public List<InstructionBlock> Predecessors { get; set; } = [];
/// <summary>
/// The blocks that this block can transfer control to
/// </summary>
public List<InstructionBlock> Successors { get; set; } = [];
/// <summary>
/// Returns a string representation of the block, including its address, instructions, and control flow information
/// </summary>
public override string ToString()
{
// Create a string for predecessors
string predecessorsStr = Predecessors.Count > 0
? $"Predecessors: {string.Join(", ", Predecessors.Select(p => $"0x{p.Address:X8}"))}"
: "No predecessors";
// Create a string for successors
string successorsStr = Successors.Count > 0
? $"Successors: {string.Join(", ", Successors.Select(s => $"0x{s.Address:X8}"))}"
: "No successors";
// Return the complete string representation
return $"Address: 0x{Address:X8}\n{predecessorsStr}\n{successorsStr}\n{string.Join("\n", Instructions)}";
}
}

View File

@@ -1,56 +0,0 @@
namespace X86Disassembler.Analysers;
public abstract class Address(ulong value, ulong imageBase)
{
/// <summary>
/// The actual value of the address, not specifically typed.
/// </summary>
protected readonly ulong Value = value;
/// <summary>
/// PE.ImageBase from which this address is constructed
/// </summary>
protected readonly ulong ImageBase = imageBase;
}
/// <summary>
/// Absolute address in the PE file
/// </summary>
public class FileAbsoluteAddress(ulong value, ulong imageBase) : Address(value, imageBase)
{
public ulong GetValue()
{
return Value;
}
public virtual VirtualAddress AsImageBaseAddress()
{
return new VirtualAddress(Value + ImageBase, ImageBase);
}
public virtual FileAbsoluteAddress AsFileAbsolute()
{
return this;
}
}
/// <summary>
/// Address from PE.ImageBase
/// </summary>
public class VirtualAddress : FileAbsoluteAddress
{
public VirtualAddress(ulong value, ulong imageBase) : base(value, imageBase)
{
}
public override VirtualAddress AsImageBaseAddress()
{
return this;
}
public override FileAbsoluteAddress AsFileAbsolute()
{
return new FileAbsoluteAddress(Value - ImageBase, ImageBase);
}
}

View File

@@ -1,40 +0,0 @@
using X86Disassembler.X86;
namespace X86Disassembler.Analysers;
public static class InstructionTypeExtensions
{
public static bool IsConditionalJump(this InstructionType type)
{
return type switch
{
InstructionType.Jg => true,
InstructionType.Jge => true,
InstructionType.Jl => true,
InstructionType.Jle => true,
InstructionType.Ja => true,
InstructionType.Jae => true,
InstructionType.Jb => true,
InstructionType.Jbe => true,
InstructionType.Jz => true,
InstructionType.Jnz => true,
InstructionType.Jo => true,
InstructionType.Jno => true,
InstructionType.Js => true,
InstructionType.Jns => true,
InstructionType.Jp => true,
InstructionType.Jnp => true,
_ => false
};
}
public static bool IsRegularJump(this InstructionType type)
{
return type == InstructionType.Jmp;
}
public static bool IsRet(this InstructionType type)
{
return type is InstructionType.Ret or InstructionType.Retf;
}
}

View File

@@ -1,16 +0,0 @@
using X86Disassembler.X86;
using X86Disassembler.X86.Operands;
namespace X86Disassembler.Analysers;
public static class OperandExtensions
{
public static uint GetValue(this Operand operand)
{
return operand switch
{
RelativeOffsetOperand roo => roo.TargetAddress,
_ => 0
};
}
}

View File

@@ -1,60 +0,0 @@
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE;
/// <summary>
/// Utility class for PE format operations
/// </summary>
public class PEUtility
{
private readonly List<SectionHeader> _sectionHeaders;
private readonly uint _sizeOfHeaders;
/// <summary>
/// Initialize a new instance of the PEUtility class
/// </summary>
/// <param name="sectionHeaders">The section headers</param>
/// <param name="sizeOfHeaders">The size of the headers</param>
public PEUtility(List<SectionHeader> sectionHeaders, uint sizeOfHeaders)
{
_sectionHeaders = sectionHeaders;
_sizeOfHeaders = sizeOfHeaders;
}
/// <summary>
/// Converts a Relative Virtual Address (RVA) to a file offset
/// </summary>
/// <param name="rva">The RVA to convert</param>
/// <returns>The corresponding file offset</returns>
public uint RvaToOffset(uint rva)
{
if (rva == 0)
{
return 0;
}
foreach (var section in _sectionHeaders)
{
// Check if the RVA is within this section
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
{
// Calculate the offset within the section
uint offsetInSection = rva - section.VirtualAddress;
// Make sure we don't exceed the raw data size
if (offsetInSection < section.SizeOfRawData)
{
return section.PointerToRawData + offsetInSection;
}
}
}
// If the RVA is not within any section, it might be in the headers
if (rva < _sizeOfHeaders)
{
return rva;
}
throw new ArgumentException($"RVA {rva:X8} is not within any section");
}
}

View File

@@ -1,56 +0,0 @@
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for the DOS header of a PE file
/// </summary>
public class DOSHeaderParser : IParser<DOSHeader>
{
// DOS Header constants
private const ushort DOS_SIGNATURE = 0x5A4D; // 'MZ'
public DOSHeader Parse(BinaryReader reader)
{
var header = new DOSHeader();
header.e_magic = reader.ReadUInt16();
if (header.e_magic != DOS_SIGNATURE)
{
throw new InvalidDataException("Invalid DOS signature (MZ)");
}
header.e_cblp = reader.ReadUInt16();
header.e_cp = reader.ReadUInt16();
header.e_crlc = reader.ReadUInt16();
header.e_cparhdr = reader.ReadUInt16();
header.e_minalloc = reader.ReadUInt16();
header.e_maxalloc = reader.ReadUInt16();
header.e_ss = reader.ReadUInt16();
header.e_sp = reader.ReadUInt16();
header.e_csum = reader.ReadUInt16();
header.e_ip = reader.ReadUInt16();
header.e_cs = reader.ReadUInt16();
header.e_lfarlc = reader.ReadUInt16();
header.e_ovno = reader.ReadUInt16();
header.e_res = new ushort[4];
for (int i = 0; i < 4; i++)
{
header.e_res[i] = reader.ReadUInt16();
}
header.e_oemid = reader.ReadUInt16();
header.e_oeminfo = reader.ReadUInt16();
header.e_res2 = new ushort[10];
for (int i = 0; i < 10; i++)
{
header.e_res2[i] = reader.ReadUInt16();
}
header.e_lfanew = reader.ReadUInt32();
return header;
}
}

View File

@@ -1,161 +0,0 @@
using System.Text;
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for the Export Directory of a PE file
/// </summary>
public class ExportDirectoryParser
{
private readonly PEUtility _utility;
public ExportDirectoryParser(PEUtility utility)
{
_utility = utility;
}
/// <summary>
/// Parse the Export Directory from the binary reader
/// </summary>
/// <param name="reader">The binary reader</param>
/// <param name="rva">The RVA of the Export Directory</param>
/// <returns>The parsed Export Directory</returns>
public ExportDirectory Parse(BinaryReader reader, uint rva)
{
ExportDirectory directory = new ExportDirectory();
reader.BaseStream.Seek(_utility.RvaToOffset(rva), SeekOrigin.Begin);
directory.Characteristics = reader.ReadUInt32();
directory.TimeDateStamp = reader.ReadUInt32();
directory.MajorVersion = reader.ReadUInt16();
directory.MinorVersion = reader.ReadUInt16();
directory.DllNameRva = reader.ReadUInt32();
directory.Base = reader.ReadUInt32();
directory.NumberOfFunctions = reader.ReadUInt32();
directory.NumberOfNames = reader.ReadUInt32();
directory.AddressOfFunctions = reader.ReadUInt32();
directory.AddressOfNames = reader.ReadUInt32();
directory.AddressOfNameOrdinals = reader.ReadUInt32();
uint dllNameOffset = _utility.RvaToOffset(directory.DllNameRva);
reader.BaseStream.Seek(dllNameOffset, SeekOrigin.Begin);
// Read the null-terminated ASCII string
var nameBuilder = new StringBuilder();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBuilder.Append((char) b);
}
directory.DllName = nameBuilder.ToString();
return directory;
}
/// <summary>
/// Parse the exported functions using the export directory information
/// </summary>
/// <param name="reader">The binary reader</param>
/// <param name="directory">The Export Directory</param>
/// <param name="exportDirRva">The RVA of the Export Directory</param>
/// <param name="exportDirSize">The size of the Export Directory</param>
/// <returns>List of exported functions</returns>
public List<ExportedFunction> ParseExportedFunctions(BinaryReader reader, ExportDirectory directory, uint exportDirRva, uint exportDirSize)
{
List<ExportedFunction> exportedFunctions = new List<ExportedFunction>();
// Read the array of function addresses (RVAs)
uint[] functionRVAs = new uint[directory.NumberOfFunctions];
reader.BaseStream.Seek(_utility.RvaToOffset(directory.AddressOfFunctions), SeekOrigin.Begin);
for (int i = 0; i < directory.NumberOfFunctions; i++)
{
functionRVAs[i] = reader.ReadUInt32();
}
// Read the array of name RVAs
uint[] nameRVAs = new uint[directory.NumberOfNames];
reader.BaseStream.Seek(_utility.RvaToOffset(directory.AddressOfNames), SeekOrigin.Begin);
for (int i = 0; i < directory.NumberOfNames; i++)
{
nameRVAs[i] = reader.ReadUInt32();
}
// Read the array of name ordinals
ushort[] nameOrdinals = new ushort[directory.NumberOfNames];
reader.BaseStream.Seek(_utility.RvaToOffset(directory.AddressOfNameOrdinals), SeekOrigin.Begin);
for (int i = 0; i < directory.NumberOfNames; i++)
{
nameOrdinals[i] = reader.ReadUInt16();
}
// Create a dictionary to map ordinals to names
Dictionary<ushort, string> ordinalToName = new Dictionary<ushort, string>();
for (int i = 0; i < directory.NumberOfNames; i++)
{
// Read the function name
reader.BaseStream.Seek(_utility.RvaToOffset(nameRVAs[i]), SeekOrigin.Begin);
var nameBuilder = new StringBuilder();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBuilder.Append((char) b);
}
string name = nameBuilder.ToString();
// Map the ordinal to the name
ordinalToName[nameOrdinals[i]] = name;
}
// Create the exported functions
for (ushort i = 0; i < directory.NumberOfFunctions; i++)
{
uint functionRVA = functionRVAs[i];
if (functionRVA == 0)
{
continue; // Skip empty entries
}
ExportedFunction function = new ExportedFunction();
function.Ordinal = (ushort) (i + directory.Base);
function.AddressRva = functionRVA;
// Check if this function has a name
if (ordinalToName.TryGetValue(i, out string? name))
{
function.Name = name;
}
else
{
function.Name = $"Ordinal_{function.Ordinal}";
}
// Check if this is a forwarder
uint exportDirEnd = exportDirRva + exportDirSize;
if (functionRVA >= exportDirRva && functionRVA < exportDirEnd)
{
function.IsForwarder = true;
// Read the forwarder string
reader.BaseStream.Seek(_utility.RvaToOffset(functionRVA), SeekOrigin.Begin);
var forwarderBuilder = new StringBuilder();
byte b;
while ((b = reader.ReadByte()) != 0)
{
forwarderBuilder.Append((char) b);
}
function.ForwarderName = forwarderBuilder.ToString();
}
exportedFunctions.Add(function);
}
return exportedFunctions;
}
}

View File

@@ -1,29 +0,0 @@
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for the File header of a PE file
/// </summary>
public class FileHeaderParser : IParser<FileHeader>
{
/// <summary>
/// Parse the File header from the binary reader
/// </summary>
/// <param name="reader">The binary reader positioned at the start of the File header</param>
/// <returns>The parsed File header</returns>
public FileHeader Parse(BinaryReader reader)
{
var header = new FileHeader();
header.Machine = reader.ReadUInt16();
header.NumberOfSections = reader.ReadUInt16();
header.TimeDateStamp = reader.ReadUInt32();
header.PointerToSymbolTable = reader.ReadUInt32();
header.NumberOfSymbols = reader.ReadUInt32();
header.SizeOfOptionalHeader = reader.ReadUInt16();
header.Characteristics = reader.ReadUInt16();
return header;
}
}

View File

@@ -1,15 +0,0 @@
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Interface for PE format component parsers
/// </summary>
/// <typeparam name="T">The type of component to parse</typeparam>
public interface IParser<out T>
{
/// <summary>
/// Parse a component from the binary reader
/// </summary>
/// <param name="reader">The binary reader positioned at the start of the component</param>
/// <returns>The parsed component</returns>
T Parse(BinaryReader reader);
}

View File

@@ -1,162 +0,0 @@
using System.Text;
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for Import Descriptors in a PE file
/// </summary>
public class ImportDescriptorParser
{
private readonly PEUtility _utility;
public ImportDescriptorParser(PEUtility utility)
{
_utility = utility;
}
/// <summary>
/// Parse the Import Descriptors from the binary reader
/// </summary>
/// <param name="reader">The binary reader</param>
/// <param name="rva">The RVA of the Import Directory</param>
/// <returns>List of Import Descriptors</returns>
public List<ImportDescriptor> Parse(BinaryReader reader, uint rva)
{
var descriptors = new List<ImportDescriptor>();
uint importTableOffset = _utility.RvaToOffset(rva);
reader.BaseStream.Seek(importTableOffset, SeekOrigin.Begin);
int descriptorCount = 0;
while (true)
{
descriptorCount++;
// Read the import descriptor
uint originalFirstThunk = reader.ReadUInt32();
uint timeDateStamp = reader.ReadUInt32();
uint forwarderChain = reader.ReadUInt32();
uint nameRva = reader.ReadUInt32();
uint firstThunk = reader.ReadUInt32();
// Check if we've reached the end of the import descriptors
if (originalFirstThunk == 0 && nameRva == 0 && firstThunk == 0)
{
break;
}
ImportDescriptor descriptor = new ImportDescriptor
{
OriginalFirstThunkRva = originalFirstThunk,
TimeDateStamp = timeDateStamp,
ForwarderChain = forwarderChain,
DllNameRva = nameRva,
FirstThunkRva = firstThunk,
DllName = "Unknown"
};
if (nameRva != 0)
{
uint nameOffset = _utility.RvaToOffset(nameRva);
reader.BaseStream.Seek(nameOffset, SeekOrigin.Begin);
// Read the null-terminated ASCII string
StringBuilder nameBuilder = new StringBuilder();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBuilder.Append((char) b);
}
descriptor.DllName = nameBuilder.ToString();
}
// Parse the imported functions
ParseImportedFunctions(reader, descriptor);
descriptors.Add(descriptor);
// Return to the import table to read the next descriptor
reader.BaseStream.Seek(importTableOffset + (descriptorCount * 20), SeekOrigin.Begin);
}
return descriptors;
}
/// <summary>
/// Parse the imported functions for a given import descriptor
/// </summary>
/// <param name="reader">The binary reader</param>
/// <param name="descriptor">The Import Descriptor</param>
private void ParseImportedFunctions(BinaryReader reader, ImportDescriptor descriptor)
{
// Use OriginalFirstThunk if available, otherwise use FirstThunk
uint thunkRva = descriptor.OriginalFirstThunkRva != 0
? descriptor.OriginalFirstThunkRva
: descriptor.FirstThunkRva;
if (thunkRva == 0)
{
return; // No functions to parse
}
uint thunkOffset = _utility.RvaToOffset(thunkRva);
int functionCount = 0;
while (true)
{
reader.BaseStream.Seek(thunkOffset + (functionCount * 4), SeekOrigin.Begin);
uint thunkData = reader.ReadUInt32();
if (thunkData == 0)
{
break; // End of the function list
}
ImportedFunction function = new ImportedFunction
{
ThunkRva = thunkRva + (uint) (functionCount * 4)
};
// Check if imported by ordinal (high bit set)
if ((thunkData & 0x80000000) != 0)
{
function.IsOrdinal = true;
function.Ordinal = (ushort) (thunkData & 0xFFFF);
function.Name = $"Ordinal {function.Ordinal}";
}
else
{
// Imported by name - the thunkData is an RVA to a hint/name structure
uint hintNameOffset = _utility.RvaToOffset(thunkData);
reader.BaseStream.Seek(hintNameOffset, SeekOrigin.Begin);
// Read the hint (2 bytes)
function.Hint = reader.ReadUInt16();
// Read the function name (null-terminated ASCII string)
StringBuilder nameBuilder = new StringBuilder();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBuilder.Append((char) b);
}
function.Name = nameBuilder.ToString();
if (string.IsNullOrEmpty(function.Name))
{
function.Name = $"Function_at_{thunkData:X8}";
}
}
descriptor.Functions.Add(function);
functionCount++;
}
}
}

View File

@@ -1,95 +0,0 @@
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for the Optional header of a PE file
/// </summary>
public class OptionalHeaderParser : IParser<OptionalHeader>
{
// Optional Header Magic values
private const ushort PE32_MAGIC = 0x10B; // 32-bit executable
private const ushort PE32PLUS_MAGIC = 0x20B; // 64-bit executable
/// <summary>
/// Parse the Optional header from the binary reader
/// </summary>
/// <param name="reader">The binary reader positioned at the start of the Optional header</param>
/// <returns>The parsed Optional header</returns>
public OptionalHeader Parse(BinaryReader reader)
{
var header = new OptionalHeader();
// Standard fields
header.Magic = reader.ReadUInt16();
// Determine if this is a PE32 or PE32+ file
var is64Bit = header.Magic == PE32PLUS_MAGIC;
header.MajorLinkerVersion = reader.ReadByte();
header.MinorLinkerVersion = reader.ReadByte();
header.SizeOfCode = reader.ReadUInt32();
header.SizeOfInitializedData = reader.ReadUInt32();
header.SizeOfUninitializedData = reader.ReadUInt32();
header.AddressOfEntryPoint = reader.ReadUInt32();
header.BaseOfCode = reader.ReadUInt32();
// PE32 has BaseOfData, PE32+ doesn't
if (!is64Bit)
{
header.BaseOfData = reader.ReadUInt32();
}
// Windows-specific fields
header.ImageBase = is64Bit
? reader.ReadUInt64()
: reader.ReadUInt32();
header.SectionAlignment = reader.ReadUInt32();
header.FileAlignment = reader.ReadUInt32();
header.MajorOperatingSystemVersion = reader.ReadUInt16();
header.MinorOperatingSystemVersion = reader.ReadUInt16();
header.MajorImageVersion = reader.ReadUInt16();
header.MinorImageVersion = reader.ReadUInt16();
header.MajorSubsystemVersion = reader.ReadUInt16();
header.MinorSubsystemVersion = reader.ReadUInt16();
header.Win32VersionValue = reader.ReadUInt32();
header.SizeOfImage = reader.ReadUInt32();
header.SizeOfHeaders = reader.ReadUInt32();
header.CheckSum = reader.ReadUInt32();
header.Subsystem = reader.ReadUInt16();
header.DllCharacteristics = reader.ReadUInt16();
// Size fields differ between PE32 and PE32+
if (is64Bit)
{
header.SizeOfStackReserve = reader.ReadUInt64();
header.SizeOfStackCommit = reader.ReadUInt64();
header.SizeOfHeapReserve = reader.ReadUInt64();
header.SizeOfHeapCommit = reader.ReadUInt64();
}
else
{
header.SizeOfStackReserve = reader.ReadUInt32();
header.SizeOfStackCommit = reader.ReadUInt32();
header.SizeOfHeapReserve = reader.ReadUInt32();
header.SizeOfHeapCommit = reader.ReadUInt32();
}
header.LoaderFlags = reader.ReadUInt32();
header.NumberOfRvaAndSizes = reader.ReadUInt32();
// Data directories
header.DataDirectories = new DataDirectory[header.NumberOfRvaAndSizes];
for (int i = 0; i < header.NumberOfRvaAndSizes; i++)
{
var dir = new DataDirectory();
dir.VirtualAddress = reader.ReadUInt32();
dir.Size = reader.ReadUInt32();
header.DataDirectories[i] = dir;
}
return header;
}
}

View File

@@ -1,38 +0,0 @@
using System.Text;
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE.Parsers;
/// <summary>
/// Parser for section headers in a PE file
/// </summary>
public class SectionHeaderParser : IParser<SectionHeader>
{
/// <summary>
/// Parse a section header from the binary reader
/// </summary>
/// <param name="reader">The binary reader positioned at the start of the section header</param>
/// <returns>The parsed section header</returns>
public SectionHeader Parse(BinaryReader reader)
{
var header = new SectionHeader();
// Read section name (8 bytes)
var nameBytes = reader.ReadBytes(8);
// Convert to string, removing any null characters
header.Name = Encoding.ASCII.GetString(nameBytes)
.TrimEnd('\0');
header.VirtualSize = reader.ReadUInt32();
header.VirtualAddress = reader.ReadUInt32();
header.SizeOfRawData = reader.ReadUInt32();
header.PointerToRawData = reader.ReadUInt32();
header.PointerToRelocations = reader.ReadUInt32();
header.PointerToLinenumbers = reader.ReadUInt32();
header.NumberOfRelocations = reader.ReadUInt16();
header.NumberOfLinenumbers = reader.ReadUInt16();
header.Characteristics = reader.ReadUInt32();
return header;
}
}

View File

@@ -1,158 +0,0 @@
using X86Disassembler.PE.Parsers;
using X86Disassembler.PE.Types;
namespace X86Disassembler.PE;
/// <summary>
/// Represents a Portable Executable (PE) file format parser
/// </summary>
public class PeFile
{
// DOS Header constants
private const ushort DOS_SIGNATURE = 0x5A4D; // 'MZ'
private const uint PE_SIGNATURE = 0x00004550; // 'PE\0\0'
// Optional Header Magic values
private const ushort PE32_MAGIC = 0x10B; // 32-bit executable
private const ushort PE32PLUS_MAGIC = 0x20B; // 64-bit executable
// Section characteristics flags
private const uint IMAGE_SCN_CNT_CODE = 0x00000020; // Section contains code
private const uint IMAGE_SCN_MEM_EXECUTE = 0x20000000; // Section is executable
private const uint IMAGE_SCN_MEM_READ = 0x40000000; // Section is readable
private const uint IMAGE_SCN_MEM_WRITE = 0x80000000; // Section is writable
// Data directories
private const int IMAGE_DIRECTORY_ENTRY_EXPORT = 0; // Export Directory
private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1; // Import Directory
private const int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2; // Resource Directory
private const int IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3; // Exception Directory
private const int IMAGE_DIRECTORY_ENTRY_SECURITY = 4; // Security Directory
private const int IMAGE_DIRECTORY_ENTRY_BASERELOC = 5; // Base Relocation Table
private const int IMAGE_DIRECTORY_ENTRY_DEBUG = 6; // Debug Directory
private const int IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7; // Architecture Specific Data
private const int IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8; // RVA of GP
private const int IMAGE_DIRECTORY_ENTRY_TLS = 9; // TLS Directory
private const int IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10; // Load Configuration Directory
private const int IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT = 11; // Bound Import Directory
private const int IMAGE_DIRECTORY_ENTRY_IAT = 12; // Import Address Table
private const int IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13; // Delay Load Import Descriptors
private const int IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR = 14; // COM Runtime descriptor
// PE file data
private byte[] _fileData;
// Parser instances
private readonly DOSHeaderParser _dosHeaderParser;
private readonly FileHeaderParser _fileHeaderParser;
private readonly OptionalHeaderParser _optionalHeaderParser;
private readonly SectionHeaderParser _sectionHeaderParser;
private PEUtility _peUtility;
private ExportDirectoryParser _exportDirectoryParser;
private ImportDescriptorParser _importDescriptorParser;
// Parsed headers
public DOSHeader DosHeader { get; private set; }
public FileHeader FileHeader { get; private set; }
public OptionalHeader OptionalHeader { get; private set; }
public List<SectionHeader> SectionHeaders { get; private set; }
public bool Is64Bit { get; private set; }
// Export and Import information
public ExportDirectory ExportDirectory { get; private set; }
public List<ExportedFunction> ExportedFunctions { get; private set; }
public List<ImportDescriptor> ImportDescriptors { get; private set; }
/// <summary>
/// Initializes a new instance of the PEFormat class
/// </summary>
/// <param name="fileData">The raw file data</param>
public PeFile(byte[] fileData)
{
_fileData = fileData;
SectionHeaders = [];
ExportedFunctions = [];
ImportDescriptors = [];
// Initialize parsers
_dosHeaderParser = new DOSHeaderParser();
_fileHeaderParser = new FileHeaderParser();
_optionalHeaderParser = new OptionalHeaderParser();
_sectionHeaderParser = new SectionHeaderParser();
// Initialize properties to avoid nullability warnings
DosHeader = new DOSHeader();
FileHeader = new FileHeader();
OptionalHeader = new OptionalHeader();
ExportDirectory = new ExportDirectory();
// These will be initialized during Parse()
_peUtility = null!;
_exportDirectoryParser = null!;
_importDescriptorParser = null!;
}
/// <summary>
/// Parses the PE file structure
/// </summary>
public void Parse()
{
using var stream = new MemoryStream(_fileData);
using var reader = new BinaryReader(stream);
// Parse DOS header
DosHeader = _dosHeaderParser.Parse(reader);
// Move to PE header
reader.BaseStream.Seek(DosHeader.e_lfanew, SeekOrigin.Begin);
// Verify PE signature
uint peSignature = reader.ReadUInt32();
if (peSignature != PE_SIGNATURE)
{
throw new InvalidDataException("Invalid PE signature");
}
// Parse File Header
FileHeader = _fileHeaderParser.Parse(reader);
// Parse Optional Header
OptionalHeader = _optionalHeaderParser.Parse(reader);
Is64Bit = OptionalHeader.Is64Bit();
// Parse Section Headers
for (int i = 0; i < FileHeader.NumberOfSections; i++)
{
SectionHeaders.Add(_sectionHeaderParser.Parse(reader));
}
// Initialize utility after section headers are parsed
_peUtility = new PEUtility(SectionHeaders, OptionalHeader.SizeOfHeaders);
_exportDirectoryParser = new ExportDirectoryParser(_peUtility);
_importDescriptorParser = new ImportDescriptorParser(_peUtility);
// Parse Export Directory
if (OptionalHeader.DataDirectories.Length > IMAGE_DIRECTORY_ENTRY_EXPORT &&
OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress != 0)
{
uint exportDirRva = OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
uint exportDirSize = OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
ExportDirectory = _exportDirectoryParser.Parse(reader, exportDirRva);
ExportedFunctions = _exportDirectoryParser.ParseExportedFunctions(
reader,
ExportDirectory,
exportDirRva,
exportDirSize
);
}
// Parse Import Descriptors
if (OptionalHeader.DataDirectories.Length > IMAGE_DIRECTORY_ENTRY_IMPORT &&
OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress != 0)
{
uint importDirRva = OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
ImportDescriptors = _importDescriptorParser.Parse(reader, importDirRva);
}
}
}

View File

@@ -1,27 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents the DOS header of a PE file
/// </summary>
public class DOSHeader
{
public ushort e_magic; // Magic number (MZ)
public ushort e_cblp; // Bytes on last page of file
public ushort e_cp; // Pages in file
public ushort e_crlc; // Relocations
public ushort e_cparhdr; // Size of header in paragraphs
public ushort e_minalloc; // Minimum extra paragraphs needed
public ushort e_maxalloc; // Maximum extra paragraphs needed
public ushort e_ss; // Initial (relative) SS value
public ushort e_sp; // Initial SP value
public ushort e_csum; // Checksum
public ushort e_ip; // Initial IP value
public ushort e_cs; // Initial (relative) CS value
public ushort e_lfarlc; // File address of relocation table
public ushort e_ovno; // Overlay number
public ushort[] e_res = new ushort[4]; // Reserved words
public ushort e_oemid; // OEM identifier (for e_oeminfo)
public ushort e_oeminfo; // OEM information; e_oemid specific
public ushort[] e_res2 = new ushort[10]; // Reserved words
public uint e_lfanew; // File address of new exe header
}

View File

@@ -1,10 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents a data directory in the optional header
/// </summary>
public class DataDirectory
{
public uint VirtualAddress; // RVA of the table
public uint Size; // Size of the table
}

View File

@@ -1,20 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents the Export Directory of a PE file
/// </summary>
public class ExportDirectory
{
public uint Characteristics; // Reserved, must be 0
public uint TimeDateStamp; // Time and date stamp
public ushort MajorVersion; // Major version
public ushort MinorVersion; // Minor version
public uint DllNameRva; // RVA of the name of the DLL
public string DllName = ""; // The actual name of the DLL
public uint Base; // Ordinal base
public uint NumberOfFunctions; // Number of functions
public uint NumberOfNames; // Number of names
public uint AddressOfFunctions; // RVA of the export address table
public uint AddressOfNames; // RVA of the export names table
public uint AddressOfNameOrdinals; // RVA of the ordinal table
}

View File

@@ -1,13 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents an exported function in a PE file
/// </summary>
public class ExportedFunction
{
public string Name = ""; // Function name
public ushort Ordinal; // Function ordinal
public uint AddressRva; // Function RVA
public bool IsForwarder; // True if this is a forwarder
public string ForwarderName = ""; // Name of the forwarded function
}

View File

@@ -1,15 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents the File header of a PE file
/// </summary>
public class FileHeader
{
public ushort Machine; // Target machine type
public ushort NumberOfSections; // Number of sections
public uint TimeDateStamp; // Time and date stamp
public uint PointerToSymbolTable; // File pointer to COFF symbol table
public uint NumberOfSymbols; // Number of symbols
public ushort SizeOfOptionalHeader; // Size of optional header
public ushort Characteristics; // Characteristics
}

View File

@@ -1,16 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents an Import Descriptor in a PE file
/// </summary>
public class ImportDescriptor
{
public uint OriginalFirstThunkRva; // RVA to original first thunk
public uint TimeDateStamp; // Time and date stamp
public uint ForwarderChain; // Forwarder chain
public uint DllNameRva; // RVA to the name of the DLL
public string DllName = ""; // The actual name of the DLL
public uint FirstThunkRva; // RVA to first thunk
public List<ImportedFunction> Functions = [];
}

View File

@@ -1,13 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents an imported function in a PE file
/// </summary>
public class ImportedFunction
{
public string Name = ""; // Function name
public ushort Hint; // Hint value
public bool IsOrdinal; // True if imported by ordinal
public ushort Ordinal; // Ordinal value (if imported by ordinal)
public uint ThunkRva; // RVA of the thunk for this function
}

View File

@@ -1,56 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents the Optional header of a PE file
/// </summary>
public class OptionalHeader
{
// Optional Header Magic values
private const ushort PE32_MAGIC = 0x10B; // 32-bit executable
private const ushort PE32PLUS_MAGIC = 0x20B; // 64-bit executable
// Standard fields
public ushort Magic; // Magic number (PE32 or PE32+)
public byte MajorLinkerVersion; // Major linker version
public byte MinorLinkerVersion; // Minor linker version
public uint SizeOfCode; // Size of code section
public uint SizeOfInitializedData; // Size of initialized data section
public uint SizeOfUninitializedData; // Size of uninitialized data section
public uint AddressOfEntryPoint; // Address of entry point
public uint BaseOfCode; // Base of code section
public uint BaseOfData; // Base of data section (PE32 only)
// Windows-specific fields
public ulong ImageBase; // Image base address (uint for PE32, ulong for PE32+)
public uint SectionAlignment; // Section alignment
public uint FileAlignment; // File alignment
public ushort MajorOperatingSystemVersion; // Major OS version
public ushort MinorOperatingSystemVersion; // Minor OS version
public ushort MajorImageVersion; // Major image version
public ushort MinorImageVersion; // Minor image version
public ushort MajorSubsystemVersion; // Major subsystem version
public ushort MinorSubsystemVersion; // Minor subsystem version
public uint Win32VersionValue; // Win32 version value
public uint SizeOfImage; // Size of image
public uint SizeOfHeaders; // Size of headers
public uint CheckSum; // Checksum
public ushort Subsystem; // Subsystem
public ushort DllCharacteristics; // DLL characteristics
public ulong SizeOfStackReserve; // Size of stack reserve (uint for PE32, ulong for PE32+)
public ulong SizeOfStackCommit; // Size of stack commit (uint for PE32, ulong for PE32+)
public ulong SizeOfHeapReserve; // Size of heap reserve (uint for PE32, ulong for PE32+)
public ulong SizeOfHeapCommit; // Size of heap commit (uint for PE32, ulong for PE32+)
public uint LoaderFlags; // Loader flags
public uint NumberOfRvaAndSizes; // Number of RVA and sizes
public DataDirectory[] DataDirectories = []; // Data directories
/// <summary>
/// Determines if the PE file is 64-bit based on the Magic value
/// </summary>
/// <returns>True if the PE file is 64-bit, false otherwise</returns>
public bool Is64Bit()
{
return Magic == PE32PLUS_MAGIC;
}
}

View File

@@ -1,45 +0,0 @@
namespace X86Disassembler.PE.Types;
/// <summary>
/// Represents a section header in a PE file
/// </summary>
public class SectionHeader
{
// Section characteristics flags
private const uint IMAGE_SCN_CNT_CODE = 0x00000020; // Section contains code
private const uint IMAGE_SCN_MEM_EXECUTE = 0x20000000; // Section is executable
private const uint IMAGE_SCN_MEM_READ = 0x40000000; // Section is readable
private const uint IMAGE_SCN_MEM_WRITE = 0x80000000; // Section is writable
public string Name = ""; // Section name
public uint VirtualSize; // Virtual size
public uint VirtualAddress; // Virtual address
public uint SizeOfRawData; // Size of raw data
public uint PointerToRawData; // Pointer to raw data
public uint PointerToRelocations; // Pointer to relocations
public uint PointerToLinenumbers; // Pointer to line numbers
public ushort NumberOfRelocations; // Number of relocations
public ushort NumberOfLinenumbers; // Number of line numbers
public uint Characteristics; // Characteristics
public bool ContainsCode()
{
return (Characteristics & IMAGE_SCN_CNT_CODE) != 0 ||
(Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
}
public bool IsReadable()
{
return (Characteristics & IMAGE_SCN_MEM_READ) != 0;
}
public bool IsWritable()
{
return (Characteristics & IMAGE_SCN_MEM_WRITE) != 0;
}
public bool IsExecutable()
{
return (Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
}
}

View File

@@ -1,27 +0,0 @@
using X86Disassembler.Analysers;
using X86Disassembler.PE;
using X86Disassembler.ProjectSystem;
namespace X86Disassembler;
public class Program
{
private const string FilePath = @"C:\Program Files (x86)\Nikita\Iron Strategy\Terrain.dll";
public static void Main(string[] args)
{
byte[] fileBytes = File.ReadAllBytes(FilePath);
PeFile peFile = new PeFile(fileBytes);
peFile.Parse();
var projectPeFile = new ProjectPeFile()
{
ImageBase = new VirtualAddress(0, peFile.OptionalHeader.ImageBase),
Architecture = peFile.OptionalHeader.Is64Bit()
? "64-bit"
: "32-bit",
Name = Path.GetFileName(FilePath),
EntryPointAddress = new FileAbsoluteAddress(peFile.OptionalHeader.AddressOfEntryPoint, peFile.OptionalHeader.ImageBase)
};
}
}

View File

@@ -1,14 +0,0 @@
using X86Disassembler.Analysers;
namespace X86Disassembler.ProjectSystem;
public class ProjectPeFile
{
public string Name { get; set; }
public string Architecture { get; set; }
public Address EntryPointAddress { get; set; }
public Address ImageBase { get; set; }
}

View File

@@ -1,14 +0,0 @@
using X86Disassembler.Analysers;
namespace X86Disassembler.ProjectSystem;
public class ProjectPeFileSection
{
public string Name { get; set; }
public Address VirtualAddress { get; set; }
public ulong Size { get; set; }
public SectionFlags Flags { get; set; }
}

View File

@@ -1,11 +0,0 @@
namespace X86Disassembler.ProjectSystem;
[Flags]
public enum SectionFlags
{
None = 0,
Code = 1,
Exec = 2,
Read = 4,
Write = 8
}

View File

@@ -1,19 +0,0 @@
namespace X86Disassembler.X86;
public class Constants
{
// ModR/M byte masks
public const byte MOD_MASK = 0xC0; // 11000000b
public const byte REG_MASK = 0x38; // 00111000b
public const byte RM_MASK = 0x07; // 00000111b
// SIB byte masks
public const byte SIB_SCALE_MASK = 0xC0; // 11000000b
public const byte SIB_INDEX_MASK = 0x38; // 00111000b
public const byte SIB_BASE_MASK = 0x07; // 00000111b
// Register names for different sizes
public static readonly string[] RegisterNames16 = {"ax", "cx", "dx", "bx", "sp", "bp", "si", "di"};
public static readonly string[] RegisterNames32 = {"eax", "ecx", "edx", "ebx", "esp", "ebp", "esi", "edi"};
}

View File

@@ -1,87 +0,0 @@
using X86Disassembler.X86.Operands;
namespace X86Disassembler.X86;
using System.Collections.Generic;
/// <summary>
/// Core x86 instruction disassembler
/// </summary>
public class Disassembler
{
// The buffer containing the code to disassemble
private readonly byte[] _codeBuffer;
// The length of the buffer
private readonly int _length;
// The base address of the code
private readonly ulong _baseAddress;
/// <summary>
/// Initializes a new instance of the Disassembler class
/// </summary>
/// <param name="codeBuffer">The buffer containing the code to disassemble</param>
/// <param name="baseAddress">The base address of the code</param>
public Disassembler(byte[] codeBuffer, ulong baseAddress)
{
_codeBuffer = codeBuffer;
_length = codeBuffer.Length;
_baseAddress = baseAddress;
}
/// <summary>
/// Disassembles the code buffer sequentially and returns all disassembled instructions
/// </summary>
/// <returns>A list of disassembled instructions</returns>
public List<Instruction> Disassemble()
{
List<Instruction> instructions = new List<Instruction>();
// Create an instruction decoder
InstructionDecoder decoder = new InstructionDecoder(_codeBuffer, _length);
// Decode instructions until the end of the buffer is reached
while (true)
{
int position = decoder.GetPosition();
// Check if we've reached the end of the buffer
if (!decoder.CanReadByte())
{
break;
}
// Store the position before decoding to handle prefixes properly
int startPosition = position;
// Decode the instruction
Instruction? instruction = decoder.DecodeInstruction();
if (instruction != null)
{
// Adjust the instruction address to include the base address
instruction.Address = _baseAddress + (uint)startPosition;
// Add the instruction to the list
instructions.Add(instruction);
}
else
{
// If decoding failed, create a dummy instruction for the unknown byte
byte unknownByte = decoder.ReadByte();
Instruction dummyInstruction = new Instruction
{
Address = _baseAddress + (uint)position,
Type = InstructionType.Unknown,
StructuredOperands = [OperandFactory.CreateImmediateOperand(unknownByte, 8),]
};
instructions.Add(dummyInstruction);
}
}
return instructions;
}
}

View File

@@ -1,33 +0,0 @@
namespace X86Disassembler.X86;
/// <summary>
/// Represents the index values for x87 floating-point unit registers.
/// These values correspond to the encoding used in x87 FPU instructions
/// for identifying the specific ST(i) register operands.
/// </summary>
public enum FpuRegisterIndex
{
/// <summary>FPU register ST(0)</summary>
ST0 = 0,
/// <summary>FPU register ST(1)</summary>
ST1 = 1,
/// <summary>FPU register ST(2)</summary>
ST2 = 2,
/// <summary>FPU register ST(3)</summary>
ST3 = 3,
/// <summary>FPU register ST(4)</summary>
ST4 = 4,
/// <summary>FPU register ST(5)</summary>
ST5 = 5,
/// <summary>FPU register ST(6)</summary>
ST6 = 6,
/// <summary>FPU register ST(7)</summary>
ST7 = 7,
}

View File

@@ -1,71 +0,0 @@
namespace X86Disassembler.X86.Handlers.Adc;
using Operands;
/// <summary>
/// Handler for ADC AX/EAX, imm16/32 instruction (opcode 0x15)
/// </summary>
public class AdcAccumulatorImmHandler : InstructionHandler
{
/// <summary>
/// Initializes a new instance of the AdcAccumulatorImmHandler class
/// </summary>
/// <param name="decoder">The instruction decoder that owns this handler</param>
public AdcAccumulatorImmHandler(InstructionDecoder decoder)
: base(decoder)
{
}
/// <summary>
/// Checks if this handler can decode the given opcode
/// </summary>
/// <param name="opcode">The opcode to check</param>
/// <returns>True if this handler can decode the opcode</returns>
public override bool CanHandle(byte opcode)
{
// ADC AX/EAX, imm16/32 is encoded as 0x15
return opcode == 0x15;
}
/// <summary>
/// Decodes a ADC AX/EAX, imm16/32 instruction
/// </summary>
/// <param name="opcode">The opcode of the instruction</param>
/// <param name="instruction">The instruction object to populate</param>
/// <returns>True if the instruction was successfully decoded</returns>
public override bool Decode(byte opcode, Instruction instruction)
{
// Set the instruction type
instruction.Type = InstructionType.Adc;
// Determine operand size based on prefix
int operandSize = Decoder.HasOperandSizePrefix() ? 16 : 32;
// Check if we have enough bytes for the immediate value
if (operandSize == 16 && !Decoder.CanReadUShort())
{
return false;
}
else if (operandSize == 32 && !Decoder.CanReadUInt())
{
return false;
}
// Create the accumulator register operand (AX or EAX)
var accumulatorOperand = OperandFactory.CreateRegisterOperand(RegisterIndex.A, operandSize);
// Read and create the immediate operand based on operand size
var immOperand = operandSize == 16
? OperandFactory.CreateImmediateOperand(Decoder.ReadUInt16(), operandSize)
: OperandFactory.CreateImmediateOperand(Decoder.ReadUInt32(), operandSize);
// Set the structured operands
instruction.StructuredOperands =
[
accumulatorOperand,
immOperand
];
return true;
}
}

View File

@@ -1,64 +0,0 @@
using X86Disassembler.X86.Operands;
namespace X86Disassembler.X86.Handlers.Adc;
/// <summary>
/// Handler for ADC AL, imm8 instruction (0x14)
/// </summary>
public class AdcAlImmHandler : InstructionHandler
{
/// <summary>
/// Initializes a new instance of the AdcAlImmHandler class
/// </summary>
/// <param name="decoder">The instruction decoder that owns this handler</param>
public AdcAlImmHandler(InstructionDecoder decoder)
: base(decoder)
{
}
/// <summary>
/// Checks if this handler can decode the given opcode
/// </summary>
/// <param name="opcode">The opcode to check</param>
/// <returns>True if this handler can decode the opcode</returns>
public override bool CanHandle(byte opcode)
{
return opcode == 0x14;
}
/// <summary>
/// Decodes an ADC AL, imm8 instruction
/// </summary>
/// <param name="opcode">The opcode of the instruction</param>
/// <param name="instruction">The instruction object to populate</param>
/// <returns>True if the instruction was successfully decoded</returns>
public override bool Decode(byte opcode, Instruction instruction)
{
// Set the instruction type
instruction.Type = InstructionType.Adc;
// Check if we have enough bytes for the immediate value
if (!Decoder.CanReadByte())
{
return false;
}
// Read the immediate byte
var imm8 = Decoder.ReadByte();
// Create the AL register operand
var destinationOperand = OperandFactory.CreateRegisterOperand8(RegisterIndex8.AL);
// Create the immediate operand
var sourceOperand = OperandFactory.CreateImmediateOperand(imm8);
// Set the structured operands
instruction.StructuredOperands =
[
destinationOperand,
sourceOperand
];
return true;
}
}

View File

@@ -1,82 +0,0 @@
namespace X86Disassembler.X86.Handlers.Adc;
using Operands;
/// <summary>
/// Handler for ADC r/m16, imm16 instruction (0x81 /2 with 0x66 prefix)
/// </summary>
public class AdcImmToRm16Handler : InstructionHandler
{
/// <summary>
/// Initializes a new instance of the AdcImmToRm16Handler class
/// </summary>
/// <param name="decoder">The instruction decoder that owns this handler</param>
public AdcImmToRm16Handler(InstructionDecoder decoder)
: base(decoder)
{
}
/// <summary>
/// Checks if this handler can decode the given opcode
/// </summary>
/// <param name="opcode">The opcode to check</param>
/// <returns>True if this handler can decode the opcode</returns>
public override bool CanHandle(byte opcode)
{
// ADC r/m16, imm16 is encoded as 0x81 /2 with 0x66 prefix
if (opcode != 0x81)
{
return false;
}
// Check if we have enough bytes to read the ModR/M byte
if (!Decoder.CanReadByte())
{
return false;
}
// Check if the reg field of the ModR/M byte is 2 (ADC)
var reg = ModRMDecoder.PeakModRMReg();
// Only handle when the operand size prefix is present
return reg == 2 && Decoder.HasOperandSizePrefix();
}
/// <summary>
/// Decodes a ADC r/m16, imm16 instruction
/// </summary>
/// <param name="opcode">The opcode of the instruction</param>
/// <param name="instruction">The instruction object to populate</param>
/// <returns>True if the instruction was successfully decoded</returns>
public override bool Decode(byte opcode, Instruction instruction)
{
// Set the instruction type
instruction.Type = InstructionType.Adc;
// Read the ModR/M byte, specifying that we're dealing with 16-bit operands
var (_, _, _, destinationOperand) = ModRMDecoder.ReadModRM16();
// Note: The operand size is already set to 16-bit by the ReadModRM16 method
// Check if we have enough bytes for the immediate value
if (!Decoder.CanReadUShort())
{
return false;
}
// Read the immediate value
ushort imm16 = Decoder.ReadUInt16();
// Create the immediate operand
var sourceOperand = OperandFactory.CreateImmediateOperand(imm16, 16);
// Set the structured operands
instruction.StructuredOperands =
[
destinationOperand,
sourceOperand
];
return true;
}
}

View File

@@ -1,84 +0,0 @@
namespace X86Disassembler.X86.Handlers.Adc;
using Operands;
/// <summary>
/// Handler for ADC r/m16, imm8 (sign-extended) instruction (0x83 /2 with 0x66 prefix)
/// </summary>
public class AdcImmToRm16SignExtendedHandler : InstructionHandler
{
/// <summary>
/// Initializes a new instance of the AdcImmToRm16SignExtendedHandler class
/// </summary>
/// <param name="decoder">The instruction decoder that owns this handler</param>
public AdcImmToRm16SignExtendedHandler(InstructionDecoder decoder)
: base(decoder)
{
}
/// <summary>
/// Checks if this handler can decode the given opcode
/// </summary>
/// <param name="opcode">The opcode to check</param>
/// <returns>True if this handler can decode the opcode</returns>
public override bool CanHandle(byte opcode)
{
// ADC r/m16, imm8 (sign-extended) is encoded as 0x83 /2 with 0x66 prefix
if (opcode != 0x83)
{
return false;
}
// Check if we have enough bytes to read the ModR/M byte
if (!Decoder.CanReadByte())
{
return false;
}
// Check if the reg field of the ModR/M byte is 2 (ADC)
var reg = ModRMDecoder.PeakModRMReg();
// Only handle when the operand size prefix is present
return reg == 2 && Decoder.HasOperandSizePrefix();
}
/// <summary>
/// Decodes a ADC r/m16, imm8 (sign-extended) instruction
/// </summary>
/// <param name="opcode">The opcode of the instruction</param>
/// <param name="instruction">The instruction object to populate</param>
/// <returns>True if the instruction was successfully decoded</returns>
public override bool Decode(byte opcode, Instruction instruction)
{
// Set the instruction type
instruction.Type = InstructionType.Adc;
// For ADC r/m16, imm8 (sign-extended) (0x83 /2 with 0x66 prefix):
// - The r/m field with mod specifies the destination operand (register or memory)
// - The immediate value is the source operand (sign-extended from 8 to 16 bits)
var (_, _, _, destinationOperand) = ModRMDecoder.ReadModRM16();
// Note: The operand size is already set to 16-bit by the ReadModRM16 method
// Check if we have enough bytes for the immediate value
if (!Decoder.CanReadByte())
{
return false;
}
// Read the immediate value (sign-extended from 8 to 16 bits)
short imm16 = (sbyte)Decoder.ReadByte();
// Create the immediate operand
var sourceOperand = OperandFactory.CreateImmediateOperand((ushort)imm16, 16);
// Set the structured operands
instruction.StructuredOperands =
[
destinationOperand,
sourceOperand
];
return true;
}
}

Some files were not shown because too many files have changed in this diff Show More