fpopov thanks

This commit is contained in:
bird_egop
2026-05-09 20:36:43 +03:00
parent d73a6dde70
commit eb458ad9a8
43 changed files with 1562 additions and 1233 deletions
+8 -3
View File
@@ -3,9 +3,14 @@ using System.Runtime.InteropServices;
namespace Common;
/// <summary>DEBUG ONLY. Одни и те же 4 байта, интерпретированные как int32 и float32.</summary>
/// <param name="AsInt">Значение как int32.</param>
/// <param name="AsFloat">Значение как float32.</param>
[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")]
public class IntFloatValue(Span<byte> span)
public record class IntFloatValue(int AsInt, float AsFloat)
{
public int AsInt { get; set; } = MemoryMarshal.Read<int>(span);
public float AsFloat { get; set; } = MemoryMarshal.Read<float>(span);
public IntFloatValue(Span<byte> span)
: this(MemoryMarshal.Read<int>(span), MemoryMarshal.Read<float>(span))
{
}
}
+107 -113
View File
@@ -1,152 +1,148 @@
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>Файл материала MAT0.</summary>
/// <param name="FileName">Имя файла из NRes metadata, не из payload материала.</param>
/// <param name="Version">Значение attr1 / ElementCount из NRes metadata.</param>
/// <param name="Magic1">Значение attr2 / Magic1 из NRes metadata.</param>
/// <param name="MaterialRenderingType">Производное от Version: (Version >> 2) &amp; 0xF. 0 = standard, 1 = special, 2 = particle.</param>
/// <param name="SupportsBumpMapping">Производное от Version: (Version &amp; 2) != 0.</param>
/// <param name="IsParticleEffect">Производное от Version: Version &amp; 40. 0 = normal, 8 = particle/effect.</param>
/// <param name="SourceBlendMode">Глобальный source blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
/// <param name="DestBlendMode">Глобальный destination blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
/// <param name="GlobalAlphaMultiplier">Глобальный alpha multiplier; читается, если Magic1 > 2. Значение по умолчанию: 1.0.</param>
/// <param name="GlobalEmissiveIntensity">Глобальная emissive intensity; читается, если Magic1 > 3. Значение по умолчанию: 0.0.</param>
/// <param name="Stages">Стадии материала в порядке чтения из файла.</param>
/// <param name="Animations">Анимации материала в порядке чтения из файла.</param>
public record MaterialFile(
string FileName,
int Version,
int Magic1,
int MaterialRenderingType,
bool SupportsBumpMapping,
int IsParticleEffect,
BlendMode SourceBlendMode,
BlendMode DestBlendMode,
float GlobalAlphaMultiplier,
float GlobalEmissiveIntensity,
List<MaterialStage> Stages,
List<MaterialAnimation> Animations);
/// <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.
/// Режимы blend для материала. Управляют смешиванием source и destination цветов.
/// Формула: FinalColor = SourceColor * SourceBlend [operation] DestColor * DestBlend.
/// Соответствуют значениям Direct3D D3DBLEND.
/// </summary>
public enum BlendMode : byte
{
/// <summary>Blend factor is (0, 0, 0, 0) - results in black/transparent</summary>
/// <summary>Blend factor = (0, 0, 0, 0).</summary>
Zero = 1,
/// <summary>Blend factor is (1, 1, 1, 1) - uses full color value</summary>
/// <summary>Blend factor = (1, 1, 1, 1).</summary>
One = 2,
/// <summary>Blend factor is (Rs, Gs, Bs, As) - uses source color</summary>
/// <summary>Blend factor = (Rs, Gs, Bs, As).</summary>
SrcColor = 3,
/// <summary>Blend factor is (1-Rs, 1-Gs, 1-Bs, 1-As) - uses inverted source color</summary>
/// <summary>Blend factor = (1-Rs, 1-Gs, 1-Bs, 1-As).</summary>
InvSrcColor = 4,
/// <summary>Blend factor is (As, As, As, As) - uses source alpha for all channels (standard transparency)</summary>
/// <summary>Blend factor = (As, As, As, As).</summary>
SrcAlpha = 5,
/// <summary>Blend factor is (1-As, 1-As, 1-As, 1-As) - uses inverted source alpha</summary>
/// <summary>Blend factor = (1-As, 1-As, 1-As, 1-As).</summary>
InvSrcAlpha = 6,
/// <summary>Blend factor is (Ad, Ad, Ad, Ad) - uses destination alpha</summary>
/// <summary>Blend factor = (Ad, Ad, Ad, Ad).</summary>
DestAlpha = 7,
/// <summary>Blend factor is (1-Ad, 1-Ad, 1-Ad, 1-Ad) - uses inverted destination alpha</summary>
/// <summary>Blend factor = (1-Ad, 1-Ad, 1-Ad, 1-Ad).</summary>
InvDestAlpha = 8,
/// <summary>Blend factor is (Rd, Gd, Bd, Ad) - uses destination color</summary>
/// <summary>Blend factor = (Rd, Gd, Bd, Ad).</summary>
DestColor = 9,
/// <summary>Blend factor is (1-Rd, 1-Gd, 1-Bd, 1-Ad) - uses inverted destination color</summary>
/// <summary>Blend factor = (1-Rd, 1-Gd, 1-Bd, 1-Ad).</summary>
InvDestColor = 10,
/// <summary>Blend factor is (f, f, f, 1) where f = min(As, 1-Ad) - saturates source alpha</summary>
/// <summary>Blend factor = (f, f, f, 1), где f = min(As, 1-Ad).</summary>
SrcAlphaSat = 11,
/// <summary>Blend factor is (As, As, As, As) for both source and dest - obsolete in D3D9+</summary>
/// <summary>Blend factor = (As, As, As, As) для source и destination; устарело в 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>
/// <summary>Blend factor = (1-As, 1-As, 1-As, 1-As) для source и destination; устарело в D3D9+.</summary>
BothInvSrcAlpha = 13,
/// <summary>Unknown or uninitialized blend mode (0xFF default value)</summary>
/// <summary>Неизвестный или неинициализированный режим blend; значение по умолчанию 0xFF.</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)
/// <summary>Стадия материала. Порядок полей соответствует порядку чтения из файла, а не C struct layout.</summary>
/// <param name="AmbientR">Ambient R, читается первым цветовым блоком.</param>
/// <param name="AmbientG">Ambient G, читается первым цветовым блоком.</param>
/// <param name="AmbientB">Ambient B, читается первым цветовым блоком.</param>
/// <param name="AmbientA">Ambient A, читается первым цветовым блоком и масштабируется на 0.01.</param>
/// <param name="DiffuseR">Diffuse R, читается вторым цветовым блоком.</param>
/// <param name="DiffuseG">Diffuse G, читается вторым цветовым блоком.</param>
/// <param name="DiffuseB">Diffuse B, читается вторым цветовым блоком.</param>
/// <param name="DiffuseA">Diffuse A, читается вторым цветовым блоком.</param>
/// <param name="SpecularR">Specular R, читается третьим цветовым блоком.</param>
/// <param name="SpecularG">Specular G, читается третьим цветовым блоком.</param>
/// <param name="SpecularB">Specular B, читается третьим цветовым блоком.</param>
/// <param name="SpecularA">Specular A, читается третьим цветовым блоком.</param>
/// <param name="EmissiveR">Emissive R, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveG">Emissive G, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveB">Emissive B, читается четвертым цветовым блоком.</param>
/// <param name="EmissiveA">Emissive A, читается четвертым цветовым блоком.</param>
/// <param name="Power">Power, один байт -> float.</param>
/// <param name="TextureStageIndex">Индекс texture stage. 255 = не задано/default, 0..47 = ссылка на texture stage.</param>
/// <param name="TextureName">Имя текстуры, строка из 16 байт.</param>
public record MaterialStage(
float AmbientR,
float AmbientG,
float AmbientB,
float AmbientA,
float DiffuseR,
float DiffuseG,
float DiffuseB,
float DiffuseA,
float SpecularR,
float SpecularG,
float SpecularB,
float SpecularA,
float EmissiveR,
float EmissiveG,
float EmissiveB,
float EmissiveA,
float Power,
int TextureStageIndex,
string TextureName);
// 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;
}
/// <summary>Анимация материала.</summary>
/// <param name="Target">Целевые компоненты анимации: биты 3..31 combined field.</param>
/// <param name="LoopMode">Режим повтора: биты 0..2 combined field.</param>
/// <param name="Keys">Ключи анимации.</param>
/// <param name="TargetDescription">Кэшированное описание target для UI.</param>
public record MaterialAnimation(
AnimationTarget Target,
AnimationLoopMode LoopMode,
List<AnimKey> Keys,
string TargetDescription);
[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.
// Это bitset: несколько флагов могут комбинироваться.
// Установленный флаг означает интерполяцию компонента между стадиями.
// Неустановленный флаг означает копирование компонента из исходной стадии.
// Если все флаги равны 0, вся стадия копируется без интерполяции.
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)
Ambient = 1, // 0x01: интерполирует Ambient RGB.
Diffuse = 2, // 0x02: интерполирует Diffuse RGB.
Specular = 4, // 0x04: интерполирует Specular RGB.
Emissive = 8, // 0x08: интерполирует Emissive RGB.
Power = 16 // 0x10: интерполирует Ambient.A и задает Power.
}
@@ -158,10 +154,8 @@ public enum AnimationLoopMode : int
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
}
/// <summary>Ключ анимации материала (length = 6).</summary>
/// <param name="StageIndex">[0x00..0x02] Индекс стадии материала.</param>
/// <param name="DurationMs">[0x02..0x04] Длительность в миллисекундах.</param>
/// <param name="InterpolationCurve">[0x04..0x06] Кривая интерполяции. В исследованных данных всегда 0.</param>
public readonly record struct AnimKey(ushort StageIndex, ushort DurationMs, ushort InterpolationCurve);
+88 -69
View File
@@ -7,17 +7,9 @@ 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;
var materialRenderingType = elementCount >> 2 & 0xf;
var supportsBumpMapping = (elementCount & 2) != 0;
var isParticleEffect = elementCount & 40;
// Reading content
var stageCount = fs.ReadUInt16LittleEndian();
@@ -25,113 +17,140 @@ public static class MaterialParser
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
// Значения по умолчанию из C-кода.
var globalAlphaMultiplier = 1.0f; // field8_0x15c
var globalEmissiveIntensity = 0.0f; // field9_0x160
var sourceBlendMode = BlendMode.Unknown; // field6_0x154
var destBlendMode = BlendMode.Unknown; // field7_0x158
if (magic >= 2)
{
file.SourceBlendMode = (BlendMode)fs.ReadByte();
file.DestBlendMode = (BlendMode)fs.ReadByte();
sourceBlendMode = (BlendMode)fs.ReadByte();
destBlendMode = (BlendMode)fs.ReadByte();
}
if (magic > 2)
{
file.GlobalAlphaMultiplier = fs.ReadFloatLittleEndian();
globalAlphaMultiplier = fs.ReadFloatLittleEndian();
}
if (magic > 3)
{
file.GlobalEmissiveIntensity = fs.ReadFloatLittleEndian();
globalEmissiveIntensity = fs.ReadFloatLittleEndian();
}
// --- 2. Material Stages ---
// Стадии материала.
const float Inv255 = 1.0f / 255.0f;
const float Field7Mult = 0.01f;
Span<byte> textureNameBuffer = stackalloc byte[16];
List<MaterialStage> stages = [];
for (int i = 0; i < stageCount; i++)
{
var stage = new MaterialStage();
// Порядок чтения соответствует файлу, а не C struct layout.
// === FILE READ ORDER (matches decompiled.c lines 159-217) ===
// Ambient: 4 байта, A масштабируется на 0.01.
var ambientR = fs.ReadByte() * Inv255;
var ambientG = fs.ReadByte() * Inv255;
var ambientB = fs.ReadByte() * Inv255;
var ambientA = fs.ReadByte() * Field7Mult; // 0.01 scaling
// 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
// Diffuse: 4 байта.
var diffuseR = fs.ReadByte() * Inv255;
var diffuseG = fs.ReadByte() * Inv255;
var diffuseB = fs.ReadByte() * Inv255;
var diffuseA = fs.ReadByte() * Inv255;
// 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;
// Specular: 4 байта.
var specularR = fs.ReadByte() * Inv255;
var specularG = fs.ReadByte() * Inv255;
var specularB = fs.ReadByte() * Inv255;
var specularA = 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;
// Emissive: 4 байта.
var emissiveR = fs.ReadByte() * Inv255;
var emissiveG = fs.ReadByte() * Inv255;
var emissiveB = fs.ReadByte() * Inv255;
var emissiveA = 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;
// Power: 1 байт -> float.
var power = (float)fs.ReadByte();
// 5. Power (1 byte → float) - Line 207
stage.Power = (float)fs.ReadByte();
// Texture stage index: 1 байт.
var textureStageIndex = fs.ReadByte();
// 6. Texture Stage Index (1 byte) - Line 210
stage.TextureStageIndex = fs.ReadByte();
// 7. Texture Name (16 bytes) - Lines 212-217
// Texture name: 16 байт.
textureNameBuffer.Clear();
fs.ReadExactly(textureNameBuffer);
stage.TextureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
var textureName = Encoding.ASCII.GetString(textureNameBuffer).TrimEnd('\0');
file.Stages.Add(stage);
stages.Add(new MaterialStage(
ambientR,
ambientG,
ambientB,
ambientA,
diffuseR,
diffuseG,
diffuseB,
diffuseA,
specularR,
specularG,
specularB,
specularA,
emissiveR,
emissiveG,
emissiveB,
emissiveA,
power,
textureStageIndex,
textureName));
}
// --- 3. Animations ---
// Анимации.
List<MaterialAnimation> 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);
var target = (AnimationTarget)(typeAndParams >> 3);
var loopMode = (AnimationLoopMode)(typeAndParams & 7);
ushort keyCount = fs.ReadUInt16LittleEndian();
List<AnimKey> keys = [];
for (int k = 0; k < keyCount; k++)
{
var key = new AnimKey
{
StageIndex = fs.ReadUInt16LittleEndian(),
DurationMs = fs.ReadUInt16LittleEndian(),
InterpolationCurve = fs.ReadUInt16LittleEndian()
};
anim.Keys.Add(key);
keys.Add(new AnimKey(
fs.ReadUInt16LittleEndian(),
fs.ReadUInt16LittleEndian(),
fs.ReadUInt16LittleEndian()));
}
// Precompute description for UI to avoid per-frame allocations
anim.TargetDescription = ComputeTargetDescription(anim.Target);
// Описание вычисляется один раз при парсинге, чтобы UI не выделял память каждый кадр.
var targetDescription = ComputeTargetDescription(target);
file.Animations.Add(anim);
animations.Add(new MaterialAnimation(target, loopMode, keys, targetDescription));
}
return file;
return new MaterialFile(
fileName,
elementCount,
magic1,
materialRenderingType,
supportsBumpMapping,
isParticleEffect,
sourceBlendMode,
destBlendMode,
globalAlphaMultiplier,
globalEmissiveIntensity,
stages,
animations);
}
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)";
return "Без интерполяции - вся стадия копируется как есть (Flags: 0x0)";
var parts = new List<string>();
@@ -146,7 +165,7 @@ public static class MaterialParser
if ((target & AnimationTarget.Power) != 0)
parts.Add("Ambient.A + Power");
return $"Interpolates: {string.Join(", ", parts)} | Other components copied (Flags: 0x{(int)target:X})";
return $"Интерполируется: {string.Join(", ", parts)} | Остальные компоненты копируются (Flags: 0x{(int)target:X})";
}
}
+26 -28
View File
@@ -1,30 +1,28 @@
namespace MissionTmaLib;
public class ClanInfo
{
public string ClanName { get; set; }
public int UnkInt1 { get; set; }
public float X { get; set; }
public float Y { get; set; }
/// <summary>
/// 1 - игрок, 2 AI, 3 - нейтральный
/// </summary>
public ClanType ClanType { get; set; }
public string ScriptsString { get; set; }
public int UnknownClanPartCount { get; set; }
public List<UnknownClanTreeInfoPart> UnknownParts { get; set; }
/// <summary>
/// Игра называет этот путь TreeName
/// </summary>
public string ResearchNResPath { get; set; }
public int Brains { get; set; }
public int AlliesMapCount { get; set; }
/// <summary>
/// мапа союзников (ключ - имя клана, значение - число, всегда либо 0 либо 1)
/// </summary>
public Dictionary<string, int> AlliesMap { get; set; }
}
/// <summary>Информация о клане из mission TMA.</summary>
/// <param name="ClanName">Имя клана.</param>
/// <param name="UnkInt1">Неизвестное целочисленное поле.</param>
/// <param name="X">TODO</param>
/// <param name="Y">TODO</param>
/// <param name="ClanType">Тип клана: 1 = игрок, 2 = AI, 3 = нейтральный.</param>
/// <param name="ScriptsString">TODO</param>
/// <param name="UnknownClanPartCount">Количество записей UnknownParts.</param>
/// <param name="UnknownParts">TODO</param>
/// <param name="ResearchNResPath">Путь к NRes с деревом исследований. Игра называет этот путь TreeName.</param>
/// <param name="Brains">Количество "мозгов" AI/brains.</param>
/// <param name="AlliesMapCount">Количество записей AlliesMap.</param>
/// <param name="AlliesMap">Мапа союзников: ключ = имя клана, значение всегда 0 или 1.</param>
public record ClanInfo(
string ClanName,
int UnkInt1,
float X,
float Y,
ClanType ClanType,
string ScriptsString,
int UnknownClanPartCount,
List<UnknownClanTreeInfoPart> UnknownParts,
string ResearchNResPath,
int Brains,
int AlliesMapCount,
Dictionary<string, int> AlliesMap);
+28 -36
View File
@@ -2,39 +2,31 @@
namespace MissionTmaLib;
public class GameObjectInfo
{
// 0 - здание, 1 - бот, 2 - окружение
public GameObjectType Type { get; set; }
public int UnknownFlags { get; set; }
public string DatString { get; set; }
/// <summary>
/// Индекс клана, которому принадлежит объект
/// </summary>
/// <remarks>
/// <para>
/// Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1
/// </para>
/// <para>
/// Может быть -1, если объект никому не принадлежит, я такое встречал только у объектов окружения
/// </para>
/// </remarks>
public int OwningClanIndex { get; set; }
public int Order { get; set; }
public Vector3 Position { get; set; }
public Vector3 Rotation { get; set; }
public Vector3 Scale { get; set; }
public string UnknownString2 { get; set; }
public int UnknownInt4 { get; set; }
public int UnknownInt5 { get; set; }
public int UnknownInt6 { get; set; }
public GameObjectSettings Settings { get; set; }
}
/// <summary>Информация об объекте миссии из mission TMA.</summary>
/// <param name="Type">Тип объекта: 0 = здание, 1 = бот, 2 = окружение.</param>
/// <param name="UnknownFlags">Неизвестные флаги объекта.</param>
/// <param name="DatString">Путь к DAT ресурсу объекта.</param>
/// <param name="OwningClanIndex">Индекс клана-владельца. Может быть -1, если объект никому не принадлежит. Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1</param>
/// <param name="Order">Порядок объекта. Для зданий парсер добавляет int.MaxValue.</param>
/// <param name="Position">Позиция объекта.</param>
/// <param name="Rotation">Поворот объекта.</param>
/// <param name="Scale">Масштаб объекта. Для старых feature set может быть принудительно (1, 1, 1).</param>
/// <param name="UnknownString2">Неизвестная строка, например у HERO бывает пустой.</param>
/// <param name="UnknownInt4">Неизвестное целочисленное поле.</param>
/// <param name="UnknownInt5">Неизвестное целочисленное поле.</param>
/// <param name="UnknownInt6">Неизвестное целочисленное поле.</param>
/// <param name="Settings">Настройки объекта.</param>
public record class GameObjectInfo(
GameObjectType Type,
int UnknownFlags,
string DatString,
int OwningClanIndex,
int Order,
Vector3 Position,
Vector3 Rotation,
Vector3 Scale,
string UnknownString2,
int UnknownInt4,
int UnknownInt5,
int UnknownInt6,
GameObjectSettings Settings);
+73 -42
View File
@@ -72,32 +72,36 @@ public class MissionTmaParser
List<ClanInfo> infos = [];
for (var i = 0; i < clanCount; i++)
{
var clanTreeInfo = new ClanInfo();
clanTreeInfo.ClanName = fileStream.ReadLengthPrefixedString();
clanTreeInfo.UnkInt1 = fileStream.ReadInt32LittleEndian();
clanTreeInfo.X = fileStream.ReadFloatLittleEndian();
clanTreeInfo.Y = fileStream.ReadFloatLittleEndian();
clanTreeInfo.ClanType = (ClanType) fileStream.ReadInt32LittleEndian();
var clanName = fileStream.ReadLengthPrefixedString();
var unkInt1 = fileStream.ReadInt32LittleEndian();
var x = fileStream.ReadFloatLittleEndian();
var y = fileStream.ReadFloatLittleEndian();
var clanType = (ClanType) fileStream.ReadInt32LittleEndian();
var scriptsString = string.Empty;
var unknownClanPartCount = 0;
List<UnknownClanTreeInfoPart> unknownParts = [];
var researchNResPath = string.Empty;
var brains = 0;
var alliesMapCount = 0;
Dictionary<string, int> alliesMap = [];
if (1 < clanFeatureSet)
{
// MISSIONS\SCRIPTS\default
// MISSIONS\SCRIPTS\tut1_pl
// MISSIONS\SCRIPTS\tut1_en
clanTreeInfo.ScriptsString = fileStream.ReadLengthPrefixedString();
scriptsString = fileStream.ReadLengthPrefixedString();
}
if (2 < clanFeatureSet)
{
clanTreeInfo.UnknownClanPartCount = fileStream.ReadInt32LittleEndian();
unknownClanPartCount = fileStream.ReadInt32LittleEndian();
// тут игра читает число, затем 12 байт и ещё 2 числа
List<UnknownClanTreeInfoPart> unknownClanTreeInfoParts = [];
for (var i1 = 0; i1 < clanTreeInfo.UnknownClanPartCount; i1++)
for (var i1 = 0; i1 < unknownClanPartCount; i1++)
{
unknownClanTreeInfoParts.Add(
unknownParts.Add(
new UnknownClanTreeInfoPart(
fileStream.ReadInt32LittleEndian(),
new Vector3(
@@ -110,8 +114,6 @@ public class MissionTmaParser
)
);
}
clanTreeInfo.UnknownParts = unknownClanTreeInfoParts;
}
if (3 < clanFeatureSet)
@@ -120,17 +122,17 @@ public class MissionTmaParser
// MISSIONS\SCRIPTS\data.trf
// указатель на NRes файл с данными
// может быть пустым, например у Ntrl в туториале
clanTreeInfo.ResearchNResPath = fileStream.ReadLengthPrefixedString();
researchNResPath = fileStream.ReadLengthPrefixedString();
}
if (4 < clanFeatureSet)
{
clanTreeInfo.Brains = fileStream.ReadInt32LittleEndian();
brains = fileStream.ReadInt32LittleEndian();
}
if (5 < clanFeatureSet)
{
clanTreeInfo.AlliesMapCount = fileStream.ReadInt32LittleEndian();
alliesMapCount = fileStream.ReadInt32LittleEndian();
// тут какая-то мапа
// в демо миссии тут
@@ -142,20 +144,29 @@ public class MissionTmaParser
// Trgt -> 1
// Enm -> 0
// Ntrl -> 1
Dictionary<string, int> map = [];
for (var i1 = 0; i1 < clanTreeInfo.AlliesMapCount; i1++)
for (var i1 = 0; i1 < alliesMapCount; i1++)
{
var keyIdString = fileStream.ReadLengthPrefixedString();
// это число всегда либо 0 либо 1
var unkNumber = fileStream.ReadInt32LittleEndian();
map[keyIdString] = unkNumber;
alliesMap[keyIdString] = unkNumber;
}
}
clanTreeInfo.AlliesMap = map;
}
infos.Add(clanTreeInfo);
infos.Add(new ClanInfo(
clanName,
unkInt1,
x,
y,
clanType,
scriptsString,
unknownClanPartCount,
unknownParts,
researchNResPath,
brains,
alliesMapCount,
alliesMap));
}
var clanInfo = new ClansFileData(clanFeatureSet, clanCount, infos);
@@ -177,51 +188,58 @@ public class MissionTmaParser
for (var i = 0; i < gameObjectsCount; i++)
{
var gameObjectInfo = new GameObjectInfo();
// ReadGameObjectData
gameObjectInfo.Type = (GameObjectType) fileStream.ReadInt32LittleEndian();
gameObjectInfo.UnknownFlags = fileStream.ReadInt32LittleEndian();
var type = (GameObjectType) fileStream.ReadInt32LittleEndian();
var unknownFlags = fileStream.ReadInt32LittleEndian();
// UNITS\UNITS\HERO\hero_t.dat
gameObjectInfo.DatString = fileStream.ReadLengthPrefixedString();
var datString = fileStream.ReadLengthPrefixedString();
var owningClanIndex = 0;
var order = 0;
var unknownString2 = string.Empty;
var unknownInt4 = 0;
var unknownInt5 = 0;
var unknownInt6 = 0;
var settingsData = new GameObjectSettings(0, 0, []);
if (2 < gameObjectsFeatureSet)
{
gameObjectInfo.OwningClanIndex = fileStream.ReadInt32LittleEndian();
owningClanIndex = fileStream.ReadInt32LittleEndian();
}
if (3 < gameObjectsFeatureSet)
{
gameObjectInfo.Order = fileStream.ReadInt32LittleEndian();
if (gameObjectInfo.Type == GameObjectType.Building)
order = fileStream.ReadInt32LittleEndian();
if (type == GameObjectType.Building)
{
gameObjectInfo.Order += int.MaxValue;
order += int.MaxValue;
}
}
// читает 12 байт
gameObjectInfo.Position = new Vector3(
var position = new Vector3(
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian()
);
// ещё раз читает 12 байт
gameObjectInfo.Rotation = new Vector3(
var rotation = new Vector3(
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian()
);
Vector3 scale;
if (gameObjectsFeatureSet < 10)
{
// если фичесет меньше 10, то игра забивает вектор единицами
gameObjectInfo.Scale = new Vector3(1, 1, 1);
scale = new Vector3(1, 1, 1);
}
else
{
// в противном случае читает ещё вектор из файла
gameObjectInfo.Scale = new Vector3(
scale = new Vector3(
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian()
@@ -231,18 +249,18 @@ public class MissionTmaParser
if (6 < gameObjectsFeatureSet)
{
// у HERO пустая строка
gameObjectInfo.UnknownString2 = fileStream.ReadLengthPrefixedString();
unknownString2 = fileStream.ReadLengthPrefixedString();
}
if (7 < gameObjectsFeatureSet)
{
gameObjectInfo.UnknownInt4 = fileStream.ReadInt32LittleEndian();
unknownInt4 = fileStream.ReadInt32LittleEndian();
}
if (8 < gameObjectsFeatureSet)
{
gameObjectInfo.UnknownInt5 = fileStream.ReadInt32LittleEndian();
gameObjectInfo.UnknownInt6 = fileStream.ReadInt32LittleEndian();
unknownInt5 = fileStream.ReadInt32LittleEndian();
unknownInt6 = fileStream.ReadInt32LittleEndian();
}
if (5 < gameObjectsFeatureSet)
@@ -318,10 +336,23 @@ public class MissionTmaParser
);
}
gameObjectInfo.Settings = new GameObjectSettings(unused, innerCount, settings);
settingsData = new GameObjectSettings(unused, innerCount, settings);
}
gameObjectInfos.Add(gameObjectInfo);
gameObjectInfos.Add(new GameObjectInfo(
type,
unknownFlags,
datString,
owningClanIndex,
order,
position,
rotation,
scale,
unknownString2,
unknownInt4,
unknownInt5,
unknownInt6,
settingsData));
// end ReadGameObjectData
}
+28 -27
View File
@@ -1,4 +1,4 @@
namespace NResLib;
namespace NResLib;
/// <summary>
/// Архив NRes (файл NRes)
@@ -8,40 +8,41 @@ public record NResArchive(NResArchiveHeader Header, List<ListMetadataItem> Files
/// <summary>
/// Заголовок файла
/// </summary>
/// <param name="NRes">[0..4] ASCII NRes</param>
/// <param name="Version">[4..8] Версия кодировщика (должно быть всегда 0x100)</param>
/// <param name="FileCount">[8..12] Количество файлов </param>
/// <param name="TotalFileLengthBytes">[12..16] Длина всего архива</param>
/// <param name="NRes">[0..4] ASCII NRes.</param>
/// <param name="Version">[4..8] Версия кодировщика, обычно 0x100.</param>
/// <param name="FileCount">[8..12] Количество записей каталога.</param>
/// <param name="TotalFileLengthBytes">[12..16] Длина всего архива.</param>
public record NResArchiveHeader(string NRes, int Version, int FileCount, int TotalFileLengthBytes);
/// <summary>
/// В конце файла есть список метаданных,
/// каждый элемент это 64 байта,
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64)
/// 64-байтная запись каталога NRes.
///
/// Важный нюанс RE-имен:
/// - ElementCount/Magic1/ElementSize оставлены как удобные имена для исследования, но на уровне
/// общего NRes-контейнера это attr1/attr2/attr3. Их смысл зависит от TypeId ресурса.
/// - FileName на диске занимает 36 байт: [20..56]. Старый разбор читал только [20..40].
/// - Старые Magic3..Magic6 оказались не отдельными полями формата, а байтами хвоста name_raw.
/// - SortIndex на диске это перестановка для бинарного поиска по имени, а не порядковый
/// индекс записи каталога. Порядковый индекс хранится отдельно в DirectoryIndex.
/// </summary>
/// <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="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param>
/// <param name="FileName">[20..40] ASCII имя файла</param>
/// <param name="Magic3">[40..44] Неизвестное число</param>
/// <param name="Magic4">[44..48] Неизвестное число</param>
/// <param name="Magic5">[48..52] Неизвестное число</param>
/// <param name="Magic6">[52..56] Неизвестное число</param>
/// <param name="OffsetInFile">[56..60] Смещение подфайла от начала NRes (именно самого NRes) в байтах</param>
/// <param name="Index">[60..64] Индекс в файле (от 0, не больше чем кол-во файлов)</param>
public record ListMetadataItem(
/// <param name="TypeId">[0x00..0x04] Resource type id.</param>
/// <param name="FileType">[0x00..0x04] Resource type id как ASCII или hex bytes для RE.</param>
/// <param name="ElementCount">[0x04..0x08] ElementCount. смысл зависит от TypeId.</param>
/// <param name="Magic1">[0x08..0x0C] смысл зависит от TypeId.</param>
/// <param name="FileLength">[0x0C..0x10] Длина payload в байтах.</param>
/// <param name="ElementSize">[0x10..0x14] ElementSize. смысл зависит от TypeId.</param>
/// <param name="FileName">[0x14..0x38] C-string из 36-байтного name_raw.</param>
/// <param name="OffsetInFile">[0x38..0x3C] Смещение payload от начала архива.</param>
/// <param name="SortIndex">[0x3C..0x40] Индекс перестановки для бинарного поиска по имени.</param>
/// <param name="DirectoryIndex">Индекс записи в порядке каталога; не хранится отдельным полем NRes.</param>
public sealed record class ListMetadataItem(
uint TypeId,
string FileType,
uint ElementCount,
int Magic1,
int FileLength,
int ElementSize,
string FileName,
int Magic3,
int Magic4,
int Magic5,
int Magic6,
int OffsetInFile,
int Index
);
int SortIndex,
int DirectoryIndex);
+1 -1
View File
@@ -30,7 +30,7 @@ public class NResExporter
extension = ".bin";
}
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.Index}_{archiveFile.FileType}_{fileName}{extension}");
var targetFilePath = Path.Combine(targetDirectoryPath, $"{archiveFile.DirectoryIndex}_{archiveFile.FileType}_{fileName}{extension}");
File.WriteAllBytes(targetFilePath, buffer);
}
+70 -32
View File
@@ -35,6 +35,16 @@ public static class NResParser
TotalFileLengthBytes: BinaryPrimitives.ReadInt32LittleEndian(buffer[12..16])
);
if (header.Version != 0x100)
{
return new NResParseResult(null, $"Неожиданная версия NRes: 0x{header.Version:X}");
}
if (header.FileCount < 0)
{
return new NResParseResult(null, $"Некорректное количество записей NRes: {header.FileCount}");
}
if (header.TotalFileLengthBytes != nResFs.Length)
{
return new NResParseResult(
@@ -45,7 +55,14 @@ public static class NResParser
);
}
nResFs.Seek(-header.FileCount * 64, SeekOrigin.End);
var directorySize = header.FileCount * 64L;
var directoryOffset = header.TotalFileLengthBytes - directorySize;
if (directoryOffset < 16 || directoryOffset + directorySize != header.TotalFileLengthBytes)
{
return new NResParseResult(null, "Некорректное расположение каталога NRes");
}
nResFs.Seek(directoryOffset, SeekOrigin.Begin);
var elements = new List<ListMetadataItem>(header.FileCount);
@@ -53,41 +70,32 @@ public static class NResParser
for (int i = 0; i < header.FileCount; i++)
{
nResFs.ReadExactly(metaDataBuffer);
var type = "";
var typeId = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[0..4]);
var type = FormatType(typeId, metaDataBuffer[0..4]);
var attr1 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[4..8]);
var attr2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[8..12]);
var fileLength = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]);
var attr3 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer[16..20]);
var name = ReadNResName(metaDataBuffer[20..56]);
var offset = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[56..60]);
var sortIndex = BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[60..64]);
for (int j = 0; j < 4; j++)
if (offset < 16 || fileLength < 0 || (long)offset + fileLength > directoryOffset)
{
if (!char.IsLetterOrDigit((char)metaDataBuffer[j]))
{
type += metaDataBuffer[j]
.ToString("X2") + " ";
}
else
{
type += (char)metaDataBuffer[j];
}
return new NResParseResult(null, $"Запись '{name}' выходит за границы data region");
}
var type2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer.Slice(4));
type = type.Trim();
elements.Add(
new ListMetadataItem(
FileType: type,
ElementCount: type2,
Magic1: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[8..12]),
FileLength: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]),
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]),
Magic5: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[48..52]),
Magic6: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[52..56]),
OffsetInFile: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[56..60]),
Index: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[60..64])
)
);
elements.Add(new ListMetadataItem(
typeId,
type,
attr1,
unchecked((int)attr2),
fileLength,
unchecked((int)attr3),
name,
offset,
sortIndex,
i));
metaDataBuffer.Clear();
}
@@ -99,4 +107,34 @@ public static class NResParser
)
);
}
private static string FormatType(uint typeId, ReadOnlySpan<byte> bytes)
{
bool formattable = true;
foreach (var b in bytes)
{
if (!char.IsLetterOrDigit((char)b))
{
formattable = false;
break;
}
}
if (!formattable)
{
return Encoding.ASCII.GetString(bytes);
}
return string.Join(" ", bytes.ToArray().Select(x => x.ToString("X2")));
}
private static string ReadNResName(ReadOnlySpan<byte> raw)
{
var nul = raw.IndexOf((byte)0);
if (nul < 0)
{
return Encoding.ASCII.GetString(raw);
}
return Encoding.ASCII.GetString(raw[..nul]);
}
}
+4 -4
View File
@@ -12,10 +12,10 @@ namespace NResUI;
public class App
{
public GL GL { get; set; }
public IInputContext Input { get; set; }
public GL GL { get; set; } = null!;
public IInputContext Input { get; set; } = null!;
public static App Instance;
public static App Instance = null!;
private static bool _dockspaceOpen = true;
private static bool _optFullscreenPersistant = true;
@@ -25,7 +25,7 @@ public class App
public ImFontPtr OpenSansFont;
private List<IImGuiPanel> _imGuiPanels;
private List<IImGuiPanel> _imGuiPanels = [];
public App()
{
+9 -27
View File
@@ -70,7 +70,7 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString());
if (ImGui.BeginTable("content", 12,
if (ImGui.BeginTable("content", 9,
ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{
ImGui.TableSetupColumn("Тип файла");
@@ -79,12 +79,9 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableSetupColumn("Длина файла в байтах");
ImGui.TableSetupColumn("Размер элемента");
ImGui.TableSetupColumn("Имя файла");
ImGui.TableSetupColumn("Magic3");
ImGui.TableSetupColumn("Magic4");
ImGui.TableSetupColumn("Magic5");
ImGui.TableSetupColumn("Magic6");
ImGui.TableSetupColumn("Смещение в байтах");
ImGui.TableSetupColumn("Индекс в файле");
ImGui.TableSetupColumn("SortIndex");
ImGui.TableSetupColumn("DirectoryIndex");
ImGui.TableHeadersRow();
@@ -124,26 +121,6 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileName);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic3.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic4.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic5.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Magic6.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.OffsetInFile.ToString()
@@ -151,7 +128,12 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.Index.ToString()
.SortIndex.ToString()
);
ImGui.TableNextColumn();
ImGui.Text(
_viewModel.Archive.Files[i]
.DirectoryIndex.ToString()
);
}
+37 -12
View File
@@ -166,6 +166,43 @@ public class TexmExplorer : IImGuiPanel
}
ImGui.Image((IntPtr) glTexture.GlTexture, imageSize);
var imageHovered = ImGui.IsItemHovered();
if (imageHovered && index < _viewModel.RgbaBytesByMipmap.Count)
{
var mousePos = ImGui.GetMousePos();
var scale = _viewModel.DoubleSize ? 2.0f : 1.0f;
var relativePos = (mousePos - screenPos) / scale;
var pixelX = (int)MathF.Floor(relativePos.X);
var pixelY = (int)MathF.Floor(relativePos.Y);
pixelX = (int)Math.Clamp(pixelX, 0, glTexture.Width - 1);
pixelY = (int)Math.Clamp(pixelY, 0, glTexture.Height - 1);
var rgbaBytes = _viewModel.RgbaBytesByMipmap[index];
var byteIndex = (pixelY * glTexture.Width + pixelX) * 4;
if (byteIndex + 3 < rgbaBytes.Length)
{
var r = rgbaBytes[byteIndex + 0];
var g = rgbaBytes[byteIndex + 1];
var b = rgbaBytes[byteIndex + 2];
var a = rgbaBytes[byteIndex + 3];
ImGui.BeginTooltip();
ImGui.Text($"Mipmap: {index}");
ImGui.Text($"Pixel: X={pixelX}, Y={pixelY}");
ImGui.Text($"RGBA: {r}, {g}, {b}, {a}");
ImGui.Text($"HEX: #{r:X2}{g:X2}{b:X2}{a:X2}");
ImGui.EndTooltip();
}
}
ImGui.SameLine();
if (_viewModel.ViewPages && _viewModel.TexmFile.Pages is not null)
@@ -180,18 +217,6 @@ public class TexmExplorer : IImGuiPanel
);
}
}
if (ImGui.IsItemHovered())
{
var mousePos = ImGui.GetMousePos();
var relativePos = (mousePos - screenPos) / (_viewModel.DoubleSize
? 2
: 1);
ImGui.Text("Hovering over: ");
ImGui.SameLine();
ImGui.Text(relativePos.ToString());
}
}
}
}
+5 -1
View File
@@ -13,6 +13,7 @@ public class TexmExplorerViewModel
public string? Path { get; set; }
public List<OpenGlTexture> GlTextures { get; set; } = [];
public List<byte[]> RgbaBytesByMipmap { get; set; } = [];
private bool _glTexturesDirty = false;
public bool IsWhiteBgEnabled;
@@ -48,11 +49,14 @@ public class TexmExplorerViewModel
}
GlTextures.Clear();
RgbaBytesByMipmap.Clear();
for (var i = 0; i < TexmFile!.Header.MipmapCount; i++)
for (var i = 0; i < TexmFile.Header.MipmapCount; i++)
{
var bytes = TexmFile.GetRgba32BytesFromMipmap(i, out var width, out var height);
RgbaBytesByMipmap.Add(bytes);
var glTexture = new OpenGlTexture(
gl,
width,
+1 -1
View File
@@ -19,7 +19,7 @@ namespace NResUI
public static float? MaxAniso;
private readonly GL _gl;
public readonly string Name;
public readonly string Name = string.Empty;
public readonly uint GlTexture;
public readonly uint Width, Height;
public readonly uint MipmapLevels;
+7 -22
View File
@@ -3,23 +3,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace PalLib;
/// <summary>
/// PAL файл по сути это indexed текстура (1024 байт - 256 цветов lookup + 4 байта "Ipol" и затем 256x256 индексов в lookup)
/// </summary>
public class PalFile
/// <summary>PAL файл: indexed texture с lookup таблицей, подписью Ipol и индексами.</summary>
/// <param name="FileName">Имя PAL файла.</param>
/// <param name="Palette">[0x0000..0x0400] 256 цветов lookup (1024 байта).</param>
/// <param name="Indices">[0x0404..0x10404] 256x256 индексов в lookup.</param>
public record class PalFile(string FileName, byte[] Palette, byte[] Indices)
{
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;
@@ -31,16 +20,12 @@ public class PalFile
{
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
// PAL, вероятно, связан с DirectX, поэтому порядок каналов похож на 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.
var a = Palette[index * 4 + 3]; // Альфа или padding; ниже пока используется непрозрачность.
rgbaBytes[i * 4 + 0] = r;
rgbaBytes[i * 4 + 1] = g;
+1 -6
View File
@@ -27,11 +27,6 @@ public class PalParser
var indices = new byte[65536];
stream.ReadExactly(indices, 0, 65536);
return new PalFile
{
FileName = filename,
Palette = palette,
Indices = indices
};
return new PalFile(filename, palette, indices);
}
}
+5 -6
View File
@@ -3,18 +3,17 @@
<File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" />
<File Path="README.md" />
<File Path="AI.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="Common\Common.csproj" Type="C#" />
<Project Path="CpDatLib\CpDatLib.csproj" Type="C#" />
<Project Path="MaterialLib\MaterialLib.csproj" Type="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="PalLib\PalLib.csproj" Type="C#" />
<Project Path="ParkanPlayground/ParkanPlayground.csproj" />
<Project Path="ResTreeLib\ResTreeLib.csproj" Type="Classic C#" />
<Project Path="ResTreeLib\ResTreeLib.csproj" Type="C#" />
<Project Path="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" />
+9 -4
View File
@@ -25,12 +25,17 @@ public static class FxidReader
{
EffectHeader h;
h.ComponentCount = br.ReadUInt32();
h.Unknown1 = br.ReadUInt32();
h.TimeMode = br.ReadUInt32();
h.Duration = br.ReadSingle();
h.Unknown2 = br.ReadSingle();
h.PhaseJitter = br.ReadSingle();
h.Flags = br.ReadUInt32();
h.Unknown3 = br.ReadUInt32();
h.Reserved = br.ReadBytes(24);
h.SettingsId = br.ReadUInt32();
h.RandShiftX = br.ReadSingle();
h.RandShiftY = br.ReadSingle();
h.RandShiftZ = br.ReadSingle();
h.PivotX = br.ReadSingle();
h.PivotY = br.ReadSingle();
h.PivotZ = br.ReadSingle();
h.ScaleX = br.ReadSingle();
h.ScaleY = br.ReadSingle();
h.ScaleZ = br.ReadSingle();
+36 -15
View File
@@ -7,18 +7,39 @@ namespace ParkanPlayground.Effects;
/// 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 record struct EffectHeader
{
/// <summary>FXID payload offset 0x00: command count.</summary>
public uint ComponentCount;
public uint Unknown1;
/// <summary>FXID payload offset 0x04: time mode used to compute effect alpha.</summary>
public uint TimeMode;
/// <summary>FXID payload offset 0x08: effect duration in seconds.</summary>
public float Duration;
public float Unknown2;
/// <summary>FXID payload offset 0x0C: random phase shift amplitude.</summary>
public float PhaseJitter;
/// <summary>FXID payload offset 0x10: effect behavior flags.</summary>
public uint Flags;
public uint Unknown3;
public byte[] Reserved; // 24 bytes
/// <summary>FXID payload offset 0x14: settings/profile id.</summary>
public uint SettingsId;
/// <summary>FXID payload offset 0x18: random spatial shift X.</summary>
public float RandShiftX;
/// <summary>FXID payload offset 0x1C: random spatial shift Y.</summary>
public float RandShiftY;
/// <summary>FXID payload offset 0x20: random spatial shift Z.</summary>
public float RandShiftZ;
/// <summary>FXID payload offset 0x24: local pivot X.</summary>
public float PivotX;
/// <summary>FXID payload offset 0x28: local pivot Y.</summary>
public float PivotY;
/// <summary>FXID payload offset 0x2C: local pivot Z.</summary>
public float PivotZ;
/// <summary>FXID payload offset 0x30: base scale X.</summary>
public float ScaleX;
/// <summary>FXID payload offset 0x34: base scale Y.</summary>
public float ScaleY;
/// <summary>FXID payload offset 0x38: base scale Z.</summary>
public float ScaleZ;
}
/// <summary>
@@ -26,7 +47,7 @@ public struct EffectHeader
/// 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 record 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
@@ -56,7 +77,7 @@ public struct BillboardComponentData
/// 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 record struct SoundComponentData
{
public uint TypeAndFlags; // component type and flags
public uint PlayMode; // playback mode (looping, one-shot, etc.)
@@ -79,7 +100,7 @@ public struct SoundComponentData
/// 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 record 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
@@ -109,7 +130,7 @@ public struct AnimParticleComponentData
/// 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 record 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
@@ -139,7 +160,7 @@ public struct AnimBillboardComponentData
/// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
/// ranges, timing, and a shared texture name at +0x30.
/// </summary>
public struct TrailComponentData
public record struct TrailComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown
@@ -157,7 +178,7 @@ public struct TrailComponentData
/// 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 record struct PointComponentData
{
public uint TypeAndFlags; // component type and flags; definition block has no payload
}
@@ -167,7 +188,7 @@ public struct PointComponentData
/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
/// followed by two dwords of plane-specific data.
/// </summary>
public struct PlaneComponentData
public record 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
@@ -180,7 +201,7 @@ public struct PlaneComponentData
/// time window, instance count, spatial extents/axes, radius triplets, and a
/// 0x40-byte texture name tail.
/// </summary>
public struct ModelComponentData
public record struct ModelComponentData
{
public uint TypeAndFlags; // component type and flags
public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear
@@ -206,7 +227,7 @@ public struct ModelComponentData
/// 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 record struct AnimModelComponentData
{
public uint TypeAndFlags; // component type and flags
public float AnimSpeed; // animation speed multiplier at +0x04
@@ -230,7 +251,7 @@ public struct AnimModelComponentData
/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
/// followed by one dword of cube-specific data.
/// </summary>
public struct CubeComponentData
public record 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
+61 -64
View File
@@ -25,21 +25,21 @@ MSH файлы — это NRes архивы, содержащие несколь
| Тип | Название | Размер элемента | Описание |
|:---:|----------|:---------------:|----------|
| 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 | переменный | Внешние ссылки на меши (строки) |
| 01 | Node table | 38 (0x26), редко 24 | Узлы модели / тайлы; старое имя: Pieces |
| 02 | Header + slots | 0x8C + n*68 | Общий заголовок и slot records; старое имя: Submeshes |
| 03 | Positions | 12 (0x0C) | Позиции вершин (Vector3); старое имя: Vertices |
| 04 | PackedNormals | 4 | `int8[4]`, normal = clamp(component / 127.0, -1..1) |
| 05 | PackedUV0 | 4 | `int16[2]`, uv = component / 1024.0 |
| 06 | Index buffer | 2 | Индексы вершин треугольников |
| 07 | Tri descriptors | 16 | Описатели треугольников для коллизии/пикинга |
| 08 | AnimKeyPool | 24 | Кейфреймы анимации меша |
| 0A | Node strings | переменный | Строки узлов; старое имя: ExternalRefs |
| 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
| 0D | неизвестно | 20 (0x14) | неизвестно (только Модель) |
| 0D | Batch table | 20 (0x14) | Батчи рендера; FParkan Res13 decimal |
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
| 12 | MicrotextureMap | 4 | неизвестно |
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
| 15 | неизвестно | 28 (0x1C) | неизвестно |
| 13 | AnimMap | 2 | Карта кадров анимации, на нее указывает `AnimMapStart` из 0x01 |
| 15 | TerrainTriangle table | 28 (0x1C) | Terrain-гипотеза |
---
@@ -52,15 +52,15 @@ MSH файлы — это NRes архивы, содержащие несколь
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник)
├─► TriStart:TriCount ──► Компонент 07 (данные на треугольник)
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи)
└─► BatchStart:BatchCount ──► Компонент 0D (батчи)
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы)
├─► IndexStart:IndexCount ──► Компонент 06 (индексы)
│ │
│ └─► Компонент 03 (вершины)
└─► IndexInto03 (базовое смещение вершины)
└─► BaseVertex (базовое смещение вершины)
```
### Ландшафт (террейн)
@@ -70,38 +70,37 @@ MSH файлы — это NRes архивы, содержащие несколь
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники)
└─► TriStart:TriCount ──► Компонент 15 (треугольники)
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15)
└─► TriStart:TriCount ──► Компонент 0B (материалы, параллельно 15)
```
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02
**Важно:** В ландшафтных мешах поля `TriStart` и `TriCount` в Компоненте 02
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
---
## Структуры компонентов
### Компонент 01 - Pieces (0x26 = 38 байт)
### Компонент 0x01 - Node table (0x26 = 38 байт)
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов).
Определяет узлы модели или тайлы terrain. Старое локальное имя: Pieces / SubMesh.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 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 = не используется) |
| 0x00 | 2 | uint16 | Header0 | Заголовочное слово узла; старые имена: Type1 + Type2 |
| 0x02 | 2 | uint16 | ParentOrLink | Индекс родителя/ссылка; старый локальный тип int16 показывал 0xFFFF как -1 |
| 0x04 | 2 | uint16 | AnimMapStart | Начало блока в 0x13 или 0xFFFF; старое имя: OffsetIntoFile13 |
| 0x06 | 2 | uint16 | FallbackKey | Индекс fallback-ключа в 0x08; старое имя: IndexInFile08 |
| 0x08 | 30 | ushort[15] | SlotIndex | Индексы slot в 0x02 по формуле `lod * 5 + group`; старое имя: Lod |
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
---
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт)
### Компонент 0x02 - Header + slots (Заголовок: 0x8C = 140 байт, slot: 0x44 = 68 байт)
#### Заголовок (140 байт)
@@ -118,15 +117,15 @@ MSH файлы — это NRes архивы, содержащие несколь
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 |
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников |
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) |
| 0x00 | 2 | ushort | TriStart | Начальный индекс в Компоненте 07; в landscape-tooling может указывать в 15 |
| 0x02 | 2 | ushort | TriCount | Количество записей в Компоненте 07; в landscape-tooling может быть count для 15 |
| 0x04 | 2 | ushort | BatchStart | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | BatchCount | Количество батчей в Компоненте 0D (только Модель) |
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
| 0x20 | 12 | Vector3 | Center | Центр сабмеша |
| 0x2C | 12 | Vector3 | Vector4 | Неизвестно |
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно |
| 0x2C | 4 | float | SphereRadius | Радиус bounding sphere; старый `Vector4` был overlay-гипотезой |
| 0x30 | 20 | uint32[5] | Opaque | Непонятый tail, сохранять 1:1; старый `Vector5` был overlay-гипотезой |
---
@@ -147,20 +146,20 @@ MSH файлы — это NRes архивы, содержащие несколь
---
### Компонент 07 - Triangle Data (0x10 = 16 байт) - Только Модель
### Компонент 0x07 - Tri descriptors (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 | Неизвестно |
| 0x00 | 2 | ushort | TriFlags | Флаги треугольника; старое имя: Flags |
| 0x02 | 2 | ushort | Link0 | Связь/opaque поле 0; старое имя: Magic02 |
| 0x04 | 2 | ushort | Link1 | Связь/opaque поле 1; старое имя: Magic04 |
| 0x06 | 2 | ushort | Link2 | Связь/opaque поле 2; старое имя: Magic06 |
| 0x08 | 2 | int16 | NormalX | Упакованная X-компонента нормали; старое имя: OffsetX |
| 0x0A | 2 | int16 | NormalY | Упакованная Y-компонента нормали; старое имя: OffsetY |
| 0x0C | 2 | int16 | NormalZ | Упакованная Z-компонента нормали; старое имя: OffsetZ |
| 0x0E | 2 | ushort | SelectorPacked | Три 2-битных селектора; `3` трактуется как `0xFFFF`; старое имя: Magic14 |
---
@@ -175,40 +174,38 @@ MSH файлы — это NRes архивы, содержащие несколь
---
### Компонент 0D - Draw Batches (0x14 = 20 байт) - Только Модель
### Компонент 0x0D - Batch table (0x14 = 20 байт)
Определяет батчи вызовов отрисовки.
Определяет батчи вызовов отрисовки. В терминах FParkan это Res13 decimal.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 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 |
| 0x00 | 2 | ushort | BatchFlags / Flags.low | Флаги батча |
| 0x02 | 2 | ushort | MaterialIndex / Flags.high | Индекс material slot |
| 0x04 | 2 | ushort | Opaque4 | Opaque, старое имя `TriangleCount` не подтверждено |
| 0x06 | 2 | ushort | Opaque6 | Opaque |
| 0x08 | 2 | ushort | IndexCount | Количество индексов для отрисовки в 0x06 |
| 0x0A | 4 | uint32 | IndexStart | Начальный индекс в Компоненте 06 |
| 0x0E | 2 | ushort | Opaque14 | Opaque, старое имя `CountOf03` не подтверждено |
| 0x10 | 4 | uint32 | BaseVertex | Базовое смещение вершины в Компоненте 03 |
---
### Компонент 15 - Triangles (0x1C = 28 байт)
### Компонент 0x15 - TerrainTriangle table (0x1C = 28 байт)
Прямые определения треугольников. Используется и Моделью и Ландшафтом,
но только Ландшафт использует их напрямую для рендеринга.
Прямые определения terrain-треугольников. Это hex-компонент 0x15 проекта, не FParkan Res15 decimal.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала; старое имя: Magic04 |
| 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 | Неизвестно |
| 0x0E | 4 | uint32 | Opaque0E | Opaque; старое имя: Magic0E |
| 0x12 | 4 | uint32 | Opaque12 | Opaque; старое имя: Magic12 |
| 0x16 | 4 | uint32 | Opaque16 | Opaque; старое имя: Magic16 |
| 0x1A | 2 | ushort | Opaque1A | Opaque; старое имя: Magic1A |
#### MaterialData (0x04) - Структура материала
@@ -298,7 +295,7 @@ var type = MshConverter.DetectMeshType(archive);
| Файл | Используется для | Треугольники в Comp15 |
|------|------------------|----------------------|
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) |
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма TriCount для LOD0) |
| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
### Пример (SC_1)
-77
View File
@@ -1,77 +0,0 @@
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
View File
@@ -1,193 +0,0 @@
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; }
}
}
-53
View File
@@ -1,53 +0,0 @@
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; }
}
}
-53
View File
@@ -1,53 +0,0 @@
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.ReadUInt32LittleEndian(x.AsSpan(0)),
TriangleCount = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
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 ushort TriangleCount { 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; }
}
}
+89
View File
@@ -0,0 +1,89 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x01: таблица узлов модели.
/// </summary>
public static class Msh0x01
{
public static Msh0x01Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain node table component (0x01)");
}
if (entry.ElementSize <= 0)
{
throw new Exception("Node table component (0x01) has invalid element size");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Node table component (0x01) payload size is not divisible by element size");
}
var elementCount = entry.FileLength / entry.ElementSize;
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var dataSpan = data.AsSpan();
var elements = new List<Node>(elementCount);
for (var i = 0; i < elementCount; i++)
{
var baseOffset = i * entry.ElementSize;
var rawBytes = dataSpan.Slice(baseOffset, entry.ElementSize).ToArray();
var slots = new ushort[15];
Array.Fill(slots, ushort.MaxValue);
var slotWords = Math.Min(slots.Length, Math.Max(0, (entry.ElementSize - 8) / 2));
for (var slotIndex = 0; slotIndex < slotWords; slotIndex++)
{
slots[slotIndex] =
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 8 + slotIndex * 2));
}
elements.Add(new Node(
rawBytes,
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset)),
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 2)),
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 4)),
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 6)),
slots));
}
return new Msh0x01Component(elements);
}
/// <summary>Результат чтения MSH-компонента 0x01.</summary>
/// <param name="Elements">Узлы компонента 0x01.</param>
public record Msh0x01Component(List<Node> Elements);
/// <summary>Узел 0x01.</summary>
/// <param name="RawBytes">Сырые байты узла (length = attr3). Нужны для copy-through редких вариантов, например attr3 = 24.</param>
/// <param name="Header0">[0x00..0x02] Заголовочное слово узла</param>
/// <param name="ParentOrLink">[0x02..0x04] Индекс родителя или связанного узла</param>
/// <param name="AnimMapStart">[0x04..0x06] Начало блока в карте анимации Msh0x13 или 0xFFFF</param>
/// <param name="FallbackKey">[0x06..0x08] Индекс fallback-ключа в пуле ключей Msh0x08</param>
/// <param name="SlotIndex">[0x08..0x26] Индексы slot в Msh0x02 по формуле lod * 5 + group</param>
public record Node(
byte[] RawBytes,
ushort Header0,
ushort ParentOrLink,
ushort AnimMapStart,
ushort FallbackKey,
ushort[] SlotIndex)
{
public ushort ResolveSlotIndex(int lod, int group = 0)
{
var index = lod * 5 + group;
return index >= 0 && index < SlotIndex.Length ? SlotIndex[index] : ushort.MaxValue;
}
}
}
+203
View File
@@ -0,0 +1,203 @@
using System.Buffers.Binary;
using Common;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x02: общий заголовок и таблица slot.
/// </summary>
public static class Msh0x02
{
public static Msh0x02Component 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 slots component (0x02)");
}
if (fileEntry.FileLength < 0x8c)
{
throw new Exception("Slots component (0x02) is smaller than the 0x8C-byte header");
}
if ((fileEntry.FileLength - 0x8c) % 68 != 0)
{
throw new Exception("Slots component (0x02) payload after header is not divisible by 68");
}
var data = new byte[fileEntry.FileLength];
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var header = data.AsSpan(0, 0x8c); // заголовок (length = 0x8C)
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(
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
),
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));
var elements = new List<Slot>();
var skippedHeader = data.AsSpan(0x8c);
var slotCount = skippedHeader.Length / 68;
for (var i = 0; i < slotCount; i++)
{
var baseOffset = 68 * i;
var opaque = new uint[5];
for (var opaqueIndex = 0; opaqueIndex < opaque.Length; opaqueIndex++)
{
opaque[opaqueIndex] =
BinaryPrimitives.ReadUInt32LittleEndian(skippedHeader.Slice(baseOffset + 48 + opaqueIndex * 4));
}
elements.Add(new Slot(
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0)),
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 2)),
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 4)),
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 6)),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 8)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 12)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 16))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 20)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 24)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 28))
),
new Vector3(
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 32)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 36)),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 40))
),
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 44)),
opaque));
}
return new Msh0x02Component(
new Msh02Header(bb, center, centerW, bottom, top, xyRadius),
elements);
}
/// <summary>Результат чтения MSH-компонента 0x02.</summary>
/// <param name="Header">Заголовок 0x02 (length = 0x8C).</param>
/// <param name="Elements">Slot records после заголовка.</param>
public record class Msh0x02Component(Msh02Header Header, List<Slot> Elements);
/// <summary>Заголовок 0x02 (length = 0x8C).</summary>
/// <param name="BoundingBox">[0x00..0x60] Bounding box из 8 точек.</param>
/// <param name="Center">[0x60..0x6C] Центральная точка.</param>
/// <param name="CenterW">[0x6C..0x70] W-компонента центра.</param>
/// <param name="Bottom">[0x70..0x7C] Нижняя точка.</param>
/// <param name="Top">[0x7C..0x88] Верхняя точка.</param>
/// <param name="XYRadius">[0x88..0x8C] Радиус в плоскости XY.</param>
public record class Msh02Header(
BoundingBox BoundingBox,
Vector3 Center,
float CenterW,
Vector3 Bottom,
Vector3 Top,
float XYRadius);
/// <summary>Slot 0x02 (length = 0x44).</summary>
/// <param name="TriStart">[0x00..0x02] Первый triangle descriptor в Msh0x07. Для terrain-гипотезы может быть диапазоном Msh0x15.</param>
/// <param name="TriCount">[0x02..0x04] Количество triangle descriptor в Msh0x07. Для terrain-гипотезы может быть count для Msh0x15.</param>
/// <param name="BatchStart">[0x04..0x06] Первый batch в таблице 0x0D.</param>
/// <param name="BatchCount">[0x06..0x08] Количество batch в таблице 0x0D</param>
/// <param name="LocalMinimum">[0x08..0x14] Минимум локального AABB</param>
/// <param name="LocalMaximum">[0x14..0x20] Максимум локального AABB</param>
/// <param name="Center">[0x20..0x2C] Центр bounding sphere</param>
/// <param name="SphereRadius">[0x2C..0x30] Радиус bounding sphere</param>
/// <param name="Opaque">[0x30..0x44] Пять opaque dword</param>
public record class Slot(
ushort TriStart,
ushort TriCount,
ushort BatchStart,
ushort BatchCount,
Vector3 LocalMinimum,
Vector3 LocalMaximum,
Vector3 Center,
float SphereRadius,
uint[] Opaque);
/// <summary>Bounding box заголовка: 8 точек по 3 float (length = 0x60).</summary>
/// <param name="BottomFrontLeft">[0x00..0x0C] Нижняя передняя левая точка.</param>
/// <param name="BottomFrontRight">[0x0C..0x18] Нижняя передняя правая точка.</param>
/// <param name="BottomBackRight">[0x18..0x24] Нижняя задняя правая точка.</param>
/// <param name="BottomBackLeft">[0x24..0x30] Нижняя задняя левая точка.</param>
/// <param name="TopFrontLeft">[0x30..0x3C] Верхняя передняя левая точка.</param>
/// <param name="TopFrontRight">[0x3C..0x48] Верхняя передняя правая точка.</param>
/// <param name="TopBackRight">[0x48..0x54] Верхняя задняя правая точка.</param>
/// <param name="TopBackLeft">[0x54..0x60] Верхняя задняя левая точка.</param>
public record class BoundingBox(
Vector3 BottomFrontLeft,
Vector3 BottomFrontRight,
Vector3 BottomBackRight,
Vector3 BottomBackLeft,
Vector3 TopFrontLeft,
Vector3 TopFrontRight,
Vector3 TopBackRight,
Vector3 TopBackLeft);
}
@@ -4,7 +4,10 @@ using NResLib;
namespace ParkanPlayground;
public class Msh03
/// <summary>
/// MSH-компонент 0x03: позиции вершин
/// </summary>
public class Msh0x03
{
public static List<Vector3> ReadComponent(FileStream mshFs, NResArchive mshNres)
{
@@ -20,7 +23,12 @@ public class Msh03
throw new Exception("Vertices file (03) element size is not 12");
}
var verticesFile = new byte[verticesFileEntry.ElementCount * verticesFileEntry.ElementSize];
if (verticesFileEntry.FileLength % verticesFileEntry.ElementSize != 0)
{
throw new Exception("Positions component (0x03) payload size is not divisible by element size");
}
var verticesFile = new byte[verticesFileEntry.FileLength];
mshFs.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(verticesFile, 0, verticesFile.Length);
+54
View File
@@ -0,0 +1,54 @@
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x04: упакованные нормали вершин. clamp(component / 127.0, -1..1).
/// </summary>
public static class Msh0x04
{
public static List<Msh04Normal> ReadComponent(FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "04 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (04)");
}
if (entry.ElementSize != 4)
{
throw new Exception("Packed normals file (04) element size is not 4");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Packed normals component (0x04) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elements = new List<Msh04Normal>(entry.FileLength / entry.ElementSize);
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
{
var offset = i * 4;
elements.Add(new Msh04Normal(
unchecked((sbyte)data[offset + 0]),
unchecked((sbyte)data[offset + 1]),
unchecked((sbyte)data[offset + 2]),
unchecked((sbyte)data[offset + 3])));
}
return elements;
}
}
/// <summary>Упакованная нормаль: четыре int8-компоненты (length = 4).</summary>
/// <param name="X">[0x00..0x01] X-компонента.</param>
/// <param name="Y">[0x01..0x02] Y-компонента.</param>
/// <param name="Z">[0x02..0x03] Z-компонента.</param>
/// <param name="W">[0x03..0x04] W-компонента.</param>
public readonly record struct Msh04Normal(sbyte X, sbyte Y, sbyte Z, sbyte W)
;
+51
View File
@@ -0,0 +1,51 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x05: упакованные UV0. component / 1024.0.
/// </summary>
public static class Msh0x05
{
public static List<Msh05Uv> ReadComponent(FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "05 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain file (05)");
}
if (entry.ElementSize != 4)
{
throw new Exception("Packed UV file (05) element size is not 4");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Packed UV component (0x05) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elements = new List<Msh05Uv>(entry.FileLength / entry.ElementSize);
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
{
var span = data.AsSpan(i * 4, 4);
elements.Add(new Msh05Uv(
BinaryPrimitives.ReadInt16LittleEndian(span[0..2]),
BinaryPrimitives.ReadInt16LittleEndian(span[2..4])));
}
return elements;
}
}
/// <summary>Упакованные UV0: две int16-компоненты (length = 4).</summary>
/// <param name="U">[0x00..0x02] U-компонента, uv = U / 1024.0.</param>
/// <param name="V">[0x02..0x04] V-компонента, uv = V / 1024.0.</param>
public readonly record struct Msh05Uv(short U, short V)
;
@@ -3,7 +3,10 @@ using NResLib;
namespace ParkanPlayground;
public static class Msh06
/// <summary>
/// MSH-компонент 0x06: индексный буфер
/// </summary>
public static class Msh0x06
{
public static List<ushort> ReadComponent(
FileStream mshFs, NResArchive archive)
@@ -15,12 +18,22 @@ public static class Msh06
throw new Exception("Archive doesn't contain file (06)");
}
var data = new byte[entry.ElementCount * entry.ElementSize];
if (entry.ElementSize != 2)
{
throw new Exception("Index buffer component (0x06) element size is not 2");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Index buffer component (0x06) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
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++)
var elements = new List<ushort>(entry.FileLength / entry.ElementSize);
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
{
elements.Add(
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2))
+80
View File
@@ -0,0 +1,80 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x07: описатели треугольников для коллизии/пикинга
/// </summary>
public static class Msh0x07
{
public static List<TriangleDescriptor> 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)");
}
if (entry.ElementSize != 16)
{
throw new Exception("Triangle descriptor component (0x07) element size is not 16");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Triangle descriptor component (0x07) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(16);
var elements = elementBytes.Select(x => new TriangleDescriptor(
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)),
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)),
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)))).ToList();
return elements;
}
/// <summary>Описатель треугольника 0x07 (length = 0x10).</summary>
/// <param name="TriFlags">[0x00..0x02] Флаги треугольника</param>
/// <param name="Link0">[0x02..0x04] Связь/opaque поле 0</param>
/// <param name="Link1">[0x04..0x06] Связь/opaque поле 1</param>
/// <param name="Link2">[0x06..0x08] Связь/opaque поле 2</param>
/// <param name="NormalX">[0x08..0x0A] Упакованная X-компонента нормали</param>
/// <param name="NormalY">[0x0A..0x0C] Упакованная Y-компонента нормали</param>
/// <param name="NormalZ">[0x0C..0x0E] Упакованная Z-компонента нормали</param>
/// <param name="SelectorPacked">[0x0E..0x10] Три 2-битных селектора; значение 3 трактуется как 0xFFFF</param>
public readonly record struct TriangleDescriptor(
ushort TriFlags,
ushort Link0,
ushort Link1,
ushort Link2,
short NormalX,
short NormalY,
short NormalZ,
ushort SelectorPacked)
{
public ushort GetSelector(int index)
{
if (index is < 0 or > 2)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
var selector = (SelectorPacked >> (index * 2)) & 0b11;
return selector == 3 ? ushort.MaxValue : (ushort)selector;
}
}
}
@@ -4,7 +4,11 @@ using NResLib;
namespace ParkanPlayground;
public class Msh0A
/// <summary>
/// MSH-компонент 0x0A: строки узлов.
/// У FParkan: Res10 / Node strings. Старое локальное имя: ExternalRefs.
/// </summary>
public class Msh0x0A
{
public static List<string> ReadComponent(FileStream mshFs, NResArchive archive)
{
@@ -23,19 +27,32 @@ public class Msh0A
var strings = new List<string>();
while (pos < data.Length)
{
if (pos + 4 > data.Length)
{
throw new Exception("Node strings component (0x0A) has truncated length prefix");
}
var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos));
if (len < 0 || pos + 4 + len > data.Length)
{
throw new Exception("Node strings component (0x0A) has invalid string length");
}
if (len == 0)
{
pos += 4; // empty entry, no string attached
strings.Add(""); // add empty string
pos += 4;
strings.Add("");
}
else
{
// len is not 0, we need to read it
var strBytes = data.AsSpan(pos + 4, len);
var str = Encoding.UTF8.GetString(strBytes);
var str = Encoding.ASCII.GetString(strBytes);
strings.Add(str);
pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated
pos += len + 4;
if (pos < data.Length && data[pos] == 0)
{
pos++;
}
}
}
+70
View File
@@ -0,0 +1,70 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x0D: таблица batch.
/// </summary>
public static class Msh0x0D
{
public const int ElementSize = 20;
public static List<Batch> 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)");
}
if (entry.ElementSize != ElementSize)
{
throw new Exception("Batch table component (0x0D) element size is not 20");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Batch table component (0x0D) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(ElementSize);
var elements = elementBytes.Select(x => new Batch(
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0xA)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0x10)))).ToList();
return elements;
}
/// <summary>Batch 0x0D</summary>
/// <param name="BatchFlags">[0x00..0x02] Флаги batch. У FParkan: batchFlags.</param>
/// <param name="MaterialIndex">[0x02..0x04] Индекс material slot, резолвится через WEAR/MAT0 pipeline.</param>
/// <param name="Opaque4">[0x04..0x06] Opaque поле. Старое локальное имя: TriangleCount; не подтверждено.</param>
/// <param name="Opaque6">[0x06..0x08] Opaque поле. Сохранять побайтно в writer.</param>
/// <param name="IndexCount">[0x08..0x0A] Количество индексов в индексном буфере 0x06.</param>
/// <param name="IndexStart">[0x0A..0x0E] Первый индекс в индексном буфере 0x06.</param>
/// <param name="Opaque14">[0x0E..0x10] Opaque поле. Старое локальное имя: CountOf03; не подтверждено.</param>
/// <param name="BaseVertex">[0x10..0x14] Базовая вершина в position stream 0x03. У FParkan: baseVertex.</param>
public readonly record struct Batch(
ushort BatchFlags,
ushort MaterialIndex,
ushort Opaque4,
ushort Opaque6,
ushort IndexCount,
uint IndexStart,
ushort Opaque14,
uint BaseVertex);
}
+73
View File
@@ -0,0 +1,73 @@
using System.Buffers.Binary;
using NResLib;
namespace ParkanPlayground;
/// <summary>
/// MSH-компонент 0x15: terrain-таблица треугольников
/// </summary>
public static class Msh0x15
{
public static List<TerrainTriangle> 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)");
}
if (entry.ElementSize != 28)
{
throw new Exception("Terrain triangle component (0x15) element size is not 28");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Terrain triangle component (0x15) payload size is not divisible by element size");
}
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var elementBytes = data.Chunk(28);
var elements = elementBytes.Select(x => new TerrainTriangle(
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)),
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)),
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)))).ToList();
return elements;
}
/// <summary>Terrain-треугольник 0x15 (length = 0x1C)</summary>
/// <param name="Flags">[0x00..0x04] Флаги треугольника для terrain path</param>
/// <param name="MaterialData">[0x04..0x08] Данные материала terrain</param>
/// <param name="Vertex1Index">[0x08..0x0A] Индекс первой вершины в position stream Msh0x03</param>
/// <param name="Vertex2Index">[0x0A..0x0C] Индекс второй вершины в position stream Msh0x03</param>
/// <param name="Vertex3Index">[0x0C..0x0E] Индекс третьей вершины в position stream Msh0x03</param>
/// <param name="Opaque0E">[0x0E..0x12] Opaque поле</param>
/// <param name="Opaque12">[0x12..0x16] Opaque поле</param>
/// <param name="Opaque16">[0x16..0x1A] Opaque поле</param>
/// <param name="Opaque1A">[0x1A..0x1C] Opaque поле</param>
public readonly record struct TerrainTriangle(
uint Flags,
uint MaterialData,
ushort Vertex1Index,
ushort Vertex2Index,
ushort Vertex3Index,
uint Opaque0E,
uint Opaque12,
uint Opaque16,
ushort Opaque1A);
}
-55
View File
@@ -1,55 +0,0 @@
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; }
}
}
+88 -71
View File
@@ -13,9 +13,7 @@ public enum MshType
public class MshConverter
{
/// <summary>
/// Detects mesh type based on which components are present in the archive.
/// </summary>
/// <summary>Определяет тип MSH по набору hex-компонентов архива.</summary>
public static MshType DetectMeshType(NResArchive archive)
{
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
@@ -33,13 +31,12 @@ public class MshConverter
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)
/// <summary>Конвертирует .msh в OBJ с автоопределением типа меша.</summary>
/// <param name="mshPath">Путь к .msh файлу.</param>
/// <param name="outputPath">Путь к OBJ, по умолчанию рядом с исходным файлом.</param>
/// <param name="lodLevel">LOD для экспорта.</param>
/// <param name="group">Группа slot внутри LOD. slotIndex[lod * 5 + group].</param>
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0, int group = 0)
{
var mshNresResult = NResParser.ReadFile(mshPath);
if (mshNresResult.Archive is null)
@@ -61,10 +58,10 @@ public class MshConverter
switch (meshType)
{
case MshType.Model:
ConvertModel(fs, archive, outputPath, lodLevel);
ConvertModel(fs, archive, outputPath, lodLevel, group);
break;
case MshType.Landscape:
ConvertLandscape(fs, archive, outputPath, lodLevel);
ConvertLandscape(fs, archive, outputPath, lodLevel, group);
break;
default:
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
@@ -73,17 +70,17 @@ public class MshConverter
}
/// <summary>
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
/// Конвертирует обычную модель в OBJ.
/// Путь данных: 0x01 -> 0x02 -> 0x0D -> 0x06 -> 0x03.
/// </summary>
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
{
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);
var component01 = Msh0x01.ReadComponent(fs, archive);
var component02 = Msh0x02.ReadComponent(fs, archive);
var component03 = Msh0x03.ReadComponent(fs, archive);
var component06 = Msh0x06.ReadComponent(fs, archive);
var component07 = Msh0x07.ReadComponent(fs, archive);
var component0D = Msh0x0D.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Pieces: {component01.Elements.Count}");
@@ -93,9 +90,10 @@ public class MshConverter
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}");
{
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
}
int exportedFaces = 0;
@@ -103,32 +101,49 @@ public class MshConverter
{
var piece = component01.Elements[pieceIndex];
// Get submesh index for requested LOD
if (lodLevel >= piece.Lod.Length)
continue;
var submeshIdx = piece.Lod[lodLevel];
var submeshIdx = piece.ResolveSlotIndex(lodLevel, group);
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;
var batchStart = submesh.BatchStart;
var batchCount = submesh.BatchCount;
if (batchStart + batchCount > component0D.Count)
{
Console.WriteLine($"WARNING: Batch range {batchStart}:{batchCount} out of range for piece {pieceIndex}");
continue;
}
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
{
var batch = component0D[batchStart + batchIdx];
var baseVertex = batch.IndexInto03;
var indexStart = batch.IndexInto06;
var indexCount = batch.CountOf06;
var baseVertex = (int)batch.BaseVertex;
var indexStart = (int)batch.IndexStart;
var indexCount = batch.IndexCount;
if (indexStart + indexCount > component06.Count)
{
Console.WriteLine($"WARNING: Index range {indexStart}:{indexCount} out of range for piece {pieceIndex}");
continue;
}
for (int i = 0; i < indexCount; i += 3)
{
if (i + 2 >= indexCount)
{
Console.WriteLine($"WARNING: Batch has non-triangle index tail in piece {pieceIndex}");
break;
}
var i1 = baseVertex + component06[indexStart + i];
var i2 = baseVertex + component06[indexStart + i + 1];
var i3 = baseVertex + component06[indexStart + i + 2];
if (i1 >= component03.Count || i2 >= component03.Count || i3 >= component03.Count)
{
Console.WriteLine($"WARNING: Vertex index out of range in piece {pieceIndex}");
continue;
}
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
exportedFaces++;
@@ -141,15 +156,15 @@ public class MshConverter
}
/// <summary>
/// Converts a landscape mesh (terrain) to OBJ.
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
/// Конвертирует terrain mesh в OBJ.
/// Путь данных является terrain-гипотезой проекта: 0x01 -> 0x02 -> 0x15.
/// </summary>
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
{
var component01 = Msh01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive);
var component15 = Msh15.ReadComponent(fs, archive);
var component01 = Msh0x01.ReadComponent(fs, archive);
var component02 = Msh0x02.ReadComponent(fs, archive);
var component03 = Msh0x03.ReadComponent(fs, archive);
var component15 = Msh0x15.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Triangles: {component15.Count}");
@@ -161,9 +176,10 @@ public class MshConverter
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}");
{
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
}
int exportedFaces = 0;
@@ -171,11 +187,7 @@ public class MshConverter
{
var tile = component01.Elements[tileIdx];
// Get submesh index for requested LOD
if (lodLevel >= tile.Lod.Length)
continue;
var submeshIdx = tile.Lod[lodLevel];
var submeshIdx = tile.ResolveSlotIndex(lodLevel, group);
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue;
@@ -183,9 +195,8 @@ public class MshConverter
var submesh = component02.Elements[submeshIdx];
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
var triangleStart = submesh.StartIndexIn07;
var triangleCount = submesh.CountIn07;
var triangleStart = submesh.TriStart;
var triangleCount = submesh.TriCount;
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
{
@@ -197,6 +208,12 @@ public class MshConverter
}
var tri = component15[triIdx];
if (tri.Vertex1Index >= component03.Count || tri.Vertex2Index >= component03.Count || tri.Vertex3Index >= component03.Count)
{
Console.WriteLine($"WARNING: Vertex index out of range for tile {tileIdx}");
continue;
}
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
exportedFaces++;
}
@@ -215,21 +232,21 @@ public class MshConverter
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)
// Запись граней: каждая грань задается четырьмя вершинами, OBJ использует индексацию с 1.
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
new int[] { 1, 2, 3, 4 }, // низ
new int[] { 5, 6, 7, 8 }, // верх
new int[] { 1, 2, 6, 5 }, // перед
new int[] { 2, 3, 7, 6 }, // право
new int[] { 3, 4, 8, 7 }, // зад
new int[] { 4, 1, 5, 8 } // лево
};
foreach (var f in faces)
@@ -248,7 +265,7 @@ public class MshConverter
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),
@@ -262,24 +279,24 @@ public class MshConverter
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)
// Описание граней: индексация с 1, порядок против часовой стрелки.
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
new int[] { 1, 2, 3, 4 }, // низ
new int[] { 5, 6, 7, 8 }, // верх
new int[] { 1, 2, 6, 5 }, // перед
new int[] { 2, 3, 7, 6 }, // право
new int[] { 3, 4, 8, 7 }, // зад
new int[] { 4, 1, 5, 8 } // лево
};
// Write faces with offset
// Запись граней со смещением индексов.
foreach (var f in faces)
{
writer.WriteLine(
@@ -297,16 +314,16 @@ public class MshConverter
{
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)
// Запись ребер как line-элементов OBJ.
foreach (var e in edges)
{
// OBJ uses 1-based indexing
// OBJ использует индексацию с 1.
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
}
}
+4 -3
View File
@@ -43,7 +43,7 @@ var targetIds = new[] { 72, 88 };
foreach (var targetId in targetIds)
{
// Material files are stored with their ID in the archive
var matEntry = matLibResult.Archive.Files.FirstOrDefault(f => f.Index == targetId);
var matEntry = matLibResult.Archive.Files.FirstOrDefault(f => f.DirectoryIndex == targetId);
if (matEntry == null)
{
@@ -52,7 +52,8 @@ foreach (var targetId in targetIds)
}
Console.WriteLine($"=== Material {targetId} ===");
Console.WriteLine($" Index: {matEntry.Index}");
Console.WriteLine($" DirectoryIndex: {matEntry.DirectoryIndex}");
Console.WriteLine($" SortIndex: {matEntry.SortIndex}");
Console.WriteLine($" Name: {matEntry.FileName}");
Console.WriteLine($" ElementCount (Version): {matEntry.ElementCount}");
Console.WriteLine($" ElementSize (Magic1): {matEntry.ElementSize}");
@@ -126,7 +127,7 @@ foreach (var name in weaMatNames)
if (found != null)
{
Console.WriteLine($" {name,-10} -> Index {found.Index,3}: {found.FileName}");
Console.WriteLine($" {name,-10} -> DirectoryIndex {found.DirectoryIndex,3}, SortIndex {found.SortIndex,3}: {found.FileName}");
}
else
{
+3 -3
View File
@@ -223,7 +223,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
- Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD
```
нулевому элементу добавляется флаг 0x1000000
Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08)
Содержит 2 ссылки на анимационные subentry: AnimMapStart указывает в файл 0x13, FallbackKey указывает в файл 0x08.
Если интерполируется анимация -0.5s короче чем magic1 у файла 13
И у файла есть OffsetIntoFile13
И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда)
@@ -260,7 +260,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time)
```
- Тип 12 - microtexture mapping
- Тип 13 - короткие меш-анимации (почему я это не дописал?)
- Тип 13 - animation map / карта кадров для выбора ключей из файла 08
```
Буквально (hex)
00 01 01 02 ...
@@ -317,7 +317,7 @@ All lightmaps named whatever.0 → they all end up with no DirectDraw palette at
По сути представляет собой последовательный список саб-эффектов идущих друг за другом.
Всего существует 9 (1..9) видов эффектов: !описать по мере реверса
Всего существует 10 (1..10) видов FXID-команд. Field-level смысл payload ещё нужно дополнять по мере реверса.
Выглядит так, словно весь файл это тоже эффект сам по себе.
+29 -41
View File
@@ -1,48 +1,36 @@
namespace ScrLib;
public class ScrFile
{
/// <summary>
/// тут всегда число 59 (0x3b) - это число известных игре скриптов
/// </summary>
public int Magic { get; set; }
/// <summary>SCR файл.</summary>
/// <param name="Magic">Число известных игре скриптов; обычно 59 (0x3B).</param>
/// <param name="EntryCount">Количество ScrEntry.</param>
/// <param name="Entries">Записи SCR.</param>
public record ScrFile(int Magic, int EntryCount, List<ScrEntry> Entries);
public int EntryCount { get; set; }
/// <summary>Запись SCR.</summary>
/// <param name="Title">Название записи.</param>
/// <param name="Index">Индекс записи.</param>
/// <param name="InnerCount">Количество вложенных записей.</param>
/// <param name="Inners">Вложенные записи.</param>
public record ScrEntry(string Title, int Index, int InnerCount, List<ScrEntryInner> Inners);
public List<ScrEntry> Entries { get; set; }
}
public class ScrEntry
{
public string Title { get; set; }
public int Index { get; set; }
public int InnerCount { get; set; }
public List<ScrEntryInner> Inners { get; set; }
}
public class ScrEntryInner
{
/// <summary>
/// Номер скрипта в игре (это тех, которых 0x3b)
/// </summary>
public int ScriptIndex { get; set; }
public int UnkInner2 { get; set; }
public int UnkInner3 { get; set; }
public ScrEntryInnerType Type { get; set; }
public int UnkInner5 { get; set; }
public int ArgumentsCount { get; set; }
public List<int> Arguments { get; set; }
public int UnkInner7 { get; set; }
}
/// <summary>Вложенная запись SCR.</summary>
/// <param name="ScriptIndex">Номер скрипта в игре из таблицы известных скриптов (обычно 0x3B записей).</param>
/// <param name="UnkInner2">Неизвестное поле. Для SetVarsetValue это индекс в Varset.</param>
/// <param name="UnkInner3">Неизвестное поле. Для SetVarsetValue это устанавливаемое значение.</param>
/// <param name="Type">Тип вложенной записи.</param>
/// <param name="UnkInner5">Неизвестное поле.</param>
/// <param name="ArgumentsCount">Количество аргументов.</param>
/// <param name="Arguments">Аргументы вложенной записи.</param>
/// <param name="UnkInner7">Неизвестное поле.</param>
public record ScrEntryInner(
int ScriptIndex,
int UnkInner2,
int UnkInner3,
ScrEntryInnerType Type,
int UnkInner5,
int ArgumentsCount,
List<int> Arguments,
int UnkInner7);
public enum ScrEntryInnerType
{
+30 -30
View File
@@ -13,50 +13,50 @@ public class ScrParser
public static ScrFile ReadFile(Stream fs)
{
var scrFile = new ScrFile();
var magic = fs.ReadInt32LittleEndian();
var entryCount = fs.ReadInt32LittleEndian();
List<ScrEntry> entries = [];
scrFile.Magic = fs.ReadInt32LittleEndian();
scrFile.EntryCount = fs.ReadInt32LittleEndian();
scrFile.Entries = [];
for (var i = 0; i < scrFile.EntryCount; i++)
for (var i = 0; i < entryCount; i++)
{
var entry = new ScrEntry();
entry.Title = fs.ReadLengthPrefixedString();
var title = fs.ReadLengthPrefixedString();
// тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char*
fs.ReadByte();
entry.Index = fs.ReadInt32LittleEndian();
entry.InnerCount = fs.ReadInt32LittleEndian();
entry.Inners = [];
for (var i1 = 0; i1 < entry.InnerCount; i1++)
var index = fs.ReadInt32LittleEndian();
var innerCount = fs.ReadInt32LittleEndian();
List<ScrEntryInner> inners = [];
for (var i1 = 0; i1 < innerCount; i1++)
{
var entryInner = new ScrEntryInner();
entryInner.ScriptIndex = fs.ReadInt32LittleEndian();
var scriptIndex = fs.ReadInt32LittleEndian();
var unkInner2 = fs.ReadInt32LittleEndian();
var unkInner3 = fs.ReadInt32LittleEndian();
var type = (ScrEntryInnerType)fs.ReadInt32LittleEndian();
var unkInner5 = fs.ReadInt32LittleEndian();
var argumentsCount = fs.ReadInt32LittleEndian();
List<int> arguments = [];
entryInner.UnkInner2 = fs.ReadInt32LittleEndian();
entryInner.UnkInner3 = fs.ReadInt32LittleEndian();
entryInner.Type = (ScrEntryInnerType)fs.ReadInt32LittleEndian();
entryInner.UnkInner5 = fs.ReadInt32LittleEndian();
entryInner.ArgumentsCount = fs.ReadInt32LittleEndian();
entryInner.Arguments = [];
for (var i2 = 0; i2 < entryInner.ArgumentsCount; i2++)
for (var i2 = 0; i2 < argumentsCount; i2++)
{
entryInner.Arguments.Add(fs.ReadInt32LittleEndian());
arguments.Add(fs.ReadInt32LittleEndian());
}
entryInner.UnkInner7 = fs.ReadInt32LittleEndian();
entry.Inners.Add(entryInner);
var unkInner7 = fs.ReadInt32LittleEndian();
inners.Add(new ScrEntryInner(
scriptIndex,
unkInner2,
unkInner3,
type,
unkInner5,
argumentsCount,
arguments,
unkInner7));
}
scrFile.Entries.Add(entry);
entries.Add(new ScrEntry(title, index, innerCount, inners));
}
return scrFile;
return new ScrFile(magic, entryCount, entries);
}
}
+6 -3
View File
@@ -6,11 +6,14 @@ public static class Extensions
{
return format switch
{
0x22B8 => 32,
0x115C => 16,
0x0 => 8,
0x235 => 16,
0x22C => 16,
0x115C => 16,
0x58 => 16,
0x378 => 32,
0 => 32
0x22B8 => 32,
_ => throw new InvalidOperationException($"Unsupported Texm format {format}")
};
}
}
+88 -73
View File
@@ -35,44 +35,31 @@ public record TexmHeader(
/// <param name="Items">Элементы</param>
public record PageHeader(string Page, int Count, List<PageItem> Items);
/// <summary>Элемент PAGE-секции TEXM.</summary>
/// <param name="X">X-координата в атласе.</param>
/// <param name="Width">Ширина элемента в атласе.</param>
/// <param name="Y">Y-координата в атласе.</param>
/// <param name="Height">Высота элемента в атласе.</param>
public record PageItem(short X, short Width, short Y, short Height);
public class TexmFile
/// <summary>TEXM файл.</summary>
/// <param name="FileName">Исходное имя файла текстуры TEXM.</param>
/// <param name="Header">Заголовок файла (length = 32).</param>
/// <param name="MipmapBytes">Байты mipmap уровней.</param>
/// <param name="Pages">PAGE-секция атласа, если присутствует.</param>
/// <param name="IsIndexed">Признак indexed texture с lookup таблицей на 1024 байта.</param>
/// <param name="LookupColors">Lookup таблица цветов: 256 цветов * 4 байта.</param>
public record class TexmFile(
string FileName,
TexmHeader Header,
List<byte[]> MipmapBytes,
PageHeader? Pages,
bool IsIndexed,
byte[] LookupColors)
{
/// <summary>
/// Исходное имя файла текстуры TEXM
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Заголовок файла, всегда 32 байта
/// </summary>
public TexmHeader Header { get; set; }
/// <summary>
/// Если в одной текстуре есть несколько MipMap уровней, тут будет несколько отдельных текстур
/// </summary>
public List<byte[]> MipmapBytes { get; set; }
/// <summary>
/// Если текстура - это атлас, то здесь будет информация о координатах в атласе
/// </summary>
public PageHeader? Pages { get; set; }
/// <summary>
/// В некоторых случаях, текстура может быть закодирована как lookup таблица на 1024 байта (256 цветов),
/// тогда сначала идёт 1024 байта lookup таблицы, а далее сами мипмапы, по 1 байту (каждый байт - индекс в lookup таблице)
/// </summary>
public bool IsIndexed { get; set; }
/// <summary>
/// Lookup таблица цветов (каждый цвет закодирован как 4 байта (ARGB))
/// </summary>
public byte[] LookupColors { get; set; }
public async Task WriteToFolder(string folder)
public Task WriteToFolder(string folder)
{
if (Directory.Exists(folder))
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
@@ -84,8 +71,8 @@ public class TexmFile
{
for (var i = 0; i < Header.MipmapCount; i++)
{
var mipWidth = Header.Width / (int) Math.Pow(2, i);
var mipHeight = Header.Height / (int) Math.Pow(2, i);
var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Math.Max(1, Header.Height >> i);
var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors);
var image = Image.LoadPixelData<Rgba32>(reinterpretedPixels, mipWidth, mipHeight);
@@ -93,13 +80,13 @@ public class TexmFile
image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{mipWidth}x{mipHeight}_indexed.png");
}
return;
return Task.CompletedTask;
}
for (var i = 0; i < Header.MipmapCount; i++)
{
var mipWidth = Header.Width / (int) Math.Pow(2, i);
var mipHeight = Header.Height / (int) Math.Pow(2, i);
var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Math.Max(1, Header.Height >> i);
var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32(
MipmapBytes[i],
@@ -112,14 +99,16 @@ public class TexmFile
image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{Header.Format}_{mipWidth}x{mipHeight}.png");
}
return Task.CompletedTask;
}
public byte[] GetRgba32BytesFromMipmap(int index, out int mipWidth, out int mipHeight)
{
var mipmapBytes = MipmapBytes[index];
mipWidth = Header.Width / (int) Math.Pow(2, index);
mipHeight = Header.Height / (int) Math.Pow(2, index);
mipWidth = Math.Max(1, Header.Width >> index);
mipHeight = Math.Max(1, Header.Height >> index);
if (IsIndexed)
{
@@ -151,7 +140,7 @@ public class TexmFile
result[i * 4 + 0] = r;
result[i * 4 + 1] = g;
result[i * 4 + 2] = b;
result[i * 4 + 3] = 255;
result[i * 4 + 3] = a;
}
return result;
@@ -165,6 +154,8 @@ public class TexmFile
888 => ReinterpretAs888(bytes, mipWidth, mipHeight),
4444 => ReinterpretAs4444(bytes, mipWidth, mipHeight),
565 => ReinterpretAs565(bytes, mipWidth, mipHeight),
556 => ReinterpretAs556(bytes, mipWidth, mipHeight),
88 => ReinterpretAs88(bytes, mipWidth, mipHeight),
_ => throw new InvalidOperationException($"Invalid format {format}")
};
@@ -178,22 +169,38 @@ public class TexmFile
var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 2)
{
var rawPixel = span.Slice(i, 2);
var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2));
// swap endianess
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31);
var g = (byte)(((rawPixel >> 5) & 0b111111) * 255 / 63);
var b = (byte)((rawPixel & 0b11111) * 255 / 31);
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 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = 0xff;
}
// swap endianess back
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]);
return result;
}
private byte[] ReinterpretAs556(byte[] bytes, int mipWidth, int mipHeight)
{
var span = bytes.AsSpan();
var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 2)
{
var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(i, 2));
var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31);
var g = (byte)(((rawPixel >> 6) & 0b11111) * 255 / 31);
var b = (byte)((rawPixel & 0b111111) * 255 / 63);
result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = 0xff;
}
return result;
@@ -206,23 +213,36 @@ public class TexmFile
var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 2)
{
var rawPixel = span.Slice(i, 2);
var rawPixel = BinaryPrimitives.ReadUInt16LittleEndian(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);
var r = (byte)(((rawPixel[1] >> 0) & 0b1111) * 17);
var a = (byte)(((rawPixel >> 12) & 0b1111) * 17);
var r = (byte)(((rawPixel >> 8) & 0b1111) * 17);
var g = (byte)(((rawPixel >> 4) & 0b1111) * 17);
var b = (byte)((rawPixel & 0b1111) * 17);
result[i / 2 * 4 + 0] = r;
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;
}
private byte[] ReinterpretAs88(byte[] bytes, int mipWidth, int mipHeight)
{
var span = bytes.AsSpan();
var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 2)
{
var l = span[i];
var a = span[i + 1];
result[i / 2 * 4 + 0] = l;
result[i / 2 * 4 + 1] = l;
result[i / 2 * 4 + 2] = l;
result[i / 2 * 4 + 3] = a;
}
return result;
@@ -237,14 +257,9 @@ public class TexmFile
{
var rawPixel = span.Slice(i, 4);
var r = rawPixel[0];
var g = rawPixel[1];
var b = rawPixel[2];
var w = rawPixel[3];
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
result[i + 0] = rawPixel[0];
result[i + 1] = rawPixel[1];
result[i + 2] = rawPixel[2];
result[i + 3] = 255;
}
@@ -270,7 +285,7 @@ public class TexmFile
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
result[i + 3] = a;
result[i + 3] = 255;
// swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]);
+23 -36
View File
@@ -20,18 +20,18 @@ public class TexmParser
var widthBytes = headerBytes[4..8];
var heightBytes = headerBytes[8..12];
var mipmapCountBytes = headerBytes[12..16];
var strideBytes = headerBytes[16..20];
var magic1Bytes = headerBytes[20..24];
var formatOptionFlagsBytes = headerBytes[24..28];
var flags4Bytes = headerBytes[16..20];
var flags5Bytes = headerBytes[20..24];
var unk6Bytes = headerBytes[24..28];
var formatBytes = headerBytes[28..32];
var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0');
var width = BinaryPrimitives.ReadInt32LittleEndian(widthBytes);
var height = BinaryPrimitives.ReadInt32LittleEndian(heightBytes);
var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes);
var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes);
var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes);
var formatOptionFlags = BinaryPrimitives.ReadInt32LittleEndian(formatOptionFlagsBytes);
var flags4 = BinaryPrimitives.ReadInt32LittleEndian(flags4Bytes);
var flags5 = BinaryPrimitives.ReadInt32LittleEndian(flags5Bytes);
var unk6 = BinaryPrimitives.ReadInt32LittleEndian(unk6Bytes);
var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes);
if (texmAscii != "Texm")
@@ -39,64 +39,56 @@ public class TexmParser
return new TexmParseResult(null, "Файл не начинается с Texm");
}
var textureFile = new TexmFile()
{
FileName = file
};
var header = new TexmHeader(
texmAscii,
width,
height,
mipmapCount,
stride,
magic1,
formatOptionFlags,
flags4,
flags5,
unk6,
format
);
textureFile.Header = header;
List<byte[]> mipmapBytesList;
var isIndexed = false;
byte[] lookupColors = [];
if (format == 0)
{
// если формат 0, то текстура использует lookup таблицу в первых 1024 байтах (256 разных цветов в формате ARGB 888)
var lookupColors = new byte[1024];
lookupColors = new byte[1024];
stream.ReadExactly(lookupColors, 0, lookupColors.Length);
textureFile.LookupColors = lookupColors;
var mipmapBytesList = ReadMipmapsAsIndexes(
mipmapBytesList = ReadMipmapsAsIndexes(
stream,
mipmapCount,
width,
height
);
textureFile.MipmapBytes = mipmapBytesList;
textureFile.IsIndexed = true;
isIndexed = true;
}
else
{
var mipmapBytesList = ReadMipmaps(
mipmapBytesList = ReadMipmaps(
stream,
format.AsStride(),
mipmapCount,
width,
height
);
textureFile.MipmapBytes = mipmapBytesList;
}
PageHeader? pages = null;
if (stream.Position < stream.Length)
{
// has PAGE data
var pageHeader = ReadPage(stream);
textureFile.Pages = pageHeader;
pages = ReadPage(stream);
}
var textureFile = new TexmFile(file, header, mipmapBytesList, pages, isIndexed, lookupColors);
return new TexmParseResult(textureFile);
}
@@ -142,17 +134,12 @@ public class TexmParser
private static List<byte[]> ReadMipmaps(Stream stream, int stride, int mipmapCount, int topWidth, int topHeight)
{
if (stride == 0)
{
stride = 16;
}
List<int> mipmapByteLengths = [];
for (int i = 0; i < mipmapCount; i++)
{
var mipWidth = topWidth / (int) Math.Pow(2, i);
var mipHeight = topHeight / (int) Math.Pow(2, i);
var mipWidth = Math.Max(1, topWidth >> i);
var mipHeight = Math.Max(1, topHeight >> i);
var imageByteLength = mipWidth * mipHeight * (stride / 8);
mipmapByteLengths.Add(imageByteLength);
@@ -178,8 +165,8 @@ public class TexmParser
for (int i = 0; i < mipmapCount; i++)
{
var mipWidth = topWidth / (int) Math.Pow(2, i);
var mipHeight = topHeight / (int) Math.Pow(2, i);
var mipWidth = Math.Max(1, topWidth >> i);
var mipHeight = Math.Max(1, topHeight >> i);
var imageByteLength = mipWidth * mipHeight;
mipmapByteLengths.Add(imageByteLength);