0
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-10-14 07:20:23 +03:00

16 Commits

Author SHA1 Message Date
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
63 changed files with 1925 additions and 1394 deletions

3
Common/Common.csproj Normal file
View File

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

View File

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

3
Common/IndexedEdge.cs Normal file
View File

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

View File

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

3
Common/Vector3.cs Normal file
View File

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

12
CpDatLib/CpDatEntry.cs Normal file
View File

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

5
CpDatLib/CpDatLib.csproj Normal file
View File

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

View File

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

68
CpDatLib/CpDatParser.cs Normal file
View File

@@ -0,0 +1,68 @@
using Common;
namespace CpDatLib;
public class CpDatParser
{
public static CpDatParseResult Parse(string filePath)
{
Span<byte> f0f1 = stackalloc byte[4];
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
if (fs.Length < 8)
return new CpDatParseResult(null, "File too small to be a valid \"cp\" .dat file.");
fs.ReadExactly(f0f1);
if (f0f1[0] != 0xf1 || f0f1[1] != 0xf0)
{
return new CpDatParseResult(null, "File does not start with expected header bytes f1_f0");
}
var schemeType = (SchemeType)fs.ReadInt32LittleEndian();
var entryLength = 0x6c + 4; // нам нужно прочитать 0x6c (108) байт - это root, и ещё 4 байта - кол-во вложенных объектов
if ((fs.Length - 8) % entryLength != 0)
{
return new CpDatParseResult(null, "File size is not valid according to expected entry length.");
}
CpDatEntry root = ReadEntryRecursive(fs);
var scheme = new CpDatScheme(schemeType, root);
return new CpDatParseResult(scheme, null);
}
private static CpDatEntry ReadEntryRecursive(FileStream fs)
{
var str1 = fs.ReadNullTerminatedString();
fs.Seek(32 - str1.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var str2 = fs.ReadNullTerminatedString();
fs.Seek(32 - str2.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var magic1 = fs.ReadInt32LittleEndian();
var magic2 = fs.ReadInt32LittleEndian();
var descriptionString = fs.ReadNullTerminatedString();
fs.Seek(32 - descriptionString.Length - 1, SeekOrigin.Current); // -1 ignore null terminator
var magic3 = fs.ReadInt32LittleEndian();
// игра не читает количество внутрь схемы, вместо этого она сразу рекурсией читает нужно количество вложенных объектов
var childCount = fs.ReadInt32LittleEndian();
List<CpDatEntry> children = new List<CpDatEntry>(childCount);
for (var i = 0; i < childCount; i++)
{
var child = ReadEntryRecursive(fs);
children.Add(child);
}
return new CpDatEntry(str1, str2, magic1, magic2, descriptionString, magic3, childCount, Children: children);
}
}

3
CpDatLib/CpDatScheme.cs Normal file
View File

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

29
CpDatLib/CpEntryType.cs Normal file
View File

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

19
Directory.Build.props Normal file
View File

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

16
Directory.Packages.props Normal file
View File

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

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

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

View File

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

View File

@@ -1,3 +1,5 @@
namespace MissionTmaLib; using Common;
namespace MissionTmaLib;
public record ArealInfo(int Index, int CoordsCount, List<Vector3> Coords); 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 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); 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); public record LodeInfo(Vector3 UnknownVector, int UnknownInt1, int UnknownFlags2, float UnknownFloat, int UnknownInt3);

View File

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

View File

@@ -1,10 +1,12 @@
namespace MissionTmaLib.Parsing; using Common;
namespace MissionTmaLib.Parsing;
public class MissionTmaParser public class MissionTmaParser
{ {
public static MissionTmaParseResult ReadFile(string filePath) public static MissionTmaParseResult ReadFile(string filePath)
{ {
var fs = new FileStream(filePath, FileMode.Open); using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
var arealData = LoadAreals(fs); var arealData = LoadAreals(fs);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ public static class NResParser
{ {
public static NResParseResult ReadFile(string path) public static NResParseResult ReadFile(string path)
{ {
using FileStream nResFs = new FileStream(path, FileMode.Open); using FileStream nResFs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
if (nResFs.Length < 16) if (nResFs.Length < 16)
{ {

View File

@@ -56,6 +56,7 @@ public class App
serviceCollection.AddSingleton(new BinaryExplorerViewModel()); serviceCollection.AddSingleton(new BinaryExplorerViewModel());
serviceCollection.AddSingleton(new ScrViewModel()); serviceCollection.AddSingleton(new ScrViewModel());
serviceCollection.AddSingleton(new VarsetViewModel()); serviceCollection.AddSingleton(new VarsetViewModel());
serviceCollection.AddSingleton(new CpDatSchemeViewModel());
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();

View File

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

View File

@@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using CpDatLib;
using ImGuiNET; using ImGuiNET;
using MissionTmaLib; using MissionTmaLib;
using MissionTmaLib.Parsing; using MissionTmaLib.Parsing;
@@ -18,6 +19,7 @@ namespace NResUI.ImGuiUI
ScrViewModel scrViewModel, ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel, MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel, VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
MessageBoxModalPanel messageBox) MessageBoxModalPanel messageBox)
: IImGuiPanel : 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 (nResExplorerViewModel.HasFile)
{ {
if (ImGui.MenuItem("Экспортировать NRes")) if (ImGui.MenuItem("Экспортировать NRes"))

View File

@@ -18,6 +18,9 @@ public class MissionTmaExplorer : IImGuiPanel
{ {
if (ImGui.Begin("Mission TMA Explorer")) if (ImGui.Begin("Mission TMA Explorer"))
{ {
ImGui.Text("data.tma - это файл миссии. Его можно найти в папке MISSIONS");
ImGui.Separator();
var mission = _viewModel.Mission; var mission = _viewModel.Mission;
if (_viewModel.HasFile && mission is not null) if (_viewModel.HasFile && mission is not null)
{ {

View File

@@ -17,6 +17,9 @@ public class NResExplorerPanel : IImGuiPanel
{ {
if (ImGui.Begin("NRes Explorer")) if (ImGui.Begin("NRes Explorer"))
{ {
ImGui.Text("NRes - это файл-архив. Они имеют разные расширения. Примеры - Textures.lib, weapon.rlb, object.dlb, behpsp.res");
ImGui.Separator();
if (!_viewModel.HasFile) if (!_viewModel.HasFile)
{ {
ImGui.Text("No NRes is opened"); ImGui.Text("No NRes is opened");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,92 +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
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
EndGlobalSection
EndGlobal

View File

@@ -1,11 +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_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_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_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_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></wpf:ResourceDictionary>

21
ParkanPlayground.slnx Normal file
View File

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

77
ParkanPlayground/Msh01.cs Normal file
View File

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

186
ParkanPlayground/Msh02.cs Normal file
View File

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

35
ParkanPlayground/Msh03.cs Normal file
View File

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

32
ParkanPlayground/Msh06.cs Normal file
View File

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

53
ParkanPlayground/Msh07.cs Normal file
View File

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

49
ParkanPlayground/Msh0A.cs Normal file
View File

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

55
ParkanPlayground/Msh0D.cs Normal file
View File

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

View File

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

View File

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

View File

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

388
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,17 +2,14 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="NativeFileDialogSharp" Version="0.5.0" /> <PackageReference Include="NativeFileDialogSharp" />
<PackageReference Include="Silk.NET" Version="2.22.0" /> <PackageReference Include="Silk.NET" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.22.0" /> <PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,834 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace X86Disassembler
{
/// <summary>
/// Represents a Portable Executable (PE) file format parser
/// </summary>
public class PEFormat
{
// DOS Header constants
private const ushort DOS_SIGNATURE = 0x5A4D; // 'MZ'
private const uint PE_SIGNATURE = 0x00004550; // 'PE\0\0'
// Optional Header Magic values
private const ushort PE32_MAGIC = 0x10B; // 32-bit executable
private const ushort PE32PLUS_MAGIC = 0x20B; // 64-bit executable
// Section characteristics flags
private const uint IMAGE_SCN_CNT_CODE = 0x00000020; // Section contains code
private const uint IMAGE_SCN_MEM_EXECUTE = 0x20000000; // Section is executable
private const uint IMAGE_SCN_MEM_READ = 0x40000000; // Section is readable
private const uint IMAGE_SCN_MEM_WRITE = 0x80000000; // Section is writable
// Data directories
private const int IMAGE_DIRECTORY_ENTRY_EXPORT = 0; // Export Directory
private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1; // Import Directory
private const int IMAGE_DIRECTORY_ENTRY_RESOURCE = 2; // Resource Directory
private const int IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3; // Exception Directory
private const int IMAGE_DIRECTORY_ENTRY_SECURITY = 4; // Security Directory
private const int IMAGE_DIRECTORY_ENTRY_BASERELOC = 5; // Base Relocation Table
private const int IMAGE_DIRECTORY_ENTRY_DEBUG = 6; // Debug Directory
private const int IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7; // Architecture Specific Data
private const int IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8; // RVA of GP
private const int IMAGE_DIRECTORY_ENTRY_TLS = 9; // TLS Directory
private const int IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10; // Load Configuration Directory
private const int IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT = 11; // Bound Import Directory
private const int IMAGE_DIRECTORY_ENTRY_IAT = 12; // Import Address Table
private const int IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13; // Delay Load Import Descriptors
private const int IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR = 14; // COM Runtime descriptor
// PE file data
private byte[] _fileData;
// Parsed headers
public DOSHeader DosHeader { get; private set; }
public FileHeader FileHeader { get; private set; }
public OptionalHeader OptionalHeader { get; private set; }
public List<SectionHeader> SectionHeaders { get; private set; }
public bool Is64Bit { get; private set; }
// Export and Import information
public ExportDirectory ExportDirectory { get; private set; }
public List<ExportedFunction> ExportedFunctions { get; private set; }
public List<ImportDescriptor> ImportDescriptors { get; private set; }
/// <summary>
/// Parses a PE file from the given byte array
/// </summary>
/// <param name="fileData">The raw file data</param>
public PEFormat(byte[] fileData)
{
_fileData = fileData;
SectionHeaders = new List<SectionHeader>();
ExportedFunctions = new List<ExportedFunction>();
ImportDescriptors = new List<ImportDescriptor>();
Parse();
}
/// <summary>
/// Parses the PE file structure
/// </summary>
private void Parse()
{
using (MemoryStream stream = new MemoryStream(_fileData))
using (BinaryReader reader = new BinaryReader(stream))
{
// Parse DOS header
DosHeader = ParseDOSHeader(reader);
// Move to PE header
reader.BaseStream.Seek(DosHeader.e_lfanew, SeekOrigin.Begin);
// Verify PE signature
uint peSignature = reader.ReadUInt32();
if (peSignature != PE_SIGNATURE)
{
throw new InvalidDataException("Invalid PE signature");
}
// Parse File Header
FileHeader = ParseFileHeader(reader);
// Parse Optional Header
OptionalHeader = ParseOptionalHeader(reader);
// Parse Section Headers
for (int i = 0; i < FileHeader.NumberOfSections; i++)
{
SectionHeaders.Add(ParseSectionHeader(reader));
}
// Parse Export Directory
if (OptionalHeader.DataDirectories.Length > IMAGE_DIRECTORY_ENTRY_EXPORT &&
OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress != 0)
{
ExportDirectory = ParseExportDirectory(reader, OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
ParseExportedFunctions(reader);
}
// Parse Import Descriptors
if (OptionalHeader.DataDirectories.Length > IMAGE_DIRECTORY_ENTRY_IMPORT &&
OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress != 0)
{
ImportDescriptors = ParseImportDescriptors(reader, OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
}
}
}
/// <summary>
/// Parses the DOS header
/// </summary>
private DOSHeader ParseDOSHeader(BinaryReader reader)
{
DOSHeader header = new DOSHeader();
header.e_magic = reader.ReadUInt16();
if (header.e_magic != DOS_SIGNATURE)
{
throw new InvalidDataException("Invalid DOS signature (MZ)");
}
header.e_cblp = reader.ReadUInt16();
header.e_cp = reader.ReadUInt16();
header.e_crlc = reader.ReadUInt16();
header.e_cparhdr = reader.ReadUInt16();
header.e_minalloc = reader.ReadUInt16();
header.e_maxalloc = reader.ReadUInt16();
header.e_ss = reader.ReadUInt16();
header.e_sp = reader.ReadUInt16();
header.e_csum = reader.ReadUInt16();
header.e_ip = reader.ReadUInt16();
header.e_cs = reader.ReadUInt16();
header.e_lfarlc = reader.ReadUInt16();
header.e_ovno = reader.ReadUInt16();
header.e_res = new ushort[4];
for (int i = 0; i < 4; i++)
{
header.e_res[i] = reader.ReadUInt16();
}
header.e_oemid = reader.ReadUInt16();
header.e_oeminfo = reader.ReadUInt16();
header.e_res2 = new ushort[10];
for (int i = 0; i < 10; i++)
{
header.e_res2[i] = reader.ReadUInt16();
}
header.e_lfanew = reader.ReadUInt32();
return header;
}
/// <summary>
/// Parses the File header
/// </summary>
private FileHeader ParseFileHeader(BinaryReader reader)
{
FileHeader header = new FileHeader();
header.Machine = reader.ReadUInt16();
header.NumberOfSections = reader.ReadUInt16();
header.TimeDateStamp = reader.ReadUInt32();
header.PointerToSymbolTable = reader.ReadUInt32();
header.NumberOfSymbols = reader.ReadUInt32();
header.SizeOfOptionalHeader = reader.ReadUInt16();
header.Characteristics = reader.ReadUInt16();
return header;
}
/// <summary>
/// Parses the Optional header
/// </summary>
private OptionalHeader ParseOptionalHeader(BinaryReader reader)
{
OptionalHeader header = new OptionalHeader();
// Standard fields
header.Magic = reader.ReadUInt16();
// Determine if this is a PE32 or PE32+ file
Is64Bit = header.Magic == PE32PLUS_MAGIC;
header.MajorLinkerVersion = reader.ReadByte();
header.MinorLinkerVersion = reader.ReadByte();
header.SizeOfCode = reader.ReadUInt32();
header.SizeOfInitializedData = reader.ReadUInt32();
header.SizeOfUninitializedData = reader.ReadUInt32();
header.AddressOfEntryPoint = reader.ReadUInt32();
header.BaseOfCode = reader.ReadUInt32();
// PE32 has BaseOfData, PE32+ doesn't
if (!Is64Bit)
{
header.BaseOfData = reader.ReadUInt32();
}
// Windows-specific fields
if (Is64Bit)
{
header.ImageBase = reader.ReadUInt64();
}
else
{
header.ImageBase = reader.ReadUInt32();
}
header.SectionAlignment = reader.ReadUInt32();
header.FileAlignment = reader.ReadUInt32();
header.MajorOperatingSystemVersion = reader.ReadUInt16();
header.MinorOperatingSystemVersion = reader.ReadUInt16();
header.MajorImageVersion = reader.ReadUInt16();
header.MinorImageVersion = reader.ReadUInt16();
header.MajorSubsystemVersion = reader.ReadUInt16();
header.MinorSubsystemVersion = reader.ReadUInt16();
header.Win32VersionValue = reader.ReadUInt32();
header.SizeOfImage = reader.ReadUInt32();
header.SizeOfHeaders = reader.ReadUInt32();
header.CheckSum = reader.ReadUInt32();
header.Subsystem = reader.ReadUInt16();
header.DllCharacteristics = reader.ReadUInt16();
// Size fields differ between PE32 and PE32+
if (Is64Bit)
{
header.SizeOfStackReserve = reader.ReadUInt64();
header.SizeOfStackCommit = reader.ReadUInt64();
header.SizeOfHeapReserve = reader.ReadUInt64();
header.SizeOfHeapCommit = reader.ReadUInt64();
}
else
{
header.SizeOfStackReserve = reader.ReadUInt32();
header.SizeOfStackCommit = reader.ReadUInt32();
header.SizeOfHeapReserve = reader.ReadUInt32();
header.SizeOfHeapCommit = reader.ReadUInt32();
}
header.LoaderFlags = reader.ReadUInt32();
header.NumberOfRvaAndSizes = reader.ReadUInt32();
// Data directories
int numDirectories = (int)Math.Min(header.NumberOfRvaAndSizes, 16); // Maximum of 16 directories
header.DataDirectories = new DataDirectory[numDirectories];
for (int i = 0; i < numDirectories; i++)
{
DataDirectory dir = new DataDirectory();
dir.VirtualAddress = reader.ReadUInt32();
dir.Size = reader.ReadUInt32();
header.DataDirectories[i] = dir;
}
return header;
}
/// <summary>
/// Parses a section header
/// </summary>
private SectionHeader ParseSectionHeader(BinaryReader reader)
{
SectionHeader header = new SectionHeader();
// Read section name (8 bytes)
byte[] nameBytes = reader.ReadBytes(8);
// Convert to string, removing any null characters
header.Name = Encoding.ASCII.GetString(nameBytes).TrimEnd('\0');
header.VirtualSize = reader.ReadUInt32();
header.VirtualAddress = reader.ReadUInt32();
header.SizeOfRawData = reader.ReadUInt32();
header.PointerToRawData = reader.ReadUInt32();
header.PointerToRelocations = reader.ReadUInt32();
header.PointerToLinenumbers = reader.ReadUInt32();
header.NumberOfRelocations = reader.ReadUInt16();
header.NumberOfLinenumbers = reader.ReadUInt16();
header.Characteristics = reader.ReadUInt32();
return header;
}
/// <summary>
/// Parses the Export Directory
/// </summary>
private ExportDirectory ParseExportDirectory(BinaryReader reader, uint rva)
{
ExportDirectory directory = new ExportDirectory();
reader.BaseStream.Seek(RvaToOffset(rva), SeekOrigin.Begin);
directory.Characteristics = reader.ReadUInt32();
directory.TimeDateStamp = reader.ReadUInt32();
directory.MajorVersion = reader.ReadUInt16();
directory.MinorVersion = reader.ReadUInt16();
directory.Name = reader.ReadUInt32();
directory.Base = reader.ReadUInt32();
directory.NumberOfFunctions = reader.ReadUInt32();
directory.NumberOfNames = reader.ReadUInt32();
directory.AddressOfFunctions = reader.ReadUInt32();
directory.AddressOfNames = reader.ReadUInt32();
directory.AddressOfNameOrdinals = reader.ReadUInt32();
// Read the DLL name
uint dllNameRVA = directory.Name;
reader.BaseStream.Seek(RvaToOffset(dllNameRVA), SeekOrigin.Begin);
byte[] dllNameBytes = reader.ReadBytes(256);
directory.DllName = Encoding.ASCII.GetString(dllNameBytes).TrimEnd('\0');
return directory;
}
/// <summary>
/// Parses the Import Descriptors
/// </summary>
private List<ImportDescriptor> ParseImportDescriptors(BinaryReader reader, uint rva)
{
List<ImportDescriptor> descriptors = new List<ImportDescriptor>();
try
{
reader.BaseStream.Seek(RvaToOffset(rva), SeekOrigin.Begin);
while (true)
{
ImportDescriptor descriptor = new ImportDescriptor();
descriptor.OriginalFirstThunk = reader.ReadUInt32();
descriptor.TimeDateStamp = reader.ReadUInt32();
descriptor.ForwarderChain = reader.ReadUInt32();
descriptor.Name = reader.ReadUInt32();
descriptor.FirstThunk = reader.ReadUInt32();
// Check if we've reached the end of the import descriptors
if (descriptor.OriginalFirstThunk == 0 && descriptor.Name == 0 && descriptor.FirstThunk == 0)
{
break;
}
try
{
// Read the DLL name
uint dllNameOffset = RvaToOffset(descriptor.Name);
reader.BaseStream.Seek(dllNameOffset, SeekOrigin.Begin);
List<byte> nameBytes = new List<byte>();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBytes.Add(b);
}
descriptor.DllName = Encoding.ASCII.GetString(nameBytes.ToArray());
// Read the imported functions (use FirstThunk if OriginalFirstThunk is 0)
uint thunkRVA = descriptor.OriginalFirstThunk != 0 ? descriptor.OriginalFirstThunk : descriptor.FirstThunk;
if (thunkRVA != 0)
{
try
{
uint thunkOffset = RvaToOffset(thunkRVA);
uint currentThunkOffset = thunkOffset;
while (true)
{
reader.BaseStream.Seek(currentThunkOffset, SeekOrigin.Begin);
uint thunk = reader.ReadUInt32();
if (thunk == 0)
{
break;
}
ImportedFunction function = new ImportedFunction();
function.ThunkRVA = thunkRVA + (currentThunkOffset - thunkOffset);
// Check if the function is imported by ordinal
if ((thunk & 0x80000000) != 0)
{
function.IsOrdinal = true;
function.Ordinal = (ushort)(thunk & 0xFFFF);
function.Name = $"Ordinal_{function.Ordinal}";
}
else
{
// Read the function name and hint
try
{
uint nameOffset = RvaToOffset(thunk);
reader.BaseStream.Seek(nameOffset, SeekOrigin.Begin);
function.Hint = reader.ReadUInt16();
List<byte> funcNameBytes = new List<byte>();
byte c;
while ((c = reader.ReadByte()) != 0)
{
funcNameBytes.Add(c);
}
function.Name = Encoding.ASCII.GetString(funcNameBytes.ToArray());
}
catch (Exception)
{
function.Name = $"Function_at_{thunk:X8}";
}
}
descriptor.Functions.Add(function);
currentThunkOffset += 4; // Move to the next thunk
}
}
catch (Exception)
{
// Skip this thunk table if there's an error
}
}
}
catch (Exception)
{
// If we can't read the DLL name, use a placeholder
descriptor.DllName = $"DLL_at_{descriptor.Name:X8}";
}
descriptors.Add(descriptor);
}
}
catch (Exception)
{
// Return whatever descriptors we've managed to parse
}
return descriptors;
}
/// <summary>
/// Parses the exported functions using the export directory information
/// </summary>
private void ParseExportedFunctions(BinaryReader reader)
{
if (ExportDirectory == null)
{
return;
}
// Read the array of function addresses (RVAs)
uint[] functionRVAs = new uint[ExportDirectory.NumberOfFunctions];
reader.BaseStream.Seek(RvaToOffset(ExportDirectory.AddressOfFunctions), SeekOrigin.Begin);
for (int i = 0; i < ExportDirectory.NumberOfFunctions; i++)
{
functionRVAs[i] = reader.ReadUInt32();
}
// Read the array of name RVAs
uint[] nameRVAs = new uint[ExportDirectory.NumberOfNames];
reader.BaseStream.Seek(RvaToOffset(ExportDirectory.AddressOfNames), SeekOrigin.Begin);
for (int i = 0; i < ExportDirectory.NumberOfNames; i++)
{
nameRVAs[i] = reader.ReadUInt32();
}
// Read the array of name ordinals
ushort[] nameOrdinals = new ushort[ExportDirectory.NumberOfNames];
reader.BaseStream.Seek(RvaToOffset(ExportDirectory.AddressOfNameOrdinals), SeekOrigin.Begin);
for (int i = 0; i < ExportDirectory.NumberOfNames; i++)
{
nameOrdinals[i] = reader.ReadUInt16();
}
// Create a dictionary to map ordinals to names
Dictionary<ushort, string> ordinalToName = new Dictionary<ushort, string>();
for (int i = 0; i < ExportDirectory.NumberOfNames; i++)
{
// Read the function name
reader.BaseStream.Seek(RvaToOffset(nameRVAs[i]), SeekOrigin.Begin);
List<byte> nameBytes = new List<byte>();
byte b;
while ((b = reader.ReadByte()) != 0)
{
nameBytes.Add(b);
}
string name = Encoding.ASCII.GetString(nameBytes.ToArray());
// Map the ordinal to the name
ordinalToName[nameOrdinals[i]] = name;
}
// Create the exported functions
for (ushort i = 0; i < ExportDirectory.NumberOfFunctions; i++)
{
uint functionRVA = functionRVAs[i];
if (functionRVA == 0)
{
continue; // Skip empty entries
}
ExportedFunction function = new ExportedFunction();
function.Ordinal = (ushort)(i + ExportDirectory.Base);
function.Address = functionRVA;
// Check if this function has a name
if (ordinalToName.TryGetValue(i, out string name))
{
function.Name = name;
}
else
{
function.Name = $"Ordinal_{function.Ordinal}";
}
// Check if this is a forwarder
uint exportDirStart = OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
uint exportDirEnd = exportDirStart + OptionalHeader.DataDirectories[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
if (functionRVA >= exportDirStart && functionRVA < exportDirEnd)
{
function.IsForwarder = true;
// Read the forwarder string
reader.BaseStream.Seek(RvaToOffset(functionRVA), SeekOrigin.Begin);
List<byte> forwarderBytes = new List<byte>();
byte b;
while ((b = reader.ReadByte()) != 0)
{
forwarderBytes.Add(b);
}
function.ForwarderName = Encoding.ASCII.GetString(forwarderBytes.ToArray());
}
ExportedFunctions.Add(function);
}
}
/// <summary>
/// Gets the raw data for a specific section
/// </summary>
/// <param name="sectionIndex">Index of the section</param>
/// <returns>Byte array containing the section data</returns>
public byte[] GetSectionData(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= SectionHeaders.Count)
{
throw new ArgumentOutOfRangeException(nameof(sectionIndex));
}
SectionHeader section = SectionHeaders[sectionIndex];
byte[] sectionData = new byte[section.SizeOfRawData];
Array.Copy(_fileData, section.PointerToRawData, sectionData, 0, section.SizeOfRawData);
return sectionData;
}
/// <summary>
/// Gets the raw data for a section by name
/// </summary>
/// <param name="sectionName">Name of the section</param>
/// <returns>Byte array containing the section data</returns>
public byte[] GetSectionData(string sectionName)
{
for (int i = 0; i < SectionHeaders.Count; i++)
{
if (SectionHeaders[i].Name == sectionName)
{
return GetSectionData(i);
}
}
throw new ArgumentException($"Section '{sectionName}' not found");
}
/// <summary>
/// Checks if a section contains code
/// </summary>
/// <param name="section">The section to check</param>
/// <returns>True if the section contains code, false otherwise</returns>
public bool IsSectionContainsCode(SectionHeader section)
{
return (section.Characteristics & IMAGE_SCN_CNT_CODE) != 0 ||
(section.Characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
}
/// <summary>
/// Gets all code sections
/// </summary>
/// <returns>List of section indices that contain code</returns>
public List<int> GetCodeSections()
{
List<int> codeSections = new List<int>();
for (int i = 0; i < SectionHeaders.Count; i++)
{
if (IsSectionContainsCode(SectionHeaders[i]))
{
codeSections.Add(i);
}
}
return codeSections;
}
/// <summary>
/// Converts a Relative Virtual Address (RVA) to a file offset
/// </summary>
/// <param name="rva">The RVA to convert</param>
/// <returns>The corresponding file offset</returns>
public uint RvaToOffset(uint rva)
{
if (rva == 0)
{
return 0;
}
foreach (var section in SectionHeaders)
{
// Check if the RVA is within this section
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
{
// Calculate the offset within the section
uint offsetInSection = rva - section.VirtualAddress;
// Make sure we don't exceed the raw data size
if (offsetInSection < section.SizeOfRawData)
{
return section.PointerToRawData + offsetInSection;
}
}
}
// If the RVA is not within any section, it might be in the headers
if (rva < OptionalHeader.SizeOfHeaders)
{
return rva;
}
throw new ArgumentException($"RVA {rva:X8} is not within any section");
}
}
#region PE Format Structures
/// <summary>
/// DOS Header structure
/// </summary>
public class DOSHeader
{
public ushort e_magic; // Magic number ("MZ")
public ushort e_cblp; // Bytes on last page of file
public ushort e_cp; // Pages in file
public ushort e_crlc; // Relocations
public ushort e_cparhdr; // Size of header in paragraphs
public ushort e_minalloc; // Minimum extra paragraphs needed
public ushort e_maxalloc; // Maximum extra paragraphs needed
public ushort e_ss; // Initial (relative) SS value
public ushort e_sp; // Initial SP value
public ushort e_csum; // Checksum
public ushort e_ip; // Initial IP value
public ushort e_cs; // Initial (relative) CS value
public ushort e_lfarlc; // File address of relocation table
public ushort e_ovno; // Overlay number
public ushort[] e_res; // Reserved words
public ushort e_oemid; // OEM identifier
public ushort e_oeminfo; // OEM information
public ushort[] e_res2; // Reserved words
public uint e_lfanew; // File address of new exe header
}
/// <summary>
/// File Header structure
/// </summary>
public class FileHeader
{
public ushort Machine; // Target machine type
public ushort NumberOfSections; // Number of sections
public uint TimeDateStamp; // Time stamp
public uint PointerToSymbolTable; // File offset of symbol table
public uint NumberOfSymbols; // Number of symbols
public ushort SizeOfOptionalHeader; // Size of optional header
public ushort Characteristics; // Characteristics
}
/// <summary>
/// Optional Header structure
/// </summary>
public class OptionalHeader
{
// Standard fields
public ushort Magic; // Magic number (PE32 or PE32+)
public byte MajorLinkerVersion; // Major linker version
public byte MinorLinkerVersion; // Minor linker version
public uint SizeOfCode; // Size of code section
public uint SizeOfInitializedData; // Size of initialized data
public uint SizeOfUninitializedData; // Size of uninitialized data
public uint AddressOfEntryPoint; // Entry point RVA
public uint BaseOfCode; // Base of code section
public uint BaseOfData; // Base of data section (PE32 only)
// Windows-specific fields
public dynamic ImageBase; // Preferred image base (uint for PE32, ulong for PE32+)
public uint SectionAlignment; // Section alignment
public uint FileAlignment; // File alignment
public ushort MajorOperatingSystemVersion; // Major OS version
public ushort MinorOperatingSystemVersion; // Minor OS version
public ushort MajorImageVersion; // Major image version
public ushort MinorImageVersion; // Minor image version
public ushort MajorSubsystemVersion; // Major subsystem version
public ushort MinorSubsystemVersion; // Minor subsystem version
public uint Win32VersionValue; // Win32 version value
public uint SizeOfImage; // Size of image
public uint SizeOfHeaders; // Size of headers
public uint CheckSum; // Checksum
public ushort Subsystem; // Subsystem
public ushort DllCharacteristics; // DLL characteristics
public dynamic SizeOfStackReserve; // Size of stack reserve (uint for PE32, ulong for PE32+)
public dynamic SizeOfStackCommit; // Size of stack commit (uint for PE32, ulong for PE32+)
public dynamic SizeOfHeapReserve; // Size of heap reserve (uint for PE32, ulong for PE32+)
public dynamic SizeOfHeapCommit; // Size of heap commit (uint for PE32, ulong for PE32+)
public uint LoaderFlags; // Loader flags
public uint NumberOfRvaAndSizes; // Number of data directories
// Data directories
public DataDirectory[] DataDirectories; // Data directories
}
/// <summary>
/// Data Directory structure
/// </summary>
public class DataDirectory
{
public uint VirtualAddress; // RVA of the directory
public uint Size; // Size of the directory
}
/// <summary>
/// Section Header structure
/// </summary>
public class SectionHeader
{
public string Name; // Section name
public uint VirtualSize; // Virtual size
public uint VirtualAddress; // Virtual address (RVA)
public uint SizeOfRawData; // Size of raw data
public uint PointerToRawData; // File pointer to raw data
public uint PointerToRelocations; // File pointer to relocations
public uint PointerToLinenumbers; // File pointer to line numbers
public ushort NumberOfRelocations; // Number of relocations
public ushort NumberOfLinenumbers; // Number of line numbers
public uint Characteristics; // Characteristics
}
#endregion
#region Export and Import Structures
/// <summary>
/// Export Directory structure
/// </summary>
public class ExportDirectory
{
public uint Characteristics;
public uint TimeDateStamp;
public ushort MajorVersion;
public ushort MinorVersion;
public uint Name; // RVA to the DLL name
public string DllName; // Actual DLL name
public uint Base; // Ordinal base
public uint NumberOfFunctions; // Number of exported functions
public uint NumberOfNames; // Number of exported names
public uint AddressOfFunctions; // RVA to function addresses
public uint AddressOfNames; // RVA to function names
public uint AddressOfNameOrdinals; // RVA to ordinals
}
/// <summary>
/// Represents an exported function
/// </summary>
public class ExportedFunction
{
public string Name; // Function name
public uint Address; // Function RVA
public ushort Ordinal; // Function ordinal
public bool IsForwarder; // True if this is a forwarder
public string ForwarderName; // Name of the forwarded function (if IsForwarder is true)
}
/// <summary>
/// Import Descriptor structure
/// </summary>
public class ImportDescriptor
{
public uint OriginalFirstThunk; // RVA to Import Lookup Table
public uint TimeDateStamp;
public uint ForwarderChain;
public uint Name; // RVA to the DLL name
public string DllName; // Actual DLL name
public uint FirstThunk; // RVA to Import Address Table
public List<ImportedFunction> Functions; // List of imported functions
public ImportDescriptor()
{
Functions = new List<ImportedFunction>();
}
}
/// <summary>
/// Represents an imported function
/// </summary>
public class ImportedFunction
{
public bool IsOrdinal; // True if imported by ordinal
public ushort Ordinal; // Ordinal value (if IsOrdinal is true)
public string Name; // Function name (if IsOrdinal is false)
public ushort Hint; // Hint value (if IsOrdinal is false)
public uint ThunkRVA; // RVA in the Import Address Table
}
#endregion
}

View File

@@ -1,153 +0,0 @@
using System;
using System.IO;
namespace X86Disassembler
{
internal class Program
{
// Path to the DLL file to disassemble
private const string DllPath = @"C:\Windows\SysWOW64\msvcrt.dll"; // Example path, replace with your target DLL
static void Main(string[] args)
{
Console.WriteLine("X86 Disassembler and Decompiler");
Console.WriteLine("--------------------------------");
Console.WriteLine($"Loading file: {DllPath}");
// Load the DLL file
byte[] binaryData = File.ReadAllBytes(DllPath);
Console.WriteLine($"Successfully loaded {DllPath}");
Console.WriteLine($"File size: {binaryData.Length} bytes");
// Parse the PE format
Console.WriteLine("\nParsing PE format...");
PEFormat peFile = new PEFormat(binaryData);
// Display basic PE information
DisplayPEInfo(peFile);
// Display exported functions
DisplayExportedFunctions(peFile);
// Display imported functions
DisplayImportedFunctions(peFile);
// Find code sections for disassembly
var codeSections = peFile.GetCodeSections();
Console.WriteLine($"\nFound {codeSections.Count} code section(s):");
foreach (int sectionIndex in codeSections)
{
var section = peFile.SectionHeaders[sectionIndex];
Console.WriteLine($" - {section.Name}: Size={section.SizeOfRawData} bytes, RVA=0x{section.VirtualAddress:X8}");
// Get the section data for disassembly
byte[] sectionData = peFile.GetSectionData(sectionIndex);
// TODO: Implement disassembling logic here
// This is where we would pass the section data to our disassembler
}
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
}
private static void DisplayPEInfo(PEFormat peFile)
{
Console.WriteLine("\nPE File Information:");
Console.WriteLine($"Architecture: {(peFile.Is64Bit ? "64-bit" : "32-bit")}");
Console.WriteLine($"Entry Point: 0x{peFile.OptionalHeader.AddressOfEntryPoint:X8}");
Console.WriteLine($"Image Base: 0x{peFile.OptionalHeader.ImageBase:X}");
Console.WriteLine($"Number of Sections: {peFile.FileHeader.NumberOfSections}");
// Display section information
Console.WriteLine("\nSections:");
for (int i = 0; i < peFile.SectionHeaders.Count; i++)
{
var section = peFile.SectionHeaders[i];
string flags = "";
if ((section.Characteristics & 0x00000020) != 0) flags += "Code "; // IMAGE_SCN_CNT_CODE
if ((section.Characteristics & 0x20000000) != 0) flags += "Exec "; // IMAGE_SCN_MEM_EXECUTE
if ((section.Characteristics & 0x40000000) != 0) flags += "Read "; // IMAGE_SCN_MEM_READ
if ((section.Characteristics & 0x80000000) != 0) flags += "Write"; // IMAGE_SCN_MEM_WRITE
Console.WriteLine($" {i}: {section.Name,-8} VA=0x{section.VirtualAddress:X8} Size={section.SizeOfRawData,-8} [{flags}]");
}
}
private static void DisplayExportedFunctions(PEFormat peFile)
{
if (peFile.ExportDirectory == null)
{
Console.WriteLine("\nNo exported functions found.");
return;
}
Console.WriteLine("\nExported Functions:");
Console.WriteLine($"DLL Name: {peFile.ExportDirectory.DllName}");
Console.WriteLine($"Number of Functions: {peFile.ExportDirectory.NumberOfFunctions}");
Console.WriteLine($"Number of Names: {peFile.ExportDirectory.NumberOfNames}");
// Display the first 10 exported functions (if any)
int count = Math.Min(10, peFile.ExportedFunctions.Count);
for (int i = 0; i < count; i++)
{
var function = peFile.ExportedFunctions[i];
Console.WriteLine($" {i}: {function.Name} (Ordinal={function.Ordinal}, RVA=0x{function.Address:X8})");
}
if (peFile.ExportedFunctions.Count > 10)
{
Console.WriteLine($" ... and {peFile.ExportedFunctions.Count - 10} more");
}
}
private static void DisplayImportedFunctions(PEFormat peFile)
{
if (peFile.ImportDescriptors.Count == 0)
{
Console.WriteLine("\nNo imported functions found.");
return;
}
Console.WriteLine("\nImported Functions:");
Console.WriteLine($"Number of Imported DLLs: {peFile.ImportDescriptors.Count}");
// Display the first 5 imported DLLs and their functions
int dllCount = Math.Min(5, peFile.ImportDescriptors.Count);
for (int i = 0; i < dllCount; i++)
{
var descriptor = peFile.ImportDescriptors[i];
Console.WriteLine($" DLL: {descriptor.DllName}");
// Display the first 5 functions from this DLL
int funcCount = Math.Min(5, descriptor.Functions.Count);
for (int j = 0; j < funcCount; j++)
{
var function = descriptor.Functions[j];
if (function.IsOrdinal)
{
Console.WriteLine($" {j}: Ordinal {function.Ordinal}");
}
else
{
Console.WriteLine($" {j}: {function.Name} (Hint={function.Hint})");
}
}
if (descriptor.Functions.Count > 5)
{
Console.WriteLine($" ... and {descriptor.Functions.Count - 5} more");
}
}
if (peFile.ImportDescriptors.Count > 5)
{
Console.WriteLine($" ... and {peFile.ImportDescriptors.Count - 5} more DLLs");
}
}
}
}

View File

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