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

37 Commits

Author SHA1 Message Date
bird_egop
a8536f938d fxid 2025-12-09 02:32:32 +03:00
bird_egop
fcfb8d0e8a feat: Add landscape mesh support to MshConverter
- Auto-detect Model vs Landscape mesh types
- Model: uses indexed triangles (06, 0D, 07)
- Landscape: uses direct triangles (0B, 15)
- Add MSH_FORMAT.md documentation (Russian)
- Add Ghidra decompiled code for CLandscape
2025-12-05 18:04:50 +03:00
bird_egop
4951550420 add better tooltips 2025-12-04 07:58:32 +03:00
bird_egop
5ea0cb143f add help 2025-12-04 03:55:41 +03:00
bird_egop
75b1548f55 Material parsing and viewing 2025-12-04 03:50:44 +03:00
bird_egop
3b1fd5ef97 more docs 2025-12-01 12:48:08 +03:00
bird_egop
60fd173c47 info about lightmap texture naming 2025-12-01 07:41:42 +03:00
bird_egop
3c9b46cbca update docs 2025-11-30 18:27:40 +03:00
bird_egop
c9d0366637 Improvements on docs, based on decompilation 2025-11-30 18:11:16 +03:00
bird_egop
6c9e1d0b98 some findings 2025-11-27 19:01:41 +03:00
bird_egop
3738881c46 drop unused projects 2025-11-23 19:33:58 +03:00
bird_egop
c27b77cbfc parse PAL file 2025-11-23 00:04:05 +03:00
bird_egop
f4442897a6 improve binary varset parsing 2025-11-22 09:02:35 +03:00
bird_egop
2a78bbebda parse binary varset file 2025-11-22 06:53:07 +03:00
bird_egop
a774db37a6 improvements 2025-10-05 19:08:23 +03:00
Bird Egop
c1ea70efe0 Create LICENSE 2025-09-06 02:25:15 +03:00
bird_egop
be60d8d72f Examples and fixes 2025-09-04 03:13:46 +03:00
bird_egop
f2bed4b141 Allow to view cp .dat in UI 2025-09-04 02:45:26 +03:00
bird_egop
7f0246f996 update docs on wea. correctly parse msh 2025-09-03 01:30:54 +03:00
bird_egop
055694a4b4 Hack .msh 2025-08-31 02:20:44 +03:00
bird_egop
fca052365f add docs 2025-08-28 03:30:17 +03:00
bird_egop
5c52ab2b2b msh and cp converters. Mesh broken. 2025-08-26 04:29:30 +03:00
bird_egop
77e7f7652c update documentation 2025-08-26 04:21:48 +03:00
bird_egop
35af4da326 read msh 0A file 2025-08-23 19:03:03 +03:00
bird_egop
4b1c4bf3aa update readme 2025-08-23 03:26:04 +03:00
bird_egop
4ea756a1a4 Update readme 2025-08-23 03:21:03 +03:00
bird_egop
b9e15541c5 Parse cp .dat files. Object schemes.
Test parsing of .msh
2025-08-23 03:00:30 +03:00
Bird Egop
67c9020b96 Update README.md 2025-08-20 15:38:44 +03:00
bird_egop
476017e9c1 upgrade to net9 2025-08-18 22:05:17 +03:00
bird_egop
ee77738713 remove leftovers 2025-08-18 21:57:18 +03:00
Bird Egop
8e31f43abf Update README.md 2025-08-18 21:09:31 +03:00
bird_egop
f5bacc018c test 2025-04-12 16:42:44 +03:00
bird_egop
a6057bf072 unfuck 565 and 4444 textures 2025-03-11 04:36:05 +03:00
bird_egop
a419be1fce update NRES file with element count and element size, seen in ResTree .trf 2025-03-09 22:56:59 +03:00
bird_egop
8c4fc8f096 комментарии и дополнительные изыскания 2025-03-05 18:15:48 +03:00
bird_egop
135777a4c6 add varset view 2025-03-01 23:03:13 +03:00
bird_egop
76ef68635e scr reversed type 2025-03-01 22:45:15 +03:00
95 changed files with 9710 additions and 514 deletions

3
Common/Common.csproj Normal file
View File

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

91
Common/Extensions.cs Normal file
View File

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

3
Common/IndexedEdge.cs Normal file
View File

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

View File

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

View File

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

31
CpDatLib/CpDatEntry.cs Normal file
View File

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

5
CpDatLib/CpDatLib.csproj Normal file
View File

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

View File

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

74
CpDatLib/CpDatParser.cs Normal file
View File

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

3
CpDatLib/CpDatScheme.cs Normal file
View File

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

29
CpDatLib/CpEntryType.cs Normal file
View File

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

19
Directory.Build.props Normal file
View File

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

16
Directory.Packages.props Normal file
View File

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

373
LICENSE Normal file
View File

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

82
MaterialLib/Interpolate.c Normal file
View File

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

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

167
MaterialLib/MaterialFile.cs Normal file
View File

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

View File

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

View File

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

290
MaterialLib/decompiled.c Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,18 @@
namespace MissionTmaLib.Parsing;
using Common;
namespace MissionTmaLib.Parsing;
public class MissionTmaParser
{
public static MissionTmaParseResult ReadFile(string filePath)
{
var fs = new FileStream(filePath, FileMode.Open);
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return ReadFile(fs);
}
public static MissionTmaParseResult ReadFile(Stream fs)
{
var arealData = LoadAreals(fs);
var clansData = LoadClans(fs);
@@ -18,7 +25,7 @@ public class MissionTmaParser
return new MissionTmaParseResult(missionDat, null);
}
private static ArealsFileData LoadAreals(FileStream fileStream)
private static ArealsFileData LoadAreals(Stream fileStream)
{
var unusedHeader = fileStream.ReadInt32LittleEndian();
var arealCount = fileStream.ReadInt32LittleEndian();
@@ -54,7 +61,7 @@ public class MissionTmaParser
return new ArealsFileData(unusedHeader, arealCount, infos);
}
private static ClansFileData? LoadClans(FileStream fileStream)
private static ClansFileData? LoadClans(Stream fileStream)
{
var clanFeatureSet = fileStream.ReadInt32LittleEndian();
@@ -118,7 +125,7 @@ public class MissionTmaParser
if (4 < clanFeatureSet)
{
clanTreeInfo.UnkInt3 = fileStream.ReadInt32LittleEndian();
clanTreeInfo.Brains = fileStream.ReadInt32LittleEndian();
}
if (5 < clanFeatureSet)
@@ -156,7 +163,7 @@ public class MissionTmaParser
return clanInfo;
}
private static GameObjectsFileData LoadGameObjects(FileStream fileStream)
private static GameObjectsFileData LoadGameObjects(Stream fileStream)
{
var gameObjectsFeatureSet = fileStream.ReadInt32LittleEndian();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,150 @@
using CpDatLib;
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
namespace NResUI.ImGuiUI;
public class CpDatSchemeExplorer : IImGuiPanel
{
private readonly CpDatSchemeViewModel _viewModel;
public CpDatSchemeExplorer(CpDatSchemeViewModel viewModel)
{
_viewModel = viewModel;
}
public void OnImGuiRender()
{
if (ImGui.Begin("cp .dat Scheme Explorer"))
{
ImGui.Text("cp .dat - это файл схема здания или робота. Их можно найти в папке UNITS");
ImGui.Separator();
var cpDat = _viewModel.CpDatScheme;
if (_viewModel.HasFile && cpDat is not null)
{
ImGui.Text("Тип объекта в схеме: ");
ImGui.SameLine();
ImGui.Text(cpDat.Type.ToString("G"));
var root = cpDat.Root;
DrawEntry(root, 0);
ImGui.Separator();
if (ImGui.BeginTable("content", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX | ImGuiTableFlags.Sortable))
{
ImGui.TableSetupColumn("Индекс");
ImGui.TableSetupColumn("Уровень вложенности");
ImGui.TableSetupColumn("Архив");
ImGui.TableSetupColumn("Элемент");
ImGui.TableSetupColumn("Magic1");
ImGui.TableSetupColumn("Magic2");
ImGui.TableSetupColumn("Описание");
ImGui.TableSetupColumn("Тип");
ImGui.TableHeadersRow();
// Handle sorting
ImGuiTableSortSpecsPtr sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
// Only handle the first sort spec for simplicity
var sortSpec = sortSpecs.Specs;
if (sortSpec.ColumnIndex == 0)
{
_viewModel.RebuildFlatList();
}
else
{
_viewModel.FlatList.Sort((a, b) =>
{
int result = 0;
switch (sortSpec.ColumnIndex)
{
case 1: result = a.Level.CompareTo(b.Level); break;
case 2: result = string.Compare(a.Entry.ArchiveFile, b.Entry.ArchiveFile, StringComparison.Ordinal); break;
case 3: result = string.Compare(a.Entry.ArchiveEntryName, b.Entry.ArchiveEntryName, StringComparison.Ordinal); break;
case 4: result = a.Entry.Magic1.CompareTo(b.Entry.Magic1); break;
case 5: result = a.Entry.Magic2.CompareTo(b.Entry.Magic2); break;
case 6: result = string.Compare(a.Entry.Description, b.Entry.Description, StringComparison.Ordinal); break;
case 7: result = a.Entry.Type.CompareTo(b.Entry.Type); break;
}
return sortSpec.SortDirection == ImGuiSortDirection.Descending ? -result : result;
});
}
sortSpecs.SpecsDirty = false;
}
for (int i = 0; i < _viewModel.FlatList.Count; i++)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(i.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Level.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveFile);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.ArchiveEntryName);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic1.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Magic2.ToString());
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Description);
ImGui.TableNextColumn();
ImGui.Text(_viewModel.FlatList[i].Entry.Type.ToString("G"));
}
ImGui.EndTable();
}
void DrawEntry(CpDatEntry entry, int index)
{
if (ImGui.TreeNodeEx($"Элемент: \"{entry.ArchiveFile}/{entry.ArchiveEntryName}\" - {entry.Description}##entry_{index}"))
{
ImGui.Text("Magic1: ");
ImGui.SameLine();
ImGui.Text(entry.Magic1.ToString());
ImGui.Text("Magic2: ");
ImGui.SameLine();
ImGui.Text(entry.Magic2.ToString());
ImGui.Text("Тип: ");
ImGui.SameLine();
ImGui.Text(entry.Type.ToString());
ImGui.Text("Кол-во дочерних элементов: ");
ImGui.SameLine();
ImGui.Text(entry.ChildCount.ToString());
foreach (var child in entry.Children)
{
DrawEntry(child, ++index);
}
ImGui.TreePop();
}
}
}
else if (_viewModel.Error is not null)
{
ImGui.Text(_viewModel.Error);
}
else
{
ImGui.Text("cp .dat не открыт");
}
}
ImGui.End();
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using System.Numerics;
using CpDatLib;
using ImGuiNET;
using MissionTmaLib;
using MissionTmaLib.Parsing;
@@ -8,6 +9,7 @@ using NResUI.Abstractions;
using NResUI.Models;
using ScrLib;
using TexmLib;
using VarsetLib;
namespace NResUI.ImGuiUI
{
@@ -16,6 +18,8 @@ namespace NResUI.ImGuiUI
TexmExplorerViewModel texmExplorerViewModel,
ScrViewModel scrViewModel,
MissionTmaViewModel missionTmaViewModel,
VarsetViewModel varsetViewModel,
CpDatSchemeViewModel cpDatSchemeViewModel,
MessageBoxModalPanel messageBox)
: IImGuiPanel
{
@@ -104,6 +108,36 @@ namespace NResUI.ImGuiUI
}
}
if (ImGui.MenuItem("Open Varset File"))
{
var result = Dialog.FileOpen("var");
if (result.IsOk)
{
var path = result.Path;
var parseResult = VarsetParser.Parse(path);
varsetViewModel.Items = parseResult;
Console.WriteLine("Read VARSET");
}
}
if (ImGui.MenuItem("Open cp .dat Scheme File"))
{
var result = Dialog.FileOpen("dat");
if (result.IsOk)
{
var path = result.Path;
var parseResult = CpDatParser.Parse(path);
cpDatSchemeViewModel.SetParseResult(parseResult, path);
Console.WriteLine("Read cp .dat");
}
}
if (nResExplorerViewModel.HasFile)
{
if (ImGui.MenuItem("Экспортировать NRes"))

View File

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

View File

@@ -18,6 +18,9 @@ public class MissionTmaExplorer : IImGuiPanel
{
if (ImGui.Begin("Mission TMA Explorer"))
{
ImGui.Text("data.tma - это файл миссии. Его можно найти в папке MISSIONS");
ImGui.Separator();
var mission = _viewModel.Mission;
if (_viewModel.HasFile && mission is not null)
{
@@ -132,7 +135,7 @@ public class MissionTmaExplorer : IImGuiPanel
ImGui.SameLine();
ImGui.Text(clanInfo.ClanType.ToReadableString());
ImGui.Text("Скрипты поведения: ");
ImGui.Text("Скрипты поведения (Mission Scripts): ");
Utils.ShowHint("Пути к файлам .scr и .fml описывающих настройку объектов и поведение AI");
ImGui.SameLine();
ImGui.Text(clanInfo.ScriptsString);
@@ -176,14 +179,14 @@ public class MissionTmaExplorer : IImGuiPanel
ImGui.Text("Отсутствует неизвестная часть");
}
ImGui.Text("Путь к файлу .trf: ");
Utils.ShowHint("Не до конца понятно, что означает, вероятно это NRes с деревом исследований");
ImGui.Text("Дерево исследований: ");
Utils.ShowHint("NRes с деревом исследований");
ImGui.SameLine();
ImGui.Text(clanInfo.ResearchNResPath);
ImGui.Text("Неизвестное число 3: ");
ImGui.Text("Количество мозгов (Brains))): ");
ImGui.SameLine();
ImGui.Text(clanInfo.UnkInt3.ToString());
ImGui.Text(clanInfo.Brains.ToString());
ImGui.Text("Матрица союзников");
Utils.ShowHint("Если 1, то кланы - союзники, и не нападают друг на друга");

View File

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

View File

@@ -1,6 +1,7 @@
using ImGuiNET;
using NResUI.Abstractions;
using NResUI.Models;
using ScrLib;
namespace NResUI.ImGuiUI;
@@ -17,6 +18,9 @@ public class ScrExplorer : IImGuiPanel
{
if (ImGui.Begin("SCR Explorer"))
{
ImGui.Text("scr - это файл AI скриптов. Их можно найти в папке MISSIONS/SCRIPTS");
ImGui.Separator();
var scr = _viewModel.Scr;
if (_viewModel.HasFile && scr is not null)
{
@@ -46,10 +50,10 @@ public class ScrExplorer : IImGuiPanel
if (ImGui.BeginTable($"Элементы##{i:0000}", 8, ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Индекс скрипта");
ImGui.TableSetupColumn("Индекс встроенного скрипта");
ImGui.TableSetupColumn("UnkInner2");
ImGui.TableSetupColumn("UnkInner3");
ImGui.TableSetupColumn("UnkInner4");
ImGui.TableSetupColumn("Тип действия");
ImGui.TableSetupColumn("UnkInner5");
ImGui.TableSetupColumn("Кол-во аргументов");
ImGui.TableSetupColumn("Аргументы");
@@ -62,21 +66,66 @@ public class ScrExplorer : IImGuiPanel
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(inner.ScriptIndex.ToString());
if (inner.ScriptIndex == 2)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
if (inner.ScriptIndex == 4)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
if (inner.ScriptIndex == 8)
{
Utils.ShowHint("Установить dCurrentProblem стейт (VARSET:arg0)");
}
if (inner.ScriptIndex == 20)
{
Utils.ShowHint("Первый аргумент - номер проблемы");
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner2.ToString());
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner3.ToString());
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner4.ToString());
ImGui.Text($"{(int) inner.Type}: {inner.Type:G}");
if (inner.Type == ScrEntryInnerType._0)
{
Utils.ShowHint("0 обязан иметь аргументы");
}
if (inner.Type == ScrEntryInnerType.CheckInternalState)
{
Utils.ShowHint("Для 5 вообще не нужны данные, игра проверяет внутренний стейт");
}
if (inner.Type == ScrEntryInnerType.SetVarsetValue)
{
Utils.ShowHint("В случае 6, игра берёт UnkInner2 (индекс в Varset) и устанавливает ему значение UnkInner3");
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner5.ToString());
ImGui.TableNextColumn();
ImGui.Text(inner.ArgumentsCount.ToString());
ImGui.TableNextColumn();
ImGui.Text(string.Join(", ", inner.Arguments));
foreach (var argument in inner.Arguments)
{
if (ImGui.Button(argument.ToString()))
{
}
ImGui.SameLine();
}
ImGui.TableNextColumn();
ImGui.Text(inner.UnkInner7.ToString());
}
ImGui.EndTable();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

54
PalLib/PalFile.cs Normal file
View File

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

7
PalLib/PalLib.csproj Normal file
View File

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

37
PalLib/PalParser.cs Normal file
View File

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

View File

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

19
ParkanPlayground.slnx Normal file
View File

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

View File

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

View File

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

View File

@@ -1,39 +0,0 @@
using System.Buffers.Binary;
using System.Text;
namespace ParkanPlayground;
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

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

77
ParkanPlayground/Msh01.cs Normal file
View File

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

193
ParkanPlayground/Msh02.cs Normal file
View File

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

35
ParkanPlayground/Msh03.cs Normal file
View File

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

32
ParkanPlayground/Msh06.cs Normal file
View File

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

53
ParkanPlayground/Msh07.cs Normal file
View File

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

49
ParkanPlayground/Msh0A.cs Normal file
View File

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

55
ParkanPlayground/Msh0D.cs Normal file
View File

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

55
ParkanPlayground/Msh15.cs Normal file
View File

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

View File

@@ -0,0 +1,314 @@
using System.Text;
using Common;
using NResLib;
namespace ParkanPlayground;
public enum MshType
{
Unknown,
Model, // Has component 06 (indices), 0D (batches), 07
Landscape // Has component 0B (per-triangle material), uses 15 directly
}
public class MshConverter
{
/// <summary>
/// Detects mesh type based on which components are present in the archive.
/// </summary>
public static MshType DetectMeshType(NResArchive archive)
{
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
bool hasComponent0B = archive.Files.Any(f => f.FileType == "0B 00 00 00");
bool hasComponent0D = archive.Files.Any(f => f.FileType == "0D 00 00 00");
// Model: Uses indexed triangles via component 06 and batches via 0D
if (hasComponent06 && hasComponent0D)
return MshType.Model;
// Landscape: Uses direct triangles in component 15, with material data in 0B
if (hasComponent0B && !hasComponent06)
return MshType.Landscape;
return MshType.Unknown;
}
/// <summary>
/// Converts a .msh file to OBJ format, auto-detecting mesh type.
/// </summary>
/// <param name="mshPath">Path to the .msh file</param>
/// <param name="outputPath">Output OBJ path (optional, defaults to input name + .obj)</param>
/// <param name="lodLevel">LOD level to export (0 = highest detail)</param>
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0)
{
var mshNresResult = NResParser.ReadFile(mshPath);
if (mshNresResult.Archive is null)
{
Console.WriteLine($"ERROR: Failed to read NRes archive: {mshNresResult.Error}");
return;
}
var archive = mshNresResult.Archive;
var meshType = DetectMeshType(archive);
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
Console.WriteLine($"Detected type: {meshType}");
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
switch (meshType)
{
case MshType.Model:
ConvertModel(fs, archive, outputPath, lodLevel);
break;
case MshType.Landscape:
ConvertLandscape(fs, archive, outputPath, lodLevel);
break;
default:
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
break;
}
}
/// <summary>
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
/// </summary>
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component06 = Msh06.ReadComponent(fs, archive);
var component07 = Msh07.ReadComponent(fs, archive);
var component0D = Msh0D.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Pieces: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
{
var piece = component01.Elements[pieceIndex];
// Get submesh index for requested LOD
if (lodLevel >= piece.Lod.Length)
continue;
var submeshIdx = piece.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g piece_{pieceIndex}");
var submesh = component02.Elements[submeshIdx];
var batchStart = submesh.StartOffsetIn0d;
var batchCount = submesh.ByteLengthIn0D;
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
{
var batch = component0D[batchStart + batchIdx];
var baseVertex = batch.IndexInto03;
var indexStart = batch.IndexInto06;
var indexCount = batch.CountOf06;
for (int i = 0; i < indexCount; i += 3)
{
var i1 = baseVertex + component06[indexStart + i];
var i2 = baseVertex + component06[indexStart + i + 1];
var i3 = baseVertex + component06[indexStart + i + 2];
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
exportedFaces++;
}
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
/// <summary>
/// Converts a landscape mesh (terrain) to OBJ.
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
/// </summary>
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component15 = Msh15.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Triangles: {component15.Count}");
Console.WriteLine($"Tiles: {component01.Elements.Count}");
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
sw.WriteLine($"# Landscape mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}");
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}");
// Write all vertices
foreach (var v in component03)
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
int exportedFaces = 0;
for (var tileIdx = 0; tileIdx < component01.Elements.Count; tileIdx++)
{
var tile = component01.Elements[tileIdx];
// Get submesh index for requested LOD
if (lodLevel >= tile.Lod.Length)
continue;
var submeshIdx = tile.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
sw.WriteLine($"g tile_{tileIdx}");
var submesh = component02.Elements[submeshIdx];
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
var triangleStart = submesh.StartIndexIn07;
var triangleCount = submesh.CountIn07;
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
{
var triIdx = triangleStart + triOffset;
if (triIdx >= component15.Count)
{
Console.WriteLine($"WARNING: Triangle index {triIdx} out of range for tile {tileIdx}");
continue;
}
var tri = component15[triIdx];
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
exportedFaces++;
}
}
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
Console.WriteLine($"Output: {outputPath}");
}
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
public static void ExportCube(string filePath, Vector3[] points)
{
if (points.Length != 8)
throw new ArgumentException("Cube must have exactly 8 points.");
using (StreamWriter writer = new StreamWriter(filePath))
{
// Write vertices
foreach (var p in points)
{
writer.WriteLine($"v {p.X} {p.Y} {p.Z}");
}
// Write faces (each face defined by 4 vertices, using 1-based indices)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
foreach (var f in faces)
{
writer.WriteLine($"f {f[0]} {f[1]} {f[2]} {f[3]}");
}
}
}
public static void ExportCubesAtPositions(string filePath, List<Vector3> centers, float size = 2f)
{
float half = size / 2f;
using (StreamWriter writer = new StreamWriter(filePath))
{
int vertexOffset = 0;
foreach (var c in centers)
{
// Generate 8 vertices for this cube
Vector3[] vertices = new Vector3[]
{
new Vector3(c.X - half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z - half),
new Vector3(c.X + half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y - half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z - half),
new Vector3(c.X + half, c.Y + half, c.Z + half),
new Vector3(c.X - half, c.Y + half, c.Z + half)
};
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
}
// Define faces (1-based indices, counter-clockwise)
int[][] faces = new int[][]
{
new int[] { 1, 2, 3, 4 }, // bottom
new int[] { 5, 6, 7, 8 }, // top
new int[] { 1, 2, 6, 5 }, // front
new int[] { 2, 3, 7, 6 }, // right
new int[] { 3, 4, 8, 7 }, // back
new int[] { 4, 1, 5, 8 } // left
};
// Write faces with offset
foreach (var f in faces)
{
writer.WriteLine(
$"f {f[0] + vertexOffset} {f[1] + vertexOffset} {f[2] + vertexOffset} {f[3] + vertexOffset}");
}
vertexOffset += 8;
}
}
}
void Export(string filePath, IEnumerable<Vector3> vertices, List<IndexedEdge> edges)
{
using (var writer = new StreamWriter(filePath))
{
writer.WriteLine("# Exported OBJ file");
// Write vertices
foreach (var v in vertices)
{
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
}
// Write edges as lines ("l" elements in .obj format)
foreach (var e in edges)
{
// OBJ uses 1-based indexing
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
}
}
}
}

View File

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

View File

@@ -1,64 +1,332 @@
using System.Buffers.Binary;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using NResLib;
using ParkanPlayground;
using ParkanPlayground.Effects;
using static ParkanPlayground.Effects.FxidReader;
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\default.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scr_pl_1.scr";
// var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream.scr";
var path = "C:\\Program Files (x86)\\Nikita\\Iron Strategy\\MISSIONS\\SCRIPTS\\scream1.scr";
Console.OutputEncoding = Encoding.UTF8;
using var fs = new FileStream(path, FileMode.Open);
// тут всегда число 59 (0x3b) - это число известных игре скриптов
var magic = fs.ReadInt32LittleEndian();
Console.WriteLine($"Count: {magic}");
var entryCount = fs.ReadInt32LittleEndian();
Console.WriteLine($"EntryCount: {entryCount}");
for (var i = 0; i < entryCount; i++)
if (args.Length == 0)
{
Console.WriteLine($"Entry: {i}");
var str = fs.ReadLengthPrefixedString();
Console.WriteLine($"\tStr: {str}");
// тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char*
fs.ReadByte();
Console.WriteLine("Usage: ParkanPlayground <effects-directory-or-fxid-file>");
return;
}
var index = fs.ReadInt32LittleEndian();
Console.WriteLine($"\tIndex: {index}");
var innerCount = fs.ReadInt32LittleEndian();
Console.WriteLine($"\tInnerCount: {innerCount}");
for (var i1 = 0; i1 < innerCount; i1++)
var path = args[0];
bool anyError = false;
var sizeByType = new Dictionary<byte, int>
{
[1] = 0xE0, // 1: Billboard
[2] = 0x94, // 2: Sound
[3] = 0xC8, // 3: AnimParticle
[4] = 0xCC, // 4: AnimBillboard
[5] = 0x70, // 5: Trail
[6] = 0x04, // 6: Point
[7] = 0xD0, // 7: Plane
[8] = 0xF8, // 8: Model
[9] = 0xD0, // 9: AnimModel
[10] = 0xD0, // 10: Cube
};
// Check for --dump-headers flag
bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
if (Directory.Exists(path))
{
var files = Directory.EnumerateFiles(path, "*.bin").ToList();
if (dumpHeaders)
{
var scriptIndex = fs.ReadInt32LittleEndian();
var unkInner2 = fs.ReadInt32LittleEndian();
var unkInner3 = fs.ReadInt32LittleEndian();
var unkInner4 = fs.ReadInt32LittleEndian();
var unkInner5 = fs.ReadInt32LittleEndian();
Console.WriteLine($"\t\tScriptIndex: {scriptIndex}");
Console.WriteLine($"\t\tUnkInner2: {unkInner2}");
Console.WriteLine($"\t\tUnkInner3: {unkInner3}");
Console.WriteLine($"\t\tUnkInner4: {unkInner4}");
Console.WriteLine($"\t\tUnkInner5: {unkInner5}");
var scriptArgumentsCount = fs.ReadInt32LittleEndian();
Console.WriteLine($"\t\tScript Arguments Count: {scriptArgumentsCount}");
for (var i2 = 0; i2 < scriptArgumentsCount; i2++)
// Collect all headers for analysis
var headers = new List<(string name, EffectHeader h)>();
foreach (var file in files)
{
var scriptArgument = fs.ReadInt32LittleEndian();
Console.WriteLine($"\t\t\t{scriptArgument}");
try
{
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length >= 60)
{
headers.Add((Path.GetFileName(file), ReadEffectHeader(br)));
}
}
catch { }
}
// Analyze unique values
Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n");
var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}");
var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}");
var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}");
var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList();
Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}");
Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}");
Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}");
// Check reserved bytes
var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList();
Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}");
// Check scales
var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList();
Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}");
Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ===");
Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale");
Console.WriteLine(new string('-', 100));
foreach (var (name, h) in headers.Take(30))
{
Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})");
}
}
else
{
foreach (var file in files)
{
if (!ValidateFxidFile(file))
{
anyError = true;
}
}
var unkInner7 = fs.ReadInt32LittleEndian();
Console.WriteLine($"\t\tUnkInner7 {unkInner7}");
Console.WriteLine("---");
Console.WriteLine(anyError
? "Validation finished with errors."
: "All FXID files parsed successfully.");
}
}
else if (File.Exists(path))
{
anyError = !ValidateFxidFile(path);
Console.WriteLine(anyError ? "Validation failed." : "Validation OK.");
}
else
{
Console.WriteLine($"Path not found: {path}");
}
void DumpEffectHeader(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
if (fs.Length < 60)
{
Console.WriteLine($"{Path.GetFileName(path)}: file too small");
return;
}
var h = ReadEffectHeader(br);
// Format reserved bytes as hex (show first 8 bytes for brevity)
var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " ");
if (h.Reserved.Length > 8) reservedHex += "...";
// Check if reserved has any non-zero bytes
bool reservedAllZero = h.Reserved.All(b => b == 0);
Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})");
}
catch (Exception ex)
{
Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}");
}
}
bool ValidateFxidFile(string path)
{
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
const int headerSize = 60; // sizeof(EffectHeader) on disk
if (fs.Length < headerSize)
{
Console.WriteLine($"{path}: file too small ({fs.Length} bytes).");
return false;
}
var header = ReadEffectHeader(br);
var typeCounts = new Dictionary<byte, int>();
for (int i = 0; i < header.ComponentCount; i++)
{
long blockStart = fs.Position;
if (fs.Position + 4 > fs.Length)
{
Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X}).");
return false;
}
uint typeAndFlags = br.ReadUInt32();
byte type = (byte)(typeAndFlags & 0xFF);
if (!typeCounts.TryGetValue(type, out var count))
{
count = 0;
}
typeCounts[type] = count + 1;
if (!sizeByType.TryGetValue(type, out int blockSize))
{
Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8}).");
return false;
}
int remaining = blockSize - 4;
if (fs.Position + remaining > fs.Length)
{
Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X}).");
return false;
}
if (type == 1)
{
var def = ReadBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x50)
{
Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50.");
return false;
}
}
else if (type == 2)
{
var def = ReadSoundComponent(br, typeAndFlags);
if (def.SoundNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 3)
{
var def = ReadAnimParticleComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 4)
{
var def = ReadAnimBillboardComponent(br, typeAndFlags);
if (def.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C.");
return false;
}
}
else if (type == 5)
{
var def = ReadTrailComponent(br, typeAndFlags);
if (def.Unknown04To10.Length != 0x10)
{
Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10.");
return false;
}
if (def.TextureNameAndReserved.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40.");
return false;
}
}
else if (type == 6)
{
// Point components have no extra bytes beyond the 4-byte typeAndFlags header.
var def = ReadPointComponent(typeAndFlags);
}
else if (type == 7)
{
var def = ReadPlaneComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x38)
{
Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38.");
return false;
}
}
else if (type == 8)
{
var def = ReadModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x40)
{
Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40.");
return false;
}
}
else if (type == 9)
{
var def = ReadAnimModelComponent(br, typeAndFlags);
if (def.TextureNameAndFlags.Length != 0x48)
{
Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48.");
return false;
}
}
else if (type == 10)
{
var def = ReadCubeComponent(br, typeAndFlags);
if (def.Base.Reserved.Length != 0x3C)
{
Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C.");
return false;
}
}
else
{
// Skip the remaining bytes for other component types.
fs.Position += remaining;
}
}
// Dump a compact per-file summary of component types and counts.
var sb = new StringBuilder();
bool first = true;
foreach (var kv in typeCounts)
{
if (!first)
{
sb.Append(", ");
}
sb.Append(kv.Key);
sb.Append('x');
sb.Append(kv.Value);
first = false;
}
Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]");
if (fs.Position != fs.Length)
{
Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes).");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{path}: exception while parsing: {ex.Message}");
return false;
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

512
README.md
View File

@@ -2,11 +2,13 @@
<div align="center">
<img width="300" height="300" src="https://github.com/user-attachments/assets/dcd9ac8f-7d30-491c-ae6c-537267beb7dc" alt="x86 Registers" />
<img width="817" height="376" alt="Image" src="https://github.com/user-attachments/assets/c4959106-9da4-4c78-a2b7-6c94e360a89e" />
</div>
## Сборка проекта
Проект написан на C# под `.NET 8`
Проект написан на C# под `.NET 9`
Вам должно хватить `dotnet build` для сборки всех проектов отдельно.
@@ -14,13 +16,13 @@
### Состояние проекта
- Распаковка всех `NRes` файлов
- Распаковка всех `TEXM` текстур
+ формат 565 работает некорректно
+ не понятно назначение двух магических чисел в заголовке
- Распаковка данных миссии `.tma`. Пока работает чтение ареалов и кланов.
- Распаковка файла NL. Есть только декодирование заголовка. Формат кажется не используется игрой, а реверс бинарника игры то ещё занятие.
- Распаковка текстуры шрифта формата TFNT. Встроен прямо в UI. По сути шрифт это 4116 байт заголовка и текстура TEXM сразу после.
- Поддержка всех `NRes` файлов - звуки, музыка, текстуры, карты и другие файлы. Есть документация.
- Поддержка всех `TEXM` текстур. Есть документация.
- Поддержка файлов миссий `.tma`.
- Поддержка шрифтов TFNT.
- Поддержка файлов скриптов `.scr`.
- Поддержка файлов параметров `.var`.
- Поддержка файлов схем объектов `.dat`.
### Структура проекта
@@ -34,47 +36,6 @@
Я конечно стараюсь, но ничего не обещаю.
#### NResUI
UI приложение на OpenGL + ImGui.
Туда постепенно добавляю логику.
#### NResLib
Библиотека распаковки формата NRes и всех файлов, которые им запакованы.
Есть логика импорта и экспорта. Работа не завершена, но уже сейчас можно читать любые архивы такого формата.
#### TexmLib
Библиотека распаковки текстур TEXM.
Есть логика импорта и экспорта, хотя к UI последняя не подключена.
#### NLUnpacker
Приложение распаковки NL.
Работа приостановлена, т.к. кажется игра не использует эти файлы.
#### MissionDataUnpacker
Приложение распаковки миссий `.tma`.
Готово чтение ареалов и кланов. Пока в процессе.
#### ParkanPlayground
Пустой проект, использую для локальных тестов.
#### TextureDecoder
Приложение для экспорта текстур TEXM.
Изначально тут игрался с текстурами.
## Для Reverse Engineering-а использую Ghidra
### Наблюдения
@@ -86,6 +47,457 @@ UI приложение на OpenGL + ImGui.
- Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки.
- Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`.
## Как быстро найти текст среди всех файлов игры
```shell
grep -rl --include="*" "s_tree_05" .
```
## Как быстро найти байты среди всех файлов игры
```shell
grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' .
```
## Как работает игра
Главное меню:
Игра сканирует хардкод папку `missions` на наличие файлов миссий. (буквально 01, 02, 03 и т.д.)
Сначала игра читает название миссии из файла `descr` - тут название для меню.
- Одиночные игры - `missions/single.{index}/descr`
- Тренировочные миссии - `missions/tutorial.{index}/descr`
- Кампания - `missions/campaign/campaign.{index1}/descr`
* Далее используются подпапки - `missions/campaign/campaign.{index1}/mission.{index2}/descr`
Как только игра не находит файл `descr`, заканчивается итерация по папкам (понял, т.к. пробуется файл 05 - он не существует).
Загрузка миссии:
Читается файл `ui/game_resources.cfg`
Из этого файла загружаются ресурсы
- `library = "ui\\ui.lib"` - загружается файл `ui.lib`
- `library = "ui\\font.lib"` - загружается файл `font.lib`
- `library = "sounds.lib"` - загружается файл `sounds.lib`
- `library = "voices.lib"` - загружается файл `voices.lib`
Затем игра читает `save/saveslots.cfg` - тут слоты сохранения
Затем `Comp.ini` - тут системные функции, которые используются для загрузки объектов.
```
IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
```
- `Host.url` - этого файла нет
- `palettes.lib` - тут палитры, но этот NRes пустой
- `system.rlb` - не понятно что
- `Textures.lib` - тут текстуры
- `Material.lib` - тут какие-то материалы - не понятно
- `LightMap.lib` - видимо это карты освещения - не понятно
- `sys.lib` - не понятно
- `ScanCode.dsc` - текстовый файл с мапом клавиш
- `command.dsc` - текстовый файл с мапом клавиш
Тут видимо идёт конфигурация ввода
- `table_1.man` - текстовый файл
- `table_2.man` - текстовый файл
- `hero.man` - текстовый файл
- `addition.man` - текстовый файл
- Снова `table_1.man`
- Снова `table_1.man`
- `M1.tbl` - текстовый файл
- Снова `table_2.man`
- Снова `table_2.man`
- `M2.tbl` - текстовый файл
- Снова `hero.man`
- Снова `hero.man`
- `HERO.TBL`
- Снова `addition.man`
- `ui/hq.cfg`
- Снова `ui/hq.cfg`
Дальше непосредственно читается миссия
- `mission.cfg` - метадата миссии
- `units\\units\\prebld\\scr_pre1.dat` из метаданных `object prebuild` - `cp` файл (грузятся подряд все)
- Опять `ui/hq.cfg`
- `mistips.mis` - описание для игрока (экран F1)
- `scancode.dsc` - хз
- `command.dsc` - хз
- `ui_hero.man` - хз
- `ui_bots.man` - хз
- `ui_hq.man` - хз
- `ui_other.man` - хз
- Цикл чтения курсоров
* `ui/cursor.cfg` - тут настройки курсора.
* `ui/{name}` - курсор
- Снова `mission.cfg` - метадата миссии
- `descr` - название
- `data/textres.cfg` - конфиг текстов
- Снова `mission.cfg` - метадата миссии
- Ещё раз `mission.cfg` - метадата миссии
- `ui/minimap.lib` - NRes с текстурами миникарты.
- `messages.cfg` - Tutorial messages
УРА НАКОНЕЦ-ТО `data.tma`
- Из `.tma` берётся LAND строка (я её так назвал)
- `DATA\\MAPS\\SC_3\\land1.wea`
- `DATA\\MAPS\\SC_3\\land2.wea`
- `BuildDat.lst` - Behaviour will use these schemes to Build Fortification
- `DATA\\MAPS\\SC_3\\land.map`
- `DATA\\MAPS\\SC_3\\land.msh`
- `effects.rlb`
Цикл по кланам из `.tma`
- `MISSIONS\\SCRIPTS\\screampl.scr`
- `varset.var`
- `MISSIONS\\SCRIPTS\\varset.var`
- `MISSIONS\\SCRIPTS\\screampl.fml`
- `missions/single.01/sky.ske`
- `missions/single.01/sky.wea`
Дальше начинаются объекты игры
- `"UNITS\\BUILDS\\BUNKER\\mbunk01.dat"` - cp файл
## Загрузка `cp` файлов
`cp` файл - схема. Он содержит дерево частей объекта.
`cp` файл читается в `ArealMap.dll/CreateObjectFromScheme`
В зависимости от типа объекта внутри схемы (байты 4..8) выбирается функция, с помощью которой загружается схема.
Функция выбирается на основе файла `Comp.ini`.
- Для ClassBuilding (0x80000000) - вызывается функция c классом 3 (по таблице ниже Building).
- Для всех остальных - функция с классом 4 (по таблице ниже Agent).
На основе файла `Comp.ini` и первом вызове внутри функции `World3D.dll/CreateObject` ремаппинг id:
| Logic ID | ClassName | Function |
|:--------:|:------------:|--------------------------------|
| 1 | Landscape | `terrain.dll LoadLandscape` |
| 2 | Agent | `animesh.dll LoadAgent` |
| 3 | Building | `terrain.dll LoadBuilding` |
| 4 | Agent | `animesh.dll LoadAgent` |
| 5 | Camera | `terrain.dll LoadCamera` |
| 7 | Atmosphere | `terrain.dll CreateAtmosphere` |
| 9 | Agent | `animesh.dll LoadAgent` |
| 10 | Agent | `animesh.dll LoadAgent` |
| 11 | Research | `misload.dll LoadResearch` |
| 12 | Agent | `animesh.dll LoadAgent` |
Будет дополняться по мере реверса.
Всем этим функциям передаётся `nres_file_name, nres_entry_name, 0, player_id`
## `fr FORT` файл
Всегда 0x80 байт
Содержит 2 ссылки на файлы:
- `.bas`
- `.ctl` - вызывается `LoadAgent`
## `.msh`
### Описание ниже валидно только для моделей роботов и зданий.
##### Land.msh использует другой формат, хотя 03 файл это всё ещё точки.
Загружается в `AniMesh.dll/LoadAniMesh`
- Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD
```
нулевому элементу добавляется флаг 0x1000000
Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08)
Если интерполируется анимация -0.5s короче чем magic1 у файла 13
И у файла есть OffsetIntoFile13
И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда)
Тогда вместо IndexInFile08 используется значение из файла 13 по этому оффсету (второй байт)
```
- Тип 02 - описание одного LOD Submesh
```
Вначале идёт заголовок 0x8C (140) байт
В заголовке:
8 Vector3 (x,y,z) - bounding box
1 Vector4 - center
1 Vector3 - bottom
1 Vector3 - top
1 float - xy_radius
Далее инфа про куски меша
```
- Тип 03 - это вершины (vertex)
- Тип 06 - индексы треугольников в файле 03
- Тип 04 - скорее всего какие-то цвета RGBA или типа того
- Тип 08 - меш-анимации (см файл 01)
```
Индексируется по IndexInFile08 из файла 01 либо по файлу 13 через OffsetIntoFile13
Структура:
Vector3 position;
float time; // содержит только целые секунды
short rotation_x; // делится на 32767
short rotation_y; // делится на 32767
short rotation_z; // делится на 32767
short rotation_w; // делится на 32767
---
Игра интерполирует анимацию между текущим стейтом и следующим по time.
Если время интерполяции совпадает с исходным time, жёстко берётся первый стейт из 0x13.
Если время интерполяции совпадает с конечным time, жёстко берётся второй стейт из 0x13.
Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time)
```
- Тип 12 - microtexture mapping
- Тип 13 - короткие меш-анимации (почему я это не дописал?)
```
Буквально (hex)
00 01 01 02 ...
```
- Тип 0A - ссылка на части меша, не упакованные в текущий меш (например у бункера 4 и 5 части хранятся в parts.rlb)
```
Не имеет фиксированной длины. Хранит строки в следующем формате.
Игра обращается по индексу, пропуская суммарную длину и пропуская 4 байта на каждую строку (длина).
т.е. буквально файл выглядит так
00 00 00 00 - пустая строка
03 00 00 00 - длина строки 1
73 74 72 00 - строка "str" + null terminator
.. и повторяется до конца файла
Кол-во элементов из файла 01 должно быть равно кол-ву строк в этом файле, хотя игра это не проверяет.
Если у элемента эта строка равна "central", ему выставляется флаг (flag |= 1)
```
## `.wea`
Загружается в `World3D.dll/LoadMatManager`
По сути это текстовый файл состоящий из 2 частей:
- Материалы
```
{count}
{id} {name}
```
- Карты освещения
```
LIGHTMAPS
{count}
{id} {name}
```
Может как-то анимироваться. Как - пока не понятно.
## Lightmap (TEXM)
У текстур использующихся как карты освещения специальные имена.
`some_name.00` или `some_name.0`
Первая цифра после `.` это группа палет (всего 0x11), вторая цифра индекс в этой группе.
```
All lightmaps named whatever.0 → they all end up with no DirectDraw palette attached.
```
## Люблю разработчиков
У них получилось сделать LightSource и MaterialDescriptor чётко одинаковыми по размеру (0x5c) из-за чего я потерял 3 дня прыгая по указателям
## `FXID` - файл эффектов
По сути представляет собой последовательный список саб-эффектов идущих друг за другом.
Всего существует 9 (1..9) видов эффектов: !описать по мере реверса
Выглядит так, словно весь файл это тоже эффект сам по себе.
```
0x00-0x04 Type (игра обрезает 1 байт, и поддерживает только значения 1-9)
0x04-0x08 unknown
0x08-0x0C unknown
0x0C-0x10 unknown
0x10-0x14 EffectTemplateFlags
...
0x30-0x34 ScaleX
0x34-0x38 ScaleY
0x38-0x3C ScaleZ
enum EffectTemplateFlags : uint32_t
{
EffectTemplateFlag_RandomizeStrength = 0x0001, // used in UpdateDust
EffectTemplateFlag_RandomOffset = 0x0008, // random worldTransform offset
EffectTemplateFlag_TriangularShape = 0x0020, // post-process strength as 0→1→0
EffectTemplateFlag_OnlyWhenEnvBit0Off = 0x0080, // gating in ComputeEffectStrength
EffectTemplateFlag_OnlyWhenEnvBit0On = 0x0100, // gating in ComputeEffectStrength
EffectTemplateFlag_MultiplyByLife = 0x0200, // multiply strength by life progress
EffectTemplateFlag_EnvFlag2_IfNotSet = 0x0800, // if NOT set → envFlags |= 0x02
EffectTemplateFlag_EnvFlag10_IfSet = 0x1000, // if set → envFlags |= 0x10
EffectTemplateFlag_AlwaysEmitDust = 0x0010, // ignore piece state / flags
EffectTemplateFlag_IgnorePieceState = 0x8000, // treat piece default state as OK
EffectTemplateFlag_AutoDeleteAtFull = 0x0002, // delete when strength >= 1
EffectTemplateFlag_DetachAfterEmit = 0x0004, // detach from attachment after update
};
```
# ЕСЛИ ГДЕ-ТО ВИДИШЬ PTR_func_ret1_no_args, ТО ЭТО ShaderConfig
В рендеринге порядок дата-стримов такой
position
normal
color
# Внутренняя система ID
- `1` - unknown (implemented by CLandscape) видимо ILandscape
- `3` - unknown (implemented by CAtmosphere) видимо IAtmosphere
- `4` - IShader
- `5` - ITerrain
- `6` - IGameObject
- `7` - ISettings
- `8` - ICamera
- `9` - IQueue
- `10` - IControl
- `0xb` - IAnimation
- `0xc` - IShadeStatsBuilder (придумал сам implemented by CShade)
- `0xd` - IMatManager
- `0xe` - ILightManager
- `0xf` - IShade
- `0x10` - IBehaviour
- `0x11` - IBasement
- `0x12` - ICamera2 или IBufferingCamera
- `0x13` - IEffectManager
- `0x14` - IPosition
- `0x15` - IAgent
- `0x16` - ILifeSystem
- `0x17` - IBuilding - точно он, т.к. ArealMap.CreateObject на него проверяет
- `0x18` - IMesh2
- `0x19` - IManManager
- `0x20` - IJointMesh
- `0x21` - IShadowProcessor (придумал сам implemented by CShade)
- `0x22` - unknown (implement by CLandscape)
- `0x23` - IGameSettingsRoot
- `0x24` - IGameObject2
- `0x25` - unknown (implemented by CAniMesh)
- `0x26` - unknown (implemented by CAniMesh and CControl and CWizard)
- `0x28` - ICollObject
- `0x29` - IPhysicalModel
- `0x101` - I3DRender
- `0x102` - ITexture (writable)
- `0x103` - IColorLookup
- `0x104` - IBitmapFont
- `0x105` - INResFile
- `0x106` - NResFileMetadata
- `0x107` - I3DSound
- `0x108` - IListenerTransform
- `0x109` - ISoundPool
- `0x10a` - ISoundBuffer
- `0x10c` - ICDPlayer
- `0x10d` - IVertexBuffer
- `0x201` - IWizard
- `0x202` - IItemManager
- `0x203` - ICollManager
- `0x301` - IArealMap
- `0x302` - ISystemArealMap
- `0x303` - IHallway
- `0x304` - IDistributor
- `0x401` - ISuperAI
- `0x501` - MissionData
- `0x502` - ResTree
- `0x700` - INetWatcher
- `0x701` - INetworkInterface
## SuperAI = Clan с точки зрения индексации
Т.е. у каждого клана свой SuperAI
## Опции
World3D.dll содержит функцию CreateGameSettings.
Она создаёт объект настроек и далее вызывает методы в соседних библиотеках.
- Terrain.dll - InitializeSettings
- Effect.dll - InitializeSettings
- Control.dll - InitializeSettings
Остальные наверное не трогают настройки.
| Resource ID | wOptionID | Name | Default | Description |
|:-----------:|:---------------:|:--------------------------:|:-------:|--------------------|
| 1 | 100 (0x64) | "Texture detail" | | |
| 2 | 101 (0x65) | "3D Sound" | | |
| 3 | 102 (0x66) | "Mouse sensitivity" | | |
| 4 | 103 (0x67) | "Joystick sensitivity" | | |
| 5 | !not a setting! | "Illegal wOptionID" | | |
| 6 | 104 (0x68) | "Wait for retrace" | | |
| 7 | 105 (0x69) | "Inverse mouse X" | | |
| 8 | 106 (0x6a) | "Inverse mouse Y" | | |
| 9 | 107 (0x6b) | "Inverse joystick X" | | |
| 10 | 108 (0x6c) | "Inverse joystick Y" | | |
| 11 | 109 (0x6d) | "Use BumpMapping" | | |
| 12 | 110 (0x6e) | "3D Sound quality" | | |
| 13 | 90 (0x5a) | "Reverse sound" | | |
| 14 | 91 (0x5b) | "Sound buffer frequency" | | |
| 15 | 92 (0x5c) | "Play sound buffer always" | | |
| 16 | 93 (0x5d) | "Select best sound device" | | |
| ---- | 30 (0x1e) | ShadeConfig | | из файла shade.cfg |
| ---- | (0x8001e) | | | добавляет AniMesh |
## Goodies
```c++
// Тип положения объекта в пространстве. Используется при расчёте эффектов,
// расстановке объектов и для получения/выдачи их матриц трансформации.
enum EPlacementType
{
// 0: Полная мировая матрица, ориентированная "смотрю на цель".
// Входная matrix — это мировая матрица (look-at к какой-то цели).
// Движок при необходимости переводит её в локальное/костное пространство
// (через inverse(parent/joint) * world и т.п.).
// Смысл: "Построй мне мировую матрицу так, чтобы я смотрел на цель;
// если я привязан к кости — учти это."
Placement_WorldLookAtTarget = 0,
// 1: Мировая матрица, где основная ось объекта (this->direction)
// выровнена по направлению к цели.
// Входная matrix — мировая; логика выравнивания использует внутренний
// вектор direction и целевую точку/направление.
// Смысл: "Используй мой внутренний direction как основную ось
// и как можно лучше направь её в сторону цели."
Placement_WorldAlignDirectionToTarget = 1,
// 2: Мировая матрица, полностью задаётся внутренним состоянием объекта.
// Входная matrix трактуется как "standalone world" — готовая мировая
// матрица объекта. Движок лишь переводит её во внутреннее локальное/
// костное пространство (inverse(parent/joint) * world), без look-at
// и без выравнивания по цели.
// Смысл: "Вот моя конечная мировая матрица. Просто встрои её в иерархию,
// не трогая ориентацию дополнительной логикой."
Placement_WorldFromStoredTransform = 2,
// 3: Мировая матрица, ориентированная вдоль вектора движения, но с учётом цели.
// Используется предыдущая мировая позиция + текущая + target, чтобы
// построить базис: основная ось — направление движения, вспомогательная —
// направление к цели.
// Смысл: "Ориентируй меня по вектору моего движения, но также учитывай цель."
Placement_WorldAlignMotionToTarget = 3
};
```
## Контакты
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).
Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ namespace TexmLib;
/// <param name="MipmapCount">Кол-во мипмапов (уменьшенные копии текстуры)</param>
/// <param name="Stride">Сколько БИТ занимает 1 пиксель</param>
/// <param name="Magic1">Неизвестно</param>
/// <param name="Magic2">Неизвестно</param>
/// <param name="FormatOptionFlags">Дополнительные флаги для текстуры</param>
/// <param name="Format">Формат пикселя(4444, 8888, 888)</param>
public record TexmHeader(
string TexmAscii,
@@ -23,7 +23,7 @@ public record TexmHeader(
int MipmapCount,
int Stride,
int Magic1,
int Magic2,
int FormatOptionFlags,
int Format
);
@@ -180,14 +180,20 @@ public class TexmFile
{
var rawPixel = span.Slice(i, 2);
var g = (byte)(((rawPixel[0] >> 3) & 0b11111) * 255 / 31);
var b = (byte)((((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111)) * 255 / 63);
var r = (byte)((rawPixel[1] & 0b11111) * 255 / 31);
// swap endianess
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = r;
var r = (byte)(((rawPixel[0] >> 3) & 0b11111) * 255 / 31);
var g = (byte)((((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111)) * 255 / 63);
var b = (byte)((rawPixel[1] & 0b11111) * 255 / 31);
result[i / 2 * 4 + 0] = (byte)(0xff - r);
result[i / 2 * 4 + 1] = (byte)(0xff - g);
result[i / 2 * 4 + 2] = (byte)(0xff - b);
result[i / 2 * 4 + 3] = 0xff;
// swap endianess back
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
}
return result;
@@ -202,6 +208,9 @@ public class TexmFile
{
var rawPixel = span.Slice(i, 2);
// swap endianess
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
var a = (byte)(((rawPixel[0] >> 4) & 0b1111) * 17);
var b = (byte)(((rawPixel[0] >> 0) & 0b1111) * 17);
var g = (byte)(((rawPixel[1] >> 4) & 0b1111) * 17);
@@ -211,6 +220,9 @@ public class TexmFile
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = a;
// swap endianess back
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
}
return result;
@@ -247,16 +259,21 @@ public class TexmFile
for (var i = 0; i < span.Length; i += 4)
{
var rawPixel = span.Slice(i, 4);
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
var b = rawPixel[0];
var r = rawPixel[0];
var g = rawPixel[1];
var r = rawPixel[2];
var b = rawPixel[2];
var a = rawPixel[3];
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
result[i + 3] = a;
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
}
return result;

View File

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

View File

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

View File

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

View File

@@ -1,22 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

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

3
VarsetLib/VarsetItem.cs Normal file
View File

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

View File

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

82
VarsetLib/VarsetParser.cs Normal file
View File

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

629
material_field_analysis.csv Normal file
View File

@@ -0,0 +1,629 @@
FileName,MaterialTypeMaybe,IsTerrainMaybe,SupportsTextureMode6,GlobalTransparencyMaybe,GlobalSelfIlluminationMaybe,StageCount,AnimCount,HasField18NonZero
B_PG3,0,0,True,1,000,0,000,1,1,True
B_PG4,0,0,True,1,000,0,000,1,1,True
B_PG6,0,0,True,1,000,0,000,1,1,True
B_PG2,1,0,False,1,000,0,000,1,1,True
B_PG5,0,0,True,1,000,0,000,1,1,True
B_GEN_05,0,0,True,1,000,0,000,1,1,True
B_GEN_06,0,0,True,1,000,0,000,1,1,True
B_GEN_07,0,0,True,1,000,0,000,1,1,True
B_GEN_08,0,0,True,1,000,0,000,1,1,True
B_DD1CL,0,0,True,1,000,0,000,8,1,True
B_DD1DK,0,0,True,1,000,0,000,1,1,True
B_DD2,0,0,True,1,000,0,000,1,1,True
B_RL_06,0,0,True,1,000,0,000,1,1,True
B_RL_02,0,0,True,1,000,0,000,1,1,True
B_RL_03,0,0,True,1,000,0,000,1,1,True
B_RL_19,0,0,True,1,000,0,000,1,1,True
B_RL_20,0,0,True,1,000,0,000,1,1,True
B_RL_21,0,0,True,1,000,0,000,1,1,True
B_S12,0,0,True,1,000,0,000,1,1,True
B_S0,0,0,True,1,000,0,000,1,1,True
B_S0A1,0,0,True,1,000,0,000,1,1,True
B_S1,0,0,True,1,000,0,000,1,1,True
B_S10,0,0,True,1,000,0,000,1,1,True
B_S2,0,0,True,1,000,0,000,1,1,True
B_S13,0,0,True,1,000,0,000,4,1,True
B_S14,0,0,True,1,000,0,000,1,1,True
B_S6,0,0,True,1,000,0,000,1,1,True
B_S3,0,0,True,1,000,0,000,1,1,True
B_S4,0,0,True,1,000,0,000,1,1,True
B_S5,1,0,False,1,000,0,000,1,1,True
B_S7,0,0,True,1,000,0,000,1,1,True
B_S0A2,0,0,True,1,000,0,000,1,1,True
R_PG3,0,0,True,1,000,0,000,1,1,True
R_PG4,0,0,True,1,000,0,000,1,1,True
R_PG6,0,0,True,1,000,0,000,1,1,True
R_PG7,0,0,True,1,000,0,000,1,1,True
R_PG2,1,0,False,1,000,0,000,1,1,True
R_PG5,0,0,True,1,000,0,000,1,1,True
R_GEN_05,0,0,True,1,000,0,000,1,1,True
R_GEN_06,0,0,True,1,000,0,000,1,1,True
R_GEN_07,0,0,True,1,000,0,000,1,1,True
R_GEN_08,0,0,True,1,000,0,000,1,1,True
R_DD1,0,0,True,1,000,0,000,1,1,True
R_DD1CL,0,0,True,1,000,0,000,1,1,True
R_DD1DK,0,0,True,1,000,0,000,1,1,True
R_DD2,0,0,True,1,000,0,000,1,1,True
R_RL_06,0,0,True,1,000,0,000,1,1,True
R_RL_02,0,0,True,1,000,0,000,1,1,True
R_RL_03,0,0,True,1,000,0,000,1,1,True
R_RL_20,0,0,True,1,000,0,000,1,1,True
R_RL_21,0,0,True,1,000,0,000,1,1,True
R_S12,0,0,True,1,000,0,000,1,1,True
R_S0,0,0,True,1,000,0,000,1,1,True
R_S0A1,0,0,True,1,000,0,000,1,1,True
R_S1,0,0,True,1,000,0,000,1,1,True
R_S10,0,0,True,1,000,0,000,1,1,True
R_S2,0,0,True,1,000,0,000,1,1,True
R_S6,0,0,True,1,000,0,000,1,1,True
R_S3,0,0,True,1,000,0,000,1,1,True
R_S4,0,0,True,1,000,0,000,1,1,True
R_S5,1,0,False,1,000,0,000,1,1,True
R_S7,0,0,True,1,000,0,000,1,1,True
R_S0A2,0,0,True,1,000,0,000,1,1,True
R_S13,0,0,True,1,000,0,000,4,1,True
R_S14,0,0,True,1,000,0,000,1,1,True
SKY1,1,0,False,1,000,0,000,1,1,True
B_RLW_4,0,0,True,1,000,0,000,1,1,True
R_RLW_4,0,0,True,1,000,0,000,1,1,True
DEFAULT,1,0,False,1,000,0,000,1,1,True
EXPL1,1,0,False,1,000,0,000,16,1,True
JET1,2,8,False,1,000,0,000,6,1,True
JET2,2,8,False,1,000,0,000,4,1,True
SHOOT1,1,0,False,1,000,0,000,4,1,True
JET3,2,8,False,1,000,0,000,1,1,True
SUN1,1,0,False,1,000,0,000,1,1,True
JET4,2,8,False,1,000,0,000,4,1,True
CLOUD1,1,0,False,1,000,0,000,15,1,True
SPLASH1,1,0,False,1,000,0,000,5,1,True
TRACE1,1,0,False,1,000,0,000,4,1,True
TRACE2,1,0,False,1,000,0,000,4,1,True
B_COMP_2,0,0,True,1,000,0,000,1,1,True
R_COMP_2,0,0,True,1,000,0,000,1,1,True
EXPL2,1,0,False,1,000,0,000,16,1,True
JET5,2,8,False,1,000,0,000,1,1,True
B_COMP_1,0,0,True,1,000,0,000,4,1,True
B_COMP_3,1,0,False,1,000,0,000,4,1,True
LIGHT1,2,8,False,1,000,0,000,1,1,True
BLACK,0,0,False,1,000,0,000,1,1,True
R_RLW_5,0,0,True,1,000,0,000,2,1,True
JET6,2,8,False,1,000,0,000,6,1,True
SHOOT2,1,0,False,1,000,0,000,4,1,True
JET7,2,8,False,1,000,0,000,4,1,True
SPLASH2,1,0,False,1,000,0,000,5,1,True
CLOUD2,1,0,False,1,000,0,000,15,1,True
R_MN_01,0,0,True,1,000,0,000,1,1,True
B_MN_01,0,0,True,1,000,0,000,1,1,True
B_MN_02,0,0,True,1,000,0,000,1,1,True
B_COMP_3G,1,0,False,1,000,0,000,4,1,True
B_COMP_3R,1,0,False,1,000,0,000,4,1,True
B_COMP_3B,1,0,False,1,000,0,000,4,1,True
JET8,2,8,False,1,000,0,000,6,1,True
JET9,2,8,False,1,000,0,000,4,1,True
B_MN_01R,0,0,True,1,000,0,000,4,1,True
B_MN_01G,0,0,True,1,000,0,000,4,1,True
B_MN_01B,0,0,True,1,000,0,000,4,1,True
STONE00,0,0,True,1,000,0,000,1,1,True
STONE01,0,0,True,1,000,0,000,1,1,True
TREE00,1,0,False,1,000,0,000,1,1,True
TREE01,0,0,False,1,000,0,000,1,1,True
TREE02,0,0,False,1,000,0,000,1,1,True
TREE03,1,0,False,1,000,0,000,1,1,True
TREE04,0,0,False,1,000,0,000,1,1,True
TREE05,0,0,False,1,000,0,000,1,1,True
R_RL_26,0,0,True,1,000,0,000,4,1,True
R_RL_26REV,0,0,True,1,000,0,000,4,1,True
R_RL_27,0,0,True,1,000,0,000,4,1,True
R_RL_27REV,0,0,True,1,000,0,000,4,1,True
R_RL_25,0,0,True,1,000,0,000,4,1,True
R_RL_25REV,0,0,True,1,000,0,000,4,1,True
R_RL_24,0,0,True,1,000,0,000,1,1,True
TREE06,1,0,False,1,000,0,000,1,1,True
TREE07,1,0,False,1,000,0,000,1,1,True
B_HNG_01,0,0,True,1,000,0,000,1,1,True
B_HNG_02,1,0,False,1,000,0,000,1,1,True
R_PG11,0,0,True,1,000,0,000,1,1,True
B_PG11,0,0,True,1,000,0,000,1,1,True
B_LIGHT,0,0,False,1,000,0,000,1,1,True
B_PG12,0,0,True,1,000,0,000,1,1,True
R_PG12,0,0,True,1,000,0,000,1,1,True
B_MTP_01,0,0,True,1,000,0,000,1,1,True
B_MTP_02,0,0,True,1,000,0,000,3,1,True
R_RL_19,0,0,True,1,000,0,000,1,1,True
B_RB_04,0,0,True,1,000,0,000,1,1,True
R_RB_04,0,0,True,1,000,0,000,1,1,True
R_RBW_03,0,0,True,1,000,0,000,1,1,True
DUST,1,0,False,1,000,0,000,4,1,True
SMOKE,1,0,False,1,000,0,000,4,1,True
FIRE_SMOKE,1,0,False,1,000,0,000,26,1,True
DARK_CLOUD,1,0,False,1,000,0,000,4,1,True
LASER_G,2,8,False,1,000,0,000,4,1,True
LASER_R,2,8,False,1,000,0,000,4,1,True
GLOW,1,0,False,1,000,0,000,1,1,True
LASER_Y,2,8,False,1,000,0,000,4,1,True
B_EFFECT_001,1,0,False,1,000,0,000,1,1,True
R_RMW_9,0,0,True,1,000,0,000,3,1,True
R_PG14,0,0,True,1,000,0,000,1,1,True
SHIELD1,2,8,False,1,000,0,000,7,1,True
B_LT_01,0,0,True,1,000,0,000,1,1,False
B_LT_03,0,0,True,1,000,0,000,1,1,True
B_LBL_01,0,0,True,1,000,0,000,8,8,True
CUTTER,1,0,False,1,000,0,000,4,1,True
GLOW_B,1,0,False,1,000,0,000,1,1,True
GLOW_G,1,0,False,1,000,0,000,1,1,True
GLOW_R,2,8,False,1,000,0,000,1,1,True
TREE08,1,0,False,1,000,0,000,1,1,True
TREE09,1,0,False,1,000,0,000,1,1,True
TREE10,1,0,False,1,000,0,000,1,1,True
TREE11,1,0,False,1,000,0,000,1,1,False
TREE12,1,0,False,1,000,0,000,1,1,True
TREE13,1,0,False,1,000,0,000,1,1,True
TREE14,1,0,False,1,000,0,000,1,1,True
TREE15,1,0,False,1,000,0,000,1,1,True
BALL_B,1,0,False,1,000,0,000,1,1,True
BALL_G,1,0,False,1,000,0,000,1,1,True
BALL_R,1,0,False,1,000,0,000,1,1,True
BALL_Y,1,0,False,1,000,0,000,1,1,True
BALL_W,1,0,False,1,000,0,000,1,1,True
GLOW_Y,1,0,False,1,000,0,000,1,1,True
GLOW_W,1,0,False,1,000,0,000,1,1,True
SPLASH3,1,0,False,1,000,0,000,5,1,True
LASER_B,2,8,False,1,000,0,000,4,1,True
LIGHT_W,1,0,False,1,000,0,000,1,1,True
SHOOT3,1,0,False,1,000,0,000,4,1,True
CUTTER_HIT,1,0,False,1,000,0,000,3,1,True
RAILGUN_HIT,2,8,False,1,000,0,000,3,1,True
EXPL3,1,0,False,1,000,0,000,24,1,True
R_PG9,0,0,True,1,000,0,000,1,1,True
BIRD_B,0,0,True,1,000,0,000,1,1,True
BIRD_T,0,0,True,1,000,0,000,1,1,True
BIRD_W,0,0,True,1,000,0,000,1,1,True
MATL,0,0,True,1,000,0,000,1,1,True
B_MTP_06,0,0,True,1,000,0,000,1,1,True
B_MTP_07,0,0,True,1,000,0,000,1,1,True
B_MTP_04,0,0,True,1,000,0,000,1,1,True
B_MTP_03,0,0,True,1,000,0,000,1,1,True
B_MTP_03R,0,0,True,1,000,0,000,1,1,True
B_MTP_03B,0,0,True,1,000,0,000,1,1,True
B_MTP_05,0,0,True,1,000,0,000,1,1,True
B_MTP_06B,0,0,True,1,000,0,000,1,1,True
B_MTP_06R,0,0,True,1,000,0,000,1,1,True
B_MTP_06G,0,0,True,1,000,0,000,1,1,True
B_MTP_06Y,0,0,True,1,000,0,000,1,1,True
B_MTP_07Y,0,0,True,1,000,0,000,1,1,True
B_MTP_07R,0,0,True,1,000,0,000,1,1,True
B_MTP_07G,0,0,True,1,000,0,000,1,1,True
B_MTP_07B,0,0,True,1,000,0,000,1,1,True
R_PG17,0,0,True,1,000,0,000,1,1,True
R_PG18,0,0,True,1,000,0,000,1,1,True
R_PG19,0,0,True,1,000,0,000,1,1,True
R_PG20,0,0,True,1,000,0,000,1,1,True
R_SLD_POD,0,0,True,1,000,0,000,1,1,True
R_SLD_GL,1,0,False,1,000,0,000,1,1,True
BIRD_U,0,0,True,1,000,0,000,1,1,True
SHIELD_R,2,8,False,1,000,0,000,10,1,True
SHIELD_G,2,8,False,1,000,0,000,10,1,True
SHIELD_B,2,8,False,1,000,0,000,10,1,True
B_MTP_07E,0,0,True,1,000,0,000,1,1,True
B_MTP_08,0,0,True,1,000,0,000,3,1,True
LASER_R_HIT,2,8,False,1,000,0,000,6,1,True
LASER_G_HIT,2,8,False,1,000,0,000,6,1,True
LASER_B_HIT,2,8,False,1,000,0,000,6,1,True
LASER_Y_HIT,2,8,False,1,000,0,000,6,1,True
B_P00,0,0,True,1,000,0,000,1,1,True
AIGRETTE_Y,1,0,False,1,000,0,000,3,1,True
AIGRETTE_R,1,0,False,1,000,0,000,3,1,True
AIGRETTE_G,1,0,False,1,000,0,000,3,1,True
AIGRETTE_B,1,0,False,1,000,0,000,3,1,True
B_LT_01R,0,0,True,1,000,0,000,1,1,False
B_LT_01G,0,0,True,1,000,0,000,1,1,False
B_LT_01B,0,0,True,1,000,0,000,1,1,False
B_LT_01Y,0,0,True,1,000,0,000,1,1,False
B_COMP_4,0,0,True,1,000,0,000,1,1,True
B_COMP_4R,0,0,True,1,000,0,000,1,1,True
B_COMP_4G,0,0,True,1,000,0,000,1,1,True
B_COMP_4B,0,0,True,1,000,0,000,1,1,True
B_COMP_4Y,0,0,True,1,000,0,000,1,1,True
B_LT_03B,0,0,True,1,000,0,000,1,1,True
B_LT_03R,0,0,True,1,000,0,000,1,1,True
B_LT_03G,0,0,True,1,000,0,000,1,1,True
B_LT_03Y,0,0,True,1,000,0,000,1,1,True
B_MN_03,0,0,True,1,000,0,000,1,1,True
B_MN_03R,0,0,True,1,000,0,000,1,1,True
B_MN_03G,0,0,True,1,000,0,000,1,1,True
B_MN_03B,0,0,True,1,000,0,000,1,1,True
B_MN_03Y,0,0,True,1,000,0,000,1,1,True
B_LT_04,0,0,True,1,000,0,000,1,1,True
CLOUD3,1,0,False,1,000,0,000,15,1,True
B_GEN_09,0,0,True,1,000,0,000,1,1,True
R_GEN_09,0,0,True,1,000,0,000,1,1,True
B_GEN_08L,0,0,True,1,000,0,000,1,1,True
B_S3L,0,0,True,1,000,0,000,1,1,True
B_S4L,0,0,True,1,000,0,000,1,1,True
PORTAL_001,1,0,False,1,000,0,000,2,1,False
PORTAL_004,1,0,False,1,000,0,000,2,1,True
B_GEN_10,0,0,True,1,000,0,000,1,1,True
B_GEN_11,0,0,True,1,000,0,000,1,1,True
HWOY01,1,0,False,1,000,0,000,1,1,True
FTREE1,1,0,False,1,000,0,000,1,1,True
HTREE1,1,0,False,1,000,0,000,1,1,True
KORA,0,0,True,1,000,0,000,1,1,True
KORA2,0,0,True,1,000,0,000,1,1,True
SKIN02,0,0,True,1,000,0,000,1,1,True
B_LT_02,0,0,True,1,000,0,000,1,1,True
EXPL4,1,0,False,1,000,0,000,29,1,True
SHOOT4,1,0,False,1,000,0,000,4,1,True
SPLASH4,1,0,False,1,000,0,000,6,1,True
EXPL5,1,0,False,1,000,0,000,4,1,True
EXPL6,1,0,False,1,000,0,000,8,1,True
SPLASH5,1,0,False,1,000,0,000,6,1,True
SPLASH6,1,0,False,1,000,0,000,6,1,True
B_MTP_02L,0,0,True,1,000,0,000,3,1,True
B_MTP_01L,0,0,True,1,000,0,000,1,1,True
B_GEN_05L,0,0,True,1,000,0,000,1,1,True
B_P08,1,0,False,1,000,0,000,2,1,True
B_P10,1,0,False,1,000,0,000,2,1,True
B_P11,1,0,False,1,000,0,000,2,1,True
B_P13,1,0,False,1,000,0,000,2,1,True
LAMP,1,0,False,1,000,0,000,1,1,True
LASER_W_HIT,2,8,False,1,000,0,000,6,1,True
SPITTLE_R,1,0,False,1,000,0,000,16,1,True
SPITTLE_G,1,0,False,1,000,0,000,16,1,True
SHOOT5,1,0,False,1,000,0,000,4,1,True
ALIEN_BALL,1,0,False,1,000,0,000,1,1,True
ALIENSKIN,0,0,True,1,000,0,000,1,1,True
CLOUD_G,1,0,False,1,000,0,000,15,1,True
CLOUD_R,1,0,False,1,000,0,000,15,1,True
CLOUD_B,1,0,False,1,000,0,000,15,1,True
BUILD_SPHERE,1,0,False,1,000,0,000,4,1,True
BUILD_MIRACLE,1,0,False,1,000,0,000,4,1,True
BUILD_GALO,1,0,False,1,000,0,000,4,1,True
SHIELD_Y,2,8,False,1,000,0,000,10,1,True
PLAS_BUL,2,8,False,1,000,0,000,4,1,True
PLAS_SHOOT,2,8,False,1,000,0,000,6,1,True
GLOW_V,1,0,False,1,000,0,000,1,1,True
LASER_V,2,8,False,1,000,0,000,4,1,True
LASER_V_HIT,2,8,False,1,000,0,000,6,1,True
ENV_MOON,1,0,False,1,000,0,000,1,1,True
ENV_STARS,1,0,False,1,000,0,000,1,1,True
ENV_FLARE_00,2,8,False,1,000,0,000,1,1,True
ENV_FLARE_01,2,8,False,1,000,0,000,1,1,True
SHOOK,1,0,False,1,000,0,000,4,1,True
SHOOK_B,1,0,False,1,000,0,000,4,1,True
ENV_CLOUDS,1,0,False,1,000,0,000,1,1,True
BUILD_SHOOT,1,0,False,1,000,0,000,1,1,True
BUILD_FLY,1,0,False,1,000,0,000,8,1,True
ENV_SUN,1,0,False,1,000,0,000,1,1,False
FIRE_SMOKE_W,1,0,False,1,000,0,000,23,1,True
B_MN_01Y,0,0,True,1,000,0,000,4,1,True
R_LBL_01,0,0,True,1,000,0,000,8,8,True
TREE16,1,0,False,1,000,0,000,1,1,True
B_RBW_3,0,0,True,1,000,0,000,1,1,True
SNOWFLAKE,1,0,False,1,000,0,000,1,1,True
FIRESTORM,1,0,False,1,000,0,000,8,1,True
RAIN_DROP,1,0,False,1,000,0,000,1,1,True
B_TELEPORT,1,0,False,1,000,0,000,3,1,True
B_TELEPORT2,1,0,False,1,000,0,000,2,1,True
WATER,0,0,False,1,000,0,000,1,1,True
WATER_DROP,1,0,False,1,000,0,000,6,1,True
WATER_DROP_R,1,0,False,1,000,0,000,6,1,True
WATER_DROP_G,1,0,False,1,000,0,000,6,1,True
WATER_DROP_B,1,0,False,1,000,0,000,6,1,True
WATER_M,0,0,False,1,000,0,000,10,1,True
B_MTP_04G,0,0,True,1,000,0,000,1,1,True
TASER_SHOOT,1,0,False,1,000,0,000,6,1,True
SHOOK2,1,0,False,1,000,0,000,4,1,True
SHOOK2_B,1,0,False,1,000,0,000,4,1,True
TASER_HIT,1,0,False,1,000,0,000,6,1,True
B_MTP_07BT,0,0,True,1,000,0,000,3,1,True
B_LT_04_PG7,0,0,True,1,000,0,000,1,1,True
B_LT_04R_PG7,0,0,True,1,000,0,000,1,1,True
B_LT_04G_PG7,0,0,True,1,000,0,000,1,1,True
B_LT_04B_PG7,0,0,True,1,000,0,000,1,1,True
B_LT_04Y_PG7,0,0,True,1,000,0,000,1,1,True
B_RB_04L,0,0,True,1,000,0,000,1,1,True
B_PG4L,0,0,True,1,000,0,000,1,1,True
B_S6L,0,0,True,1,000,0,000,1,1,True
B_PG11L,0,0,True,1,000,0,000,1,1,True
B_RBW_3L,0,0,True,1,000,0,000,1,1,True
B_RL_06L,0,0,True,1,000,0,000,1,1,True
B_DD1DK_DAM,0,0,True,1,000,1000,000,1,1,True
B_S0_DAM,0,0,True,1,000,1000,000,1,1,True
B_PLT_01,0,0,True,1,000,0,000,1,1,True
B_PLT_02,0,0,True,1,000,0,000,1,1,True
B_PLT_03,0,0,True,1,000,0,000,1,1,True
B_PLT_05,0,0,True,1,000,0,000,1,1,False
B_PLT_06,0,0,True,1,000,0,000,1,1,True
B_PLT_04,0,0,True,1,000,0,000,2,1,True
ENV_LAVA,1,0,False,1,000,0,000,1,1,True
ENV_LAVA_M,1,0,False,1,000,0,000,4,1,True
DARK_SMOKE,1,0,False,1,000,0,000,17,1,True
ENV_CLOUDS_2,1,0,False,1,000,0,000,1,1,True
ENV_SUN_2,1,0,False,1,000,0,000,1,1,True
ENV_FLARE_01_2,2,8,False,1,000,0,000,1,1,True
ENV_FLARE_00_2,2,8,False,1,000,0,000,1,1,True
ENV_NEBULA_2,1,0,False,1,000,0,000,1,1,True
ENV_MOON_2,1,0,False,1,000,0,000,1,1,True
ENV_CLOUDS_3,1,0,False,1,000,0,000,1,1,True
ENV_CLOUDS_4,1,0,False,1,000,0,000,1,1,True
FIRE_SMOKE_Y,1,0,False,1,000,0,000,23,1,True
APKORA,0,0,True,1,000,0,000,1,1,True
NTREE1,1,0,False,1,000,0,000,1,1,True
B_RU01,0,0,True,1,000,0,000,1,1,True
B_RU02,0,0,True,1,000,0,000,1,1,True
B_RU03,0,0,True,1,000,0,000,1,1,True
FIRE,1,0,False,1,000,0,000,23,1,True
PG24,1,0,False,1,000,0,000,1,1,True
PG25,1,0,False,1,000,0,000,1,1,True
PG26,0,0,True,1,000,0,000,1,1,True
ICE2,1,0,False,1,000,0,000,1,1,True
ENV_MOON_3,1,0,False,1,000,0,000,1,1,True
ENV_MOON_4,1,0,False,1,000,0,000,1,1,True
ENV_MOON_5,1,0,False,1,000,0,000,1,1,True
ENV_SUN_1,1,0,False,1,000,0,000,1,1,False
B_PLT_07,0,0,True,1,000,0,000,1,1,True
ENV_CLOUDS_5,1,0,False,1,000,0,000,1,1,True
MAT_DROP,1,0,False,1,000,0,000,1,1,True
R_TPG02,0,0,True,1,000,0,000,1,1,True
R_TPG01,0,0,True,1,000,0,000,1,1,True
R_LAUSE2,0,0,True,1,000,0,000,1,1,True
R_LAUSE,0,0,True,1,000,0,000,1,1,True
R_LAULEG1,0,0,True,1,000,0,000,1,1,True
ENV_CLOUDS_6,1,0,False,1,000,0,000,1,1,True
R_AIM,0,0,True,1,000,0,000,1,1,True
B_FOUND,1,0,False,1,000,0,000,1,1,True
ENV_NEBULA_0,1,0,False,1,000,0,000,1,1,True
ENV_NEBULA_1,1,0,False,1,000,0,000,1,1,True
ENV_NEBULA_3,1,0,False,1,000,0,000,1,1,True
ENV_SUN_6,1,0,False,1,000,0,000,1,1,True
ENV_SUN_3,1,0,False,1,000,0,000,1,1,False
ENV_SUN_4,1,0,False,1,000,0,000,1,1,True
ENV_SUN_5,1,0,False,1,000,0,000,1,1,True
B_PLT_06_NS,0,0,True,1,000,0,000,1,1,True
SNOWFLAKE_B,1,0,False,1,000,0,000,1,1,True
B_A_BRIGE,1,0,False,1,000,0,000,4,1,True
B_PLT_08,0,0,True,1,000,0,000,1,1,True
R_NP01,0,0,True,1,000,0,000,1,1,True
R_NP03,0,0,True,1,000,0,000,1,1,True
R_NP04,0,0,True,1,000,0,000,1,1,True
R_TB08,0,0,True,1,000,0,000,1,1,False
R_TB09,0,0,True,1,000,0,000,1,1,True
R_TB10,0,0,True,1,000,0,000,1,1,True
R_TB12,0,0,True,1,000,0,000,1,1,True
R_TB19,0,0,True,1,000,0,000,1,1,True
R_NP05,0,0,True,1,000,0,000,1,1,True
ENV_LIGHTNING,2,8,False,1,000,0,000,1,1,True
MINERALS,1,0,False,1,000,0,000,4,1,True
MINERAL_CIRCLE,1,0,False,1,000,0,000,1,1,True
MINERAL_RAY,1,0,False,1,000,0,000,4,1,True
R_NP06,0,0,True,1,000,0,000,1,1,True
R_BG_15,0,0,True,1,000,0,000,1,1,True
R_BG_10,0,0,True,1,000,0,000,1,1,False
R_BG_11,0,0,True,1,000,0,000,1,1,True
R_MG_01,0,0,True,1,000,0,000,1,1,True
R_MG_02,0,0,True,1,000,0,000,1,1,True
R_LG_01,0,0,True,1,000,0,000,1,1,True
R_LG_02,0,0,True,1,000,0,000,1,1,True
R_TG_01,0,0,True,1,000,0,000,1,1,True
R_TG_02,0,0,True,1,000,0,000,1,1,True
R_NP07,0,0,True,1,000,0,000,1,1,True
R_NP08,0,0,True,1,000,0,000,1,1,True
R_NP09,0,0,True,1,000,0,000,1,1,True
R_TB32,0,0,True,1,000,0,000,1,1,True
R_TB33,0,0,True,1,000,0,000,1,1,True
R_TB34,0,0,True,1,000,0,000,1,1,True
R_TB26,0,0,True,1,000,0,000,1,1,False
R_GB05,0,0,True,1,000,0,000,1,1,False
R_GB10,0,0,True,1,000,0,000,1,1,True
R_GB09,0,0,True,1,000,0,000,1,1,True
R_FL_PG,0,0,True,1,000,0,000,1,1,True
R_NP10,0,0,True,1,000,0,000,1,1,True
R_GB17,0,0,True,1,000,0,000,1,1,True
R_GB19,0,0,True,1,000,0,000,1,1,True
R_GB18,0,0,True,1,000,0,000,1,1,True
R_GB16,0,0,True,1,000,0,000,4,1,True
R_GB24,0,0,True,1,000,0,000,1,1,True
R_GB27,0,0,True,1,000,0,000,1,1,True
R_GB33,0,0,True,1,000,0,000,1,1,True
R_GB34,0,0,True,1,000,0,000,1,1,False
R_GB46,0,0,True,1,000,0,000,1,1,True
R_GB45,0,0,True,1,000,0,000,1,1,True
R_GB47,0,0,True,1,000,0,000,1,1,True
R_GB48,0,0,True,1,000,0,000,1,1,True
R_GB50,0,0,True,1,000,0,000,4,1,True
R_GB51,0,0,True,1,000,0,000,4,1,True
R_GB52,0,0,True,1,000,0,000,4,1,True
R_ENG_PG,0,0,True,1,000,0,000,1,1,True
R_NM_PG,0,0,True,1,000,0,000,1,1,True
R_NRS_PG,0,0,True,1,000,0,000,1,1,True
R_NSS_PG,0,0,True,1,000,0,000,1,1,True
L00,0,0,False,1,000,0,000,2,2,True
L01,0,0,False,1,000,0,000,2,2,True
L02,0,0,False,1,000,0,000,2,2,True
L03,0,0,False,1,000,0,000,2,2,True
L04,0,0,False,1,000,0,000,2,2,True
L05,0,0,False,1,000,0,000,2,2,True
L06,0,0,False,1,000,0,000,2,2,True
L07,0,0,False,1,000,0,000,2,2,True
L08,0,0,False,1,000,0,000,2,1,True
L09,0,0,False,1,000,0,000,2,2,True
L10,0,0,False,1,000,0,000,2,2,True
L11,0,0,False,1,000,0,000,2,2,True
L13,0,0,False,1,000,0,000,2,2,True
L14,0,0,False,1,000,0,000,2,2,True
L15,0,0,False,1,000,0,000,2,2,True
L16,0,0,False,1,000,0,000,2,2,True
L17,0,0,False,1,000,0,000,2,2,True
L18,0,0,False,1,000,0,000,2,2,True
L19,0,0,False,1,000,0,000,2,2,True
L20,0,0,False,1,000,0,000,2,2,True
L21,0,0,False,1,000,0,000,2,2,True
L22,0,0,False,1,000,0,000,2,2,True
L23,0,0,False,1,000,0,000,2,2,True
L24,0,0,False,1,000,0,000,2,2,True
L25,0,0,False,1,000,0,000,2,2,True
L26,0,0,False,1,000,0,000,2,2,True
L27,0,0,False,1,000,0,000,2,2,True
L28,0,0,False,1,000,0,000,2,2,True
L29,0,0,False,1,000,0,000,2,2,True
L30,0,0,False,1,000,0,000,2,2,True
L31,0,0,False,1,000,0,000,2,2,True
L32,0,0,False,1,000,0,000,2,1,True
L33,0,0,False,1,000,0,000,2,2,True
L34,0,0,False,1,000,0,000,2,2,True
L35,0,0,False,1,000,0,000,2,2,True
L36,0,0,False,1,000,0,000,2,2,True
L37,0,0,False,1,000,0,000,2,2,True
L38,0,0,False,1,000,0,000,2,2,True
L39,0,0,False,1,000,0,000,2,2,True
L40,0,0,False,1,000,0,000,2,2,True
L41,0,0,False,1,000,0,000,2,2,True
L42,0,0,False,1,000,0,000,2,2,True
L43,0,0,False,1,000,0,000,2,2,True
R_NP12,0,0,True,1,000,0,000,1,1,True
R_NP11,0,0,True,1,000,0,000,1,1,True
R_TM05,0,0,True,1,000,0,000,1,1,False
R_TM13,0,0,True,1,000,0,000,1,1,True
R_TM12,0,0,True,1,000,0,000,1,1,True
R_TT05,0,0,True,1,000,0,000,1,1,False
R_NTT05,0,0,True,1,000,0,000,4,1,True
R_NTT08,0,0,True,1,000,0,000,1,1,True
R_NSM_PG,0,0,True,1,000,0,000,1,1,True
R_NP13,0,0,True,1,000,0,000,1,1,True
R_NTT22,0,0,True,1,000,0,000,1,1,False
B_PG30,0,0,True,1,000,0,000,1,1,True
B_PG31,0,0,True,1,000,0,000,1,1,True
R_PG30,0,0,True,1,000,0,000,1,1,True
R_PG31,0,0,True,1,000,0,000,1,1,True
ELKA,1,0,False,1,000,0,000,1,1,False
TRCH02,0,0,True,1,000,0,000,1,1,False
GRASS,1,0,False,1,000,0,000,1,1,True
ICE01,1,0,False,1,000,0,000,1,1,True
LEAF01,1,0,False,1,000,0,000,1,1,False
SUNFLOWR,0,0,True,1,000,0,000,1,1,True
BAO01,1,0,False,1,000,0,000,1,1,True
DHORNO1,0,0,True,1,000,0,000,1,1,True
DHORN01,0,0,True,1,000,0,000,1,1,True
TRCH01,1,0,False,1,000,0,000,1,1,True
TRCH03,0,0,True,1,000,0,000,1,1,True
TRCH04,0,0,True,1,000,0,000,1,1,True
TRCH05,0,0,True,1,000,0,000,1,1,True
DEFL_B,1,0,False,1,000,0,000,4,1,True
ENV_WAVE,2,8,False,1,000,0,000,1,1,True
DEFL_G,1,0,False,1,000,0,000,4,1,True
SHOOK_BC,1,0,False,1,000,0,000,1,1,True
SMOKE_W,1,0,False,1,000,0,000,10,1,True
ENV_LAVA_BOT,0,0,False,1,000,10000,000,2,2,True
WATER_BOT,0,0,False,1,000,10000,000,2,2,True
ENV_WAVE_B,2,8,False,1,000,0,000,1,1,True
ENV_WAVE_G,2,8,False,1,000,0,000,1,1,True
HLP_RED_ME,0,0,True,1,000,0,000,1,1,True
B_COMP_1SI,0,0,True,1,000,0,000,4,1,True
HLP_GLOW_B,1,0,False,1,000,0,000,3,1,True
HLP_LAMP_B,0,0,True,1,000,0,000,3,1,True
HLP_PLACE_B,0,0,True,1,000,0,000,3,1,True
HLP_RAY_B,1,0,False,1,000,0,000,3,1,True
HLP_SIGN_B,1,0,False,1,000,0,000,3,1,True
ENV_WAVE_R,2,8,False,1,000,0,000,1,1,True
R_NRS_PGL,0,0,True,1,000,0,000,3,1,True
R_NM_RGL,0,0,True,1,000,0,000,3,1,True
R_PG_RGL,0,0,True,1,000,0,000,3,1,True
R_FL_RGL,0,0,True,1,000,0,000,3,1,True
DEFL_BL,2,8,False,1,000,0,000,4,1,True
CLOUD2L,2,8,False,1,000,0,000,15,1,True
ENV_NLAVA,1,0,False,1,000,0,000,8,1,True
ENV_NLAVA_M,1,0,False,1,000,0,000,8,1,True
GLOW_ENG,2,8,False,1,000,0,000,1,1,True
FIRE_ADD,2,8,False,1,000,0,000,23,1,True
SMOKE_ADD,2,8,False,1,000,0,000,4,1,True
OK_TEST1,0,0,True,1,000,0,000,1,1,True
SMOKE_Y,1,0,False,1,000,0,000,16,1,True
SMOKE_Y_ADD,2,8,False,1,000,0,000,16,1,True
DUST_Y,1,0,False,1,000,0,000,4,1,True
FIRE_SMOKE_G,1,0,False,1,000,0,000,23,1,True
FIRE_SMOKE_GADD,2,8,False,1,000,0,000,23,1,True
SMOKE_YG,1,0,False,1,000,0,000,16,1,True
SMOKE_YG_ADD,2,8,False,1,000,0,000,16,1,True
SMOKE_G,1,0,False,1,000,0,000,16,1,True
SMOKE_G_ADD,2,8,False,1,000,0,000,16,1,True
SPITTLE_G_ADD,2,8,False,1,000,0,000,16,1,True
WD_G_ADD,2,8,False,1,000,0,000,6,1,True
TOK41,1,0,False,1,000,0,000,1,1,True
DUST_ADD,2,8,False,1,000,0,000,4,1,True
TOK31,0,0,True,1,000,0,000,1,1,True
TOK31DARK,0,0,True,1,000,0,000,1,1,True
TOK32,1,0,False,1,000,0,000,4,1,True
TOK42,0,0,True,1,000,0,000,1,1,True
BUILD_SPHERE_A,1,0,False,1,000,0,000,4,1,True
LASER_RN,1,0,False,1,000,0,000,4,1,True
TOK43,0,0,True,1,000,0,000,1,1,True
TOK43DARK,0,0,True,1,000,0,000,1,1,True
TOK43_2,0,0,True,1,000,0,000,4,1,True
TOK43_2D,0,0,True,1,000,0,000,1,1,False
TOK43A,0,0,True,1,000,0,000,1,1,True
SMOKE_R,1,0,False,1,000,0,000,16,1,True
SMOKE_R_ADD,2,8,False,1,000,0,000,16,1,True
TOK41_RAY,2,8,False,1,000,0,000,8,1,True
SMOKE_GG,1,0,False,1,000,0,000,16,1,True
SMOKE_GG_ADD,2,8,False,1,000,0,000,16,1,True
SMOKE_W_ADD,2,8,False,1,000,0,000,10,1,True
TOK41_PLC,1,0,False,1,000,0,000,4,1,True
TOK33_BASE,2,8,False,1,000,0,000,4,1,True
TOK44_BASE,2,8,False,1,000,0,000,4,1,True
SMOKE_W1_ADD,2,8,False,1,000,0,000,10,1,True
TOK42W,0,0,True,1,000,0,000,1,1,True
TOK42_W4,0,0,True,1,000,0,000,3,1,True
TOK42_W1E,0,0,True,1,000,0,000,2,1,True
TOK42_W1M,0,0,True,1,000,0,000,2,1,True
TOK42_W5,0,0,True,1,000,0,000,2,1,True
SMOKE_R1_ADD,2,8,False,1,000,0,000,16,1,True
DUST_WORM_ADD,2,8,False,1,000,0,000,4,1,True
DUST_WORM,1,0,False,1,000,0,000,4,1,True
TOK11,0,0,True,1,000,0,000,1,1,True
TOK11_H,0,0,True,1,000,0,000,3,1,True
TOK11_Y,0,0,True,1,000,0,000,3,1,True
R_RLW_5L,0,0,True,1,000,0,000,1,1,True
PMAP_5,0,0,True,1,000,0,000,1,1,True
PMAP_42,0,0,True,1,000,0,000,1,1,False
PMAP_1,0,0,True,1,000,0,000,1,1,True
PMAP_2,0,0,True,1,000,0,000,1,1,True
PMAP_3,0,0,True,1,000,0,000,1,1,True
PMAP_4,0,0,True,1,000,0,000,1,1,True
PMAP_18,0,0,True,1,000,0,000,1,1,True
PMAP_31,0,0,True,1,000,0,000,1,1,False
PMAP_14,0,0,True,1,000,0,000,1,1,False
PMAP_15,0,0,True,1,000,0,000,1,1,True
PMAP_16,0,0,True,1,000,0,000,1,1,True
PMAP_17,0,0,True,1,000,0,000,1,1,True
HLP_GLOW_G,1,0,False,1,000,0,000,3,1,True
HLP_LAMP_G,0,0,True,1,000,0,000,3,1,True
HLP_PLACE_G,0,0,True,1,000,0,000,3,1,True
HLP_RAY_G,1,0,False,1,000,0,000,3,1,True
HLP_SIGN_G,1,0,False,1,000,0,000,3,1,True
HLP_GLOW_R,1,0,False,1,000,0,000,3,1,True
HLP_LAMP_R,0,0,True,1,000,0,000,3,1,True
HLP_PLACE_R,0,0,True,1,000,0,000,3,1,True
HLP_RAY_R,1,0,False,1,000,0,000,3,1,True
R_SRL_25,0,0,True,1,000,0,000,1,1,True
R_SRL_25REV,0,0,True,1,000,0,000,1,1,True
R_SRL_26,0,0,True,1,000,0,000,1,1,False
R_SRL_26REV,0,0,True,1,000,0,000,1,1,True
R_SRL_27,0,0,True,1,000,0,000,1,1,True
R_SRL_27REV,0,0,True,1,000,0,000,1,1,True
DEFL_Y_ADD,2,8,False,1,000,0,000,4,1,True
DEFL_Y1_ADD,2,8,False,1,000,0,000,4,1,True
TOK51,1,0,False,1,000,0,000,1,1,True
TF2,1,0,False,1,000,0,000,1,1,True
TF1,0,0,True,1,000,0,000,1,1,True
TF1_05,0,0,True,1,000,0,000,1,1,False
TF1_06A,0,0,True,1,000,0,000,4,1,True
TF1_BLACK,0,0,True,1,000,0,000,1,1,True
TF1_06,0,0,True,1,000,0,000,1,1,True
LASER_R_ADD,2,8,False,1,000,0,000,6,1,True
TOK76_UP,1,0,False,1,000,0,000,3,1,True
TOK76_DN,1,0,False,1,000,0,000,1,1,True
TOK41_RAYR,2,8,False,1,000,0,000,8,1,True
SMOKE_R2_ADD,2,8,False,1,000,0,000,10,1,True
JET_R,2,8,False,1,000,0,000,1,1,True
JET_G,2,8,False,1,000,0,000,1,1,True
1 FileName,MaterialTypeMaybe,IsTerrainMaybe,SupportsTextureMode6,GlobalTransparencyMaybe,GlobalSelfIlluminationMaybe,StageCount,AnimCount,HasField18NonZero
2 B_PG3,0,0,True,1,000,0,000,1,1,True
3 B_PG4,0,0,True,1,000,0,000,1,1,True
4 B_PG6,0,0,True,1,000,0,000,1,1,True
5 B_PG2,1,0,False,1,000,0,000,1,1,True
6 B_PG5,0,0,True,1,000,0,000,1,1,True
7 B_GEN_05,0,0,True,1,000,0,000,1,1,True
8 B_GEN_06,0,0,True,1,000,0,000,1,1,True
9 B_GEN_07,0,0,True,1,000,0,000,1,1,True
10 B_GEN_08,0,0,True,1,000,0,000,1,1,True
11 B_DD1CL,0,0,True,1,000,0,000,8,1,True
12 B_DD1DK,0,0,True,1,000,0,000,1,1,True
13 B_DD2,0,0,True,1,000,0,000,1,1,True
14 B_RL_06,0,0,True,1,000,0,000,1,1,True
15 B_RL_02,0,0,True,1,000,0,000,1,1,True
16 B_RL_03,0,0,True,1,000,0,000,1,1,True
17 B_RL_19,0,0,True,1,000,0,000,1,1,True
18 B_RL_20,0,0,True,1,000,0,000,1,1,True
19 B_RL_21,0,0,True,1,000,0,000,1,1,True
20 B_S12,0,0,True,1,000,0,000,1,1,True
21 B_S0,0,0,True,1,000,0,000,1,1,True
22 B_S0A1,0,0,True,1,000,0,000,1,1,True
23 B_S1,0,0,True,1,000,0,000,1,1,True
24 B_S10,0,0,True,1,000,0,000,1,1,True
25 B_S2,0,0,True,1,000,0,000,1,1,True
26 B_S13,0,0,True,1,000,0,000,4,1,True
27 B_S14,0,0,True,1,000,0,000,1,1,True
28 B_S6,0,0,True,1,000,0,000,1,1,True
29 B_S3,0,0,True,1,000,0,000,1,1,True
30 B_S4,0,0,True,1,000,0,000,1,1,True
31 B_S5,1,0,False,1,000,0,000,1,1,True
32 B_S7,0,0,True,1,000,0,000,1,1,True
33 B_S0A2,0,0,True,1,000,0,000,1,1,True
34 R_PG3,0,0,True,1,000,0,000,1,1,True
35 R_PG4,0,0,True,1,000,0,000,1,1,True
36 R_PG6,0,0,True,1,000,0,000,1,1,True
37 R_PG7,0,0,True,1,000,0,000,1,1,True
38 R_PG2,1,0,False,1,000,0,000,1,1,True
39 R_PG5,0,0,True,1,000,0,000,1,1,True
40 R_GEN_05,0,0,True,1,000,0,000,1,1,True
41 R_GEN_06,0,0,True,1,000,0,000,1,1,True
42 R_GEN_07,0,0,True,1,000,0,000,1,1,True
43 R_GEN_08,0,0,True,1,000,0,000,1,1,True
44 R_DD1,0,0,True,1,000,0,000,1,1,True
45 R_DD1CL,0,0,True,1,000,0,000,1,1,True
46 R_DD1DK,0,0,True,1,000,0,000,1,1,True
47 R_DD2,0,0,True,1,000,0,000,1,1,True
48 R_RL_06,0,0,True,1,000,0,000,1,1,True
49 R_RL_02,0,0,True,1,000,0,000,1,1,True
50 R_RL_03,0,0,True,1,000,0,000,1,1,True
51 R_RL_20,0,0,True,1,000,0,000,1,1,True
52 R_RL_21,0,0,True,1,000,0,000,1,1,True
53 R_S12,0,0,True,1,000,0,000,1,1,True
54 R_S0,0,0,True,1,000,0,000,1,1,True
55 R_S0A1,0,0,True,1,000,0,000,1,1,True
56 R_S1,0,0,True,1,000,0,000,1,1,True
57 R_S10,0,0,True,1,000,0,000,1,1,True
58 R_S2,0,0,True,1,000,0,000,1,1,True
59 R_S6,0,0,True,1,000,0,000,1,1,True
60 R_S3,0,0,True,1,000,0,000,1,1,True
61 R_S4,0,0,True,1,000,0,000,1,1,True
62 R_S5,1,0,False,1,000,0,000,1,1,True
63 R_S7,0,0,True,1,000,0,000,1,1,True
64 R_S0A2,0,0,True,1,000,0,000,1,1,True
65 R_S13,0,0,True,1,000,0,000,4,1,True
66 R_S14,0,0,True,1,000,0,000,1,1,True
67 SKY1,1,0,False,1,000,0,000,1,1,True
68 B_RLW_4,0,0,True,1,000,0,000,1,1,True
69 R_RLW_4,0,0,True,1,000,0,000,1,1,True
70 DEFAULT,1,0,False,1,000,0,000,1,1,True
71 EXPL1,1,0,False,1,000,0,000,16,1,True
72 JET1,2,8,False,1,000,0,000,6,1,True
73 JET2,2,8,False,1,000,0,000,4,1,True
74 SHOOT1,1,0,False,1,000,0,000,4,1,True
75 JET3,2,8,False,1,000,0,000,1,1,True
76 SUN1,1,0,False,1,000,0,000,1,1,True
77 JET4,2,8,False,1,000,0,000,4,1,True
78 CLOUD1,1,0,False,1,000,0,000,15,1,True
79 SPLASH1,1,0,False,1,000,0,000,5,1,True
80 TRACE1,1,0,False,1,000,0,000,4,1,True
81 TRACE2,1,0,False,1,000,0,000,4,1,True
82 B_COMP_2,0,0,True,1,000,0,000,1,1,True
83 R_COMP_2,0,0,True,1,000,0,000,1,1,True
84 EXPL2,1,0,False,1,000,0,000,16,1,True
85 JET5,2,8,False,1,000,0,000,1,1,True
86 B_COMP_1,0,0,True,1,000,0,000,4,1,True
87 B_COMP_3,1,0,False,1,000,0,000,4,1,True
88 LIGHT1,2,8,False,1,000,0,000,1,1,True
89 BLACK,0,0,False,1,000,0,000,1,1,True
90 R_RLW_5,0,0,True,1,000,0,000,2,1,True
91 JET6,2,8,False,1,000,0,000,6,1,True
92 SHOOT2,1,0,False,1,000,0,000,4,1,True
93 JET7,2,8,False,1,000,0,000,4,1,True
94 SPLASH2,1,0,False,1,000,0,000,5,1,True
95 CLOUD2,1,0,False,1,000,0,000,15,1,True
96 R_MN_01,0,0,True,1,000,0,000,1,1,True
97 B_MN_01,0,0,True,1,000,0,000,1,1,True
98 B_MN_02,0,0,True,1,000,0,000,1,1,True
99 B_COMP_3G,1,0,False,1,000,0,000,4,1,True
100 B_COMP_3R,1,0,False,1,000,0,000,4,1,True
101 B_COMP_3B,1,0,False,1,000,0,000,4,1,True
102 JET8,2,8,False,1,000,0,000,6,1,True
103 JET9,2,8,False,1,000,0,000,4,1,True
104 B_MN_01R,0,0,True,1,000,0,000,4,1,True
105 B_MN_01G,0,0,True,1,000,0,000,4,1,True
106 B_MN_01B,0,0,True,1,000,0,000,4,1,True
107 STONE00,0,0,True,1,000,0,000,1,1,True
108 STONE01,0,0,True,1,000,0,000,1,1,True
109 TREE00,1,0,False,1,000,0,000,1,1,True
110 TREE01,0,0,False,1,000,0,000,1,1,True
111 TREE02,0,0,False,1,000,0,000,1,1,True
112 TREE03,1,0,False,1,000,0,000,1,1,True
113 TREE04,0,0,False,1,000,0,000,1,1,True
114 TREE05,0,0,False,1,000,0,000,1,1,True
115 R_RL_26,0,0,True,1,000,0,000,4,1,True
116 R_RL_26REV,0,0,True,1,000,0,000,4,1,True
117 R_RL_27,0,0,True,1,000,0,000,4,1,True
118 R_RL_27REV,0,0,True,1,000,0,000,4,1,True
119 R_RL_25,0,0,True,1,000,0,000,4,1,True
120 R_RL_25REV,0,0,True,1,000,0,000,4,1,True
121 R_RL_24,0,0,True,1,000,0,000,1,1,True
122 TREE06,1,0,False,1,000,0,000,1,1,True
123 TREE07,1,0,False,1,000,0,000,1,1,True
124 B_HNG_01,0,0,True,1,000,0,000,1,1,True
125 B_HNG_02,1,0,False,1,000,0,000,1,1,True
126 R_PG11,0,0,True,1,000,0,000,1,1,True
127 B_PG11,0,0,True,1,000,0,000,1,1,True
128 B_LIGHT,0,0,False,1,000,0,000,1,1,True
129 B_PG12,0,0,True,1,000,0,000,1,1,True
130 R_PG12,0,0,True,1,000,0,000,1,1,True
131 B_MTP_01,0,0,True,1,000,0,000,1,1,True
132 B_MTP_02,0,0,True,1,000,0,000,3,1,True
133 R_RL_19,0,0,True,1,000,0,000,1,1,True
134 B_RB_04,0,0,True,1,000,0,000,1,1,True
135 R_RB_04,0,0,True,1,000,0,000,1,1,True
136 R_RBW_03,0,0,True,1,000,0,000,1,1,True
137 DUST,1,0,False,1,000,0,000,4,1,True
138 SMOKE,1,0,False,1,000,0,000,4,1,True
139 FIRE_SMOKE,1,0,False,1,000,0,000,26,1,True
140 DARK_CLOUD,1,0,False,1,000,0,000,4,1,True
141 LASER_G,2,8,False,1,000,0,000,4,1,True
142 LASER_R,2,8,False,1,000,0,000,4,1,True
143 GLOW,1,0,False,1,000,0,000,1,1,True
144 LASER_Y,2,8,False,1,000,0,000,4,1,True
145 B_EFFECT_001,1,0,False,1,000,0,000,1,1,True
146 R_RMW_9,0,0,True,1,000,0,000,3,1,True
147 R_PG14,0,0,True,1,000,0,000,1,1,True
148 SHIELD1,2,8,False,1,000,0,000,7,1,True
149 B_LT_01,0,0,True,1,000,0,000,1,1,False
150 B_LT_03,0,0,True,1,000,0,000,1,1,True
151 B_LBL_01,0,0,True,1,000,0,000,8,8,True
152 CUTTER,1,0,False,1,000,0,000,4,1,True
153 GLOW_B,1,0,False,1,000,0,000,1,1,True
154 GLOW_G,1,0,False,1,000,0,000,1,1,True
155 GLOW_R,2,8,False,1,000,0,000,1,1,True
156 TREE08,1,0,False,1,000,0,000,1,1,True
157 TREE09,1,0,False,1,000,0,000,1,1,True
158 TREE10,1,0,False,1,000,0,000,1,1,True
159 TREE11,1,0,False,1,000,0,000,1,1,False
160 TREE12,1,0,False,1,000,0,000,1,1,True
161 TREE13,1,0,False,1,000,0,000,1,1,True
162 TREE14,1,0,False,1,000,0,000,1,1,True
163 TREE15,1,0,False,1,000,0,000,1,1,True
164 BALL_B,1,0,False,1,000,0,000,1,1,True
165 BALL_G,1,0,False,1,000,0,000,1,1,True
166 BALL_R,1,0,False,1,000,0,000,1,1,True
167 BALL_Y,1,0,False,1,000,0,000,1,1,True
168 BALL_W,1,0,False,1,000,0,000,1,1,True
169 GLOW_Y,1,0,False,1,000,0,000,1,1,True
170 GLOW_W,1,0,False,1,000,0,000,1,1,True
171 SPLASH3,1,0,False,1,000,0,000,5,1,True
172 LASER_B,2,8,False,1,000,0,000,4,1,True
173 LIGHT_W,1,0,False,1,000,0,000,1,1,True
174 SHOOT3,1,0,False,1,000,0,000,4,1,True
175 CUTTER_HIT,1,0,False,1,000,0,000,3,1,True
176 RAILGUN_HIT,2,8,False,1,000,0,000,3,1,True
177 EXPL3,1,0,False,1,000,0,000,24,1,True
178 R_PG9,0,0,True,1,000,0,000,1,1,True
179 BIRD_B,0,0,True,1,000,0,000,1,1,True
180 BIRD_T,0,0,True,1,000,0,000,1,1,True
181 BIRD_W,0,0,True,1,000,0,000,1,1,True
182 MATL,0,0,True,1,000,0,000,1,1,True
183 B_MTP_06,0,0,True,1,000,0,000,1,1,True
184 B_MTP_07,0,0,True,1,000,0,000,1,1,True
185 B_MTP_04,0,0,True,1,000,0,000,1,1,True
186 B_MTP_03,0,0,True,1,000,0,000,1,1,True
187 B_MTP_03R,0,0,True,1,000,0,000,1,1,True
188 B_MTP_03B,0,0,True,1,000,0,000,1,1,True
189 B_MTP_05,0,0,True,1,000,0,000,1,1,True
190 B_MTP_06B,0,0,True,1,000,0,000,1,1,True
191 B_MTP_06R,0,0,True,1,000,0,000,1,1,True
192 B_MTP_06G,0,0,True,1,000,0,000,1,1,True
193 B_MTP_06Y,0,0,True,1,000,0,000,1,1,True
194 B_MTP_07Y,0,0,True,1,000,0,000,1,1,True
195 B_MTP_07R,0,0,True,1,000,0,000,1,1,True
196 B_MTP_07G,0,0,True,1,000,0,000,1,1,True
197 B_MTP_07B,0,0,True,1,000,0,000,1,1,True
198 R_PG17,0,0,True,1,000,0,000,1,1,True
199 R_PG18,0,0,True,1,000,0,000,1,1,True
200 R_PG19,0,0,True,1,000,0,000,1,1,True
201 R_PG20,0,0,True,1,000,0,000,1,1,True
202 R_SLD_POD,0,0,True,1,000,0,000,1,1,True
203 R_SLD_GL,1,0,False,1,000,0,000,1,1,True
204 BIRD_U,0,0,True,1,000,0,000,1,1,True
205 SHIELD_R,2,8,False,1,000,0,000,10,1,True
206 SHIELD_G,2,8,False,1,000,0,000,10,1,True
207 SHIELD_B,2,8,False,1,000,0,000,10,1,True
208 B_MTP_07E,0,0,True,1,000,0,000,1,1,True
209 B_MTP_08,0,0,True,1,000,0,000,3,1,True
210 LASER_R_HIT,2,8,False,1,000,0,000,6,1,True
211 LASER_G_HIT,2,8,False,1,000,0,000,6,1,True
212 LASER_B_HIT,2,8,False,1,000,0,000,6,1,True
213 LASER_Y_HIT,2,8,False,1,000,0,000,6,1,True
214 B_P00,0,0,True,1,000,0,000,1,1,True
215 AIGRETTE_Y,1,0,False,1,000,0,000,3,1,True
216 AIGRETTE_R,1,0,False,1,000,0,000,3,1,True
217 AIGRETTE_G,1,0,False,1,000,0,000,3,1,True
218 AIGRETTE_B,1,0,False,1,000,0,000,3,1,True
219 B_LT_01R,0,0,True,1,000,0,000,1,1,False
220 B_LT_01G,0,0,True,1,000,0,000,1,1,False
221 B_LT_01B,0,0,True,1,000,0,000,1,1,False
222 B_LT_01Y,0,0,True,1,000,0,000,1,1,False
223 B_COMP_4,0,0,True,1,000,0,000,1,1,True
224 B_COMP_4R,0,0,True,1,000,0,000,1,1,True
225 B_COMP_4G,0,0,True,1,000,0,000,1,1,True
226 B_COMP_4B,0,0,True,1,000,0,000,1,1,True
227 B_COMP_4Y,0,0,True,1,000,0,000,1,1,True
228 B_LT_03B,0,0,True,1,000,0,000,1,1,True
229 B_LT_03R,0,0,True,1,000,0,000,1,1,True
230 B_LT_03G,0,0,True,1,000,0,000,1,1,True
231 B_LT_03Y,0,0,True,1,000,0,000,1,1,True
232 B_MN_03,0,0,True,1,000,0,000,1,1,True
233 B_MN_03R,0,0,True,1,000,0,000,1,1,True
234 B_MN_03G,0,0,True,1,000,0,000,1,1,True
235 B_MN_03B,0,0,True,1,000,0,000,1,1,True
236 B_MN_03Y,0,0,True,1,000,0,000,1,1,True
237 B_LT_04,0,0,True,1,000,0,000,1,1,True
238 CLOUD3,1,0,False,1,000,0,000,15,1,True
239 B_GEN_09,0,0,True,1,000,0,000,1,1,True
240 R_GEN_09,0,0,True,1,000,0,000,1,1,True
241 B_GEN_08L,0,0,True,1,000,0,000,1,1,True
242 B_S3L,0,0,True,1,000,0,000,1,1,True
243 B_S4L,0,0,True,1,000,0,000,1,1,True
244 PORTAL_001,1,0,False,1,000,0,000,2,1,False
245 PORTAL_004,1,0,False,1,000,0,000,2,1,True
246 B_GEN_10,0,0,True,1,000,0,000,1,1,True
247 B_GEN_11,0,0,True,1,000,0,000,1,1,True
248 HWOY01,1,0,False,1,000,0,000,1,1,True
249 FTREE1,1,0,False,1,000,0,000,1,1,True
250 HTREE1,1,0,False,1,000,0,000,1,1,True
251 KORA,0,0,True,1,000,0,000,1,1,True
252 KORA2,0,0,True,1,000,0,000,1,1,True
253 SKIN02,0,0,True,1,000,0,000,1,1,True
254 B_LT_02,0,0,True,1,000,0,000,1,1,True
255 EXPL4,1,0,False,1,000,0,000,29,1,True
256 SHOOT4,1,0,False,1,000,0,000,4,1,True
257 SPLASH4,1,0,False,1,000,0,000,6,1,True
258 EXPL5,1,0,False,1,000,0,000,4,1,True
259 EXPL6,1,0,False,1,000,0,000,8,1,True
260 SPLASH5,1,0,False,1,000,0,000,6,1,True
261 SPLASH6,1,0,False,1,000,0,000,6,1,True
262 B_MTP_02L,0,0,True,1,000,0,000,3,1,True
263 B_MTP_01L,0,0,True,1,000,0,000,1,1,True
264 B_GEN_05L,0,0,True,1,000,0,000,1,1,True
265 B_P08,1,0,False,1,000,0,000,2,1,True
266 B_P10,1,0,False,1,000,0,000,2,1,True
267 B_P11,1,0,False,1,000,0,000,2,1,True
268 B_P13,1,0,False,1,000,0,000,2,1,True
269 LAMP,1,0,False,1,000,0,000,1,1,True
270 LASER_W_HIT,2,8,False,1,000,0,000,6,1,True
271 SPITTLE_R,1,0,False,1,000,0,000,16,1,True
272 SPITTLE_G,1,0,False,1,000,0,000,16,1,True
273 SHOOT5,1,0,False,1,000,0,000,4,1,True
274 ALIEN_BALL,1,0,False,1,000,0,000,1,1,True
275 ALIENSKIN,0,0,True,1,000,0,000,1,1,True
276 CLOUD_G,1,0,False,1,000,0,000,15,1,True
277 CLOUD_R,1,0,False,1,000,0,000,15,1,True
278 CLOUD_B,1,0,False,1,000,0,000,15,1,True
279 BUILD_SPHERE,1,0,False,1,000,0,000,4,1,True
280 BUILD_MIRACLE,1,0,False,1,000,0,000,4,1,True
281 BUILD_GALO,1,0,False,1,000,0,000,4,1,True
282 SHIELD_Y,2,8,False,1,000,0,000,10,1,True
283 PLAS_BUL,2,8,False,1,000,0,000,4,1,True
284 PLAS_SHOOT,2,8,False,1,000,0,000,6,1,True
285 GLOW_V,1,0,False,1,000,0,000,1,1,True
286 LASER_V,2,8,False,1,000,0,000,4,1,True
287 LASER_V_HIT,2,8,False,1,000,0,000,6,1,True
288 ENV_MOON,1,0,False,1,000,0,000,1,1,True
289 ENV_STARS,1,0,False,1,000,0,000,1,1,True
290 ENV_FLARE_00,2,8,False,1,000,0,000,1,1,True
291 ENV_FLARE_01,2,8,False,1,000,0,000,1,1,True
292 SHOOK,1,0,False,1,000,0,000,4,1,True
293 SHOOK_B,1,0,False,1,000,0,000,4,1,True
294 ENV_CLOUDS,1,0,False,1,000,0,000,1,1,True
295 BUILD_SHOOT,1,0,False,1,000,0,000,1,1,True
296 BUILD_FLY,1,0,False,1,000,0,000,8,1,True
297 ENV_SUN,1,0,False,1,000,0,000,1,1,False
298 FIRE_SMOKE_W,1,0,False,1,000,0,000,23,1,True
299 B_MN_01Y,0,0,True,1,000,0,000,4,1,True
300 R_LBL_01,0,0,True,1,000,0,000,8,8,True
301 TREE16,1,0,False,1,000,0,000,1,1,True
302 B_RBW_3,0,0,True,1,000,0,000,1,1,True
303 SNOWFLAKE,1,0,False,1,000,0,000,1,1,True
304 FIRESTORM,1,0,False,1,000,0,000,8,1,True
305 RAIN_DROP,1,0,False,1,000,0,000,1,1,True
306 B_TELEPORT,1,0,False,1,000,0,000,3,1,True
307 B_TELEPORT2,1,0,False,1,000,0,000,2,1,True
308 WATER,0,0,False,1,000,0,000,1,1,True
309 WATER_DROP,1,0,False,1,000,0,000,6,1,True
310 WATER_DROP_R,1,0,False,1,000,0,000,6,1,True
311 WATER_DROP_G,1,0,False,1,000,0,000,6,1,True
312 WATER_DROP_B,1,0,False,1,000,0,000,6,1,True
313 WATER_M,0,0,False,1,000,0,000,10,1,True
314 B_MTP_04G,0,0,True,1,000,0,000,1,1,True
315 TASER_SHOOT,1,0,False,1,000,0,000,6,1,True
316 SHOOK2,1,0,False,1,000,0,000,4,1,True
317 SHOOK2_B,1,0,False,1,000,0,000,4,1,True
318 TASER_HIT,1,0,False,1,000,0,000,6,1,True
319 B_MTP_07BT,0,0,True,1,000,0,000,3,1,True
320 B_LT_04_PG7,0,0,True,1,000,0,000,1,1,True
321 B_LT_04R_PG7,0,0,True,1,000,0,000,1,1,True
322 B_LT_04G_PG7,0,0,True,1,000,0,000,1,1,True
323 B_LT_04B_PG7,0,0,True,1,000,0,000,1,1,True
324 B_LT_04Y_PG7,0,0,True,1,000,0,000,1,1,True
325 B_RB_04L,0,0,True,1,000,0,000,1,1,True
326 B_PG4L,0,0,True,1,000,0,000,1,1,True
327 B_S6L,0,0,True,1,000,0,000,1,1,True
328 B_PG11L,0,0,True,1,000,0,000,1,1,True
329 B_RBW_3L,0,0,True,1,000,0,000,1,1,True
330 B_RL_06L,0,0,True,1,000,0,000,1,1,True
331 B_DD1DK_DAM,0,0,True,1,000,1000,000,1,1,True
332 B_S0_DAM,0,0,True,1,000,1000,000,1,1,True
333 B_PLT_01,0,0,True,1,000,0,000,1,1,True
334 B_PLT_02,0,0,True,1,000,0,000,1,1,True
335 B_PLT_03,0,0,True,1,000,0,000,1,1,True
336 B_PLT_05,0,0,True,1,000,0,000,1,1,False
337 B_PLT_06,0,0,True,1,000,0,000,1,1,True
338 B_PLT_04,0,0,True,1,000,0,000,2,1,True
339 ENV_LAVA,1,0,False,1,000,0,000,1,1,True
340 ENV_LAVA_M,1,0,False,1,000,0,000,4,1,True
341 DARK_SMOKE,1,0,False,1,000,0,000,17,1,True
342 ENV_CLOUDS_2,1,0,False,1,000,0,000,1,1,True
343 ENV_SUN_2,1,0,False,1,000,0,000,1,1,True
344 ENV_FLARE_01_2,2,8,False,1,000,0,000,1,1,True
345 ENV_FLARE_00_2,2,8,False,1,000,0,000,1,1,True
346 ENV_NEBULA_2,1,0,False,1,000,0,000,1,1,True
347 ENV_MOON_2,1,0,False,1,000,0,000,1,1,True
348 ENV_CLOUDS_3,1,0,False,1,000,0,000,1,1,True
349 ENV_CLOUDS_4,1,0,False,1,000,0,000,1,1,True
350 FIRE_SMOKE_Y,1,0,False,1,000,0,000,23,1,True
351 APKORA,0,0,True,1,000,0,000,1,1,True
352 NTREE1,1,0,False,1,000,0,000,1,1,True
353 B_RU01,0,0,True,1,000,0,000,1,1,True
354 B_RU02,0,0,True,1,000,0,000,1,1,True
355 B_RU03,0,0,True,1,000,0,000,1,1,True
356 FIRE,1,0,False,1,000,0,000,23,1,True
357 PG24,1,0,False,1,000,0,000,1,1,True
358 PG25,1,0,False,1,000,0,000,1,1,True
359 PG26,0,0,True,1,000,0,000,1,1,True
360 ICE2,1,0,False,1,000,0,000,1,1,True
361 ENV_MOON_3,1,0,False,1,000,0,000,1,1,True
362 ENV_MOON_4,1,0,False,1,000,0,000,1,1,True
363 ENV_MOON_5,1,0,False,1,000,0,000,1,1,True
364 ENV_SUN_1,1,0,False,1,000,0,000,1,1,False
365 B_PLT_07,0,0,True,1,000,0,000,1,1,True
366 ENV_CLOUDS_5,1,0,False,1,000,0,000,1,1,True
367 MAT_DROP,1,0,False,1,000,0,000,1,1,True
368 R_TPG02,0,0,True,1,000,0,000,1,1,True
369 R_TPG01,0,0,True,1,000,0,000,1,1,True
370 R_LAUSE2,0,0,True,1,000,0,000,1,1,True
371 R_LAUSE,0,0,True,1,000,0,000,1,1,True
372 R_LAULEG1,0,0,True,1,000,0,000,1,1,True
373 ENV_CLOUDS_6,1,0,False,1,000,0,000,1,1,True
374 R_AIM,0,0,True,1,000,0,000,1,1,True
375 B_FOUND,1,0,False,1,000,0,000,1,1,True
376 ENV_NEBULA_0,1,0,False,1,000,0,000,1,1,True
377 ENV_NEBULA_1,1,0,False,1,000,0,000,1,1,True
378 ENV_NEBULA_3,1,0,False,1,000,0,000,1,1,True
379 ENV_SUN_6,1,0,False,1,000,0,000,1,1,True
380 ENV_SUN_3,1,0,False,1,000,0,000,1,1,False
381 ENV_SUN_4,1,0,False,1,000,0,000,1,1,True
382 ENV_SUN_5,1,0,False,1,000,0,000,1,1,True
383 B_PLT_06_NS,0,0,True,1,000,0,000,1,1,True
384 SNOWFLAKE_B,1,0,False,1,000,0,000,1,1,True
385 B_A_BRIGE,1,0,False,1,000,0,000,4,1,True
386 B_PLT_08,0,0,True,1,000,0,000,1,1,True
387 R_NP01,0,0,True,1,000,0,000,1,1,True
388 R_NP03,0,0,True,1,000,0,000,1,1,True
389 R_NP04,0,0,True,1,000,0,000,1,1,True
390 R_TB08,0,0,True,1,000,0,000,1,1,False
391 R_TB09,0,0,True,1,000,0,000,1,1,True
392 R_TB10,0,0,True,1,000,0,000,1,1,True
393 R_TB12,0,0,True,1,000,0,000,1,1,True
394 R_TB19,0,0,True,1,000,0,000,1,1,True
395 R_NP05,0,0,True,1,000,0,000,1,1,True
396 ENV_LIGHTNING,2,8,False,1,000,0,000,1,1,True
397 MINERALS,1,0,False,1,000,0,000,4,1,True
398 MINERAL_CIRCLE,1,0,False,1,000,0,000,1,1,True
399 MINERAL_RAY,1,0,False,1,000,0,000,4,1,True
400 R_NP06,0,0,True,1,000,0,000,1,1,True
401 R_BG_15,0,0,True,1,000,0,000,1,1,True
402 R_BG_10,0,0,True,1,000,0,000,1,1,False
403 R_BG_11,0,0,True,1,000,0,000,1,1,True
404 R_MG_01,0,0,True,1,000,0,000,1,1,True
405 R_MG_02,0,0,True,1,000,0,000,1,1,True
406 R_LG_01,0,0,True,1,000,0,000,1,1,True
407 R_LG_02,0,0,True,1,000,0,000,1,1,True
408 R_TG_01,0,0,True,1,000,0,000,1,1,True
409 R_TG_02,0,0,True,1,000,0,000,1,1,True
410 R_NP07,0,0,True,1,000,0,000,1,1,True
411 R_NP08,0,0,True,1,000,0,000,1,1,True
412 R_NP09,0,0,True,1,000,0,000,1,1,True
413 R_TB32,0,0,True,1,000,0,000,1,1,True
414 R_TB33,0,0,True,1,000,0,000,1,1,True
415 R_TB34,0,0,True,1,000,0,000,1,1,True
416 R_TB26,0,0,True,1,000,0,000,1,1,False
417 R_GB05,0,0,True,1,000,0,000,1,1,False
418 R_GB10,0,0,True,1,000,0,000,1,1,True
419 R_GB09,0,0,True,1,000,0,000,1,1,True
420 R_FL_PG,0,0,True,1,000,0,000,1,1,True
421 R_NP10,0,0,True,1,000,0,000,1,1,True
422 R_GB17,0,0,True,1,000,0,000,1,1,True
423 R_GB19,0,0,True,1,000,0,000,1,1,True
424 R_GB18,0,0,True,1,000,0,000,1,1,True
425 R_GB16,0,0,True,1,000,0,000,4,1,True
426 R_GB24,0,0,True,1,000,0,000,1,1,True
427 R_GB27,0,0,True,1,000,0,000,1,1,True
428 R_GB33,0,0,True,1,000,0,000,1,1,True
429 R_GB34,0,0,True,1,000,0,000,1,1,False
430 R_GB46,0,0,True,1,000,0,000,1,1,True
431 R_GB45,0,0,True,1,000,0,000,1,1,True
432 R_GB47,0,0,True,1,000,0,000,1,1,True
433 R_GB48,0,0,True,1,000,0,000,1,1,True
434 R_GB50,0,0,True,1,000,0,000,4,1,True
435 R_GB51,0,0,True,1,000,0,000,4,1,True
436 R_GB52,0,0,True,1,000,0,000,4,1,True
437 R_ENG_PG,0,0,True,1,000,0,000,1,1,True
438 R_NM_PG,0,0,True,1,000,0,000,1,1,True
439 R_NRS_PG,0,0,True,1,000,0,000,1,1,True
440 R_NSS_PG,0,0,True,1,000,0,000,1,1,True
441 L00,0,0,False,1,000,0,000,2,2,True
442 L01,0,0,False,1,000,0,000,2,2,True
443 L02,0,0,False,1,000,0,000,2,2,True
444 L03,0,0,False,1,000,0,000,2,2,True
445 L04,0,0,False,1,000,0,000,2,2,True
446 L05,0,0,False,1,000,0,000,2,2,True
447 L06,0,0,False,1,000,0,000,2,2,True
448 L07,0,0,False,1,000,0,000,2,2,True
449 L08,0,0,False,1,000,0,000,2,1,True
450 L09,0,0,False,1,000,0,000,2,2,True
451 L10,0,0,False,1,000,0,000,2,2,True
452 L11,0,0,False,1,000,0,000,2,2,True
453 L13,0,0,False,1,000,0,000,2,2,True
454 L14,0,0,False,1,000,0,000,2,2,True
455 L15,0,0,False,1,000,0,000,2,2,True
456 L16,0,0,False,1,000,0,000,2,2,True
457 L17,0,0,False,1,000,0,000,2,2,True
458 L18,0,0,False,1,000,0,000,2,2,True
459 L19,0,0,False,1,000,0,000,2,2,True
460 L20,0,0,False,1,000,0,000,2,2,True
461 L21,0,0,False,1,000,0,000,2,2,True
462 L22,0,0,False,1,000,0,000,2,2,True
463 L23,0,0,False,1,000,0,000,2,2,True
464 L24,0,0,False,1,000,0,000,2,2,True
465 L25,0,0,False,1,000,0,000,2,2,True
466 L26,0,0,False,1,000,0,000,2,2,True
467 L27,0,0,False,1,000,0,000,2,2,True
468 L28,0,0,False,1,000,0,000,2,2,True
469 L29,0,0,False,1,000,0,000,2,2,True
470 L30,0,0,False,1,000,0,000,2,2,True
471 L31,0,0,False,1,000,0,000,2,2,True
472 L32,0,0,False,1,000,0,000,2,1,True
473 L33,0,0,False,1,000,0,000,2,2,True
474 L34,0,0,False,1,000,0,000,2,2,True
475 L35,0,0,False,1,000,0,000,2,2,True
476 L36,0,0,False,1,000,0,000,2,2,True
477 L37,0,0,False,1,000,0,000,2,2,True
478 L38,0,0,False,1,000,0,000,2,2,True
479 L39,0,0,False,1,000,0,000,2,2,True
480 L40,0,0,False,1,000,0,000,2,2,True
481 L41,0,0,False,1,000,0,000,2,2,True
482 L42,0,0,False,1,000,0,000,2,2,True
483 L43,0,0,False,1,000,0,000,2,2,True
484 R_NP12,0,0,True,1,000,0,000,1,1,True
485 R_NP11,0,0,True,1,000,0,000,1,1,True
486 R_TM05,0,0,True,1,000,0,000,1,1,False
487 R_TM13,0,0,True,1,000,0,000,1,1,True
488 R_TM12,0,0,True,1,000,0,000,1,1,True
489 R_TT05,0,0,True,1,000,0,000,1,1,False
490 R_NTT05,0,0,True,1,000,0,000,4,1,True
491 R_NTT08,0,0,True,1,000,0,000,1,1,True
492 R_NSM_PG,0,0,True,1,000,0,000,1,1,True
493 R_NP13,0,0,True,1,000,0,000,1,1,True
494 R_NTT22,0,0,True,1,000,0,000,1,1,False
495 B_PG30,0,0,True,1,000,0,000,1,1,True
496 B_PG31,0,0,True,1,000,0,000,1,1,True
497 R_PG30,0,0,True,1,000,0,000,1,1,True
498 R_PG31,0,0,True,1,000,0,000,1,1,True
499 ELKA,1,0,False,1,000,0,000,1,1,False
500 TRCH02,0,0,True,1,000,0,000,1,1,False
501 GRASS,1,0,False,1,000,0,000,1,1,True
502 ICE01,1,0,False,1,000,0,000,1,1,True
503 LEAF01,1,0,False,1,000,0,000,1,1,False
504 SUNFLOWR,0,0,True,1,000,0,000,1,1,True
505 BAO01,1,0,False,1,000,0,000,1,1,True
506 DHORNO1,0,0,True,1,000,0,000,1,1,True
507 DHORN01,0,0,True,1,000,0,000,1,1,True
508 TRCH01,1,0,False,1,000,0,000,1,1,True
509 TRCH03,0,0,True,1,000,0,000,1,1,True
510 TRCH04,0,0,True,1,000,0,000,1,1,True
511 TRCH05,0,0,True,1,000,0,000,1,1,True
512 DEFL_B,1,0,False,1,000,0,000,4,1,True
513 ENV_WAVE,2,8,False,1,000,0,000,1,1,True
514 DEFL_G,1,0,False,1,000,0,000,4,1,True
515 SHOOK_BC,1,0,False,1,000,0,000,1,1,True
516 SMOKE_W,1,0,False,1,000,0,000,10,1,True
517 ENV_LAVA_BOT,0,0,False,1,000,10000,000,2,2,True
518 WATER_BOT,0,0,False,1,000,10000,000,2,2,True
519 ENV_WAVE_B,2,8,False,1,000,0,000,1,1,True
520 ENV_WAVE_G,2,8,False,1,000,0,000,1,1,True
521 HLP_RED_ME,0,0,True,1,000,0,000,1,1,True
522 B_COMP_1SI,0,0,True,1,000,0,000,4,1,True
523 HLP_GLOW_B,1,0,False,1,000,0,000,3,1,True
524 HLP_LAMP_B,0,0,True,1,000,0,000,3,1,True
525 HLP_PLACE_B,0,0,True,1,000,0,000,3,1,True
526 HLP_RAY_B,1,0,False,1,000,0,000,3,1,True
527 HLP_SIGN_B,1,0,False,1,000,0,000,3,1,True
528 ENV_WAVE_R,2,8,False,1,000,0,000,1,1,True
529 R_NRS_PGL,0,0,True,1,000,0,000,3,1,True
530 R_NM_RGL,0,0,True,1,000,0,000,3,1,True
531 R_PG_RGL,0,0,True,1,000,0,000,3,1,True
532 R_FL_RGL,0,0,True,1,000,0,000,3,1,True
533 DEFL_BL,2,8,False,1,000,0,000,4,1,True
534 CLOUD2L,2,8,False,1,000,0,000,15,1,True
535 ENV_NLAVA,1,0,False,1,000,0,000,8,1,True
536 ENV_NLAVA_M,1,0,False,1,000,0,000,8,1,True
537 GLOW_ENG,2,8,False,1,000,0,000,1,1,True
538 FIRE_ADD,2,8,False,1,000,0,000,23,1,True
539 SMOKE_ADD,2,8,False,1,000,0,000,4,1,True
540 OK_TEST1,0,0,True,1,000,0,000,1,1,True
541 SMOKE_Y,1,0,False,1,000,0,000,16,1,True
542 SMOKE_Y_ADD,2,8,False,1,000,0,000,16,1,True
543 DUST_Y,1,0,False,1,000,0,000,4,1,True
544 FIRE_SMOKE_G,1,0,False,1,000,0,000,23,1,True
545 FIRE_SMOKE_GADD,2,8,False,1,000,0,000,23,1,True
546 SMOKE_YG,1,0,False,1,000,0,000,16,1,True
547 SMOKE_YG_ADD,2,8,False,1,000,0,000,16,1,True
548 SMOKE_G,1,0,False,1,000,0,000,16,1,True
549 SMOKE_G_ADD,2,8,False,1,000,0,000,16,1,True
550 SPITTLE_G_ADD,2,8,False,1,000,0,000,16,1,True
551 WD_G_ADD,2,8,False,1,000,0,000,6,1,True
552 TOK41,1,0,False,1,000,0,000,1,1,True
553 DUST_ADD,2,8,False,1,000,0,000,4,1,True
554 TOK31,0,0,True,1,000,0,000,1,1,True
555 TOK31DARK,0,0,True,1,000,0,000,1,1,True
556 TOK32,1,0,False,1,000,0,000,4,1,True
557 TOK42,0,0,True,1,000,0,000,1,1,True
558 BUILD_SPHERE_A,1,0,False,1,000,0,000,4,1,True
559 LASER_RN,1,0,False,1,000,0,000,4,1,True
560 TOK43,0,0,True,1,000,0,000,1,1,True
561 TOK43DARK,0,0,True,1,000,0,000,1,1,True
562 TOK43_2,0,0,True,1,000,0,000,4,1,True
563 TOK43_2D,0,0,True,1,000,0,000,1,1,False
564 TOK43A,0,0,True,1,000,0,000,1,1,True
565 SMOKE_R,1,0,False,1,000,0,000,16,1,True
566 SMOKE_R_ADD,2,8,False,1,000,0,000,16,1,True
567 TOK41_RAY,2,8,False,1,000,0,000,8,1,True
568 SMOKE_GG,1,0,False,1,000,0,000,16,1,True
569 SMOKE_GG_ADD,2,8,False,1,000,0,000,16,1,True
570 SMOKE_W_ADD,2,8,False,1,000,0,000,10,1,True
571 TOK41_PLC,1,0,False,1,000,0,000,4,1,True
572 TOK33_BASE,2,8,False,1,000,0,000,4,1,True
573 TOK44_BASE,2,8,False,1,000,0,000,4,1,True
574 SMOKE_W1_ADD,2,8,False,1,000,0,000,10,1,True
575 TOK42W,0,0,True,1,000,0,000,1,1,True
576 TOK42_W4,0,0,True,1,000,0,000,3,1,True
577 TOK42_W1E,0,0,True,1,000,0,000,2,1,True
578 TOK42_W1M,0,0,True,1,000,0,000,2,1,True
579 TOK42_W5,0,0,True,1,000,0,000,2,1,True
580 SMOKE_R1_ADD,2,8,False,1,000,0,000,16,1,True
581 DUST_WORM_ADD,2,8,False,1,000,0,000,4,1,True
582 DUST_WORM,1,0,False,1,000,0,000,4,1,True
583 TOK11,0,0,True,1,000,0,000,1,1,True
584 TOK11_H,0,0,True,1,000,0,000,3,1,True
585 TOK11_Y,0,0,True,1,000,0,000,3,1,True
586 R_RLW_5L,0,0,True,1,000,0,000,1,1,True
587 PMAP_5,0,0,True,1,000,0,000,1,1,True
588 PMAP_42,0,0,True,1,000,0,000,1,1,False
589 PMAP_1,0,0,True,1,000,0,000,1,1,True
590 PMAP_2,0,0,True,1,000,0,000,1,1,True
591 PMAP_3,0,0,True,1,000,0,000,1,1,True
592 PMAP_4,0,0,True,1,000,0,000,1,1,True
593 PMAP_18,0,0,True,1,000,0,000,1,1,True
594 PMAP_31,0,0,True,1,000,0,000,1,1,False
595 PMAP_14,0,0,True,1,000,0,000,1,1,False
596 PMAP_15,0,0,True,1,000,0,000,1,1,True
597 PMAP_16,0,0,True,1,000,0,000,1,1,True
598 PMAP_17,0,0,True,1,000,0,000,1,1,True
599 HLP_GLOW_G,1,0,False,1,000,0,000,3,1,True
600 HLP_LAMP_G,0,0,True,1,000,0,000,3,1,True
601 HLP_PLACE_G,0,0,True,1,000,0,000,3,1,True
602 HLP_RAY_G,1,0,False,1,000,0,000,3,1,True
603 HLP_SIGN_G,1,0,False,1,000,0,000,3,1,True
604 HLP_GLOW_R,1,0,False,1,000,0,000,3,1,True
605 HLP_LAMP_R,0,0,True,1,000,0,000,3,1,True
606 HLP_PLACE_R,0,0,True,1,000,0,000,3,1,True
607 HLP_RAY_R,1,0,False,1,000,0,000,3,1,True
608 R_SRL_25,0,0,True,1,000,0,000,1,1,True
609 R_SRL_25REV,0,0,True,1,000,0,000,1,1,True
610 R_SRL_26,0,0,True,1,000,0,000,1,1,False
611 R_SRL_26REV,0,0,True,1,000,0,000,1,1,True
612 R_SRL_27,0,0,True,1,000,0,000,1,1,True
613 R_SRL_27REV,0,0,True,1,000,0,000,1,1,True
614 DEFL_Y_ADD,2,8,False,1,000,0,000,4,1,True
615 DEFL_Y1_ADD,2,8,False,1,000,0,000,4,1,True
616 TOK51,1,0,False,1,000,0,000,1,1,True
617 TF2,1,0,False,1,000,0,000,1,1,True
618 TF1,0,0,True,1,000,0,000,1,1,True
619 TF1_05,0,0,True,1,000,0,000,1,1,False
620 TF1_06A,0,0,True,1,000,0,000,4,1,True
621 TF1_BLACK,0,0,True,1,000,0,000,1,1,True
622 TF1_06,0,0,True,1,000,0,000,1,1,True
623 LASER_R_ADD,2,8,False,1,000,0,000,6,1,True
624 TOK76_UP,1,0,False,1,000,0,000,3,1,True
625 TOK76_DN,1,0,False,1,000,0,000,1,1,True
626 TOK41_RAYR,2,8,False,1,000,0,000,8,1,True
627 SMOKE_R2_ADD,2,8,False,1,000,0,000,10,1,True
628 JET_R,2,8,False,1,000,0,000,1,1,True
629 JET_G,2,8,False,1,000,0,000,1,1,True