1
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-12-11 00:41:20 +04:00

31 Commits

Author SHA1 Message Date
bird_egop
a8536f938d fxid 2025-12-09 02:32:32 +03:00
bird_egop
fcfb8d0e8a feat: Add landscape mesh support to MshConverter
- Auto-detect Model vs Landscape mesh types
- Model: uses indexed triangles (06, 0D, 07)
- Landscape: uses direct triangles (0B, 15)
- Add MSH_FORMAT.md documentation (Russian)
- Add Ghidra decompiled code for CLandscape
2025-12-05 18:04:50 +03:00
bird_egop
4951550420 add better tooltips 2025-12-04 07:58:32 +03:00
bird_egop
5ea0cb143f add help 2025-12-04 03:55:41 +03:00
bird_egop
75b1548f55 Material parsing and viewing 2025-12-04 03:50:44 +03:00
bird_egop
3b1fd5ef97 more docs 2025-12-01 12:48:08 +03:00
bird_egop
60fd173c47 info about lightmap texture naming 2025-12-01 07:41:42 +03:00
bird_egop
3c9b46cbca update docs 2025-11-30 18:27:40 +03:00
bird_egop
c9d0366637 Improvements on docs, based on decompilation 2025-11-30 18:11:16 +03:00
bird_egop
6c9e1d0b98 some findings 2025-11-27 19:01:41 +03:00
bird_egop
3738881c46 drop unused projects 2025-11-23 19:33:58 +03:00
bird_egop
c27b77cbfc parse PAL file 2025-11-23 00:04:05 +03:00
bird_egop
f4442897a6 improve binary varset parsing 2025-11-22 09:02:35 +03:00
bird_egop
2a78bbebda parse binary varset file 2025-11-22 06:53:07 +03:00
bird_egop
a774db37a6 improvements 2025-10-05 19:08:23 +03:00
Bird Egop
c1ea70efe0 Create LICENSE 2025-09-06 02:25:15 +03:00
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
610 changed files with 9468 additions and 42552 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>

91
Common/Extensions.cs Normal file
View File

@@ -0,0 +1,91 @@
using System.Buffers.Binary;
using System.Text;
namespace Common;
public static class Extensions
{
public static int ReadInt32LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadInt32LittleEndian(buf);
}
public static uint ReadUInt32LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadUInt32LittleEndian(buf);
}
public static ushort ReadUInt16LittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[2];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadUInt16LittleEndian(buf);
}
public static float ReadFloatLittleEndian(this Stream fs)
{
Span<byte> buf = stackalloc byte[4];
fs.ReadExactly(buf);
return BinaryPrimitives.ReadSingleLittleEndian(buf);
}
public static string ReadNullTerminatedString(this Stream 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 ReadNullTerminated1251String(this Stream fs)
{
var sb = new StringBuilder();
while (true)
{
var b = (byte)fs.ReadByte();
if (b == 0)
{
break;
}
sb.Append(Encoding.GetEncoding("windows-1251").GetString([b]));
}
return sb.ToString();
}
public static string ReadLengthPrefixedString(this Stream fs)
{
var len = fs.ReadInt32LittleEndian();
if (len == 0)
{
return "";
}
var buffer = new byte[len];
fs.ReadExactly(buffer, 0, len);
return Encoding.ASCII.GetString(buffer, 0, len);
}
}

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)

View File

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

31
CpDatLib/CpDatEntry.cs Normal file
View File

@@ -0,0 +1,31 @@
namespace CpDatLib;
public record CpDatEntry(
string ArchiveFile,
string ArchiveEntryName,
int Magic1,
int Magic2,
string Description,
DatEntryType Type,
int ChildCount, // игра не хранит это число в объекте, но оно есть в файле
List<CpDatEntry> Children
);
// Magic3 seems to be a type
// 0 - chassis
// 1 - turret (у зданий почему-то дефлектор тоже 1), может быть потому, что дефлектор вращается так же как башня у юнитов
// 2 - armour
// 3 - part
// 4 - cannon
// 5 - ammo
public enum DatEntryType
{
Unspecified = -1,
Chassis = 0,
Turret = 1,
Armour = 2,
Part = 3,
Cannon = 4,
Ammo = 5,
}

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

74
CpDatLib/CpDatParser.cs Normal file
View File

@@ -0,0 +1,74 @@
using Common;
namespace CpDatLib;
public class CpDatParser
{
public static CpDatParseResult Parse(string filePath)
{
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return Parse(fs);
}
public static CpDatParseResult Parse(Stream fs)
{
Span<byte> f0f1 = stackalloc byte[4];
if (fs.Length < 8)
return new CpDatParseResult(null, "File too small to be a valid \"cp\" .dat file.");
fs.ReadExactly(f0f1);
if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0)
{
return new CpDatParseResult(null, "File does not start with expected header bytes f1_f0");
}
var schemeType = (SchemeType)fs.ReadInt32LittleEndian();
var entryLength =
0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов
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(Stream 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.ReadNullTerminated1251String();
fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var type = (DatEntryType)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, type, 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>

373
LICENSE Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

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

82
MaterialLib/Interpolate.c Normal file
View File

@@ -0,0 +1,82 @@

void __cdecl
Interpolate(MaterialExternal *src1_ptr,MaterialExternal *src2_ptr,float progress,
MaterialExternal *dst_ptr,uint targetFlags)
{
if ((targetFlags & 2) == 0) {
(dst_ptr->stage).diffuse.R = (src1_ptr->stage).diffuse.R;
(dst_ptr->stage).diffuse.G = (src1_ptr->stage).diffuse.G;
(dst_ptr->stage).diffuse.B = (src1_ptr->stage).diffuse.B;
}
else {
(dst_ptr->stage).diffuse.R =
((src2_ptr->stage).diffuse.R - (src1_ptr->stage).diffuse.R) * progress +
(src1_ptr->stage).diffuse.R;
(dst_ptr->stage).diffuse.G =
((src2_ptr->stage).diffuse.G - (src1_ptr->stage).diffuse.G) * progress +
(src1_ptr->stage).diffuse.G;
(dst_ptr->stage).diffuse.B =
((src2_ptr->stage).diffuse.B - (src1_ptr->stage).diffuse.B) * progress +
(src1_ptr->stage).diffuse.B;
}
if ((targetFlags & 1) == 0) {
(dst_ptr->stage).ambient.R = (src1_ptr->stage).ambient.R;
(dst_ptr->stage).ambient.G = (src1_ptr->stage).ambient.G;
(dst_ptr->stage).ambient.B = (src1_ptr->stage).ambient.B;
}
else {
(dst_ptr->stage).ambient.R =
((src2_ptr->stage).ambient.R - (src1_ptr->stage).ambient.R) * progress +
(src1_ptr->stage).ambient.R;
(dst_ptr->stage).ambient.G =
((src2_ptr->stage).ambient.G - (src1_ptr->stage).ambient.G) * progress +
(src1_ptr->stage).ambient.G;
(dst_ptr->stage).ambient.B =
((src2_ptr->stage).ambient.B - (src1_ptr->stage).ambient.B) * progress +
(src1_ptr->stage).ambient.B;
}
if ((targetFlags & 4) == 0) {
(dst_ptr->stage).specular.R = (src1_ptr->stage).specular.R;
(dst_ptr->stage).specular.G = (src1_ptr->stage).specular.G;
(dst_ptr->stage).specular.B = (src1_ptr->stage).specular.B;
}
else {
(dst_ptr->stage).specular.R =
((src2_ptr->stage).specular.R - (src1_ptr->stage).specular.R) * progress +
(src1_ptr->stage).specular.R;
(dst_ptr->stage).specular.G =
((src2_ptr->stage).specular.G - (src1_ptr->stage).specular.G) * progress +
(src1_ptr->stage).specular.G;
(dst_ptr->stage).specular.B =
((src2_ptr->stage).specular.B - (src1_ptr->stage).specular.B) * progress +
(src1_ptr->stage).specular.B;
}
if ((targetFlags & 8) == 0) {
(dst_ptr->stage).emissive.R = (src1_ptr->stage).emissive.R;
(dst_ptr->stage).emissive.G = (src1_ptr->stage).emissive.G;
(dst_ptr->stage).emissive.B = (src1_ptr->stage).emissive.B;
}
else {
(dst_ptr->stage).emissive.R =
((src2_ptr->stage).emissive.R - (src1_ptr->stage).emissive.R) * progress +
(src1_ptr->stage).emissive.R;
(dst_ptr->stage).emissive.G =
((src2_ptr->stage).emissive.G - (src1_ptr->stage).emissive.G) * progress +
(src1_ptr->stage).emissive.G;
(dst_ptr->stage).emissive.B =
((src2_ptr->stage).emissive.B - (src1_ptr->stage).emissive.B) * progress +
(src1_ptr->stage).emissive.B;
}
if ((targetFlags & 0x10) != 0) {
(dst_ptr->stage).ambient.A =
((src2_ptr->stage).ambient.A - (src1_ptr->stage).ambient.A) * progress +
(src1_ptr->stage).ambient.A;
(dst_ptr->stage).Power = (src1_ptr->stage).Power;
return;
}
(dst_ptr->stage).ambient.A = (src1_ptr->stage).ambient.A;
(dst_ptr->stage).Power = (src1_ptr->stage).Power;
return;
}

167
MaterialLib/MaterialFile.cs Normal file
View File

@@ -0,0 +1,167 @@
namespace MaterialLib;
public class MaterialFile
{
// === Metadata (not from file content) ===
public string FileName { get; set; }
public int Version { get; set; } // From NRes ElementCount
public int Magic1 { get; set; } // From NRes Magic1
// === Derived from Version/ElementCount ===
public int MaterialRenderingType { get; set; } // (Version >> 2) & 0xF - 0=Standard, 1=Special, 2=Particle
public bool SupportsBumpMapping { get; set; } // (Version & 2) != 0
public int IsParticleEffect { get; set; } // Version & 40 - 0=Normal, 8=Particle/Effect
// === File Content (in read order) ===
// Read order: StageCount (ushort), AnimCount (ushort), then conditionally blend modes and params
// Global Blend Modes (read if Magic1 >= 2)
public BlendMode SourceBlendMode { get; set; } // Default: Unknown (0xFF)
public BlendMode DestBlendMode { get; set; } // Default: Unknown (0xFF)
// Global Parameters (read if Magic1 > 2 and > 3 respectively)
public float GlobalAlphaMultiplier { get; set; } // Default: 1.0 (always 1.0 in all 628 materials)
public float GlobalEmissiveIntensity { get; set; } // Default: 0.0 (0=no glow, rare values: 1000, 10000)
public List<MaterialStage> Stages { get; set; } = new();
public List<MaterialAnimation> Animations { get; set; } = new();
}
/// <summary>
/// Blend modes for material rendering. These control how source and destination colors are combined.
/// Formula: FinalColor = (SourceColor * SourceBlend) [operation] (DestColor * DestBlend)
/// Maps to Direct3D D3DBLEND values.
/// </summary>
public enum BlendMode : byte
{
/// <summary>Blend factor is (0, 0, 0, 0) - results in black/transparent</summary>
Zero = 1,
/// <summary>Blend factor is (1, 1, 1, 1) - uses full color value</summary>
One = 2,
/// <summary>Blend factor is (Rs, Gs, Bs, As) - uses source color</summary>
SrcColor = 3,
/// <summary>Blend factor is (1-Rs, 1-Gs, 1-Bs, 1-As) - uses inverted source color</summary>
InvSrcColor = 4,
/// <summary>Blend factor is (As, As, As, As) - uses source alpha for all channels (standard transparency)</summary>
SrcAlpha = 5,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) - uses inverted source alpha</summary>
InvSrcAlpha = 6,
/// <summary>Blend factor is (Ad, Ad, Ad, Ad) - uses destination alpha</summary>
DestAlpha = 7,
/// <summary>Blend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad) - uses inverted destination alpha</summary>
InvDestAlpha = 8,
/// <summary>Blend factor is (Rd, Gd, Bd, Ad) - uses destination color</summary>
DestColor = 9,
/// <summary>Blend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad) - uses inverted destination color</summary>
InvDestColor = 10,
/// <summary>Blend factor is (f, f, f, 1) where f = min(As, 1-Ad) - saturates source alpha</summary>
SrcAlphaSat = 11,
/// <summary>Blend factor is (As, As, As, As) for both source and dest - obsolete in D3D9+</summary>
BothSrcAlpha = 12,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest - obsolete in D3D9+</summary>
BothInvSrcAlpha = 13,
/// <summary>Unknown or uninitialized blend mode (0xFF default value)</summary>
Unknown = 0xFF
}
public class MaterialStage
{
// === FILE READ ORDER (34 bytes per stage) ===
// This matches the order bytes are read from the file (decompiled.c lines 159-217)
// NOT the C struct memory layout (which is Diffuse, Ambient, Specular, Emissive)
// 1. Ambient Color (4 bytes, read first from file)
public float AmbientR;
public float AmbientG;
public float AmbientB;
public float AmbientA; // Scaled by 0.01 when read from file
// 2. Diffuse Color (4 bytes, read second from file)
public float DiffuseR;
public float DiffuseG;
public float DiffuseB;
public float DiffuseA;
// 3. Specular Color (4 bytes, read third from file)
public float SpecularR;
public float SpecularG;
public float SpecularB;
public float SpecularA;
// 4. Emissive Color (4 bytes, read fourth from file)
public float EmissiveR;
public float EmissiveG;
public float EmissiveB;
public float EmissiveA;
// 5. Power (1 byte → float, read fifth from file)
public float Power;
// 6. Texture Stage Index (1 byte, read sixth from file)
// 255 = not set/default, 0-47 = reference to specific texture stage
public int TextureStageIndex;
// 7. Texture Name (16 bytes, read seventh from file)
public string TextureName { get; set; }
}
public class MaterialAnimation
{
// === File Read Order ===
// Combined field (4 bytes): bits 3-31 = Target, bits 0-2 = LoopMode
public AnimationTarget Target;
public AnimationLoopMode LoopMode;
// Key count (2 bytes), then keys
public List<AnimKey> Keys { get; set; } = new();
// Cached description for UI (computed once during parsing)
public string TargetDescription { get; set; } = string.Empty;
}
[Flags]
public enum AnimationTarget : int
{
// NOTE: This is a BITSET (flags enum). Multiple flags can be combined.
// When a flag is SET, that component is INTERPOLATED between stages.
// When a flag is NOT SET, that component is COPIED from the source stage (no interpolation).
// If ALL flags are 0, the ENTIRE stage is copied without any interpolation.
Ambient = 1, // 0x01 - Interpolates Ambient RGB (Interpolate.c lines 23-37)
Diffuse = 2, // 0x02 - Interpolates Diffuse RGB (Interpolate.c lines 7-21)
Specular = 4, // 0x04 - Interpolates Specular RGB (Interpolate.c lines 39-53)
Emissive = 8, // 0x08 - Interpolates Emissive RGB (Interpolate.c lines 55-69)
Power = 16 // 0x10 - Interpolates Ambient.A and sets Power (Interpolate.c lines 71-76)
}
public enum AnimationLoopMode : int
{
Loop = 0,
PingPong = 1,
Clamp = 2,
Random = 3
}
public struct AnimKey
{
// === File Read Order (6 bytes per key) ===
public ushort StageIndex; // Read first
public ushort DurationMs; // Read second
public ushort InterpolationCurve; // Read third - Always 0 (linear interpolation) in all 1848 keys
}

View File

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

View File

@@ -0,0 +1,176 @@
using System.Buffers.Binary;
using System.Text;
namespace MaterialLib;
public static class MaterialParser
{
public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1)
{
var file = new MaterialFile
{
FileName = fileName,
Version = elementCount,
Magic1 = magic1
};
// Derived fields
file.MaterialRenderingType = elementCount >> 2 & 0xf;
file.SupportsBumpMapping = (elementCount & 2) != 0;
file.IsParticleEffect = elementCount & 40;
// Reading content
var stageCount = fs.ReadUInt16LittleEndian();
var animCount = fs.ReadUInt16LittleEndian();
uint magic = (uint)magic1;
// Defaults found in C code
file.GlobalAlphaMultiplier = 1.0f; // field8_0x15c
file.GlobalEmissiveIntensity = 0.0f; // field9_0x160
file.SourceBlendMode = BlendMode.Unknown; // field6_0x154
file.DestBlendMode = BlendMode.Unknown; // field7_0x158
if (magic >= 2)
{
file.SourceBlendMode = (BlendMode)fs.ReadByte();
file.DestBlendMode = (BlendMode)fs.ReadByte();
}
if (magic > 2)
{
file.GlobalAlphaMultiplier = fs.ReadFloatLittleEndian();
}
if (magic > 3)
{
file.GlobalEmissiveIntensity = fs.ReadFloatLittleEndian();
}
// --- 2. Material Stages ---
const float Inv255 = 1.0f / 255.0f;
const float Field7Mult = 0.01f;
Span<byte> textureNameBuffer = stackalloc byte[16];
for (int i = 0; i < stageCount; i++)
{
var stage = new MaterialStage();
// === FILE READ ORDER (matches decompiled.c lines 159-217) ===
// 1. Ambient (4 bytes, A scaled by 0.01) - Lines 159-168
stage.AmbientR = fs.ReadByte() * Inv255;
stage.AmbientG = fs.ReadByte() * Inv255;
stage.AmbientB = fs.ReadByte() * Inv255;
stage.AmbientA = fs.ReadByte() * Field7Mult; // 0.01 scaling
// 2. Diffuse (4 bytes) - Lines 171-180
stage.DiffuseR = fs.ReadByte() * Inv255;
stage.DiffuseG = fs.ReadByte() * Inv255;
stage.DiffuseB = fs.ReadByte() * Inv255;
stage.DiffuseA = fs.ReadByte() * Inv255;
// 3. Specular (4 bytes) - Lines 183-192
stage.SpecularR = fs.ReadByte() * Inv255;
stage.SpecularG = fs.ReadByte() * Inv255;
stage.SpecularB = fs.ReadByte() * Inv255;
stage.SpecularA = fs.ReadByte() * Inv255;
// 4. Emissive (4 bytes) - Lines 195-204
stage.EmissiveR = fs.ReadByte() * Inv255;
stage.EmissiveG = fs.ReadByte() * Inv255;
stage.EmissiveB = fs.ReadByte() * Inv255;
stage.EmissiveA = fs.ReadByte() * Inv255;
// 5. Power (1 byte → float) - Line 207
stage.Power = (float)fs.ReadByte();
// 6. Texture Stage Index (1 byte) - Line 210
stage.TextureStageIndex = fs.ReadByte();
// 7. Texture Name (16 bytes) - Lines 212-217
textureNameBuffer.Clear();
fs.ReadExactly(textureNameBuffer);
stage.TextureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
file.Stages.Add(stage);
}
// --- 3. Animations ---
for (int i = 0; i < animCount; i++)
{
var anim = new MaterialAnimation();
uint typeAndParams = fs.ReadUInt32LittleEndian();
anim.Target = (AnimationTarget)(typeAndParams >> 3);
anim.LoopMode = (AnimationLoopMode)(typeAndParams & 7);
ushort keyCount = fs.ReadUInt16LittleEndian();
for (int k = 0; k < keyCount; k++)
{
var key = new AnimKey
{
StageIndex = fs.ReadUInt16LittleEndian(),
DurationMs = fs.ReadUInt16LittleEndian(),
InterpolationCurve = fs.ReadUInt16LittleEndian()
};
anim.Keys.Add(key);
}
// Precompute description for UI to avoid per-frame allocations
anim.TargetDescription = ComputeTargetDescription(anim.Target);
file.Animations.Add(anim);
}
return file;
}
private static string ComputeTargetDescription(AnimationTarget target)
{
// Precompute the description once during parsing
if ((int)target == 0)
return "No interpolation - entire stage is copied as-is (Flags: 0x0)";
var parts = new List<string>();
if ((target & AnimationTarget.Ambient) != 0)
parts.Add("Ambient RGB");
if ((target & AnimationTarget.Diffuse) != 0)
parts.Add("Diffuse RGB");
if ((target & AnimationTarget.Specular) != 0)
parts.Add("Specular RGB");
if ((target & AnimationTarget.Emissive) != 0)
parts.Add("Emissive RGB");
if ((target & AnimationTarget.Power) != 0)
parts.Add("Ambient.A + Power");
return $"Interpolates: {string.Join(", ", parts)} | Other components copied (Flags: 0x{(int)target:X})";
}
}
// Helper extensions
internal static class StreamExtensions
{
public static float ReadFloatLittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[4];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadSingleLittleEndian(buffer);
}
public static ushort ReadUInt16LittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[2];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadUInt16LittleEndian(buffer);
}
public static uint ReadUInt32LittleEndian(this Stream stream)
{
Span<byte> buffer = stackalloc byte[4];
stream.ReadExactly(buffer);
return BinaryPrimitives.ReadUInt32LittleEndian(buffer);
}
}

290
MaterialLib/decompiled.c Normal file
View File

@@ -0,0 +1,290 @@
/* returns index into g_MaterialGlobalDescriptors */
int __fastcall LoadMaterialByName(char *itemname)
{
char *pcVar1;
undefined1 *puVar2;
byte *pbVar3;
uint uVar4;
undefined4 *puVar5;
int iVar6;
int iVar7;
MaterialGlobalDescriptor_ptr_4_undefined4 piVar8;
nres_metadata_item *pnVar8;
int iVar9;
MaterialStageWorldDllInternal *pMVar10;
int iVar11;
ITexture **ppIVar12;
MaterialAnimationKey *pMVar13;
MaterialGlobalDescriptor *pMVar14;
float unaff_EBX;
MaterialStageExternal *pfVar16;
ushort unaff_DI;
MaterialAnimation *pMVar15;
uint uVar16;
int iStack_90;
uint uStack_8c;
char acStack_88 [8];
int iStack_80;
char acStack_58 [88];
iVar6 = (*(*g_material_resfile)->get_index_in_file_by_itemname)(g_material_resfile,itemname);
if (iVar6 < 0) {
sprintf(acStack_58,s_Material_%s_not_found._100246f8,itemname);
/* WARNING: Subroutine does not return */
write_error_to_file_and_msgbox(acStack_58,(void *)0x0);
}
iVar7 = 0;
if (0 < g_MaterialGlobalDescriptor_count) {
pMVar14 = g_MaterialGlobalDescriptors;
do {
if (pMVar14->index_in_file == iVar6) {
if (-1 < iVar7) {
g_MaterialGlobalDescriptors[iVar7].RefCount =
g_MaterialGlobalDescriptors[iVar7].RefCount + 1;
return iVar7;
}
break;
}
iVar7 += 1;
pMVar14 = pMVar14 + 1;
} while (iVar7 < g_MaterialGlobalDescriptor_count);
}
iVar7 = 0;
if (0 < g_MaterialGlobalDescriptor_count) {
piVar8 = &g_MaterialGlobalDescriptors[0].RefCount;
do {
if (ADJ(piVar8)->RefCount == 0) break;
iVar7 += 1;
piVar8 = piVar8 + 0x5c;
} while (iVar7 < g_MaterialGlobalDescriptor_count);
}
if (iVar7 == g_MaterialGlobalDescriptor_count) {
g_MaterialGlobalDescriptor_count += 1;
}
iStack_80 = iVar7;
g_currentMaterialFileData_ptr =
(*(*g_material_resfile)->get_item_data_ptr_by_index)(g_material_resfile,iVar6,1);
pnVar8 = (*(*g_material_resfile)->get_metadata_ptr)(g_material_resfile);
uVar16 = 0;
uVar4 = pnVar8[iVar6].magic1;
if ((pnVar8[iVar6].element_count_or_version & 1) != 0) {
uVar16 = 0x200000;
}
g_MaterialGlobalDescriptors[iVar7].extra_meta.field0_0x0 = 0;
g_MaterialGlobalDescriptors[iVar7].extra_meta.field2_0x8 = 0;
g_MaterialGlobalDescriptors[iVar7].extra_meta.field1_0x4 =
pnVar8[iVar6].element_count_or_version >> 2 & 0xf;
iVar9 = g_bumpmapping_enabled_value;
if (((pnVar8[iVar6].element_count_or_version & 2) != 0) &&
((g_MaterialGlobalDescriptors[iVar7].extra_meta.field0_0x0 = 1, iVar9 == 0 ||
(g_supports_texture_mode_6 == 0)))) {
uVar16 |= 0x80000;
}
if ((pnVar8[iVar6].element_count_or_version & 0x40) != 0) {
g_MaterialGlobalDescriptors[iVar7].extra_meta.field2_0x8 = 1;
}
iVar9 = 0;
g_MaterialGlobalDescriptors[iVar7].RefCount = g_MaterialGlobalDescriptors[iVar7].RefCount + 1;
puVar5 = g_currentMaterialFileData_ptr;
g_MaterialGlobalDescriptors[iVar7].index_in_file = iVar6;
iVar6 = 0;
do {
pcVar1 = (char *)(iVar9 + (int)puVar5);
iVar9 += 1;
acStack_88[iVar6 + -0x1c] = *pcVar1;
iVar6 += 1;
} while (iVar6 < 2);
g_MaterialGlobalDescriptors[iVar7].stageCount = (uint)unaff_DI;
iVar6 = 0;
do {
iVar11 = iVar9;
iVar9 = iVar11 + 1;
acStack_88[iVar6 + -0x1c] = *(char *)(iVar11 + (int)puVar5);
iVar6 += 1;
} while (iVar6 < 2);
DAT_10128674 = iVar9;
g_MaterialGlobalDescriptors[iVar7].animCount = (uint)unaff_DI;
if (0x13 < unaff_DI) {
/* WARNING: Subroutine does not return */
write_error_to_file_and_msgbox(s_Too_many_animations_for_material_100246cc,(void *)0x0);
}
g_MaterialGlobalDescriptors[iVar7].field8_0x15c = 1.0;
g_MaterialGlobalDescriptors[iVar7].field9_0x160 = 0;
if (uVar4 < 2) {
g_MaterialGlobalDescriptors[iVar7].field6_0x154 = 0xff;
g_MaterialGlobalDescriptors[iVar7].field7_0x158 = 0xff;
}
else {
g_MaterialGlobalDescriptors[iVar7].field6_0x154 = (uint)*(byte *)(iVar9 + (int)puVar5);
iVar6 = iVar11 + 3;
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field7_0x158 = (uint)*(byte *)(iVar11 + 2 + (int)puVar5);
if (2 < uVar4) {
iVar9 = 0;
do {
puVar2 = (undefined1 *)(iVar6 + (int)puVar5);
iVar6 += 1;
(&stack0xffffff68)[iVar9] = *puVar2;
iVar9 += 1;
} while (iVar9 < 4);
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field8_0x15c = unaff_EBX;
if (3 < uVar4) {
iVar9 = 0;
do {
puVar2 = (undefined1 *)(iVar6 + (int)puVar5);
iVar6 += 1;
(&stack0xffffff68)[iVar9] = *puVar2;
iVar9 += 1;
} while (iVar9 < 4);
DAT_10128674 = iVar6;
g_MaterialGlobalDescriptors[iVar7].field9_0x160 = unaff_EBX;
}
}
}
pMVar10 = (MaterialStageWorldDllInternal *)
_malloc(g_MaterialGlobalDescriptors[iVar7].stageCount * 0x4c);
g_MaterialGlobalDescriptors[iVar7].stages = pMVar10;
iVar6 = 0;
if (0 < g_MaterialGlobalDescriptors[iVar7].stageCount) {
iVar9 = 0;
do {
pfVar16 = (MaterialStageExternal *)
((int)&((g_MaterialGlobalDescriptors[iVar7].stages)->diffuse).R + iVar9);
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->ambient).A = (float)*pbVar3 * 0.01;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->diffuse).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->specular).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).R = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).G = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).B = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
(pfVar16->emissive).A = (float)*pbVar3 * 1/255f;
pbVar3 = (byte *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
pfVar16->Power = (float)(uint)*pbVar3;
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 = DAT_10128674 + 1;
pfVar16->current_m_LL_ITexture = (ITexture **)(int)*pcVar1;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 0x10);
if (acStack_88[0] == '\0') {
pfVar16->m_LL_ITexture = (ITexture **)0xffffffff;
pfVar16->current_m_LL_ITexture = (ITexture **)0xffffffff;
}
else {
ppIVar12 = (ITexture **)LoadAndCacheTexture(acStack_88,uVar16);
pfVar16->m_LL_ITexture = ppIVar12;
}
iVar6 += 1;
iVar9 += 0x4c;
} while (iVar6 < g_MaterialGlobalDescriptors[iVar7].stageCount);
}
iVar6 = 0;
if (0 < g_MaterialGlobalDescriptors[iVar7].animCount) {
pMVar15 = g_MaterialGlobalDescriptors[iVar7].animations;
do {
puVar5 = g_currentMaterialFileData_ptr;
iVar9 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar9 + -4] = *pcVar1;
iVar9 += 1;
} while (iVar9 < 4);
pMVar15->FieldSelector = (int)uStack_8c >> 3;
pMVar15->loop_mode = uStack_8c & 7;
iVar9 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)puVar5);
DAT_10128674 += 1;
acStack_88[iVar9 + -0x1c] = *pcVar1;
iVar9 += 1;
} while (iVar9 < 2);
pMVar15->keyCount = (uint)unaff_DI;
pMVar13 = (MaterialAnimationKey *)_malloc((uint)unaff_DI << 3);
pMVar15->keys = pMVar13;
iVar9 = 0;
if (0 < (int)pMVar15->keyCount) {
do {
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].stage_index = (uint)unaff_DI;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].duration_ms = unaff_DI;
iVar11 = 0;
do {
pcVar1 = (char *)(DAT_10128674 + (int)g_currentMaterialFileData_ptr);
DAT_10128674 += 1;
acStack_88[iVar11 + -0x1c] = *pcVar1;
iVar11 += 1;
} while (iVar11 < 2);
pMVar15->keys[iVar9].field2_0x6 = unaff_DI;
iVar9 += 1;
} while (iVar9 < (int)pMVar15->keyCount);
}
iVar6 += 1;
pMVar15 = pMVar15 + 1;
} while (iVar6 < g_MaterialGlobalDescriptors[iVar7].animCount);
}
return iStack_90;
}

View File

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

View File

@@ -1,14 +0,0 @@
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
using var fs = new FileStream("C:\\ParkanUnpacked\\11_fr_e_brige.msh\\0_fr_e_brige.bin", FileMode.Open);
byte[] buffer = new byte[38];
for (int i = 0; i < 6; i++)
{
fs.ReadExactly(buffer);
Console.WriteLine(string.Join(" ", buffer.Select(x => x.ToString("X2"))));
}

View File

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

View File

@@ -1,9 +0,0 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Autodemo.00\\data.tma";
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Tutorial.01\\data.tma";
var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\CAMPAIGN\\CAMPAIGN.01\\Mission.02\\data.tma";
// var missionFilePath = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\Single.01\\data.tma";

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,11 +1,18 @@
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);
return ReadFile(fs);
}
public static MissionTmaParseResult ReadFile(Stream fs)
{
var arealData = LoadAreals(fs);
var clansData = LoadClans(fs);
@@ -18,7 +25,7 @@ public class MissionTmaParser
return new MissionTmaParseResult(missionDat, null);
}
private static ArealsFileData LoadAreals(FileStream fileStream)
private static ArealsFileData LoadAreals(Stream fileStream)
{
var unusedHeader = fileStream.ReadInt32LittleEndian();
var arealCount = fileStream.ReadInt32LittleEndian();
@@ -54,7 +61,7 @@ public class MissionTmaParser
return new ArealsFileData(unusedHeader, arealCount, infos);
}
private static ClansFileData? LoadClans(FileStream fileStream)
private static ClansFileData? LoadClans(Stream fileStream)
{
var clanFeatureSet = fileStream.ReadInt32LittleEndian();
@@ -156,7 +163,7 @@ public class MissionTmaParser
return clanInfo;
}
private static GameObjectsFileData LoadGameObjects(FileStream fileStream)
private static GameObjectsFileData LoadGameObjects(Stream fileStream)
{
var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian();

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,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,42 @@
using System.Buffers.Binary;
using System.Buffers.Binary;
using System.Text;
var fileBytes = File.ReadAllBytes("C:\\Program Files (x86)\\Nikita\\Iron Strategy\\gamefont.rlb");
// Unfinished
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

@@ -20,7 +20,7 @@ public record NResArchiveHeader(string NRes, int Version, int FileCount, int Tot
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64)
/// </summary>
/// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param>
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) </param>
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) или версия, например у материалов или флаги</param>
/// <param name="Magic1">[8..12] Неизвестное число</param>
/// <param name="FileLength">[12..16] Длина файла в байтах</param>
/// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param>

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,8 +7,13 @@ 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);
return ReadFile(nResFs);
}
public static NResParseResult ReadFile(Stream nResFs)
{
if (nResFs.Length < 16)
{
return new NResParseResult(null, "Файл не может быть NRes, менее 16 байт");

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System.Text;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using NResUI.Abstractions;
@@ -33,6 +34,8 @@ public class App
public void Init(IWindow window, GL openGl, ImFontPtr openSansFont)
{
// Call this once at program startup
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
ImGui.StyleColorsLight();
IServiceCollection serviceCollection = new ServiceCollection();
@@ -56,6 +59,8 @@ public class App
serviceCollection.AddSingleton(new BinaryExplorerViewModel());
serviceCollection.AddSingleton(new ScrViewModel());
serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel());
serviceCollection.AddSingleton(new MaterialViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider();

View File

@@ -0,0 +1,150 @@
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", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.Sortable))
{
ImGui.TableSetupColumn("Индекс");
ImGui.TableSetupColumn("Уровень вложенности");
ImGui.TableSetupColumn("Архив");
ImGui.TableSetupColumn("Элемент");
ImGui.TableSetupColumn("Magic1");
ImGui.TableSetupColumn("Magic2");
ImGui.TableSetupColumn("Описание");
ImGui.TableSetupColumn("Тип");
ImGui.TableHeadersRow();
// Handle sorting
ImGuiTableSortSpecsPtr sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
// Only handle the first sort spec for simplicity
var sortSpec = sortSpecs.Specs;
if (sortSpec.ColumnIndex == 0)
{
_viewModel.RebuildFlatList();
}
else
{
_viewModel.FlatList.Sort((a, b) =>
{
int result = 0;
switch (sortSpec.ColumnIndex)
{
case 1: result = a.Level.CompareTo(b.Level); break;
case 2: result = string.Compare(a.Entry.ArchiveFile, b.Entry.ArchiveFile, StringComparison.Ordinal); break;
case 3: result = string.Compare(a.Entry.ArchiveEntryName, b.Entry.ArchiveEntryName, StringComparison.Ordinal); break;
case 4: result = a.Entry.Magic1.CompareTo(b.Entry.Magic1); break;
case 5: result = a.Entry.Magic2.CompareTo(b.Entry.Magic2); break;
case 6: result = string.Compare(a.Entry.Description, b.Entry.Description, StringComparison.Ordinal); break;
case 7: result = a.Entry.Type.CompareTo(b.Entry.Type); break;
}
return sortSpec.SortDirection == ImGuiSortDirection.Descending ? -result : result;
});
}
sortSpecs.SpecsDirty = false;
}
for (int i = 0; i < _viewModel.FlatList.Count; i++)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(i.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Level.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile);
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.Type.ToString("G"));
}
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("Тип: ");
ImGui.SameLine();
ImGui.Text(entry.Type.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

@@ -0,0 +1,301 @@
using System.Numerics;
using ImGuiNET;
using MaterialLib;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class MaterialExplorerPanel : IImGuiPanel
{
private readonly MaterialViewModel _viewModel;
public MaterialExplorerPanel(MaterialViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("Material Explorer"))
{
ImGui.Text("Материал это бинарный файл, который можно найти в materials.lib.");
ImGui.Text("Его можно открыть только из открытого NRes архива, т.к. чтение полагается на флаги из архива");
if (!_viewModel.HasFile)
{
ImGui.Text("No Material file opened");
}
else if (_viewModel.Error != null)
{
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Error: {_viewModel.Error}");
}
else
{
var mat = _viewModel.MaterialFile!;
ImGui.Text($"File: {_viewModel.FilePath}");
ImGui.Separator();
// === FILE READ SEQUENCE ===
if (ImGui.CollapsingHeader("1. Metadata & Derived Fields", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextDisabled("(Not from file content)");
ImGui.Text($"File Name: {mat.FileName}");
ImGui.Text($"Version (ElementCount): {mat.Version}");
ImGui.Text($"Magic1: {mat.Magic1}");
ImGui.Separator();
ImGui.TextDisabled("(Derived from Version)");
ImGui.Text($"Material Rendering Type: {mat.MaterialRenderingType}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=Standard, 1=Special, 2=Particle/Effect");
}
ImGui.Text($"Supports Bump Mapping: {mat.SupportsBumpMapping}");
ImGui.Text($"Is Particle Effect: {mat.IsParticleEffect}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=Normal material, 8=Particle/Effect (e.g., jet engines)");
}
}
if (ImGui.CollapsingHeader("2. File Header (Read Order)", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.Text($"Stage Count: {mat.Stages.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
ImGui.Text($"Animation Count: {mat.Animations.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
ImGui.Separator();
ImGui.TextDisabled($"(Read if Magic1 >= 2)");
ImGui.Text($"Source Blend Mode: {mat.SourceBlendMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetBlendModeDescription(mat.SourceBlendMode));
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
ImGui.Text($"Dest Blend Mode: {mat.DestBlendMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetBlendModeDescription(mat.DestBlendMode));
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
ImGui.Separator();
ImGui.TextDisabled("(Read if Magic1 > 2)");
ImGui.Text($"Global Alpha Multiplier: {mat.GlobalAlphaMultiplier:F3}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Always 1.0 in all 628 materials - global alpha multiplier");
}
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, float)");
ImGui.Separator();
ImGui.TextDisabled("(Read if Magic1 > 3)");
ImGui.Text($"Global Emissive Intensity: {mat.GlobalEmissiveIntensity:F3}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("0=no glow (99.4%), rare values: 1000, 10000");
}
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, float)");
}
if (ImGui.CollapsingHeader($"3. Stages ({mat.Stages.Count}) - 34 bytes each", ImGuiTreeNodeFlags.DefaultOpen))
{
for (int i = 0; i < mat.Stages.Count; i++)
{
var stage = mat.Stages[i];
if (ImGui.TreeNode($"Stage {i}: {stage.TextureName}"))
{
ImGui.TextDisabled("=== File Read Order (34 bytes) ===");
// 1. Ambient (4 bytes)
ImGui.Text("1. Ambient Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes, A scaled by 0.01)");
var ambient = new Vector4(stage.AmbientR, stage.AmbientG, stage.AmbientB, stage.AmbientA);
ImGui.ColorEdit4("##Ambient", ref ambient, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.AmbientR:F3}, {stage.AmbientG:F3}, {stage.AmbientB:F3}, {stage.AmbientA:F3})");
// 2. Diffuse (4 bytes)
ImGui.Text("2. Diffuse Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var diffuse = new Vector4(stage.DiffuseR, stage.DiffuseG, stage.DiffuseB, stage.DiffuseA);
ImGui.ColorEdit4("##Diffuse", ref diffuse, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.DiffuseR:F3}, {stage.DiffuseG:F3}, {stage.DiffuseB:F3}, {stage.DiffuseA:F3})");
// 3. Specular (4 bytes)
ImGui.Text("3. Specular Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var specular = new Vector4(stage.SpecularR, stage.SpecularG, stage.SpecularB, stage.SpecularA);
ImGui.ColorEdit4("##Specular", ref specular, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.SpecularR:F3}, {stage.SpecularG:F3}, {stage.SpecularB:F3}, {stage.SpecularA:F3})");
// 4. Emissive (4 bytes)
ImGui.Text("4. Emissive Color:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes)");
var emissive = new Vector4(stage.EmissiveR, stage.EmissiveG, stage.EmissiveB, stage.EmissiveA);
ImGui.ColorEdit4("##Emissive", ref emissive, ImGuiColorEditFlags.NoInputs);
ImGui.SameLine();
ImGui.Text($"RGBA: ({stage.EmissiveR:F3}, {stage.EmissiveG:F3}, {stage.EmissiveB:F3}, {stage.EmissiveA:F3})");
// 5. Power (1 byte)
ImGui.Text($"5. Power: {stage.Power:F3}");
ImGui.SameLine();
ImGui.TextDisabled("(1 byte -> float)");
// 6. Texture Stage Index (1 byte)
ImGui.Text($"6. Texture Stage Index: {stage.TextureStageIndex}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("255 = not set/default, 0-47 = reference to specific texture stage");
}
ImGui.SameLine();
ImGui.TextDisabled("(1 byte)");
// 7. Texture Name (16 bytes)
ImGui.Text($"7. Texture Name: {stage.TextureName}");
ImGui.SameLine();
ImGui.TextDisabled("(16 bytes, ASCII)");
ImGui.TreePop();
}
}
}
if (ImGui.CollapsingHeader($"4. Animations ({mat.Animations.Count})"))
{
for (int i = 0; i < mat.Animations.Count; i++)
{
var anim = mat.Animations[i];
if (ImGui.TreeNode($"Anim {i}: {anim.Target} ({anim.LoopMode})"))
{
ImGui.TextDisabled("=== File Read Order ===");
// Combined field (4 bytes)
ImGui.Text("1. Target & Loop Mode:");
ImGui.SameLine();
ImGui.TextDisabled("(4 bytes combined)");
ImGui.Indent();
ImGui.Text($"Target (bits 3-31): {anim.Target}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(anim.TargetDescription);
}
ImGui.Text($"Loop Mode (bits 0-2): {anim.LoopMode}");
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(GetAnimationLoopModeDescription(anim.LoopMode));
}
ImGui.Unindent();
// Key count (2 bytes)
ImGui.Text($"2. Key Count: {anim.Keys.Count}");
ImGui.SameLine();
ImGui.TextDisabled("(2 bytes, ushort)");
// Keys (6 bytes each)
ImGui.Text($"3. Keys ({anim.Keys.Count} × 6 bytes):");
if (ImGui.BeginTable($"keys_{i}", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg))
{
ImGui.TableSetupColumn("#");
ImGui.TableSetupColumn("Stage Index (2 bytes)");
ImGui.TableSetupColumn("Duration ms (2 bytes)");
ImGui.TableSetupColumn("Interpolation (2 bytes)");
ImGui.TableHeadersRow();
int keyIdx = 0;
foreach (var key in anim.Keys)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(keyIdx.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.StageIndex.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.DurationMs.ToString());
ImGui.TableNextColumn();
ImGui.Text(key.InterpolationCurve.ToString());
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Always 0 = linear interpolation");
}
keyIdx++;
}
ImGui.EndTable();
}
ImGui.TreePop();
}
}
}
}
}
ImGui.End();
}
private static string GetBlendModeDescription(BlendMode mode)
{
return mode switch
{
BlendMode.Zero => "D3DBLEND_ZERO\nBlend factor is (0, 0, 0, 0)\nResults in black/transparent",
BlendMode.One => "D3DBLEND_ONE\nBlend factor is (1, 1, 1, 1)\nUses full color value (no blending)",
BlendMode.SrcColor => "D3DBLEND_SRCCOLOR\nBlend factor is (Rs, Gs, Bs, As)\nUses source color",
BlendMode.InvSrcColor => "D3DBLEND_INVSRCCOLOR\nBlend factor is (1-Rs, 1-Gs, 1-Bs, 1-As)\nUses inverted source color",
BlendMode.SrcAlpha => "D3DBLEND_SRCALPHA\nBlend factor is (As, As, As, As)\nUses source alpha for all channels\n Standard transparency (common with InvSrcAlpha for dest)",
BlendMode.InvSrcAlpha => "D3DBLEND_INVSRCALPHA\nBlend factor is (1-As, 1-As, 1-As, 1-As)\nUses inverted source alpha\n Standard transparency (common with SrcAlpha for source)",
BlendMode.DestAlpha => "D3DBLEND_DESTALPHA\nBlend factor is (Ad, Ad, Ad, Ad)\nUses destination alpha",
BlendMode.InvDestAlpha => "D3DBLEND_INVDESTALPHA\nBlend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad)\nUses inverted destination alpha",
BlendMode.DestColor => "D3DBLEND_DESTCOLOR\nBlend factor is (Rd, Gd, Bd, Ad)\nUses destination color",
BlendMode.InvDestColor => "D3DBLEND_INVDESTCOLOR\nBlend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad)\nUses inverted destination color",
BlendMode.SrcAlphaSat => "D3DBLEND_SRCALPHASAT\nBlend factor is (f, f, f, 1) where f = min(As, 1-Ad)\nSaturates source alpha",
BlendMode.BothSrcAlpha => "D3DBLEND_BOTHSRCALPHA (Obsolete in D3D9+)\nBlend factor is (As, As, As, As) for both source and dest\nSource: (As, As, As, As), Dest: (1-As, 1-As, 1-As, 1-As)",
BlendMode.BothInvSrcAlpha => "D3DBLEND_BOTHINVSRCALPHA (Obsolete in D3D9+)\nBlend factor is (1-As, 1-As, 1-As, 1-As) for both source and dest\nSource: (1-As, 1-As, 1-As, 1-As), Dest: (As, As, As, As)",
BlendMode.Unknown => "Unknown/Default (0xFF)\nUninitialized or opaque rendering",
_ => "Unknown blend mode"
};
}
private static string GetAnimationLoopModeDescription(AnimationLoopMode mode)
{
return mode switch
{
AnimationLoopMode.Loop => "Loop: Animation repeats continuously from start to end",
AnimationLoopMode.PingPong => "PingPong: Animation plays forward then backward repeatedly",
AnimationLoopMode.Clamp => "Clamp: Animation plays once and holds at the final value",
AnimationLoopMode.Random => "Random: Animation selects random keyframes",
_ => "Unknown loop mode"
};
}
}

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

@@ -1,22 +1,47 @@
using ImGuiNET;
using CpDatLib;
using ImGuiNET;
using MissionTmaLib.Parsing;
using NResLib;
using NResUI.Abstractions;
using NResUI.Models;
using ScrLib;
using TexmLib;
using VarsetLib;
namespace NResUI.ImGuiUI;
public class NResExplorerPanel : IImGuiPanel
{
private readonly NResExplorerViewModel _viewModel;
private readonly TexmExplorerViewModel _texmExplorerViewModel;
private readonly VarsetViewModel _varsetViewModel;
private readonly CpDatSchemeViewModel _cpDatSchemeViewModel;
private readonly MissionTmaViewModel _missionTmaViewModel;
private readonly ScrViewModel _scrViewModel;
private readonly MaterialViewModel _materialViewModel;
public NResExplorerPanel(NResExplorerViewModel viewModel)
public NResExplorerPanel(NResExplorerViewModel viewModel, TexmExplorerViewModel texmExplorerViewModel,
VarsetViewModel varsetViewModel, CpDatSchemeViewModel cpDatSchemeViewModel, MissionTmaViewModel missionTmaViewModel, ScrViewModel scrViewModel, MaterialViewModel materialViewModel)
{
_viewModel = viewModel;
_texmExplorerViewModel = texmExplorerViewModel;
_varsetViewModel = varsetViewModel;
_cpDatSchemeViewModel = cpDatSchemeViewModel;
_missionTmaViewModel = missionTmaViewModel;
_scrViewModel = scrViewModel;
_materialViewModel = materialViewModel;
}
int contextMenuRow = -1;
public void OnImGuiRender()
{
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");
@@ -31,7 +56,7 @@ public class NResExplorerPanel : IImGuiPanel
if (_viewModel.Archive is not null)
{
ImGui.Text(_viewModel.Path);
ImGui.Text("Header: ");
ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Header.NRes);
@@ -45,8 +70,8 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString());
if (ImGui.BeginTable("content", 12, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
if (ImGui.BeginTable("content", 12,
ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Тип файла");
ImGui.TableSetupColumn("Кол-во элементов");
@@ -67,6 +92,17 @@ public class NResExplorerPanel : IImGuiPanel
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Selectable("##row_select" + i, false, ImGuiSelectableFlags.SpanAllColumns);
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right))
{
Console.WriteLine("Context menu for row " + i);
contextMenuRow = i;
ImGui.OpenPopup("row_context_menu");
}
ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Files[i].FileType);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].ElementCount.ToString());
@@ -82,8 +118,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);
@@ -119,6 +155,152 @@ public class NResExplorerPanel : IImGuiPanel
);
}
if (ImGui.BeginPopup("row_context_menu"))
{
if (contextMenuRow == -1 || contextMenuRow > _viewModel.Archive.Files.Count)
{
ImGui.Text("Broken context menu :(. Reopen");
}
else
{
var file = _viewModel.Archive.Files[contextMenuRow];
ImGui.Text("Actions for file " + file.FileName);
ImGui.TextDisabled("Program has no understading of file format(");
ImGui.Separator();
if (ImGui.MenuItem("Open as Texture TEXM"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = TexmParser.ReadFromStream(ms, file.FileName);
_texmExplorerViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read TEXM from context menu");
}
if (ImGui.MenuItem("Open as Archive NRes"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = NResParser.ReadFile(ms);
_viewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read NRes from context menu");
}
if (ImGui.MenuItem("Open as Varset .var"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = VarsetParser.Parse(ms);
_varsetViewModel.Items = parseResult;
Console.WriteLine("Read Varset from context menu");
}
if (ImGui.MenuItem("Open as Scheme cp.dat"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = CpDatParser.Parse(ms);
_cpDatSchemeViewModel.SetParseResult(parseResult, file.FileName);
Console.WriteLine("Read cp.dat from context menu");
}
if (ImGui.MenuItem("Open as Mission .tma"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = MissionTmaParser.ReadFile(ms);
_missionTmaViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read .tma from context menu");
}
if (ImGui.MenuItem("Open as Scripts .scr"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read,
FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
var parseResult = ScrParser.ReadFile(ms);
_scrViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read .scr from context menu");
}
if (ImGui.MenuItem("Open as Material"))
{
using var fs = new FileStream(_viewModel.Path!, FileMode.Open, FileAccess.Read, FileShare.Read);
fs.Seek(file.OffsetInFile, SeekOrigin.Begin);
var buffer = new byte[file.FileLength];
fs.ReadExactly(buffer, 0, file.FileLength);
using var ms = new MemoryStream(buffer);
try
{
var parseResult = MaterialLib.MaterialParser.ReadFromStream(ms, file.FileName, (int)file.ElementCount, file.Magic1);
_materialViewModel.SetParseResult(parseResult, Path.Combine(_viewModel.Path!, file.FileName));
Console.WriteLine("Read Material from context menu");
}
catch (Exception ex)
{
Console.WriteLine($"Error reading material: {ex}");
_materialViewModel.SetError(ex.Message);
}
}
}
ImGui.EndPopup();
}
ImGui.EndTable();
}
}

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");
@@ -59,7 +62,7 @@ public class TexmExplorer : IImGuiPanel
ImGui.Text("Magic2: ");
ImGui.SameLine();
ImGui.Text(_viewModel.TexmFile.Header.Magic2.ToString());
ImGui.Text(_viewModel.TexmFile.Header.FormatOptionFlags.ToString());
ImGui.Text("Format: ");
ImGui.SameLine();

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,50 @@
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)
{
RebuildFlatList();
}
}
public void RebuildFlatList()
{
FlatList = [];
if (CpDatScheme is null)
{
return;
}
CollectEntries(CpDatScheme.Root, 0);
void CollectEntries(CpDatEntry entry, int level)
{
FlatList.Add((level, entry));
foreach (var child in entry.Children)
{
CollectEntries(child, level + 1);
}
}
}
}

View File

@@ -0,0 +1,26 @@
using MaterialLib;
namespace NResUI.Models;
public class MaterialViewModel
{
public MaterialFile? MaterialFile { get; private set; }
public string? FilePath { get; private set; }
public string? Error { get; private set; }
public bool HasFile => MaterialFile != null;
public void SetParseResult(MaterialFile materialFile, string filePath)
{
MaterialFile = materialFile;
FilePath = filePath;
Error = null;
}
public void SetError(string error)
{
MaterialFile = null;
FilePath = null;
Error = error;
}
}

View File

@@ -1,10 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType Condition="'$(OS)' == 'Windows_NT'">WinExe</OutputType>
<OutputType Condition="'$(OS)' != 'Windows_NT'">Exe</OutputType>
</PropertyGroup>
<ItemGroup>
@@ -12,18 +10,20 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageReference Include="NativeFileDialogSharp" Version="0.5.0" />
<PackageReference Include="Silk.NET" Version="2.22.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="NativeFileDialogSharp" />
<PackageReference Include="Silk.NET" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CpDatLib\CpDatLib.csproj" />
<ProjectReference Include="..\MissionTmaLib\MissionTmaLib.csproj" />
<ProjectReference Include="..\NResLib\NResLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
</ItemGroup>
</Project>

View File

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

54
PalLib/PalFile.cs Normal file
View File

@@ -0,0 +1,54 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace PalLib;
/// <summary>
/// PAL файл по сути это indexed текстура (1024 байт - 256 цветов lookup + 4 байта "Ipol" и затем 256x256 индексов в lookup)
/// </summary>
public class PalFile
{
public required string FileName { get; set; }
/// <summary>
/// 256 цветов lookup (1024 байт)
/// </summary>
public required byte[] Palette { get; set; }
/// <summary>
/// 256x256 индексов в lookup
/// </summary>
public required byte[] Indices { get; set; }
public void SaveAsPng(string outputPath)
{
const int width = 256;
const int height = 256;
var rgbaBytes = new byte[width * height * 4];
for (int i = 0; i < Indices.Length; i++)
{
var index = Indices[i];
// Palette is 256 colors * 4 bytes (ARGB usually, based on TexmLib)
// TexmLib: r = lookup[i*4+0], g = lookup[i*4+1], b = lookup[i*4+2], a = lookup[i*4+3]
// Assuming same format here.
// since PAL is likely directx related, the format is is likely BGRA
var b = Palette[index * 4 + 0];
var g = Palette[index * 4 + 1];
var r = Palette[index * 4 + 2];
var a = Palette[index * 4 + 3]; // Alpha? Or is it unused/padding? TexmLib sets alpha to 255 manually for indexed.
rgbaBytes[i * 4 + 0] = r;
rgbaBytes[i * 4 + 1] = g;
rgbaBytes[i * 4 + 2] = b;
rgbaBytes[i * 4 + 3] = 255;
}
using var image = Image.LoadPixelData<Rgba32>(rgbaBytes, width, height);
image.SaveAsPng(outputPath);
}
}

7
PalLib/PalLib.csproj Normal file
View File

@@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp"/>
</ItemGroup>
</Project>

37
PalLib/PalParser.cs Normal file
View File

@@ -0,0 +1,37 @@
using System.Text;
namespace PalLib;
public class PalParser
{
public static PalFile ReadFromStream(Stream stream, string filename)
{
// Expected size: 1024 (palette) + 4 ("Ipol") + 65536 (indices) = 66564
if (stream.Length != 66564)
{
throw new InvalidDataException($"Invalid PAL file size. Expected 66564 bytes, got {stream.Length}.");
}
var palette = new byte[1024];
stream.ReadExactly(palette, 0, 1024);
var signatureBytes = new byte[4];
stream.ReadExactly(signatureBytes, 0, 4);
var signature = Encoding.ASCII.GetString(signatureBytes);
if (signature != "Ipol")
{
throw new InvalidDataException($"Invalid PAL file signature. Expected 'Ipol', got '{signature}'.");
}
var indices = new byte[65536];
stream.ReadExactly(indices, 0, 65536);
return new PalFile
{
FileName = filename,
Palette = palette,
Indices = indices
};
}
}

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>

19
ParkanPlayground.slnx Normal file
View File

@@ -0,0 +1,19 @@
<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="MaterialLib\MaterialLib.csproj" Type="Classic C#" />
<Project Path="MissionTmaLib/MissionTmaLib.csproj" />
<Project Path="NLUnpacker/NLUnpacker.csproj" />
<Project Path="NResLib/NResLib.csproj" />
<Project Path="NResUI/NResUI.csproj" />
<Project Path="PalLib\PalLib.csproj" Type="Classic C#" />
<Project Path="ParkanPlayground/ParkanPlayground.csproj" />
<Project Path="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" />
</Solution>

View File

@@ -0,0 +1,255 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// Static reader methods for parsing FXID effect definition structures from binary streams.
/// </summary>
public static class FxidReader
{
/// <summary>
/// Reads a Vector3 (3 floats: X, Y, Z) from the binary stream.
/// </summary>
public static Vector3 ReadVector3(BinaryReader br)
{
float x = br.ReadSingle();
float y = br.ReadSingle();
float z = br.ReadSingle();
return new Vector3(x, y, z);
}
/// <summary>
/// Reads the 60-byte effect header from the binary stream.
/// </summary>
public static EffectHeader ReadEffectHeader(BinaryReader br)
{
EffectHeader h;
h.ComponentCount = br.ReadUInt32();
h.Unknown1 = br.ReadUInt32();
h.Duration = br.ReadSingle();
h.Unknown2 = br.ReadSingle();
h.Flags = br.ReadUInt32();
h.Unknown3 = br.ReadUInt32();
h.Reserved = br.ReadBytes(24);
h.ScaleX = br.ReadSingle();
h.ScaleY = br.ReadSingle();
h.ScaleZ = br.ReadSingle();
return h;
}
/// <summary>
/// Reads a BillboardComponentData (type 1) from the binary stream.
/// </summary>
public static BillboardComponentData ReadBillboardComponent(BinaryReader br, uint typeAndFlags)
{
BillboardComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x50);
return d;
}
/// <summary>
/// Reads a SoundComponentData (type 2) from the binary stream.
/// </summary>
public static SoundComponentData ReadSoundComponent(BinaryReader br, uint typeAndFlags)
{
SoundComponentData d;
d.TypeAndFlags = typeAndFlags;
d.PlayMode = br.ReadUInt32();
d.StartTime = br.ReadSingle();
d.EndTime = br.ReadSingle();
d.Pos0 = ReadVector3(br);
d.Pos1 = ReadVector3(br);
d.Offset0 = ReadVector3(br);
d.Offset1 = ReadVector3(br);
d.Scalar0Min = br.ReadSingle();
d.Scalar0Max = br.ReadSingle();
d.Scalar1Min = br.ReadSingle();
d.Scalar1Max = br.ReadSingle();
d.SoundFlags = br.ReadUInt32();
d.SoundNameAndReserved = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads an AnimParticleComponentData (type 3) from the binary stream.
/// </summary>
public static AnimParticleComponentData ReadAnimParticleComponent(BinaryReader br, uint typeAndFlags)
{
AnimParticleComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x38);
return d;
}
/// <summary>
/// Reads an AnimBillboardComponentData (type 4) from the binary stream.
/// </summary>
public static AnimBillboardComponentData ReadAnimBillboardComponent(BinaryReader br, uint typeAndFlags)
{
AnimBillboardComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04 = br.ReadSingle();
d.ScalarAMin = br.ReadSingle();
d.ScalarAMax = br.ReadSingle();
d.ScalarAExp = br.ReadSingle();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.SampleSpreadParam = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.PrimarySampleCount = br.ReadUInt32();
d.SecondarySampleCount = br.ReadUInt32();
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.ExtentVec2 = ReadVector3(br);
d.ExponentTriplet0 = ReadVector3(br);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.RadiusTriplet2 = ReadVector3(br);
d.ExponentTriplet1 = ReadVector3(br);
d.NoiseAmplitude = br.ReadSingle();
d.Reserved = br.ReadBytes(0x3C);
return d;
}
/// <summary>
/// Reads a TrailComponentData (type 5) from the binary stream.
/// </summary>
public static TrailComponentData ReadTrailComponent(BinaryReader br, uint typeAndFlags)
{
TrailComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unknown04To10 = br.ReadBytes(0x10);
d.SegmentCount = br.ReadUInt32();
d.Param0 = br.ReadSingle();
d.Param1 = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.Unknown24 = br.ReadUInt32();
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.TextureNameAndReserved = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads a PointComponentData (type 6) from the binary stream.
/// Note: Point components have no payload beyond the typeAndFlags header.
/// </summary>
public static PointComponentData ReadPointComponent(uint typeAndFlags)
{
PointComponentData d;
d.TypeAndFlags = typeAndFlags;
return d;
}
/// <summary>
/// Reads a PlaneComponentData (type 7) from the binary stream.
/// </summary>
public static PlaneComponentData ReadPlaneComponent(BinaryReader br, uint typeAndFlags)
{
PlaneComponentData d;
d.Base = ReadAnimParticleComponent(br, typeAndFlags);
d.ExtraPlaneParam0 = br.ReadUInt32();
d.ExtraPlaneParam1 = br.ReadUInt32();
return d;
}
/// <summary>
/// Reads a ModelComponentData (type 8) from the binary stream.
/// </summary>
public static ModelComponentData ReadModelComponent(BinaryReader br, uint typeAndFlags)
{
ModelComponentData d;
d.TypeAndFlags = typeAndFlags;
d.Unk04 = br.ReadBytes(0x14);
d.ActiveTimeStart = br.ReadSingle();
d.ActiveTimeEnd = br.ReadSingle();
d.Unknown20 = br.ReadUInt32();
d.InstanceCount = br.ReadUInt32();
d.BasePos = ReadVector3(br);
d.OffsetPos = ReadVector3(br);
d.ScatterExtent = ReadVector3(br);
d.Axis0 = ReadVector3(br);
d.Axis1 = ReadVector3(br);
d.Axis2 = ReadVector3(br);
d.Reserved70 = br.ReadBytes(0x18);
d.RadiusTriplet0 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.ReservedA0 = br.ReadBytes(0x18);
d.TextureNameAndFlags = br.ReadBytes(0x40);
return d;
}
/// <summary>
/// Reads an AnimModelComponentData (type 9) from the binary stream.
/// </summary>
public static AnimModelComponentData ReadAnimModelComponent(BinaryReader br, uint typeAndFlags)
{
AnimModelComponentData d;
d.TypeAndFlags = typeAndFlags;
d.AnimSpeed = br.ReadSingle();
d.MinTime = br.ReadSingle();
d.MaxTime = br.ReadSingle();
d.Exponent = br.ReadSingle();
d.Reserved14 = br.ReadBytes(0x14);
d.DirVec0 = ReadVector3(br);
d.Reserved34 = br.ReadBytes(0x0C);
d.RadiusTriplet0 = ReadVector3(br);
d.DirVec1 = ReadVector3(br);
d.RadiusTriplet1 = ReadVector3(br);
d.ExtentVec0 = ReadVector3(br);
d.ExtentVec1 = ReadVector3(br);
d.Reserved7C = br.ReadBytes(0x0C);
d.TextureNameAndFlags = br.ReadBytes(0x48);
return d;
}
/// <summary>
/// Reads a CubeComponentData (type 10) from the binary stream.
/// </summary>
public static CubeComponentData ReadCubeComponent(BinaryReader br, uint typeAndFlags)
{
CubeComponentData d;
d.Base = ReadAnimBillboardComponent(br, typeAndFlags);
d.ExtraCubeParam0 = br.ReadUInt32();
return d;
}
}

View File

@@ -0,0 +1,237 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// Effect-level header at the start of each FXID file (60 bytes total).
/// Parsed from CEffect_InitFromDef: defines component count, global duration/flags,
/// some unknown control fields, and the uniform scale vector applied to the effect.
/// </summary>
public struct EffectHeader
{
public uint ComponentCount;
public uint Unknown1;
public float Duration;
public float Unknown2;
public uint Flags;
public uint Unknown3;
public byte[] Reserved; // 24 bytes
public float ScaleX;
public float ScaleY;
public float ScaleZ;
}
/// <summary>
/// Shared on-disk definition layout for billboard-style components (type 1).
/// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha
/// curves and sample scattering within a 3D extent volume.
/// </summary>
public struct BillboardComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering samples
public uint Unknown20; // used as integer param in billboard code, exact meaning unknown
public uint PrimarySampleCount; // number of samples along primary axis
public uint SecondarySampleCount; // number of samples along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-sample noise amplitude
public byte[] Reserved; // 0x50-byte tail, currently not touched by billboard code
}
/// <summary>
/// 3D sound component definition (type 2).
/// Used by CSoundComponent_Initialize/Update to drive positional audio, playback
/// window, and scalar ranges (e.g. volume / pitch), plus a 0x40-byte sound name tail.
/// </summary>
public struct SoundComponentData
{
public uint TypeAndFlags; // component type and flags
public uint PlayMode; // playback mode (looping, one-shot, etc.)
public float StartTime; // playback window start (seconds)
public float EndTime; // playback window end (seconds)
public Vector3 Pos0; // base 3D position or path start
public Vector3 Pos1; // secondary position / path end
public Vector3 Offset0; // random offset range 0
public Vector3 Offset1; // random offset range 1
public float Scalar0Min; // scalar range 0 min (e.g. volume)
public float Scalar0Max; // scalar range 0 max
public float Scalar1Min; // scalar range 1 min (e.g. pitch)
public float Scalar1Max; // scalar range 1 max
public uint SoundFlags; // misc sound control flags
public byte[] SoundNameAndReserved; // 0x40-byte tail; sound name plus padding/unused
}
/// <summary>
/// Animated particle component definition (type 3).
/// Prefix layout matches BillboardComponentData and is used to allocate a grid of
/// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture.
/// </summary>
public struct AnimParticleComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering particles
public uint Unknown20; // used as integer param in anim particle code, exact meaning unknown
public uint PrimarySampleCount; // number of particles along primary axis
public uint SecondarySampleCount; // number of particles along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-particle noise amplitude
public byte[] Reserved; // 0x38-byte tail; forwarded to CFxManager_LoadTexture unchanged
}
/// <summary>
/// Animated billboard component definition (type 4).
/// Shares the same prefix layout as BillboardComponentData, including extents and
/// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture.
/// </summary>
public struct AnimBillboardComponentData
{
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear
public float ScalarAMin; // base scalar A (e.g. base radius)
public float ScalarAMax; // max scalar A
public float ScalarAExp; // exponent applied to scalar A curve
public float ActiveTimeStart; // activation window start (seconds)
public float ActiveTimeEnd; // activation window end (seconds)
public float SampleSpreadParam; // extra param used when scattering animated billboards
public uint Unknown20; // used as integer param in anim billboard code, exact meaning unknown
public uint PrimarySampleCount; // number of samples along primary axis
public uint SecondarySampleCount; // number of samples along secondary axis
public Vector3 ExtentVec0; // base extent/origin vector
public Vector3 ExtentVec1; // extent center / offset
public Vector3 ExtentVec2; // extent size used for random boxing
public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
public Vector3 RadiusTriplet0; // radius curve key 0
public Vector3 RadiusTriplet1; // radius curve key 1
public Vector3 RadiusTriplet2; // radius curve key 2
public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
public float NoiseAmplitude; // per-sample noise amplitude
public byte[] Reserved; // 0x3C-byte tail; forwarded to CFxManager_LoadTexture unchanged
}
/// <summary>
/// Compact definition for trail / ribbon components (type 5).
/// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
/// ranges, timing, and a shared texture name at +0x30.
/// </summary>
public struct TrailComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown
public uint SegmentCount; // number of trail segments (particles)
public float Param0; // first width/alpha/UV control value (start)
public float Param1; // second width/alpha/UV control value (end)
public uint Unknown20; // extra integer parameter, purpose unknown
public uint Unknown24; // extra integer parameter, purpose unknown
public float ActiveTimeStart; // trail activation start time (>= 0)
public float ActiveTimeEnd; // trail activation end time
public byte[] TextureNameAndReserved; // 0x40-byte tail containing texture name and padding/flags
}
/// <summary>
/// Simple point component definition (type 6).
/// Definition block is just the 4-byte typeAndFlags header; no extra data on disk.
/// </summary>
public struct PointComponentData
{
public uint TypeAndFlags; // component type and flags; definition block has no payload
}
/// <summary>
/// Plane component definition (type 7).
/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
/// followed by two dwords of plane-specific data.
/// </summary>
public struct PlaneComponentData
{
public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves
public uint ExtraPlaneParam0; // plane-specific parameter, semantics not yet reversed
public uint ExtraPlaneParam1; // plane-specific parameter, semantics not yet reversed
}
/// <summary>
/// Static model component definition (type 8).
/// Layout fully matches the IDA typedef used by CModelComponent_Initialize:
/// time window, instance count, spatial extents/axes, radius triplets, and a
/// 0x40-byte texture name tail.
/// </summary>
public struct ModelComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear
public float ActiveTimeStart; // activation window start (seconds), +0x18
public float ActiveTimeEnd; // activation window end (seconds), +0x1C
public uint Unknown20; // extra flags/int parameter at +0x20
public uint InstanceCount; // number of model instances to spawn at +0x24
public Vector3 BasePos; // base position of the emitter / origin, +0x28
public Vector3 OffsetPos; // positional offset applied per-instance, +0x34
public Vector3 ScatterExtent; // extent volume used for random scattering, +0x40
public Vector3 Axis0; // local axis 0 (orientation / shape), +0x4C
public Vector3 Axis1; // local axis 1 (orientation / shape), +0x58
public Vector3 Axis2; // local axis 2 (orientation / shape), +0x64
public byte[] Reserved70; // 0x18 bytes at +0x70..+0x87, not directly used
public Vector3 RadiusTriplet0; // radius / extent triplet 0 at +0x88
public Vector3 RadiusTriplet1; // radius / extent triplet 1 at +0x94
public byte[] ReservedA0; // 0x18 bytes at +0xA0..+0xB7, not directly used
public byte[] TextureNameAndFlags; // 0x40-byte tail at +0xB8: texture name + padding/flags
}
/// <summary>
/// Animated model component definition (type 9).
/// Layout derived from CAnimModelComponent_Initialize: time params, direction vectors,
/// radius triplets, extent vectors, and a 0x48-byte texture name tail.
/// </summary>
public struct AnimModelComponentData
{
public uint TypeAndFlags; // component type and flags
public float AnimSpeed; // animation speed multiplier at +0x04
public float MinTime; // activation window start (clamped >= 0) at +0x08
public float MaxTime; // activation window end at +0x0C
public float Exponent; // exponent for time interpolation at +0x10
public byte[] Reserved14; // 0x14 bytes at +0x14..+0x27, padding
public Vector3 DirVec0; // normalized direction vector 0 at +0x28
public byte[] Reserved34; // 0x0C bytes at +0x34..+0x3F, padding
public Vector3 RadiusTriplet0; // radius triplet 0 at +0x40
public Vector3 DirVec1; // normalized direction vector 1 at +0x4C
public Vector3 RadiusTriplet1; // radius triplet 1 at +0x58
public Vector3 ExtentVec0; // extent vector 0 at +0x64
public Vector3 ExtentVec1; // extent vector 1 at +0x70
public byte[] Reserved7C; // 0x0C bytes at +0x7C..+0x87, padding
public byte[] TextureNameAndFlags; // 0x48-byte tail at +0x88: texture/model name + padding
}
/// <summary>
/// Cube component definition (type 10).
/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
/// followed by one dword of cube-specific data.
/// </summary>
public struct CubeComponentData
{
public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves
public uint ExtraCubeParam0; // cube-specific parameter, semantics not yet reversed
}

View File

@@ -0,0 +1,350 @@
# Документация формата MSH
Формат `.msh` используется игрой Parkan: Железная стратегия (1998) для хранения 3D-мешей.
MSH файлы — это NRes архивы, содержащие несколько типизированных компонентов.
## Обзор
Существует **два варианта** формата MSH:
| Вариант | Применение | Ключевые компоненты | Хранение треугольников |
|---------|------------|---------------------|------------------------|
| **Модель** | Роботы, здания, объекты | 06, 0D, 07 | Индексированные треугольники |
| **Ландшафт** | Террейн | 0B, 15 | Прямые треугольники |
### Автоопределение типа
```
Модель: Есть компонент 06 (индексы) И 0D (батчи)
Ландшафт: Есть компонент 0B (материалы) И НЕТ компонента 06
```
---
## Сводка компонентов
| Тип | Название | Размер элемента | Описание |
|:---:|----------|:---------------:|----------|
| 01 | Pieces | 38 (0x26) | Части меша / тайлы с LOD-ссылками |
| 02 | Submeshes | 68 (0x44) | LOD части с баундинг-боксами |
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) |
| 04 | неизвестно | 4 | (неизвестно) |
| 05 | неизвестно | 4 | (неизвестно) |
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) |
| 07 | неизвестно | 16 | (только Модель) |
| 08 | Animations | 4 | Кейфреймы анимации меша |
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) |
| 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
| 0D | неизвестно | 20 (0x14) | неизвестно (только Модель) |
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
| 12 | MicrotextureMap | 4 | неизвестно |
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
| 15 | неизвестно | 28 (0x1C) | неизвестно |
---
## Поток данных
### Модель (роботы, здания)
```
Компонент 01 (Pieces - части)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник)
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи)
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы)
│ │
│ └─► Компонент 03 (вершины)
└─► IndexInto03 (базовое смещение вершины)
```
### Ландшафт (террейн)
```
Компонент 01 (Тайлы, обычно 16×16 = 256)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники)
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15)
```
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
---
## Структуры компонентов
### Компонент 01 - Pieces (0x26 = 38 байт)
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов).
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 1 | byte | Type1 | Флаги типа части |
| 0x01 | 1 | byte | Type2 | Дополнительные флаги |
| 0x02 | 2 | int16 | ParentIndex | Индекс родителя (-1 = корень) |
| 0x04 | 2 | int16 | OffsetIntoFile13 | Смещение в короткие анимации |
| 0x06 | 2 | int16 | IndexInFile08 | Индекс в анимации |
| 0x08 | 30 | ushort[15] | Lod | Индексы сабмешей по LOD-уровням (0xFFFF = не используется) |
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
---
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт)
#### Заголовок (140 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 96 | Vector3[8] | BoundingBox | 8-точечный баундинг-бокс |
| 0x60 | 12 | Vector3 | Center | Центральная точка |
| 0x6C | 4 | float | CenterW | W-компонента |
| 0x70 | 12 | Vector3 | Bottom | Нижняя точка |
| 0x7C | 12 | Vector3 | Top | Верхняя точка |
| 0x88 | 4 | float | XYRadius | Радиус в плоскости XY |
#### Элемент (68 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 |
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников |
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) |
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
| 0x20 | 12 | Vector3 | Center | Центр сабмеша |
| 0x2C | 12 | Vector3 | Vector4 | Неизвестно |
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно |
---
### Компонент 03 - Vertices (0x0C = 12 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | float | X | Координата X |
| 0x04 | 4 | float | Y | Координата Y |
| 0x08 | 4 | float | Z | Координата Z |
---
### Компонент 06 - Indices (2 байта) - Только Модель
Массив `ushort` значений — индексы вершин треугольников.
Используются группами по 3 для каждого треугольника. Ссылки через батчи Компонента 0D.
---
### Компонент 07 - Triangle Data (0x10 = 16 байт) - Только Модель
Данные рендеринга на каждый треугольник.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | Flags | Флаги рендера |
| 0x02 | 2 | ushort | Magic02 | Неизвестно |
| 0x04 | 2 | ushort | Magic04 | Неизвестно |
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
| 0x08 | 2 | int16 | OffsetX | Нормализованный X (÷32767 для -1..1) |
| 0x0A | 2 | int16 | OffsetY | Нормализованный Y (÷32767 для -1..1) |
| 0x0C | 2 | int16 | OffsetZ | Нормализованный Z (÷32767 для -1..1) |
| 0x0E | 2 | ushort | Magic14 | Неизвестно |
---
### Компонент 0B - Material Data (4 байта) - Только Ландшафт
Информация о материале/текстуре на каждый треугольник. Параллельный массив к Компоненту 15.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | HighWord | Индекс материала/текстуры |
| 0x02 | 2 | ushort | LowWord | Индекс треугольника (последовательный) |
---
### Компонент 0D - Draw Batches (0x14 = 20 байт) - Только Модель
Определяет батчи вызовов отрисовки.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | Flags | Флаги батча |
| 0x02 | 2 | - | Padding | - |
| 0x04 | 1 | byte | Magic04 | Неизвестно |
| 0x05 | 1 | byte | Magic05 | Неизвестно |
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
| 0x08 | 2 | ushort | CountOf06 | Количество индексов для отрисовки |
| 0x0A | 4 | int32 | IndexInto06 | Начальный индекс в Компоненте 06 |
| 0x0E | 2 | ushort | CountOf03 | Количество вершин |
| 0x10 | 4 | int32 | IndexInto03 | Базовое смещение вершины в Компоненте 03 |
---
### Компонент 15 - Triangles (0x1C = 28 байт)
Прямые определения треугольников. Используется и Моделью и Ландшафтом,
но только Ландшафт использует их напрямую для рендеринга.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) |
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
| 0x0E | 4 | uint32 | Magic0E | Неизвестно |
| 0x12 | 4 | uint32 | Magic12 | Неизвестно |
| 0x16 | 4 | uint32 | Magic16 | Неизвестно |
| 0x1A | 2 | ushort | Magic1A | Неизвестно |
#### MaterialData (0x04) - Структура материала
```
MaterialData = 0xFFFF_SSPP
│ │└─ PP: Основной материал (byte 0)
│ └─── SS: Вторичный материал для блендинга (byte 1)
└────── Всегда 0xFFFF (байты 2-3)
```
| Значение SS | Описание |
|:-----------:|----------|
| 0xFF | Сплошной материал (без блендинга) |
| 0x01-0xFE | Индекс вторичного материала для блендинга |
Примеры:
- `0xFFFFFF01` = Сплошной материал 1
- `0xFFFF0203` = Материал 3 с блендингом в материал 2
---
### Компонент 0A - External References (переменный размер)
Таблица строк для внешних ссылок на части меша. Формат:
```
[4 байта: длина] [байты строки] [null-терминатор]
...повтор...
```
Длина 0 означает пустую запись. Строки типа `"central"` имеют особое значение (flag |= 1).
---
## Пример: Ландшафт SC_1
```
Land.msh (SC_1):
├── 01: 256 тайлов (сетка 16×16)
├── 02: 512 сабмешей (256 LOD0 + 256 LOD1)
├── 03: 10 530 вершин
├── 04: 10 530 данных на вершину
├── 05: 10 530 данных на вершину
├── 0B: 7 882 записи материалов
├── 0E: 10 530 данных на вершину
├── 12: 10 530 микротекстурный маппинг
└── 15: 7 882 треугольника
├── LOD 0: 4 993 треугольника (тайлы 0-255 → сабмеши 0-255)
└── LOD 1: 2 889 треугольников (тайлы 0-255 → сабмеши 256-511)
```
---
## Использование
```csharp
var converter = new MshConverter();
// Автоопределение типа и конвертация в OBJ
converter.Convert("Land.msh", "terrain.obj", lodLevel: 0);
converter.Convert("robot.msh", "robot.obj", lodLevel: 0);
// Ручное определение типа
var archive = NResParser.ReadFile("mesh.msh").Archive;
var type = MshConverter.DetectMeshType(archive);
// Возвращает: MshType.Model или MshType.Landscape
```
---
## Формат WEA - Файлы материалов ландшафта
Файлы `.wea` — текстовые файлы, определяющие таблицу материалов для ландшафта.
### Формат
```
{count}
{index} {material_name}
{index} {material_name}
...
```
### Связь с Land.msh
Каждая карта имеет два файла материалов:
| Файл | Используется для | Треугольники в Comp15 |
|------|------------------|----------------------|
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) |
| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
### Пример (SC_1)
**Land1.wea:**
```
4
0 B_S0
1 L04
2 L02
3 L00
```
**Land2.wea:**
```
4
0 DEFAULT
1 L05
2 L03
3 L01
```
### Маппинг материалов
Индекс материала в `Comp15.MaterialData & 0xFF` → строка в `.wea` файле.
```
Треугольник с MaterialData = 0xFFFF0102
└─ Основной материал = 02 → Land1.wea[2] = "L02"
└─ Блендинг с материалом = 01 → Land1.wea[1] = "L04"
```
### Типичные имена материалов
| Префикс | Назначение |
|---------|------------|
| L00-L05 | Текстуры ландшафта (grass, dirt, etc.) |
| B_S0 | Базовая текстура |
| DEFAULT | Фолбэк для LOD1 |
| WATER | Вода (поверхность) |
| WATER_BOT | Вода (дно) |
| WATER_M | Вода LOD1 |
---
## Источники
- Реверс-инжиниринг `Terrain.dll` (класс CLandscape)
- Декомпиляция Ghidra: `CLandscape::ctor` и `IMesh2_of_CLandscape::Render`

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

193
ParkanPlayground/Msh02.cs Normal file
View File

@@ -0,0 +1,193 @@
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.BottomFrontLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
);
bb.BottomFrontRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
);
bb.BottomBackRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
);
bb.BottomBackLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
);
bb.TopFrontLeft = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
);
bb.TopFrontRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
);
bb.TopBackRight = new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
);
bb.TopBackLeft = 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; }
}
/// <summary>
/// 140 байт в начале файла
/// </summary>
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; }
}
/// <summary>
/// 96 bytes - bounding box (8 points each 3 float = 96 bytes)
/// 0x60 bytes or 0x18 by 4 bytes
/// </summary>
public class BoundingBox
{
public Vector3 BottomFrontLeft { get; set; }
public Vector3 BottomFrontRight { get; set; }
public Vector3 BottomBackRight { get; set; }
public Vector3 BottomBackLeft { get; set; }
public Vector3 TopBackRight { get; set; }
public Vector3 TopFrontRight { get; set; }
public Vector3 TopBackLeft { get; set; }
public Vector3 TopFrontLeft { 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; }
}
}

55
ParkanPlayground/Msh15.cs Normal file
View File

@@ -0,0 +1,55 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
public static class Msh15
{
public static List<Msh15Element> ReadComponent(
FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "15 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (15)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(28);
var elements = elementBytes.Select(x => new Msh15Element()
{
Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)),
Vertex1Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
Vertex2Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)),
Vertex3Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)),
Magic0E = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)),
Magic12 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)),
Magic16 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)),
Magic1A = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)),
}).ToList();
return elements;
}
public class Msh15Element
{
public uint Flags { get; set; }
public uint Magic04 { get; set; }
public ushort Vertex1Index { get; set; }
public ushort Vertex2Index { get; set; }
public ushort Vertex3Index { get; set; }
public uint Magic0E { get; set; }
public uint Magic12 { get; set; }
public uint Magic16 { get; set; }
public ushort Magic1A { get; set; }
}
}

View File

@@ -0,0 +1,314 @@
using System.Text;
using Common;
using NResLib;
namespace ParkanPlayground;
public enum MshType
{
Unknown,
Model, // Has component 06 (indices), 0D (batches), 07
Landscape // Has component 0B (per-triangle material), uses 15 directly
}
public class MshConverter
{
/// <summary>
/// Detects mesh type based on which components are present in the archive.
/// </summary>
public static MshType DetectMeshType(NResArchive archive)
{
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
bool hasComponent0B = archive.Files.Any(f => f.FileType == "0B 00 00 00");
bool hasComponent0D = archive.Files.Any(f => f.FileType == "0D 00 00 00");
// Model: Uses indexed triangles via component 06 and batches via 0D
if (hasComponent06 && hasComponent0D)
return MshType.Model;
// Landscape: Uses direct triangles in component 15, with material data in 0B
if (hasComponent0B && !hasComponent06)
return MshType.Landscape;
return MshType.Unknown;
}
/// <summary>
/// Converts a .msh file to OBJ format, auto-detecting mesh type.
/// </summary>
/// <param name="mshPath">Path to the .msh file</param>
/// <param name="outputPath">Output OBJ path (optional, defaults to input name + .obj)</param>
/// <param name="lodLevel">LOD level to export (0 = highest detail)</param>
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0)
{
var mshNresResult = NResParser.ReadFile(mshPath);
if (mshNresResult.Archive is null)
{
Console.WriteLine($"ERROR: Failed to read NRes archive: {mshNresResult.Error}");
return;
}
var archive = mshNresResult.Archive;
var meshType = DetectMeshType(archive);
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
Console.WriteLine($"Detected type: {meshType}");
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
switch (meshType)
{
case MshType.Model:
ConvertModel(fs, archive, outputPath, lodLevel);
break;
case MshType.Landscape:
ConvertLandscape(fs, archive, outputPath, lodLevel);
break;
default:
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
break;
}
}
/// <summary>
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
/// </summary>
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component06 = Msh06.ReadComponent(fs, archive);
var component07 = Msh07.ReadComponent(fs, archive);
var component0D = Msh0D.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Pieces: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
{
var piece = component01.Elements[pieceIndex];
// Get submesh index for requested LOD
if (lodLevel >= piece.Lod.Length)
continue;
var submeshIdx = piece.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g piece_{pieceIndex}");
var submesh = component02.Elements[submeshIdx];
var batchStart = submesh.StartOffsetIn0d;
var batchCount = submesh.ByteLengthIn0D;
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
{
var batch = component0D[batchStart + batchIdx];
var baseVertex = batch.IndexInto03;
var indexStart = batch.IndexInto06;
var indexCount = batch.CountOf06;
for (int i = 0; i < indexCount; i += 3)
{
var i1 = baseVertex + component06[indexStart + i];
var i2 = baseVertex + component06[indexStart + i + 1];
var i3 = baseVertex + component06[indexStart + i + 2];
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
exportedFaces++;
}
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
/// <summary>
/// Converts a landscape mesh (terrain) to OBJ.
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
/// </summary>
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component15 = Msh15.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Triangles: {component15.Count}");
Console.WriteLine($"Tiles: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Landscape mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var tileIdx = 0; tileIdx < component01.Elements.Count; tileIdx++)
{
var tile = component01.Elements[tileIdx];
// Get submesh index for requested LOD
if (lodLevel >= tile.Lod.Length)
continue;
var submeshIdx = tile.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g tile_{tileIdx}");
var submesh = component02.Elements[submeshIdx];
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
var triangleStart = submesh.StartIndexIn07;
var triangleCount = submesh.CountIn07;
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
{
var triIdx = triangleStart + triOffset;
if (triIdx >= component15.Count)
{
Console.WriteLine($"WARNING: Triangle index {triIdx} out of range for tile {tileIdx}");
continue;
}
var tri = component15[triIdx];
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
exportedFaces++;
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
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,16 @@
<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="..\PalLib\PalLib.csproj" />
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SharpDisasm" Version="1.1.11" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,116 +1,332 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Text.Json;
using ScrLib;
using SharpDisasm;
using VarsetLib;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using ParkanPlayground.Effects;
using static ParkanPlayground.Effects.FxidReader;
Console.OutputEncoding = Encoding.UTF8;
// 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 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)
if (args.Length == 0)
{
var x = fs.ReadFloatLittleEndian();
var y = fs.ReadFloatLittleEndian();
var z = fs.ReadFloatLittleEndian();
Console.WriteLine("Usage: ParkanPlayground <effects-directory-or-fxid-file>");
return;
}
var vertex = new Vector3D(x, y, z);
sw.WriteLine($"v {x} {y} {z}");
var path = args[0];
bool anyError = false;
var sizeByType = new Dictionary<byte, int>
{
[1] = 0xE0, // 1: Billboard
[2] = 0x94, // 2: Sound
[3] = 0xC8, // 3: AnimParticle
[4] = 0xCC, // 4: AnimBillboard
[5] = 0x70, // 5: Trail
[6] = 0x04, // 6: Point
[7] = 0xD0, // 7: Plane
[8] = 0xF8, // 8: Model
[9] = 0xD0, // 9: AnimModel
[10] = 0xD0, // 10: Cube
};
var seenIndex = points.FindIndex(vec => vec == vertex);
if (seenIndex != -1)
{
vertex.Duplicates = seenIndex;
}
// Check for --dump-headers flag
bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
if (Directory.Exists(path))
{
var files = Directory.EnumerateFiles(path, "*.bin").ToList();
points.Add(vertex);
count++;
if (dumpHeaders)
{
// Collect all headers for analysis
var headers = new List<(string name, EffectHeader h)>();
foreach (var file in files)
{
try
{
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length >= 60)
{
headers.Add((Path.GetFileName(file), ReadEffectHeader(br)));
}
}
catch { }
}
// Analyze unique values
Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n");
var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}");
var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}");
var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}");
var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}");
Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}");
Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}");
// Check reserved bytes
var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList();
Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}");
// Check scales
var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList();
Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}");
Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ===");
Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale");
Console.WriteLine(new string('-', 100));
foreach (var (name, h) in headers.Take(30))
{
Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})");
}
}
else
{
foreach (var file in files)
{
if (!ValidateFxidFile(file))
{
anyError = true;
}
}
Console.WriteLine(anyError
? "Validation finished with errors."
: "All FXID files parsed successfully.");
}
}
else if (File.Exists(path))
{
anyError = !ValidateFxidFile(path);
Console.WriteLine(anyError ? "Validation failed." : "Validation OK.");
}
else
{
Console.WriteLine($"Path not found: {path}");
}
File.WriteAllText("human-readable.json", JsonSerializer.Serialize(points, new JsonSerializerOptions()
void DumpEffectHeader(string path)
{
WriteIndented = true
}));
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
Console.WriteLine($"Total vertices: {count}");
if (fs.Length < 60)
{
Console.WriteLine($"{Path.GetFileName(path)}: file too small");
return;
}
// for (int i = 0; i < count / 4; i++)
public record Vector3D(float X, float Y, float Z)
{
public int Duplicates { get; set; }
var h = ReadEffectHeader(br);
// Format reserved bytes as hex (show first 8 bytes for brevity)
var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " ");
if (h.Reserved.Length > 8) reservedHex += "...";
// Check if reserved has any non-zero bytes
bool reservedAllZero = h.Reserved.All(b => b == 0);
Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})");
}
catch (Exception ex)
{
Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}");
}
}
// 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
bool ValidateFxidFile(string path)
{
public string Action { get; set; } = "";
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
public List<string> Arguments { get; set; } = [];
const int headerSize = 60; // sizeof(EffectHeader) on disk
if (fs.Length < headerSize)
{
Console.WriteLine($"{path}: file too small ({fs.Length} bytes).");
return false;
}
var header = ReadEffectHeader(br);
var typeCounts = new Dictionary<byte, int>();
for (int i = 0; i < header.ComponentCount; i++)
{
long blockStart = fs.Position;
if (fs.Position + 4 > fs.Length)
{
Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X}).");
return false;
}
uint typeAndFlags = br.ReadUInt32();
byte type = (byte)(typeAndFlags & 0xFF);
if (!typeCounts.TryGetValue(type, out var count))
{
count = 0;
}
typeCounts[type] = count + 1;
if (!sizeByType.TryGetValue(type, out int blockSize))
{
Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8}).");
return false;
}
int remaining = blockSize - 4;
if (fs.Position + remaining > fs.Length)
{
Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X}).");
return false;
}
if (type == 1)
{
var def = ReadBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x50)
{
Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50.");
return false;
}
}
else if (type == 2)
{
var def = ReadSoundComponent(br, typeAndFlags);
if (def.SoundNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 3)
{
var def = ReadAnimParticleComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 4)
{
var def = ReadAnimBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C.");
return false;
}
}
else if (type == 5)
{
var def = ReadTrailComponent(br, typeAndFlags);
if (def.Unknown04To10.Length != 0x10)
{
Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10.");
return false;
}
if (def.TextureNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 6)
{
// Point components have no extra bytes beyond the 4-byte typeAndFlags header.
var def = ReadPointComponent(typeAndFlags);
}
else if (type == 7)
{
var def = ReadPlaneComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 8)
{
var def = ReadModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40.");
return false;
}
}
else if (type == 9)
{
var def = ReadAnimModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x48)
{
Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48.");
return false;
}
}
else if (type == 10)
{
var def = ReadCubeComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C.");
return false;
}
}
else
{
// Skip the remaining bytes for other component types.
fs.Position += remaining;
}
}
// Dump a compact per-file summary of component types and counts.
var sb = new StringBuilder();
bool first = true;
foreach (var kv in typeCounts)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(kv.Key);
sb.Append('x');
sb.Append(kv.Value);
first = false;
}
Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]");
if (fs.Position != fs.Length)
{
Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes).");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{path}: exception while parsing: {ex.Message}");
return false;
}
}

View File

@@ -0,0 +1,10 @@
typedef struct BoundingBox {
Vector3 BottomFrontLeft;
Vector3 BottomFrontRight;
Vector3 BottomBackRight;
Vector3 BottomBackLeft;
Vector3 TopBackRight;
Vector3 TopFrontRight;
Vector3 TopBackLeft;
Vector3 TopFrontLeft;
} BoundingBox;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
typedef struct CLandscape {
IComponent * IComponent;
CGameObject game_object;
ITerrain * ITerrain;
IMesh2 * IMesh2;
I0x1 * I0x1;
IControl * IControl;
I0x22 * IUnknown_0x22;
undefined4 01_data_ptr;
MSH_02_file * msh_02_data_ptr;
undefined4 0b_data_ptr;
// collapse unknown fields
int[3][5] field_532;
// collapse unknown fields
undefined4 IComponent_owner;
KindaArray array_0x278;
int _count_unk_obj_array_elemsize_212;
undefined4 vertices_count;
uint 15_element_count;
// collapse unknown fields
Vector3 * 03_vertices_data_ptr;
int 03_vertices_stride;
undefined4 04_data_ptr;
undefined4 04_stride;
// collapse unknown fields
undefined4 05_data_ptr;
undefined4 05_stride;
undefined4 12_microtexture_mapping_data_ptr;
undefined4 12_microtexture_mapping_stride;
// collapse unknown fields
MshMetaForLandscape * mshmeta_ptr;
// collapse unknown fields
KindaArray array_0x7a40;
BoundingBox bounding_box_from_msh_02;
IMatManager * * IMatManager;
undefined4 wear_ptr;
IEffectManager * * ResolvedIEffectManager;
ISystemArealMap * * ResolvedISystemArealMap;
ICollManager * * ResolvedICollManager;
ISoundPool * * ISoundPool;
I3DSound * * I3DSound;
ILightManager * * ILightManager_owned;
KindaArray array_0x7c00;
KindaArray array_0x7c0c;
CShade * Shade;
undefined4 I3DRender??;
undefined4 field_31776;
undefined4 flags_of_mesh_part;
INResFile * * INResFile;
KindaArray array_0x7c2c;
undefined4 current_visible_primitives_count;
IMesh2 * * meshes;
IGameObject * * game_objects;
// collapse unknown fields
KindaArray array_0x7c84;
undefined4 * field_31888;
undefined4 _CheckMaxBasementAngleStep;
CollisionContext m_CollisionContext;
BoundingBox bounding_box;
} CLandscape;

File diff suppressed because it is too large Load Diff

512
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,457 @@ 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:
| Logic 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 | Atmosphere | `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}
```
Может как-то анимироваться. Как - пока не понятно.
## Lightmap (TEXM)
У текстур использующихся как карты освещения специальные имена.
`some_name.00` или `some_name.0`
Первая цифра после `.` это группа палет (всего 0x11), вторая цифра индекс в этой группе.
```
All lightmaps named whatever.0 → they all end up with no DirectDraw palette attached.
```
## Люблю разработчиков
У них получилось сделать LightSource и MaterialDescriptor чётко одинаковыми по размеру (0x5c) из-за чего я потерял 3 дня прыгая по указателям
## `FXID` - файл эффектов
По сути представляет собой последовательный список саб-эффектов идущих друг за другом.
Всего существует 9 (1..9) видов эффектов: !описать по мере реверса
Выглядит так, словно весь файл это тоже эффект сам по себе.
```
0x00-0x04 Type (игра обрезает 1 байт, и поддерживает только значения 1-9)
0x04-0x08 unknown
0x08-0x0C unknown
0x0C-0x10 unknown
0x10-0x14 EffectTemplateFlags
...
0x30-0x34 ScaleX
0x34-0x38 ScaleY
0x38-0x3C ScaleZ
enum EffectTemplateFlags : uint32_t
{
EffectTemplateFlag_RandomizeStrength = 0x0001, // used in UpdateDust
EffectTemplateFlag_RandomOffset = 0x0008, // random worldTransform offset
EffectTemplateFlag_TriangularShape = 0x0020, // post-process strength as 0→1→0
EffectTemplateFlag_OnlyWhenEnvBit0Off = 0x0080, // gating in ComputeEffectStrength
EffectTemplateFlag_OnlyWhenEnvBit0On = 0x0100, // gating in ComputeEffectStrength
EffectTemplateFlag_MultiplyByLife = 0x0200, // multiply strength by life progress
EffectTemplateFlag_EnvFlag2_IfNotSet = 0x0800, // if NOT set → envFlags |= 0x02
EffectTemplateFlag_EnvFlag10_IfSet = 0x1000, // if set → envFlags |= 0x10
EffectTemplateFlag_AlwaysEmitDust = 0x0010, // ignore piece state / flags
EffectTemplateFlag_IgnorePieceState = 0x8000, // treat piece default state as OK
EffectTemplateFlag_AutoDeleteAtFull = 0x0002, // delete when strength >= 1
EffectTemplateFlag_DetachAfterEmit = 0x0004, // detach from attachment after update
};
```
# ЕСЛИ ГДЕ-ТО ВИДИШЬ PTR_func_ret1_no_args, ТО ЭТО ShaderConfig
В рендеринге порядок дата-стримов такой
position
normal
color
# Внутренняя система ID
- `1` - unknown (implemented by CLandscape) видимо ILandscape
- `3` - unknown (implemented by CAtmosphere) видимо IAtmosphere
- `4` - IShader
- `5` - ITerrain
- `6` - IGameObject
- `7` - ISettings
- `8` - ICamera
- `9` - IQueue
- `10` - IControl
- `0xb` - IAnimation
- `0xc` - IShadeStatsBuilder (придумал сам implemented by CShade)
- `0xd` - IMatManager
- `0xe` - ILightManager
- `0xf` - IShade
- `0x10` - IBehaviour
- `0x11` - IBasement
- `0x12` - ICamera2 или IBufferingCamera
- `0x13` - IEffectManager
- `0x14` - IPosition
- `0x15` - IAgent
- `0x16` - ILifeSystem
- `0x17` - IBuilding - точно он, т.к. ArealMap.CreateObject на него проверяет
- `0x18` - IMesh2
- `0x19` - IManManager
- `0x20` - IJointMesh
- `0x21` - IShadowProcessor (придумал сам implemented by CShade)
- `0x22` - unknown (implement by CLandscape)
- `0x23` - IGameSettingsRoot
- `0x24` - IGameObject2
- `0x25` - unknown (implemented by CAniMesh)
- `0x26` - unknown (implemented by CAniMesh and CControl and CWizard)
- `0x28` - ICollObject
- `0x29` - IPhysicalModel
- `0x101` - I3DRender
- `0x102` - ITexture (writable)
- `0x103` - IColorLookup
- `0x104` - IBitmapFont
- `0x105` - INResFile
- `0x106` - NResFileMetadata
- `0x107` - I3DSound
- `0x108` - IListenerTransform
- `0x109` - ISoundPool
- `0x10a` - ISoundBuffer
- `0x10c` - ICDPlayer
- `0x10d` - IVertexBuffer
- `0x201` - IWizard
- `0x202` - IItemManager
- `0x203` - ICollManager
- `0x301` - IArealMap
- `0x302` - ISystemArealMap
- `0x303` - IHallway
- `0x304` - IDistributor
- `0x401` - ISuperAI
- `0x501` - MissionData
- `0x502` - ResTree
- `0x700` - INetWatcher
- `0x701` - INetworkInterface
## SuperAI = Clan с точки зрения индексации
Т.е. у каждого клана свой SuperAI
## Опции
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 |
## Goodies
```c++
// Тип положения объекта в пространстве. Используется при расчёте эффектов,
// расстановке объектов и для получения/выдачи их матриц трансформации.
enum EPlacementType
{
// 0: Полная мировая матрица, ориентированная "смотрю на цель".
// Входная matrix — это мировая матрица (look-at к какой-то цели).
// Движок при необходимости переводит её в локальное/костное пространство
// (через inverse(parent/joint) * world и т.п.).
// Смысл: "Построй мне мировую матрицу так, чтобы я смотрел на цель;
// если я привязан к кости — учти это."
Placement_WorldLookAtTarget = 0,
// 1: Мировая матрица, где основная ось объекта (this->direction)
// выровнена по направлению к цели.
// Входная matrix — мировая; логика выравнивания использует внутренний
// вектор direction и целевую точку/направление.
// Смысл: "Используй мой внутренний direction как основную ось
// и как можно лучше направь её в сторону цели."
Placement_WorldAlignDirectionToTarget = 1,
// 2: Мировая матрица, полностью задаётся внутренним состоянием объекта.
// Входная matrix трактуется как "standalone world" — готовая мировая
// матрица объекта. Движок лишь переводит её во внутреннее локальное/
// костное пространство (inverse(parent/joint) * world), без look-at
// и без выравнивания по цели.
// Смысл: "Вот моя конечная мировая матрица. Просто встрои её в иерархию,
// не трогая ориентацию дополнительной логикой."
Placement_WorldFromStoredTransform = 2,
// 3: Мировая матрица, ориентированная вдоль вектора движения, но с учётом цели.
// Используется предыдущая мировая позиция + текущая + target, чтобы
// построить базис: основная ось — направление движения, вспомогательная —
// направление к цели.
// Смысл: "Ориентируй меня по вектору моего движения, но также учитывай цель."
Placement_WorldAlignMotionToTarget = 3
};
```
## Контакты
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).

View File

@@ -1,39 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace ScrLib;
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,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,13 +1,20 @@
namespace ScrLib;
using Common;
namespace ScrLib;
public class ScrParser
{
public static ScrFile ReadFile(string filePath)
{
var fs = new FileStream(filePath, FileMode.Open);
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return ReadFile(fs);
}
public static ScrFile ReadFile(Stream fs)
{
var scrFile = new ScrFile();
scrFile.Magic = fs.ReadInt32LittleEndian();
scrFile.EntryCount = fs.ReadInt32LittleEndian();
@@ -17,7 +24,7 @@ public class ScrParser
{
var entry = new ScrEntry();
entry.Title = fs.ReadLengthPrefixedString();
// тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char*
fs.ReadByte();
@@ -28,12 +35,12 @@ public class ScrParser
{
var entryInner = new ScrEntryInner();
entryInner.ScriptIndex = fs.ReadInt32LittleEndian();
entryInner.UnkInner2 = fs.ReadInt32LittleEndian();
entryInner.UnkInner3 = fs.ReadInt32LittleEndian();
entryInner.Type = (ScrEntryInnerType)fs.ReadInt32LittleEndian();
entryInner.UnkInner5 = fs.ReadInt32LittleEndian();
entryInner.ArgumentsCount = fs.ReadInt32LittleEndian();
entryInner.Arguments = [];
@@ -46,7 +53,7 @@ public class ScrParser
entryInner.UnkInner7 = fs.ReadInt32LittleEndian();
entry.Inners.Add(entryInner);
}
scrFile.Entries.Add(entry);
}

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

@@ -6,10 +6,10 @@ public static class Extensions
{
return format switch
{
8888 => 32,
4444 => 16,
565 => 16,
888 => 32,
0x22B8 => 32,
0x115C => 16,
0x235 => 16,
0x378 => 32,
0 => 32
};
}

View File

@@ -14,7 +14,7 @@ namespace TexmLib;
/// <param name="MipmapCount">Кол-во мипмапов (уменьшенные копии текстуры)</param>
/// <param name="Stride">Сколько БИТ занимает 1 пиксель</param>
/// <param name="Magic1">Неизвестно</param>
/// <param name="Magic2">Неизвестно</param>
/// <param name="FormatOptionFlags">Дополнительные флаги для текстуры</param>
/// <param name="Format">Формат пикселя(4444, 8888, 888)</param>
public record TexmHeader(
string TexmAscii,
@@ -23,7 +23,7 @@ public record TexmHeader(
int MipmapCount,
int Stride,
int Magic1,
int Magic2,
int FormatOptionFlags,
int Format
);

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

@@ -22,7 +22,7 @@ public class TexmParser
var mipmapCountBytes = headerBytes[12..16];
var strideBytes = headerBytes[16..20];
var magic1Bytes = headerBytes[20..24];
var magic2Bytes = headerBytes[24..28];
var formatOptionFlagsBytes = headerBytes[24..28];
var formatBytes = headerBytes[28..32];
var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0');
@@ -31,7 +31,7 @@ public class TexmParser
var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes);
var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes);
var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes);
var magic2 = BinaryPrimitives.ReadInt32LittleEndian(magic2Bytes);
var formatOptionFlags = BinaryPrimitives.ReadInt32LittleEndian(formatOptionFlagsBytes);
var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes);
if (texmAscii != "Texm")
@@ -51,7 +51,7 @@ public class TexmParser
mipmapCount,
stride,
magic1,
magic2,
formatOptionFlags,
format
);

View File

@@ -1,34 +0,0 @@
using System.Buffers.Binary;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using TexmLib;
var folder = "C:\\Projects\\CSharp\\ParkanPlayground\\ParkanPlayground\\bin\\Debug\\net8.0\\ui.lib";
var files = Directory.EnumerateFiles(folder);
List<TexmFile> textureFiles = [];
foreach (var file in files)
{
try
{
var fs = new FileStream(file, FileMode.Open);
var parseResult = TexmParser.ReadFromStream(fs, file);
textureFiles.Add(parseResult.TexmFile);
Console.WriteLine($"Successfully read: {file}");
}
catch
{
Console.WriteLine($"Failed read: {file}");
}
}
foreach (var textureFile in textureFiles)
{
await textureFile.WriteToFolder("unpacked");
Console.WriteLine($"Unpacked {Path.GetFileName(textureFile.FileName)} into folder");
}

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace VarsetLib;
/// <summary>
/// Бинарный файл, который можно найти в behpsp.res
/// </summary>
public record BinaryVarsetFile(int Count, List<BinaryVarsetItem> Items);

View File

@@ -0,0 +1,93 @@
using Common;
namespace VarsetLib;
public class BinaryVarsetFileParser
{
public static BinaryVarsetFile Parse(string path)
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return Parse(fs);
}
public static BinaryVarsetFile Parse(Stream fs)
{
var count = fs.ReadInt32LittleEndian();
var items = new List<BinaryVarsetItem>(count);
Span<byte> buf4 = stackalloc byte[4];
for (int i = 0; i < count; i++)
{
var valueLength = fs.ReadInt32LittleEndian();
var valueType = (BinaryVarsetValueType)fs.ReadInt32LittleEndian();
fs.ReadExactly(buf4);
var magic3 = new IntFloatValue(buf4);
var name = fs.ReadLengthPrefixedString();
var string2 = fs.ReadLengthPrefixedString();
fs.ReadExactly(buf4);
var magic4 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic5 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic6 = new IntFloatValue(buf4);
fs.ReadExactly(buf4);
var magic7 = new IntFloatValue(buf4);
if(string2.Length != 0)
{
_ = 5;
}
if (valueType is BinaryVarsetValueType.Bool)
{
items.Add(new BinaryVarsetItem<bool>(
valueLength,
valueType,
magic3.AsInt != 0,
name,
string2,
magic4.AsInt != 0,
magic5.AsInt != 0,
magic6.AsInt != 0,
magic7.AsInt != 0));
}
else if (valueType is BinaryVarsetValueType.Dword)
{
items.Add(new BinaryVarsetItem<uint>(
valueLength,
valueType,
(uint)magic3.AsInt,
name,
string2,
(uint)magic4.AsInt,
(uint)magic5.AsInt,
(uint)magic6.AsInt,
(uint)magic7.AsInt));
}
else if (valueType is BinaryVarsetValueType.Float)
{
items.Add(new BinaryVarsetItem<float>(
valueLength,
valueType,
magic3.AsFloat,
name,
string2,
magic4.AsFloat,
magic5.AsFloat,
magic6.AsFloat,
magic7.AsFloat));
}
else
{
throw new InvalidOperationException($"Unknown value type {valueType}");
}
}
return new BinaryVarsetFile(count, items);
}
}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics;
using Common;
namespace VarsetLib;
[DebuggerDisplay("{DebugDisplay}")]
public abstract record BinaryVarsetItem()
{
protected abstract string DebugDisplay { get; }
}
public record BinaryVarsetItem<TValue>(
int ValueLength, // длина значения
BinaryVarsetValueType ValueType, // тип значения
TValue Magic3, // кажется 0 всегда
string Name, // имя переменной
string String2, // кажется всегда пусто
TValue Magic4,
TValue Magic5,
TValue Magic6, // минимум
TValue Magic7 // максимум
) : BinaryVarsetItem
{
protected override string DebugDisplay => $"{typeof(TValue).Name}, {ValueLength} bytes. magic3: {Magic3}. {Name,-30} {String2} - {Magic4} {Magic5} {Magic6} {Magic7}";
};
public enum BinaryVarsetValueType
{
Float0 = 0,
Int = 1,
Bool = 2,
Float = 3,
CharPtr = 4,
Dword = 5
}

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

@@ -4,65 +4,79 @@ public class VarsetParser
{
public static List<VarsetItem> Parse(string path)
{
FileStream fs = new FileStream(path, FileMode.Open);
var reader = new StreamReader(fs);
List<VarsetItem> varsetItems = [];
var lineIndex = 1;
while (!reader.EndOfStream)
{
var line = reader.ReadLine()!;
if (line.Length == 0)
{
lineIndex++;
continue;
}
if (line.StartsWith("//") || line.Trim().StartsWith("//"))
{
lineIndex++;
continue;
}
if (!line.StartsWith("VAR"))
{
Console.WriteLine($"Error on line: {lineIndex}! Not starting with VAR");
lineIndex++;
continue;
}
var openParenthesisIndex = line.IndexOf("(");
var closeParenthesisIndex = line.IndexOf(")");
if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 || closeParenthesisIndex <= openParenthesisIndex)
{
Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid");
lineIndex++;
continue;
}
var arguments = line.Substring(openParenthesisIndex + 1, closeParenthesisIndex - openParenthesisIndex - 1);
var parts = arguments.Trim()
.Split(',');
var type = parts[0]
.Trim();
var name = parts[1]
.Trim();
var value = parts[2]
.Trim();
var item = new VarsetItem(type, name, value);
varsetItems.Add(item);
lineIndex++;
}
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
return varsetItems;
return Parse(fs);
}
public static List<VarsetItem> Parse(Stream fs)
{
try
{
var reader = new StreamReader(fs);
List<VarsetItem> varsetItems = [];
var lineIndex = 1;
while (!reader.EndOfStream)
{
var line = reader.ReadLine()!;
if (line.Length == 0)
{
lineIndex++;
continue;
}
if (line.StartsWith("//") || line.Trim().StartsWith("//"))
{
lineIndex++;
continue;
}
if (!line.StartsWith("VAR"))
{
Console.WriteLine($"Error on line: {lineIndex}! Not starting with VAR");
lineIndex++;
continue;
}
var openParenthesisIndex = line.IndexOf("(");
var closeParenthesisIndex = line.IndexOf(")");
if (openParenthesisIndex == -1 || closeParenthesisIndex == -1 ||
closeParenthesisIndex <= openParenthesisIndex)
{
Console.WriteLine($"Error on line: {lineIndex}! VAR() format invalid");
lineIndex++;
continue;
}
var arguments = line.Substring(openParenthesisIndex + 1,
closeParenthesisIndex - openParenthesisIndex - 1);
var parts = arguments.Trim()
.Split(',');
var type = parts[0]
.Trim();
var name = parts[1]
.Trim();
var value = parts[2]
.Trim();
var item = new VarsetItem(type, name, value);
varsetItems.Add(item);
lineIndex++;
}
return varsetItems;
}
catch
{
return [];
}
}
}

View File

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

View File

@@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<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" />
</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);
}
}

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