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; namespace Common;
/// <summary>DEBUG ONLY. Одни и те же 4 байта, интерпретированные как int32 и float32.</summary>
/// <param name="AsInt">Значение как int32.</param>
/// <param name="AsFloat">Значение как float32.</param>
[DebuggerDisplay("AsInt = {AsInt}, AsFloat = {AsFloat}")] [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 IntFloatValue(Span<byte> span)
public float AsFloat { get; set; } = MemoryMarshal.Read<float>(span); : this(MemoryMarshal.Read<int>(span), MemoryMarshal.Read<float>(span))
{
}
} }
+107 -113
View File
@@ -1,152 +1,148 @@
namespace MaterialLib; namespace MaterialLib;
public class MaterialFile /// <summary>Файл материала MAT0.</summary>
{ /// <param name="FileName">Имя файла из NRes metadata, не из payload материала.</param>
// === Metadata (not from file content) === /// <param name="Version">Значение attr1 / ElementCount из NRes metadata.</param>
public string FileName { get; set; } /// <param name="Magic1">Значение attr2 / Magic1 из NRes metadata.</param>
public int Version { get; set; } // From NRes ElementCount /// <param name="MaterialRenderingType">Производное от Version: (Version >> 2) &amp; 0xF. 0 = standard, 1 = special, 2 = particle.</param>
public int Magic1 { get; set; } // From NRes Magic1 /// <param name="SupportsBumpMapping">Производное от Version: (Version &amp; 2) != 0.</param>
/// <param name="IsParticleEffect">Производное от Version: Version &amp; 40. 0 = normal, 8 = particle/effect.</param>
// === Derived from Version/ElementCount === /// <param name="SourceBlendMode">Глобальный source blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
public int MaterialRenderingType { get; set; } // (Version >> 2) & 0xF - 0=Standard, 1=Special, 2=Particle /// <param name="DestBlendMode">Глобальный destination blend mode; читается, если Magic1 >= 2. Значение по умолчанию: Unknown (0xFF).</param>
public bool SupportsBumpMapping { get; set; } // (Version & 2) != 0 /// <param name="GlobalAlphaMultiplier">Глобальный alpha multiplier; читается, если Magic1 > 2. Значение по умолчанию: 1.0.</param>
public int IsParticleEffect { get; set; } // Version & 40 - 0=Normal, 8=Particle/Effect /// <param name="GlobalEmissiveIntensity">Глобальная emissive intensity; читается, если Magic1 > 3. Значение по умолчанию: 0.0.</param>
/// <param name="Stages">Стадии материала в порядке чтения из файла.</param>
// === File Content (in read order) === /// <param name="Animations">Анимации материала в порядке чтения из файла.</param>
// Read order: StageCount (ushort), AnimCount (ushort), then conditionally blend modes and params public record MaterialFile(
string FileName,
// Global Blend Modes (read if Magic1 >= 2) int Version,
public BlendMode SourceBlendMode { get; set; } // Default: Unknown (0xFF) int Magic1,
public BlendMode DestBlendMode { get; set; } // Default: Unknown (0xFF) int MaterialRenderingType,
bool SupportsBumpMapping,
// Global Parameters (read if Magic1 > 2 and > 3 respectively) int IsParticleEffect,
public float GlobalAlphaMultiplier { get; set; } // Default: 1.0 (always 1.0 in all 628 materials) BlendMode SourceBlendMode,
public float GlobalEmissiveIntensity { get; set; } // Default: 0.0 (0=no glow, rare values: 1000, 10000) BlendMode DestBlendMode,
float GlobalAlphaMultiplier,
public List<MaterialStage> Stages { get; set; } = new(); float GlobalEmissiveIntensity,
public List<MaterialAnimation> Animations { get; set; } = new(); List<MaterialStage> Stages,
} List<MaterialAnimation> Animations);
/// <summary> /// <summary>
/// Blend modes for material rendering. These control how source and destination colors are combined. /// Режимы blend для материала. Управляют смешиванием source и destination цветов.
/// Formula: FinalColor = (SourceColor * SourceBlend) [operation] (DestColor * DestBlend) /// Формула: FinalColor = SourceColor * SourceBlend [operation] DestColor * DestBlend.
/// Maps to Direct3D D3DBLEND values. /// Соответствуют значениям Direct3D D3DBLEND.
/// </summary> /// </summary>
public enum BlendMode : byte 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, 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, One = 2,
/// <summary>Blend factor is (Rs, Gs, Bs, As) - uses source color</summary> /// <summary>Blend factor = (Rs, Gs, Bs, As).</summary>
SrcColor = 3, 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, 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, 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, InvSrcAlpha = 6,
/// <summary>Blend factor is (Ad, Ad, Ad, Ad) - uses destination alpha</summary> /// <summary>Blend factor = (Ad, Ad, Ad, Ad).</summary>
DestAlpha = 7, 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, InvDestAlpha = 8,
/// <summary>Blend factor is (Rd, Gd, Bd, Ad) - uses destination color</summary> /// <summary>Blend factor = (Rd, Gd, Bd, Ad).</summary>
DestColor = 9, 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, 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, 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, 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, BothInvSrcAlpha = 13,
/// <summary>Unknown or uninitialized blend mode (0xFF default value)</summary> /// <summary>Неизвестный или неинициализированный режим blend; значение по умолчанию 0xFF.</summary>
Unknown = 0xFF Unknown = 0xFF
} }
public class MaterialStage /// <summary>Стадия материала. Порядок полей соответствует порядку чтения из файла, а не C struct layout.</summary>
{ /// <param name="AmbientR">Ambient R, читается первым цветовым блоком.</param>
// === FILE READ ORDER (34 bytes per stage) === /// <param name="AmbientG">Ambient G, читается первым цветовым блоком.</param>
// This matches the order bytes are read from the file (decompiled.c lines 159-217) /// <param name="AmbientB">Ambient B, читается первым цветовым блоком.</param>
// NOT the C struct memory layout (which is Diffuse, Ambient, Specular, Emissive) /// <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) /// <summary>Анимация материала.</summary>
public float AmbientR; /// <param name="Target">Целевые компоненты анимации: биты 3..31 combined field.</param>
public float AmbientG; /// <param name="LoopMode">Режим повтора: биты 0..2 combined field.</param>
public float AmbientB; /// <param name="Keys">Ключи анимации.</param>
public float AmbientA; // Scaled by 0.01 when read from file /// <param name="TargetDescription">Кэшированное описание target для UI.</param>
public record MaterialAnimation(
// 2. Diffuse Color (4 bytes, read second from file) AnimationTarget Target,
public float DiffuseR; AnimationLoopMode LoopMode,
public float DiffuseG; List<AnimKey> Keys,
public float DiffuseB; string TargetDescription);
public float DiffuseA;
// 3. Specular Color (4 bytes, read third from file)
public float SpecularR;
public float SpecularG;
public float SpecularB;
public float SpecularA;
// 4. Emissive Color (4 bytes, read fourth from file)
public float EmissiveR;
public float EmissiveG;
public float EmissiveB;
public float EmissiveA;
// 5. Power (1 byte → float, read fifth from file)
public float Power;
// 6. Texture Stage Index (1 byte, read sixth from file)
// 255 = not set/default, 0-47 = reference to specific texture stage
public int TextureStageIndex;
// 7. Texture Name (16 bytes, read seventh from file)
public string TextureName { get; set; }
}
public class MaterialAnimation
{
// === File Read Order ===
// Combined field (4 bytes): bits 3-31 = Target, bits 0-2 = LoopMode
public AnimationTarget Target;
public AnimationLoopMode LoopMode;
// Key count (2 bytes), then keys
public List<AnimKey> Keys { get; set; } = new();
// Cached description for UI (computed once during parsing)
public string TargetDescription { get; set; } = string.Empty;
}
[Flags] [Flags]
public enum AnimationTarget : int public enum AnimationTarget : int
{ {
// NOTE: This is a BITSET (flags enum). Multiple flags can be combined. // Это bitset: несколько флагов могут комбинироваться.
// 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. // Если все флаги равны 0, вся стадия копируется без интерполяции.
Ambient = 1, // 0x01 - Interpolates Ambient RGB (Interpolate.c lines 23-37) Ambient = 1, // 0x01: интерполирует Ambient RGB.
Diffuse = 2, // 0x02 - Interpolates Diffuse RGB (Interpolate.c lines 7-21) Diffuse = 2, // 0x02: интерполирует Diffuse RGB.
Specular = 4, // 0x04 - Interpolates Specular RGB (Interpolate.c lines 39-53) Specular = 4, // 0x04: интерполирует Specular RGB.
Emissive = 8, // 0x08 - Interpolates Emissive RGB (Interpolate.c lines 55-69) Emissive = 8, // 0x08: интерполирует Emissive RGB.
Power = 16 // 0x10 - Interpolates Ambient.A and sets Power (Interpolate.c lines 71-76) Power = 16 // 0x10: интерполирует Ambient.A и задает Power.
} }
@@ -158,10 +154,8 @@ public enum AnimationLoopMode : int
Random = 3 Random = 3
} }
public struct AnimKey /// <summary>Ключ анимации материала (length = 6).</summary>
{ /// <param name="StageIndex">[0x00..0x02] Индекс стадии материала.</param>
// === File Read Order (6 bytes per key) === /// <param name="DurationMs">[0x02..0x04] Длительность в миллисекундах.</param>
public ushort StageIndex; // Read first /// <param name="InterpolationCurve">[0x04..0x06] Кривая интерполяции. В исследованных данных всегда 0.</param>
public ushort DurationMs; // Read second public readonly record struct AnimKey(ushort StageIndex, ushort DurationMs, ushort InterpolationCurve);
public ushort InterpolationCurve; // Read third - Always 0 (linear interpolation) in all 1848 keys
}
+88 -69
View File
@@ -7,17 +7,9 @@ public static class MaterialParser
{ {
public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1) public static MaterialFile ReadFromStream(Stream fs, string fileName, int elementCount, int magic1)
{ {
var file = new MaterialFile var materialRenderingType = elementCount >> 2 & 0xf;
{ var supportsBumpMapping = (elementCount & 2) != 0;
FileName = fileName, var isParticleEffect = elementCount & 40;
Version = elementCount,
Magic1 = magic1
};
// Derived fields
file.MaterialRenderingType = elementCount >> 2 & 0xf;
file.SupportsBumpMapping = (elementCount & 2) != 0;
file.IsParticleEffect = elementCount & 40;
// Reading content // Reading content
var stageCount = fs.ReadUInt16LittleEndian(); var stageCount = fs.ReadUInt16LittleEndian();
@@ -25,113 +17,140 @@ public static class MaterialParser
uint magic = (uint)magic1; uint magic = (uint)magic1;
// Defaults found in C code // Значения по умолчанию из C-кода.
file.GlobalAlphaMultiplier = 1.0f; // field8_0x15c var globalAlphaMultiplier = 1.0f; // field8_0x15c
file.GlobalEmissiveIntensity = 0.0f; // field9_0x160 var globalEmissiveIntensity = 0.0f; // field9_0x160
file.SourceBlendMode = BlendMode.Unknown; // field6_0x154 var sourceBlendMode = BlendMode.Unknown; // field6_0x154
file.DestBlendMode = BlendMode.Unknown; // field7_0x158 var destBlendMode = BlendMode.Unknown; // field7_0x158
if (magic >= 2) if (magic >= 2)
{ {
file.SourceBlendMode = (BlendMode)fs.ReadByte(); sourceBlendMode = (BlendMode)fs.ReadByte();
file.DestBlendMode = (BlendMode)fs.ReadByte(); destBlendMode = (BlendMode)fs.ReadByte();
} }
if (magic > 2) if (magic > 2)
{ {
file.GlobalAlphaMultiplier = fs.ReadFloatLittleEndian(); globalAlphaMultiplier = fs.ReadFloatLittleEndian();
} }
if (magic > 3) if (magic > 3)
{ {
file.GlobalEmissiveIntensity = fs.ReadFloatLittleEndian(); globalEmissiveIntensity = fs.ReadFloatLittleEndian();
} }
// --- 2. Material Stages --- // Стадии материала.
const float Inv255 = 1.0f / 255.0f; const float Inv255 = 1.0f / 255.0f;
const float Field7Mult = 0.01f; const float Field7Mult = 0.01f;
Span<byte> textureNameBuffer = stackalloc byte[16]; Span<byte> textureNameBuffer = stackalloc byte[16];
List<MaterialStage> stages = [];
for (int i = 0; i < stageCount; i++) 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 // Diffuse: 4 байта.
stage.AmbientR = fs.ReadByte() * Inv255; var diffuseR = fs.ReadByte() * Inv255;
stage.AmbientG = fs.ReadByte() * Inv255; var diffuseG = fs.ReadByte() * Inv255;
stage.AmbientB = fs.ReadByte() * Inv255; var diffuseB = fs.ReadByte() * Inv255;
stage.AmbientA = fs.ReadByte() * Field7Mult; // 0.01 scaling var diffuseA = fs.ReadByte() * Inv255;
// 2. Diffuse (4 bytes) - Lines 171-180 // Specular: 4 байта.
stage.DiffuseR = fs.ReadByte() * Inv255; var specularR = fs.ReadByte() * Inv255;
stage.DiffuseG = fs.ReadByte() * Inv255; var specularG = fs.ReadByte() * Inv255;
stage.DiffuseB = fs.ReadByte() * Inv255; var specularB = fs.ReadByte() * Inv255;
stage.DiffuseA = fs.ReadByte() * Inv255; var specularA = fs.ReadByte() * Inv255;
// 3. Specular (4 bytes) - Lines 183-192 // Emissive: 4 байта.
stage.SpecularR = fs.ReadByte() * Inv255; var emissiveR = fs.ReadByte() * Inv255;
stage.SpecularG = fs.ReadByte() * Inv255; var emissiveG = fs.ReadByte() * Inv255;
stage.SpecularB = fs.ReadByte() * Inv255; var emissiveB = fs.ReadByte() * Inv255;
stage.SpecularA = fs.ReadByte() * Inv255; var emissiveA = fs.ReadByte() * Inv255;
// 4. Emissive (4 bytes) - Lines 195-204 // Power: 1 байт -> float.
stage.EmissiveR = fs.ReadByte() * Inv255; var power = (float)fs.ReadByte();
stage.EmissiveG = fs.ReadByte() * Inv255;
stage.EmissiveB = fs.ReadByte() * Inv255;
stage.EmissiveA = fs.ReadByte() * Inv255;
// 5. Power (1 byte → float) - Line 207 // Texture stage index: 1 байт.
stage.Power = (float)fs.ReadByte(); var textureStageIndex = fs.ReadByte();
// 6. Texture Stage Index (1 byte) - Line 210 // Texture name: 16 байт.
stage.TextureStageIndex = fs.ReadByte();
// 7. Texture Name (16 bytes) - Lines 212-217
textureNameBuffer.Clear(); textureNameBuffer.Clear();
fs.ReadExactly(textureNameBuffer); 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++) for (int i = 0; i < animCount; i++)
{ {
var anim = new MaterialAnimation();
uint typeAndParams = fs.ReadUInt32LittleEndian(); uint typeAndParams = fs.ReadUInt32LittleEndian();
anim.Target = (AnimationTarget)(typeAndParams >> 3); var target = (AnimationTarget)(typeAndParams >> 3);
anim.LoopMode = (AnimationLoopMode)(typeAndParams & 7); var loopMode = (AnimationLoopMode)(typeAndParams & 7);
ushort keyCount = fs.ReadUInt16LittleEndian(); ushort keyCount = fs.ReadUInt16LittleEndian();
List<AnimKey> keys = [];
for (int k = 0; k < keyCount; k++) for (int k = 0; k < keyCount; k++)
{ {
var key = new AnimKey keys.Add(new AnimKey(
{ fs.ReadUInt16LittleEndian(),
StageIndex = fs.ReadUInt16LittleEndian(), fs.ReadUInt16LittleEndian(),
DurationMs = fs.ReadUInt16LittleEndian(), fs.ReadUInt16LittleEndian()));
InterpolationCurve = fs.ReadUInt16LittleEndian()
};
anim.Keys.Add(key);
} }
// Precompute description for UI to avoid per-frame allocations // Описание вычисляется один раз при парсинге, чтобы UI не выделял память каждый кадр.
anim.TargetDescription = ComputeTargetDescription(anim.Target); 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) private static string ComputeTargetDescription(AnimationTarget target)
{ {
// Precompute the description once during parsing // Precompute the description once during parsing
if ((int)target == 0) if ((int)target == 0)
return "No interpolation - entire stage is copied as-is (Flags: 0x0)"; return "Без интерполяции - вся стадия копируется как есть (Flags: 0x0)";
var parts = new List<string>(); var parts = new List<string>();
@@ -146,7 +165,7 @@ public static class MaterialParser
if ((target & AnimationTarget.Power) != 0) if ((target & AnimationTarget.Power) != 0)
parts.Add("Ambient.A + Power"); 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; namespace MissionTmaLib;
public class ClanInfo /// <summary>Информация о клане из mission TMA.</summary>
{ /// <param name="ClanName">Имя клана.</param>
public string ClanName { get; set; } /// <param name="UnkInt1">Неизвестное целочисленное поле.</param>
public int UnkInt1 { get; set; } /// <param name="X">TODO</param>
public float X { get; set; } /// <param name="Y">TODO</param>
public float Y { get; set; } /// <param name="ClanType">Тип клана: 1 = игрок, 2 = AI, 3 = нейтральный.</param>
/// <param name="ScriptsString">TODO</param>
/// <summary> /// <param name="UnknownClanPartCount">Количество записей UnknownParts.</param>
/// 1 - игрок, 2 AI, 3 - нейтральный /// <param name="UnknownParts">TODO</param>
/// </summary> /// <param name="ResearchNResPath">Путь к NRes с деревом исследований. Игра называет этот путь TreeName.</param>
public ClanType ClanType { get; set; } /// <param name="Brains">Количество "мозгов" AI/brains.</param>
/// <param name="AlliesMapCount">Количество записей AlliesMap.</param>
public string ScriptsString { get; set; } /// <param name="AlliesMap">Мапа союзников: ключ = имя клана, значение всегда 0 или 1.</param>
public int UnknownClanPartCount { get; set; } public record ClanInfo(
public List<UnknownClanTreeInfoPart> UnknownParts { get; set; } string ClanName,
int UnkInt1,
/// <summary> float X,
/// Игра называет этот путь TreeName float Y,
/// </summary> ClanType ClanType,
public string ResearchNResPath { get; set; } string ScriptsString,
public int Brains { get; set; } int UnknownClanPartCount,
public int AlliesMapCount { get; set; } List<UnknownClanTreeInfoPart> UnknownParts,
string ResearchNResPath,
/// <summary> int Brains,
/// мапа союзников (ключ - имя клана, значение - число, всегда либо 0 либо 1) int AlliesMapCount,
/// </summary> Dictionary<string, int> AlliesMap);
public Dictionary<string, int> AlliesMap { get; set; }
}
+28 -36
View File
@@ -2,39 +2,31 @@
namespace MissionTmaLib; namespace MissionTmaLib;
public class GameObjectInfo /// <summary>Информация об объекте миссии из mission TMA.</summary>
{ /// <param name="Type">Тип объекта: 0 = здание, 1 = бот, 2 = окружение.</param>
// 0 - здание, 1 - бот, 2 - окружение /// <param name="UnknownFlags">Неизвестные флаги объекта.</param>
public GameObjectType Type { get; set; } /// <param name="DatString">Путь к DAT ресурсу объекта.</param>
/// <param name="OwningClanIndex">Индекс клана-владельца. Может быть -1, если объект никому не принадлежит. Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1</param>
public int UnknownFlags { get; set; } /// <param name="Order">Порядок объекта. Для зданий парсер добавляет int.MaxValue.</param>
/// <param name="Position">Позиция объекта.</param>
public string DatString { get; set; } /// <param name="Rotation">Поворот объекта.</param>
/// <param name="Scale">Масштаб объекта. Для старых feature set может быть принудительно (1, 1, 1).</param>
/// <summary> /// <param name="UnknownString2">Неизвестная строка, например у HERO бывает пустой.</param>
/// Индекс клана, которому принадлежит объект /// <param name="UnknownInt4">Неизвестное целочисленное поле.</param>
/// </summary> /// <param name="UnknownInt5">Неизвестное целочисленное поле.</param>
/// <remarks> /// <param name="UnknownInt6">Неизвестное целочисленное поле.</param>
/// <para> /// <param name="Settings">Настройки объекта.</param>
/// Некоторые объекты окружения иногда почему-то принадлежат клану отличному от -1 public record class GameObjectInfo(
/// </para> GameObjectType Type,
/// <para> int UnknownFlags,
/// Может быть -1, если объект никому не принадлежит, я такое встречал только у объектов окружения string DatString,
/// </para> int OwningClanIndex,
/// </remarks> int Order,
public int OwningClanIndex { get; set; } Vector3 Position,
Vector3 Rotation,
public int Order { get; set; } Vector3 Scale,
string UnknownString2,
public Vector3 Position { get; set; } int UnknownInt4,
public Vector3 Rotation { get; set; } int UnknownInt5,
public Vector3 Scale { get; set; } int UnknownInt6,
GameObjectSettings Settings);
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; }
}
+73 -42
View File
@@ -72,32 +72,36 @@ public class MissionTmaParser
List<ClanInfo> infos = []; List<ClanInfo> infos = [];
for (var i = 0; i < clanCount; i++) for (var i = 0; i < clanCount; i++)
{ {
var clanTreeInfo = new ClanInfo(); var clanName = fileStream.ReadLengthPrefixedString();
var unkInt1 = fileStream.ReadInt32LittleEndian();
clanTreeInfo.ClanName = fileStream.ReadLengthPrefixedString(); var x = fileStream.ReadFloatLittleEndian();
clanTreeInfo.UnkInt1 = fileStream.ReadInt32LittleEndian(); var y = fileStream.ReadFloatLittleEndian();
clanTreeInfo.X = fileStream.ReadFloatLittleEndian(); var clanType = (ClanType) fileStream.ReadInt32LittleEndian();
clanTreeInfo.Y = fileStream.ReadFloatLittleEndian(); var scriptsString = string.Empty;
clanTreeInfo.ClanType = (ClanType) fileStream.ReadInt32LittleEndian(); var unknownClanPartCount = 0;
List<UnknownClanTreeInfoPart> unknownParts = [];
var researchNResPath = string.Empty;
var brains = 0;
var alliesMapCount = 0;
Dictionary<string, int> alliesMap = [];
if (1 < clanFeatureSet) if (1 < clanFeatureSet)
{ {
// MISSIONS\SCRIPTS\default // MISSIONS\SCRIPTS\default
// MISSIONS\SCRIPTS\tut1_pl // MISSIONS\SCRIPTS\tut1_pl
// MISSIONS\SCRIPTS\tut1_en // MISSIONS\SCRIPTS\tut1_en
clanTreeInfo.ScriptsString = fileStream.ReadLengthPrefixedString(); scriptsString = fileStream.ReadLengthPrefixedString();
} }
if (2 < clanFeatureSet) if (2 < clanFeatureSet)
{ {
clanTreeInfo.UnknownClanPartCount = fileStream.ReadInt32LittleEndian(); unknownClanPartCount = fileStream.ReadInt32LittleEndian();
// тут игра читает число, затем 12 байт и ещё 2 числа // тут игра читает число, затем 12 байт и ещё 2 числа
List<UnknownClanTreeInfoPart> unknownClanTreeInfoParts = []; for (var i1 = 0; i1 < unknownClanPartCount; i1++)
for (var i1 = 0; i1 < clanTreeInfo.UnknownClanPartCount; i1++)
{ {
unknownClanTreeInfoParts.Add( unknownParts.Add(
new UnknownClanTreeInfoPart( new UnknownClanTreeInfoPart(
fileStream.ReadInt32LittleEndian(), fileStream.ReadInt32LittleEndian(),
new Vector3( new Vector3(
@@ -110,8 +114,6 @@ public class MissionTmaParser
) )
); );
} }
clanTreeInfo.UnknownParts = unknownClanTreeInfoParts;
} }
if (3 < clanFeatureSet) if (3 < clanFeatureSet)
@@ -120,17 +122,17 @@ public class MissionTmaParser
// MISSIONS\SCRIPTS\data.trf // MISSIONS\SCRIPTS\data.trf
// указатель на NRes файл с данными // указатель на NRes файл с данными
// может быть пустым, например у Ntrl в туториале // может быть пустым, например у Ntrl в туториале
clanTreeInfo.ResearchNResPath = fileStream.ReadLengthPrefixedString(); researchNResPath = fileStream.ReadLengthPrefixedString();
} }
if (4 < clanFeatureSet) if (4 < clanFeatureSet)
{ {
clanTreeInfo.Brains = fileStream.ReadInt32LittleEndian(); brains = fileStream.ReadInt32LittleEndian();
} }
if (5 < clanFeatureSet) if (5 < clanFeatureSet)
{ {
clanTreeInfo.AlliesMapCount = fileStream.ReadInt32LittleEndian(); alliesMapCount = fileStream.ReadInt32LittleEndian();
// тут какая-то мапа // тут какая-то мапа
// в демо миссии тут // в демо миссии тут
@@ -142,20 +144,29 @@ public class MissionTmaParser
// Trgt -> 1 // Trgt -> 1
// Enm -> 0 // Enm -> 0
// Ntrl -> 1 // Ntrl -> 1
Dictionary<string, int> map = []; for (var i1 = 0; i1 < alliesMapCount; i1++)
for (var i1 = 0; i1 < clanTreeInfo.AlliesMapCount; i1++)
{ {
var keyIdString = fileStream.ReadLengthPrefixedString(); var keyIdString = fileStream.ReadLengthPrefixedString();
// это число всегда либо 0 либо 1 // это число всегда либо 0 либо 1
var unkNumber = fileStream.ReadInt32LittleEndian(); var unkNumber = fileStream.ReadInt32LittleEndian();
map[keyIdString] = unkNumber; alliesMap[keyIdString] = unkNumber;
}
} }
clanTreeInfo.AlliesMap = map; infos.Add(new ClanInfo(
} clanName,
unkInt1,
infos.Add(clanTreeInfo); x,
y,
clanType,
scriptsString,
unknownClanPartCount,
unknownParts,
researchNResPath,
brains,
alliesMapCount,
alliesMap));
} }
var clanInfo = new ClansFileData(clanFeatureSet, clanCount, infos); var clanInfo = new ClansFileData(clanFeatureSet, clanCount, infos);
@@ -177,51 +188,58 @@ public class MissionTmaParser
for (var i = 0; i < gameObjectsCount; i++) for (var i = 0; i < gameObjectsCount; i++)
{ {
var gameObjectInfo = new GameObjectInfo();
// ReadGameObjectData // ReadGameObjectData
gameObjectInfo.Type = (GameObjectType) fileStream.ReadInt32LittleEndian(); var type = (GameObjectType) fileStream.ReadInt32LittleEndian();
gameObjectInfo.UnknownFlags = fileStream.ReadInt32LittleEndian(); var unknownFlags = fileStream.ReadInt32LittleEndian();
// UNITS\UNITS\HERO\hero_t.dat // 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) if (2 < gameObjectsFeatureSet)
{ {
gameObjectInfo.OwningClanIndex = fileStream.ReadInt32LittleEndian(); owningClanIndex = fileStream.ReadInt32LittleEndian();
} }
if (3 < gameObjectsFeatureSet) if (3 < gameObjectsFeatureSet)
{ {
gameObjectInfo.Order = fileStream.ReadInt32LittleEndian(); order = fileStream.ReadInt32LittleEndian();
if (gameObjectInfo.Type == GameObjectType.Building) if (type == GameObjectType.Building)
{ {
gameObjectInfo.Order += int.MaxValue; order += int.MaxValue;
} }
} }
// читает 12 байт // читает 12 байт
gameObjectInfo.Position = new Vector3( var position = new Vector3(
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian() fileStream.ReadFloatLittleEndian()
); );
// ещё раз читает 12 байт // ещё раз читает 12 байт
gameObjectInfo.Rotation = new Vector3( var rotation = new Vector3(
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian() fileStream.ReadFloatLittleEndian()
); );
Vector3 scale;
if (gameObjectsFeatureSet < 10) if (gameObjectsFeatureSet < 10)
{ {
// если фичесет меньше 10, то игра забивает вектор единицами // если фичесет меньше 10, то игра забивает вектор единицами
gameObjectInfo.Scale = new Vector3(1, 1, 1); scale = new Vector3(1, 1, 1);
} }
else else
{ {
// в противном случае читает ещё вектор из файла // в противном случае читает ещё вектор из файла
gameObjectInfo.Scale = new Vector3( scale = new Vector3(
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian(), fileStream.ReadFloatLittleEndian(),
fileStream.ReadFloatLittleEndian() fileStream.ReadFloatLittleEndian()
@@ -231,18 +249,18 @@ public class MissionTmaParser
if (6 < gameObjectsFeatureSet) if (6 < gameObjectsFeatureSet)
{ {
// у HERO пустая строка // у HERO пустая строка
gameObjectInfo.UnknownString2 = fileStream.ReadLengthPrefixedString(); unknownString2 = fileStream.ReadLengthPrefixedString();
} }
if (7 < gameObjectsFeatureSet) if (7 < gameObjectsFeatureSet)
{ {
gameObjectInfo.UnknownInt4 = fileStream.ReadInt32LittleEndian(); unknownInt4 = fileStream.ReadInt32LittleEndian();
} }
if (8 < gameObjectsFeatureSet) if (8 < gameObjectsFeatureSet)
{ {
gameObjectInfo.UnknownInt5 = fileStream.ReadInt32LittleEndian(); unknownInt5 = fileStream.ReadInt32LittleEndian();
gameObjectInfo.UnknownInt6 = fileStream.ReadInt32LittleEndian(); unknownInt6 = fileStream.ReadInt32LittleEndian();
} }
if (5 < gameObjectsFeatureSet) 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 // end ReadGameObjectData
} }
+28 -27
View File
@@ -1,4 +1,4 @@
namespace NResLib; namespace NResLib;
/// <summary> /// <summary>
/// Архив NRes (файл NRes) /// Архив NRes (файл NRes)
@@ -8,40 +8,41 @@ public record NResArchive(NResArchiveHeader Header, List<ListMetadataItem> Files
/// <summary> /// <summary>
/// Заголовок файла /// Заголовок файла
/// </summary> /// </summary>
/// <param name="NRes">[0..4] ASCII NRes</param> /// <param name="NRes">[0..4] ASCII NRes.</param>
/// <param name="Version">[4..8] Версия кодировщика (должно быть всегда 0x100)</param> /// <param name="Version">[4..8] Версия кодировщика, обычно 0x100.</param>
/// <param name="FileCount">[8..12] Количество файлов </param> /// <param name="FileCount">[8..12] Количество записей каталога.</param>
/// <param name="TotalFileLengthBytes">[12..16] Длина всего архива</param> /// <param name="TotalFileLengthBytes">[12..16] Длина всего архива.</param>
public record NResArchiveHeader(string NRes, int Version, int FileCount, int TotalFileLengthBytes); public record NResArchiveHeader(string NRes, int Version, int FileCount, int TotalFileLengthBytes);
/// <summary> /// <summary>
/// В конце файла есть список метаданных, /// 64-байтная запись каталога NRes.
/// каждый элемент это 64 байта, ///
/// найти начало можно как (Header.TotalFileLengthBytes - Header.FileCount * 64) /// Важный нюанс RE-имен:
/// - ElementCount/Magic1/ElementSize оставлены как удобные имена для исследования, но на уровне
/// общего NRes-контейнера это attr1/attr2/attr3. Их смысл зависит от TypeId ресурса.
/// - FileName на диске занимает 36 байт: [20..56]. Старый разбор читал только [20..40].
/// - Старые Magic3..Magic6 оказались не отдельными полями формата, а байтами хвоста name_raw.
/// - SortIndex на диске это перестановка для бинарного поиска по имени, а не порядковый
/// индекс записи каталога. Порядковый индекс хранится отдельно в DirectoryIndex.
/// </summary> /// </summary>
/// <param name="FileType">[0..4] ASCII описание типа файла, например TEXM или MAT0</param> /// <param name="TypeId">[0x00..0x04] Resource type id.</param>
/// <param name="ElementCount">[4..8] Количество элементов в файле (если файл составной, например .trf) или версия, например у материалов или флаги</param> /// <param name="FileType">[0x00..0x04] Resource type id как ASCII или hex bytes для RE.</param>
/// <param name="Magic1">[8..12] Неизвестное число</param> /// <param name="ElementCount">[0x04..0x08] ElementCount. смысл зависит от TypeId.</param>
/// <param name="FileLength">[12..16] Длина файла в байтах</param> /// <param name="Magic1">[0x08..0x0C] смысл зависит от TypeId.</param>
/// <param name="ElementSize">[16..20] Размер элемента в файле (если файл составной, например .trf) </param> /// <param name="FileLength">[0x0C..0x10] Длина payload в байтах.</param>
/// <param name="FileName">[20..40] ASCII имя файла</param> /// <param name="ElementSize">[0x10..0x14] ElementSize. смысл зависит от TypeId.</param>
/// <param name="Magic3">[40..44] Неизвестное число</param> /// <param name="FileName">[0x14..0x38] C-string из 36-байтного name_raw.</param>
/// <param name="Magic4">[44..48] Неизвестное число</param> /// <param name="OffsetInFile">[0x38..0x3C] Смещение payload от начала архива.</param>
/// <param name="Magic5">[48..52] Неизвестное число</param> /// <param name="SortIndex">[0x3C..0x40] Индекс перестановки для бинарного поиска по имени.</param>
/// <param name="Magic6">[52..56] Неизвестное число</param> /// <param name="DirectoryIndex">Индекс записи в порядке каталога; не хранится отдельным полем NRes.</param>
/// <param name="OffsetInFile">[56..60] Смещение подфайла от начала NRes (именно самого NRes) в байтах</param> public sealed record class ListMetadataItem(
/// <param name="Index">[60..64] Индекс в файле (от 0, не больше чем кол-во файлов)</param> uint TypeId,
public record ListMetadataItem(
string FileType, string FileType,
uint ElementCount, uint ElementCount,
int Magic1, int Magic1,
int FileLength, int FileLength,
int ElementSize, int ElementSize,
string FileName, string FileName,
int Magic3,
int Magic4,
int Magic5,
int Magic6,
int OffsetInFile, int OffsetInFile,
int Index int SortIndex,
); int DirectoryIndex);
+1 -1
View File
@@ -30,7 +30,7 @@ public class NResExporter
extension = ".bin"; 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); File.WriteAllBytes(targetFilePath, buffer);
} }
+70 -32
View File
@@ -35,6 +35,16 @@ public static class NResParser
TotalFileLengthBytes: BinaryPrimitives.ReadInt32LittleEndian(buffer[12..16]) 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) if (header.TotalFileLengthBytes != nResFs.Length)
{ {
return new NResParseResult( 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); var elements = new List<ListMetadataItem>(header.FileCount);
@@ -53,41 +70,32 @@ public static class NResParser
for (int i = 0; i < header.FileCount; i++) for (int i = 0; i < header.FileCount; i++)
{ {
nResFs.ReadExactly(metaDataBuffer); 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])) return new NResParseResult(null, $"Запись '{name}' выходит за границы data region");
{
type += metaDataBuffer[j]
.ToString("X2") + " ";
}
else
{
type += (char)metaDataBuffer[j];
}
} }
var type2 = BinaryPrimitives.ReadUInt32LittleEndian(metaDataBuffer.Slice(4)); elements.Add(new ListMetadataItem(
typeId,
type = type.Trim(); type,
attr1,
elements.Add( unchecked((int)attr2),
new ListMetadataItem( fileLength,
FileType: type, unchecked((int)attr3),
ElementCount: type2, name,
Magic1: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[8..12]), offset,
FileLength: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[12..16]), sortIndex,
ElementSize: BinaryPrimitives.ReadInt32LittleEndian(metaDataBuffer[16..20]), i));
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])
)
);
metaDataBuffer.Clear(); 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 class App
{ {
public GL GL { get; set; } public GL GL { get; set; } = null!;
public IInputContext Input { get; set; } public IInputContext Input { get; set; } = null!;
public static App Instance; public static App Instance = null!;
private static bool _dockspaceOpen = true; private static bool _dockspaceOpen = true;
private static bool _optFullscreenPersistant = true; private static bool _optFullscreenPersistant = true;
@@ -25,7 +25,7 @@ public class App
public ImFontPtr OpenSansFont; public ImFontPtr OpenSansFont;
private List<IImGuiPanel> _imGuiPanels; private List<IImGuiPanel> _imGuiPanels = [];
public App() public App()
{ {
+9 -27
View File
@@ -70,7 +70,7 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.SameLine(); ImGui.SameLine();
ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString()); ImGui.Text(_viewModel.Archive.Header.TotalFileLengthBytes.ToString());
if (ImGui.BeginTable("content", 12, if (ImGui.BeginTable("content", 9,
ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX)) ImGuiTableFlags.Borders | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoHostExtendX))
{ {
ImGui.TableSetupColumn("Тип файла"); ImGui.TableSetupColumn("Тип файла");
@@ -79,12 +79,9 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableSetupColumn("Длина файла в байтах"); ImGui.TableSetupColumn("Длина файла в байтах");
ImGui.TableSetupColumn("Размер элемента"); ImGui.TableSetupColumn("Размер элемента");
ImGui.TableSetupColumn("Имя файла"); ImGui.TableSetupColumn("Имя файла");
ImGui.TableSetupColumn("Magic3");
ImGui.TableSetupColumn("Magic4");
ImGui.TableSetupColumn("Magic5");
ImGui.TableSetupColumn("Magic6");
ImGui.TableSetupColumn("Смещение в байтах"); ImGui.TableSetupColumn("Смещение в байтах");
ImGui.TableSetupColumn("Индекс в файле"); ImGui.TableSetupColumn("SortIndex");
ImGui.TableSetupColumn("DirectoryIndex");
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
@@ -124,26 +121,6 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text(_viewModel.Archive.Files[i].FileName); ImGui.Text(_viewModel.Archive.Files[i].FileName);
ImGui.TableNextColumn(); 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( ImGui.Text(
_viewModel.Archive.Files[i] _viewModel.Archive.Files[i]
.OffsetInFile.ToString() .OffsetInFile.ToString()
@@ -151,7 +128,12 @@ public class NResExplorerPanel : IImGuiPanel
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Text( ImGui.Text(
_viewModel.Archive.Files[i] _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); 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(); ImGui.SameLine();
if (_viewModel.ViewPages && _viewModel.TexmFile.Pages is not null) 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 string? Path { get; set; }
public List<OpenGlTexture> GlTextures { get; set; } = []; public List<OpenGlTexture> GlTextures { get; set; } = [];
public List<byte[]> RgbaBytesByMipmap { get; set; } = [];
private bool _glTexturesDirty = false; private bool _glTexturesDirty = false;
public bool IsWhiteBgEnabled; public bool IsWhiteBgEnabled;
@@ -48,11 +49,14 @@ public class TexmExplorerViewModel
} }
GlTextures.Clear(); 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); var bytes = TexmFile.GetRgba32BytesFromMipmap(i, out var width, out var height);
RgbaBytesByMipmap.Add(bytes);
var glTexture = new OpenGlTexture( var glTexture = new OpenGlTexture(
gl, gl,
width, width,
+1 -1
View File
@@ -19,7 +19,7 @@ namespace NResUI
public static float? MaxAniso; public static float? MaxAniso;
private readonly GL _gl; private readonly GL _gl;
public readonly string Name; public readonly string Name = string.Empty;
public readonly uint GlTexture; public readonly uint GlTexture;
public readonly uint Width, Height; public readonly uint Width, Height;
public readonly uint MipmapLevels; public readonly uint MipmapLevels;
+7 -22
View File
@@ -3,23 +3,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace PalLib; namespace PalLib;
/// <summary> /// <summary>PAL файл: indexed texture с lookup таблицей, подписью Ipol и индексами.</summary>
/// PAL файл по сути это indexed текстура (1024 байт - 256 цветов lookup + 4 байта "Ipol" и затем 256x256 индексов в lookup) /// <param name="FileName">Имя PAL файла.</param>
/// </summary> /// <param name="Palette">[0x0000..0x0400] 256 цветов lookup (1024 байта).</param>
public class PalFile /// <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) public void SaveAsPng(string outputPath)
{ {
const int width = 256; const int width = 256;
@@ -31,16 +20,12 @@ public class PalFile
{ {
var index = Indices[i]; var index = Indices[i];
// Palette is 256 colors * 4 bytes (ARGB usually, based on TexmLib) // PAL, вероятно, связан с DirectX, поэтому порядок каналов похож на BGRA.
// TexmLib: r = lookup[i*4+0], g = lookup[i*4+1], b = lookup[i*4+2], a = lookup[i*4+3]
// Assuming same format here.
// since PAL is likely directx related, the format is is likely BGRA
var b = Palette[index * 4 + 0]; var b = Palette[index * 4 + 0];
var g = Palette[index * 4 + 1]; var g = Palette[index * 4 + 1];
var r = Palette[index * 4 + 2]; 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 + 0] = r;
rgbaBytes[i * 4 + 1] = g; rgbaBytes[i * 4 + 1] = g;
+1 -6
View File
@@ -27,11 +27,6 @@ public class PalParser
var indices = new byte[65536]; var indices = new byte[65536];
stream.ReadExactly(indices, 0, 65536); stream.ReadExactly(indices, 0, 65536);
return new PalFile return new PalFile(filename, palette, indices);
{
FileName = filename,
Palette = palette,
Indices = indices
};
} }
} }
+5 -6
View File
@@ -3,18 +3,17 @@
<File Path="Directory.Build.props" /> <File Path="Directory.Build.props" />
<File Path="Directory.Packages.props" /> <File Path="Directory.Packages.props" />
<File Path="README.md" /> <File Path="README.md" />
<File Path="AI.md" />
</Folder> </Folder>
<Project Path="Common\Common.csproj" Type="Classic C#" /> <Project Path="Common\Common.csproj" Type="C#" />
<Project Path="CpDatLib\CpDatLib.csproj" Type="Classic C#" /> <Project Path="CpDatLib\CpDatLib.csproj" Type="C#" />
<Project Path="MaterialLib\MaterialLib.csproj" Type="Classic C#" /> <Project Path="MaterialLib\MaterialLib.csproj" Type="C#" />
<Project Path="MissionTmaLib/MissionTmaLib.csproj" /> <Project Path="MissionTmaLib/MissionTmaLib.csproj" />
<Project Path="NLUnpacker/NLUnpacker.csproj" /> <Project Path="NLUnpacker/NLUnpacker.csproj" />
<Project Path="NResLib/NResLib.csproj" /> <Project Path="NResLib/NResLib.csproj" />
<Project Path="NResUI/NResUI.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="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="ScrLib/ScrLib.csproj" />
<Project Path="TexmLib/TexmLib.csproj" /> <Project Path="TexmLib/TexmLib.csproj" />
<Project Path="VarsetLib/VarsetLib.csproj" /> <Project Path="VarsetLib/VarsetLib.csproj" />
+9 -4
View File
@@ -25,12 +25,17 @@ public static class FxidReader
{ {
EffectHeader h; EffectHeader h;
h.ComponentCount = br.ReadUInt32(); h.ComponentCount = br.ReadUInt32();
h.Unknown1 = br.ReadUInt32(); h.TimeMode = br.ReadUInt32();
h.Duration = br.ReadSingle(); h.Duration = br.ReadSingle();
h.Unknown2 = br.ReadSingle(); h.PhaseJitter = br.ReadSingle();
h.Flags = br.ReadUInt32(); h.Flags = br.ReadUInt32();
h.Unknown3 = br.ReadUInt32(); h.SettingsId = br.ReadUInt32();
h.Reserved = br.ReadBytes(24); 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.ScaleX = br.ReadSingle();
h.ScaleY = br.ReadSingle(); h.ScaleY = br.ReadSingle();
h.ScaleZ = 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, /// Parsed from CEffect_InitFromDef: defines component count, global duration/flags,
/// some unknown control fields, and the uniform scale vector applied to the effect. /// some unknown control fields, and the uniform scale vector applied to the effect.
/// </summary> /// </summary>
public struct EffectHeader public record struct EffectHeader
{ {
/// <summary>FXID payload offset 0x00: command count.</summary>
public uint ComponentCount; 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 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 Flags;
public uint Unknown3; /// <summary>FXID payload offset 0x14: settings/profile id.</summary>
public byte[] Reserved; // 24 bytes 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; public float ScaleX;
/// <summary>FXID payload offset 0x34: base scale Y.</summary>
public float ScaleY; public float ScaleY;
/// <summary>FXID payload offset 0x38: base scale Z.</summary>
public float ScaleZ; public float ScaleZ;
} }
/// <summary> /// <summary>
@@ -26,7 +47,7 @@ public struct EffectHeader
/// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha /// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha
/// curves and sample scattering within a 3D extent volume. /// curves and sample scattering within a 3D extent volume.
/// </summary> /// </summary>
public struct BillboardComponentData public record struct BillboardComponentData
{ {
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear public float 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 /// 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. /// window, and scalar ranges (e.g. volume / pitch), plus a 0x40-byte sound name tail.
/// </summary> /// </summary>
public struct SoundComponentData public record struct SoundComponentData
{ {
public uint TypeAndFlags; // component type and flags public uint TypeAndFlags; // component type and flags
public uint PlayMode; // playback mode (looping, one-shot, etc.) 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 /// Prefix layout matches BillboardComponentData and is used to allocate a grid of
/// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture. /// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture.
/// </summary> /// </summary>
public struct AnimParticleComponentData public record struct AnimParticleComponentData
{ {
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear public float 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 /// Shares the same prefix layout as BillboardComponentData, including extents and
/// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture. /// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture.
/// </summary> /// </summary>
public struct AnimBillboardComponentData public record struct AnimBillboardComponentData
{ {
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
public float Unknown04; // mode / flag-like float, semantics not fully clear public float 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 /// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
/// ranges, timing, and a shared texture name at +0x30. /// ranges, timing, and a shared texture name at +0x30.
/// </summary> /// </summary>
public struct TrailComponentData public record struct TrailComponentData
{ {
public uint TypeAndFlags; // component type and flags public uint TypeAndFlags; // component type and flags
public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown 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). /// Simple point component definition (type 6).
/// Definition block is just the 4-byte typeAndFlags header; no extra data on disk. /// Definition block is just the 4-byte typeAndFlags header; no extra data on disk.
/// </summary> /// </summary>
public struct PointComponentData public record struct PointComponentData
{ {
public uint TypeAndFlags; // component type and flags; definition block has no payload 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), /// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
/// followed by two dwords of plane-specific data. /// followed by two dwords of plane-specific data.
/// </summary> /// </summary>
public struct PlaneComponentData public record struct PlaneComponentData
{ {
public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves
public uint ExtraPlaneParam0; // plane-specific parameter, semantics not yet reversed public uint 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 /// time window, instance count, spatial extents/axes, radius triplets, and a
/// 0x40-byte texture name tail. /// 0x40-byte texture name tail.
/// </summary> /// </summary>
public struct ModelComponentData public record struct ModelComponentData
{ {
public uint TypeAndFlags; // component type and flags public uint TypeAndFlags; // component type and flags
public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear 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, /// Layout derived from CAnimModelComponent_Initialize: time params, direction vectors,
/// radius triplets, extent vectors, and a 0x48-byte texture name tail. /// radius triplets, extent vectors, and a 0x48-byte texture name tail.
/// </summary> /// </summary>
public struct AnimModelComponentData public record struct AnimModelComponentData
{ {
public uint TypeAndFlags; // component type and flags public uint TypeAndFlags; // component type and flags
public float AnimSpeed; // animation speed multiplier at +0x04 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), /// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
/// followed by one dword of cube-specific data. /// followed by one dword of cube-specific data.
/// </summary> /// </summary>
public struct CubeComponentData public record struct CubeComponentData
{ {
public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves
public uint ExtraCubeParam0; // cube-specific parameter, semantics not yet reversed 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-ссылками | | 01 | Node table | 38 (0x26), редко 24 | Узлы модели / тайлы; старое имя: Pieces |
| 02 | Submeshes | 68 (0x44) | LOD части с баундинг-боксами | | 02 | Header + slots | 0x8C + n*68 | Общий заголовок и slot records; старое имя: Submeshes |
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) | | 03 | Positions | 12 (0x0C) | Позиции вершин (Vector3); старое имя: Vertices |
| 04 | неизвестно | 4 | (неизвестно) | | 04 | PackedNormals | 4 | `int8[4]`, normal = clamp(component / 127.0, -1..1) |
| 05 | неизвестно | 4 | (неизвестно) | | 05 | PackedUV0 | 4 | `int16[2]`, uv = component / 1024.0 |
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) | | 06 | Index buffer | 2 | Индексы вершин треугольников |
| 07 | неизвестно | 16 | (только Модель) | | 07 | Tri descriptors | 16 | Описатели треугольников для коллизии/пикинга |
| 08 | Animations | 4 | Кейфреймы анимации меша | | 08 | AnimKeyPool | 24 | Кейфреймы анимации меша |
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) | | 0A | Node strings | переменный | Строки узлов; старое имя: ExternalRefs |
| 0B | неизвестно | 4 | неизвестно (только Ландшафт) | | 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
| 0D | неизвестно | 20 (0x14) | неизвестно (только Модель) | | 0D | Batch table | 20 (0x14) | Батчи рендера; FParkan Res13 decimal |
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) | | 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
| 12 | MicrotextureMap | 4 | неизвестно | | 12 | MicrotextureMap | 4 | неизвестно |
| 13 | ShortAnims | 2 | Короткие индексы анимаций | | 13 | AnimMap | 2 | Карта кадров анимации, на нее указывает `AnimMapStart` из 0x01 |
| 15 | неизвестно | 28 (0x1C) | неизвестно | | 15 | TerrainTriangle table | 28 (0x1C) | Terrain-гипотеза |
--- ---
@@ -52,15 +52,15 @@ MSH файлы — это NRes архивы, содержащие несколь
└─► Lod[n] ──► Компонент 02 (индекс сабмеша) └─► Lod[n] ──► Компонент 02 (индекс сабмеша)
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник) ├─► TriStart:TriCount ──► Компонент 07 (данные на треугольник)
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи) └─► BatchStart:BatchCount ──► Компонент 0D (батчи)
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы) ├─► IndexStart:IndexCount ──► Компонент 06 (индексы)
│ │ │ │
│ └─► Компонент 03 (вершины) │ └─► Компонент 03 (вершины)
└─► IndexInto03 (базовое смещение вершины) └─► BaseVertex (базовое смещение вершины)
``` ```
### Ландшафт (террейн) ### Ландшафт (террейн)
@@ -70,38 +70,37 @@ MSH файлы — это NRes архивы, содержащие несколь
└─► Lod[n] ──► Компонент 02 (индекс сабмеша) └─► Lod[n] ──► Компонент 02 (индекс сабмеша)
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники) └─► TriStart:TriCount ──► Компонент 15 (треугольники)
└─► Vertex1/2/3Index ──► Компонент 03 (вершины) └─► Vertex1/2/3Index ──► Компонент 03 (вершины)
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15) └─► TriStart:TriCount ──► Компонент 0B (материалы, параллельно 15)
``` ```
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02 **Важно:** В ландшафтных мешах поля `TriStart` и `TriCount` в Компоненте 02
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07. используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
--- ---
## Структуры компонентов ## Структуры компонентов
### Компонент 01 - Pieces (0x26 = 38 байт) ### Компонент 0x01 - Node table (0x26 = 38 байт)
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов). Определяет узлы модели или тайлы terrain. Старое локальное имя: Pieces / SubMesh.
| Смещение | Размер | Тип | Поле | Описание | | Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------| |:--------:|:------:|:---:|------|----------|
| 0x00 | 1 | byte | Type1 | Флаги типа части | | 0x00 | 2 | uint16 | Header0 | Заголовочное слово узла; старые имена: Type1 + Type2 |
| 0x01 | 1 | byte | Type2 | Дополнительные флаги | | 0x02 | 2 | uint16 | ParentOrLink | Индекс родителя/ссылка; старый локальный тип int16 показывал 0xFFFF как -1 |
| 0x02 | 2 | int16 | ParentIndex | Индекс родителя (-1 = корень) | | 0x04 | 2 | uint16 | AnimMapStart | Начало блока в 0x13 или 0xFFFF; старое имя: OffsetIntoFile13 |
| 0x04 | 2 | int16 | OffsetIntoFile13 | Смещение в короткие анимации | | 0x06 | 2 | uint16 | FallbackKey | Индекс fallback-ключа в 0x08; старое имя: IndexInFile08 |
| 0x06 | 2 | int16 | IndexInFile08 | Индекс в анимации | | 0x08 | 30 | ushort[15] | SlotIndex | Индексы slot в 0x02 по формуле `lod * 5 + group`; старое имя: Lod |
| 0x08 | 30 | ushort[15] | Lod | Индексы сабмешей по LOD-уровням (0xFFFF = не используется) |
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511). **Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
--- ---
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт) ### Компонент 0x02 - Header + slots (Заголовок: 0x8C = 140 байт, slot: 0x44 = 68 байт)
#### Заголовок (140 байт) #### Заголовок (140 байт)
@@ -118,15 +117,15 @@ MSH файлы — это NRes архивы, содержащие несколь
| Смещение | Размер | Тип | Поле | Описание | | Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------| |:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 | | 0x00 | 2 | ushort | TriStart | Начальный индекс в Компоненте 07; в landscape-tooling может указывать в 15 |
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников | | 0x02 | 2 | ushort | TriCount | Количество записей в Компоненте 07; в landscape-tooling может быть count для 15 |
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) | | 0x04 | 2 | ushort | BatchStart | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) | | 0x06 | 2 | ushort | BatchCount | Количество батчей в Компоненте 0D (только Модель) |
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса | | 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса | | 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
| 0x20 | 12 | Vector3 | Center | Центр сабмеша | | 0x20 | 12 | Vector3 | Center | Центр сабмеша |
| 0x2C | 12 | Vector3 | Vector4 | Неизвестно | | 0x2C | 4 | float | SphereRadius | Радиус bounding sphere; старый `Vector4` был overlay-гипотезой |
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно | | 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 | Флаги рендера | | 0x00 | 2 | ushort | TriFlags | Флаги треугольника; старое имя: Flags |
| 0x02 | 2 | ushort | Magic02 | Неизвестно | | 0x02 | 2 | ushort | Link0 | Связь/opaque поле 0; старое имя: Magic02 |
| 0x04 | 2 | ushort | Magic04 | Неизвестно | | 0x04 | 2 | ushort | Link1 | Связь/opaque поле 1; старое имя: Magic04 |
| 0x06 | 2 | ushort | Magic06 | Неизвестно | | 0x06 | 2 | ushort | Link2 | Связь/opaque поле 2; старое имя: Magic06 |
| 0x08 | 2 | int16 | OffsetX | Нормализованный X (÷32767 для -1..1) | | 0x08 | 2 | int16 | NormalX | Упакованная X-компонента нормали; старое имя: OffsetX |
| 0x0A | 2 | int16 | OffsetY | Нормализованный Y (÷32767 для -1..1) | | 0x0A | 2 | int16 | NormalY | Упакованная Y-компонента нормали; старое имя: OffsetY |
| 0x0C | 2 | int16 | OffsetZ | Нормализованный Z (÷32767 для -1..1) | | 0x0C | 2 | int16 | NormalZ | Упакованная Z-компонента нормали; старое имя: OffsetZ |
| 0x0E | 2 | ushort | Magic14 | Неизвестно | | 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 | Флаги батча | | 0x00 | 2 | ushort | BatchFlags / Flags.low | Флаги батча |
| 0x02 | 2 | - | Padding | - | | 0x02 | 2 | ushort | MaterialIndex / Flags.high | Индекс material slot |
| 0x04 | 1 | byte | Magic04 | Неизвестно | | 0x04 | 2 | ushort | Opaque4 | Opaque, старое имя `TriangleCount` не подтверждено |
| 0x05 | 1 | byte | Magic05 | Неизвестно | | 0x06 | 2 | ushort | Opaque6 | Opaque |
| 0x06 | 2 | ushort | Magic06 | Неизвестно | | 0x08 | 2 | ushort | IndexCount | Количество индексов для отрисовки в 0x06 |
| 0x08 | 2 | ushort | CountOf06 | Количество индексов для отрисовки | | 0x0A | 4 | uint32 | IndexStart | Начальный индекс в Компоненте 06 |
| 0x0A | 4 | int32 | IndexInto06 | Начальный индекс в Компоненте 06 | | 0x0E | 2 | ushort | Opaque14 | Opaque, старое имя `CountOf03` не подтверждено |
| 0x0E | 2 | ushort | CountOf03 | Количество вершин | | 0x10 | 4 | uint32 | BaseVertex | Базовое смещение вершины в Компоненте 03 |
| 0x10 | 4 | int32 | IndexInto03 | Базовое смещение вершины в Компоненте 03 |
--- ---
### Компонент 15 - Triangles (0x1C = 28 байт) ### Компонент 0x15 - TerrainTriangle table (0x1C = 28 байт)
Прямые определения треугольников. Используется и Моделью и Ландшафтом, Прямые определения terrain-треугольников. Это hex-компонент 0x15 проекта, не FParkan Res15 decimal.
но только Ландшафт использует их напрямую для рендеринга.
| Смещение | Размер | Тип | Поле | Описание | | Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------| |:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) | | 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) | | 0x04 | 4 | uint32 | MaterialData | Данные материала; старое имя: Magic04 |
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины | | 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины | | 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины | | 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
| 0x0E | 4 | uint32 | Magic0E | Неизвестно | | 0x0E | 4 | uint32 | Opaque0E | Opaque; старое имя: Magic0E |
| 0x12 | 4 | uint32 | Magic12 | Неизвестно | | 0x12 | 4 | uint32 | Opaque12 | Opaque; старое имя: Magic12 |
| 0x16 | 4 | uint32 | Magic16 | Неизвестно | | 0x16 | 4 | uint32 | Opaque16 | Opaque; старое имя: Magic16 |
| 0x1A | 2 | ushort | Magic1A | Неизвестно | | 0x1A | 2 | ushort | Opaque1A | Opaque; старое имя: Magic1A |
#### MaterialData (0x04) - Структура материала #### MaterialData (0x04) - Структура материала
@@ -298,7 +295,7 @@ var type = MshConverter.DetectMeshType(archive);
| Файл | Используется для | Треугольники в Comp15 | | Файл | Используется для | Треугольники в Comp15 |
|------|------------------|----------------------| |------|------------------|----------------------|
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) | | `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма TriCount для LOD0) |
| `Land2.wea` | LOD1 (низкая детализация) | Остальные | | `Land2.wea` | LOD1 (низкая детализация) | Остальные |
### Пример (SC_1) ### Пример (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; namespace ParkanPlayground;
public class Msh03 /// <summary>
/// MSH-компонент 0x03: позиции вершин
/// </summary>
public class Msh0x03
{ {
public static List<Vector3> ReadComponent(FileStream mshFs, NResArchive mshNres) 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"); 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.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(verticesFile, 0, verticesFile.Length); 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; namespace ParkanPlayground;
public static class Msh06 /// <summary>
/// MSH-компонент 0x06: индексный буфер
/// </summary>
public static class Msh0x06
{ {
public static List<ushort> ReadComponent( public static List<ushort> ReadComponent(
FileStream mshFs, NResArchive archive) FileStream mshFs, NResArchive archive)
@@ -15,12 +18,22 @@ public static class Msh06
throw new Exception("Archive doesn't contain file (06)"); 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.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length); mshFs.ReadExactly(data, 0, data.Length);
var elements = new List<ushort>((int)entry.ElementCount); var elements = new List<ushort>(entry.FileLength / entry.ElementSize);
for (var i = 0; i < entry.ElementCount; i++) for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
{ {
elements.Add( elements.Add(
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2)) 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; 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) public static List<string> ReadComponent(FileStream mshFs, NResArchive archive)
{ {
@@ -23,19 +27,32 @@ public class Msh0A
var strings = new List<string>(); var strings = new List<string>();
while (pos < data.Length) 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)); 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) if (len == 0)
{ {
pos += 4; // empty entry, no string attached pos += 4;
strings.Add(""); // add empty string strings.Add("");
} }
else else
{ {
// len is not 0, we need to read it
var strBytes = data.AsSpan(pos + 4, len); var strBytes = data.AsSpan(pos + 4, len);
var str = Encoding.UTF8.GetString(strBytes); var str = Encoding.ASCII.GetString(strBytes);
strings.Add(str); 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 public class MshConverter
{ {
/// <summary> /// <summary>Определяет тип MSH по набору hex-компонентов архива.</summary>
/// Detects mesh type based on which components are present in the archive.
/// </summary>
public static MshType DetectMeshType(NResArchive archive) public static MshType DetectMeshType(NResArchive archive)
{ {
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00"); bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
@@ -33,13 +31,12 @@ public class MshConverter
return MshType.Unknown; return MshType.Unknown;
} }
/// <summary> /// <summary>Конвертирует .msh в OBJ с автоопределением типа меша.</summary>
/// Converts a .msh file to OBJ format, auto-detecting mesh type. /// <param name="mshPath">Путь к .msh файлу.</param>
/// </summary> /// <param name="outputPath">Путь к OBJ, по умолчанию рядом с исходным файлом.</param>
/// <param name="mshPath">Path to the .msh file</param> /// <param name="lodLevel">LOD для экспорта.</param>
/// <param name="outputPath">Output OBJ path (optional, defaults to input name + .obj)</param> /// <param name="group">Группа slot внутри LOD. slotIndex[lod * 5 + group].</param>
/// <param name="lodLevel">LOD level to export (0 = highest detail)</param> public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0, int group = 0)
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0)
{ {
var mshNresResult = NResParser.ReadFile(mshPath); var mshNresResult = NResParser.ReadFile(mshPath);
if (mshNresResult.Archive is null) if (mshNresResult.Archive is null)
@@ -61,10 +58,10 @@ public class MshConverter
switch (meshType) switch (meshType)
{ {
case MshType.Model: case MshType.Model:
ConvertModel(fs, archive, outputPath, lodLevel); ConvertModel(fs, archive, outputPath, lodLevel, group);
break; break;
case MshType.Landscape: case MshType.Landscape:
ConvertLandscape(fs, archive, outputPath, lodLevel); ConvertLandscape(fs, archive, outputPath, lodLevel, group);
break; break;
default: default:
Console.WriteLine("ERROR: Unknown mesh type, cannot convert."); Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
@@ -73,17 +70,17 @@ public class MshConverter
} }
/// <summary> /// <summary>
/// Converts a model mesh (robots, buildings, etc.) to OBJ. /// Конвертирует обычную модель в OBJ.
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03 /// Путь данных: 0x01 -> 0x02 -> 0x0D -> 0x06 -> 0x03.
/// </summary> /// </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 component01 = Msh0x01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive); var component02 = Msh0x02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive); var component03 = Msh0x03.ReadComponent(fs, archive);
var component06 = Msh06.ReadComponent(fs, archive); var component06 = Msh0x06.ReadComponent(fs, archive);
var component07 = Msh07.ReadComponent(fs, archive); var component07 = Msh0x07.ReadComponent(fs, archive);
var component0D = Msh0D.ReadComponent(fs, archive); var component0D = Msh0x0D.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}"); Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Pieces: {component01.Elements.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($"# Model mesh converted from {Path.GetFileName(outputPath)}");
sw.WriteLine($"# LOD level: {lodLevel}"); sw.WriteLine($"# LOD level: {lodLevel}");
// Write all vertices
foreach (var v in component03) 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; int exportedFaces = 0;
@@ -103,32 +101,49 @@ public class MshConverter
{ {
var piece = component01.Elements[pieceIndex]; var piece = component01.Elements[pieceIndex];
// Get submesh index for requested LOD var submeshIdx = piece.ResolveSlotIndex(lodLevel, group);
if (lodLevel >= piece.Lod.Length)
continue;
var submeshIdx = piece.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count) if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue; continue;
sw.WriteLine($"g piece_{pieceIndex}"); sw.WriteLine($"g piece_{pieceIndex}");
var submesh = component02.Elements[submeshIdx]; var submesh = component02.Elements[submeshIdx];
var batchStart = submesh.StartOffsetIn0d; var batchStart = submesh.BatchStart;
var batchCount = submesh.ByteLengthIn0D; 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++) for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
{ {
var batch = component0D[batchStart + batchIdx]; var batch = component0D[batchStart + batchIdx];
var baseVertex = batch.IndexInto03; var baseVertex = (int)batch.BaseVertex;
var indexStart = batch.IndexInto06; var indexStart = (int)batch.IndexStart;
var indexCount = batch.CountOf06; 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) 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 i1 = baseVertex + component06[indexStart + i];
var i2 = baseVertex + component06[indexStart + i + 1]; var i2 = baseVertex + component06[indexStart + i + 1];
var i3 = baseVertex + component06[indexStart + i + 2]; 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}"); sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
exportedFaces++; exportedFaces++;
@@ -141,15 +156,15 @@ public class MshConverter
} }
/// <summary> /// <summary>
/// Converts a landscape mesh (terrain) to OBJ. /// Конвертирует terrain mesh в OBJ.
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07) /// Путь данных является terrain-гипотезой проекта: 0x01 -> 0x02 -> 0x15.
/// </summary> /// </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 component01 = Msh0x01.ReadComponent(fs, archive);
var component02 = Msh02.ReadComponent(fs, archive); var component02 = Msh0x02.ReadComponent(fs, archive);
var component03 = Msh03.ReadComponent(fs, archive); var component03 = Msh0x03.ReadComponent(fs, archive);
var component15 = Msh15.ReadComponent(fs, archive); var component15 = Msh0x15.ReadComponent(fs, archive);
Console.WriteLine($"Vertices: {component03.Count}"); Console.WriteLine($"Vertices: {component03.Count}");
Console.WriteLine($"Triangles: {component15.Count}"); Console.WriteLine($"Triangles: {component15.Count}");
@@ -161,9 +176,10 @@ public class MshConverter
sw.WriteLine($"# LOD level: {lodLevel}"); sw.WriteLine($"# LOD level: {lodLevel}");
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}"); 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) 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; int exportedFaces = 0;
@@ -171,11 +187,7 @@ public class MshConverter
{ {
var tile = component01.Elements[tileIdx]; var tile = component01.Elements[tileIdx];
// Get submesh index for requested LOD var submeshIdx = tile.ResolveSlotIndex(lodLevel, group);
if (lodLevel >= tile.Lod.Length)
continue;
var submeshIdx = tile.Lod[lodLevel];
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count) if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
continue; continue;
@@ -183,9 +195,8 @@ public class MshConverter
var submesh = component02.Elements[submeshIdx]; var submesh = component02.Elements[submeshIdx];
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count var triangleStart = submesh.TriStart;
var triangleStart = submesh.StartIndexIn07; var triangleCount = submesh.TriCount;
var triangleCount = submesh.CountIn07;
for (var triOffset = 0; triOffset < triangleCount; triOffset++) for (var triOffset = 0; triOffset < triangleCount; triOffset++)
{ {
@@ -197,6 +208,12 @@ public class MshConverter
} }
var tri = component15[triIdx]; 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}"); sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
exportedFaces++; exportedFaces++;
} }
@@ -215,21 +232,21 @@ public class MshConverter
using (StreamWriter writer = new StreamWriter(filePath)) using (StreamWriter writer = new StreamWriter(filePath))
{ {
// Write vertices // Запись вершин.
foreach (var p in points) foreach (var p in points)
{ {
writer.WriteLine($"v {p.X} {p.Y} {p.Z}"); 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[][] int[][] faces = new int[][]
{ {
new int[] { 1, 2, 3, 4 }, // bottom new int[] { 1, 2, 3, 4 }, // низ
new int[] { 5, 6, 7, 8 }, // top new int[] { 5, 6, 7, 8 }, // верх
new int[] { 1, 2, 6, 5 }, // front new int[] { 1, 2, 6, 5 }, // перед
new int[] { 2, 3, 7, 6 }, // right new int[] { 2, 3, 7, 6 }, // право
new int[] { 3, 4, 8, 7 }, // back new int[] { 3, 4, 8, 7 }, // зад
new int[] { 4, 1, 5, 8 } // left new int[] { 4, 1, 5, 8 } // лево
}; };
foreach (var f in faces) foreach (var f in faces)
@@ -248,7 +265,7 @@ public class MshConverter
foreach (var c in centers) foreach (var c in centers)
{ {
// Generate 8 vertices for this cube // Генерация восьми вершин куба.
Vector3[] vertices = new Vector3[] Vector3[] vertices = new Vector3[]
{ {
new Vector3(c.X - half, c.Y - half, c.Z - half), new Vector3(c.X - half, c.Y - half, c.Z - half),
@@ -262,24 +279,24 @@ public class MshConverter
new Vector3(c.X - half, c.Y + half, c.Z + half) new Vector3(c.X - half, c.Y + half, c.Z + half)
}; };
// Write vertices // Запись вершин.
foreach (var v in vertices) foreach (var v in vertices)
{ {
writer.WriteLine($"v {v.X} {v.Y} {v.Z}"); writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
} }
// Define faces (1-based indices, counter-clockwise) // Описание граней: индексация с 1, порядок против часовой стрелки.
int[][] faces = new int[][] int[][] faces = new int[][]
{ {
new int[] { 1, 2, 3, 4 }, // bottom new int[] { 1, 2, 3, 4 }, // низ
new int[] { 5, 6, 7, 8 }, // top new int[] { 5, 6, 7, 8 }, // верх
new int[] { 1, 2, 6, 5 }, // front new int[] { 1, 2, 6, 5 }, // перед
new int[] { 2, 3, 7, 6 }, // right new int[] { 2, 3, 7, 6 }, // право
new int[] { 3, 4, 8, 7 }, // back new int[] { 3, 4, 8, 7 }, // зад
new int[] { 4, 1, 5, 8 } // left new int[] { 4, 1, 5, 8 } // лево
}; };
// Write faces with offset // Запись граней со смещением индексов.
foreach (var f in faces) foreach (var f in faces)
{ {
writer.WriteLine( writer.WriteLine(
@@ -297,16 +314,16 @@ public class MshConverter
{ {
writer.WriteLine("# Exported OBJ file"); writer.WriteLine("# Exported OBJ file");
// Write vertices // Запись вершин.
foreach (var v in vertices) foreach (var v in vertices)
{ {
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}"); 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) foreach (var e in edges)
{ {
// OBJ uses 1-based indexing // OBJ использует индексацию с 1.
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 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) foreach (var targetId in targetIds)
{ {
// Material files are stored with their ID in the archive // 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) if (matEntry == null)
{ {
@@ -52,7 +52,8 @@ foreach (var targetId in targetIds)
} }
Console.WriteLine($"=== Material {targetId} ==="); 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($" Name: {matEntry.FileName}");
Console.WriteLine($" ElementCount (Version): {matEntry.ElementCount}"); Console.WriteLine($" ElementCount (Version): {matEntry.ElementCount}");
Console.WriteLine($" ElementSize (Magic1): {matEntry.ElementSize}"); Console.WriteLine($" ElementSize (Magic1): {matEntry.ElementSize}");
@@ -126,7 +127,7 @@ foreach (var name in weaMatNames)
if (found != null) 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 else
{ {
+3 -3
View File
@@ -223,7 +223,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
- Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD - Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD
``` ```
нулевому элементу добавляется флаг 0x1000000 нулевому элементу добавляется флаг 0x1000000
Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08) Содержит 2 ссылки на анимационные subentry: AnimMapStart указывает в файл 0x13, FallbackKey указывает в файл 0x08.
Если интерполируется анимация -0.5s короче чем magic1 у файла 13 Если интерполируется анимация -0.5s короче чем magic1 у файла 13
И у файла есть OffsetIntoFile13 И у файла есть OffsetIntoFile13
И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда) И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда)
@@ -260,7 +260,7 @@ IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4)
Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time) Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time)
``` ```
- Тип 12 - microtexture mapping - Тип 12 - microtexture mapping
- Тип 13 - короткие меш-анимации (почему я это не дописал?) - Тип 13 - animation map / карта кадров для выбора ключей из файла 08
``` ```
Буквально (hex) Буквально (hex)
00 01 01 02 ... 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; namespace ScrLib;
public class ScrFile /// <summary>SCR файл.</summary>
{ /// <param name="Magic">Число известных игре скриптов; обычно 59 (0x3B).</param>
/// <summary> /// <param name="EntryCount">Количество ScrEntry.</param>
/// тут всегда число 59 (0x3b) - это число известных игре скриптов /// <param name="Entries">Записи SCR.</param>
/// </summary> public record ScrFile(int Magic, int EntryCount, List<ScrEntry> Entries);
public int Magic { get; set; }
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; } /// <summary>Вложенная запись SCR.</summary>
} /// <param name="ScriptIndex">Номер скрипта в игре из таблицы известных скриптов (обычно 0x3B записей).</param>
/// <param name="UnkInner2">Неизвестное поле. Для SetVarsetValue это индекс в Varset.</param>
public class ScrEntry /// <param name="UnkInner3">Неизвестное поле. Для SetVarsetValue это устанавливаемое значение.</param>
{ /// <param name="Type">Тип вложенной записи.</param>
public string Title { get; set; } /// <param name="UnkInner5">Неизвестное поле.</param>
/// <param name="ArgumentsCount">Количество аргументов.</param>
public int Index { get; set; } /// <param name="Arguments">Аргументы вложенной записи.</param>
/// <param name="UnkInner7">Неизвестное поле.</param>
public int InnerCount { get; set; } public record ScrEntryInner(
int ScriptIndex,
public List<ScrEntryInner> Inners { get; set; } int UnkInner2,
} int UnkInner3,
ScrEntryInnerType Type,
public class ScrEntryInner int UnkInner5,
{ int ArgumentsCount,
/// <summary> List<int> Arguments,
/// Номер скрипта в игре (это тех, которых 0x3b) int UnkInner7);
/// </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; }
}
public enum ScrEntryInnerType public enum ScrEntryInnerType
{ {
+30 -30
View File
@@ -13,50 +13,50 @@ public class ScrParser
public static ScrFile ReadFile(Stream fs) 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(); for (var i = 0; i < entryCount; i++)
scrFile.EntryCount = fs.ReadInt32LittleEndian();
scrFile.Entries = [];
for (var i = 0; i < scrFile.EntryCount; i++)
{ {
var entry = new ScrEntry(); var title = fs.ReadLengthPrefixedString();
entry.Title = fs.ReadLengthPrefixedString();
// тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char* // тут игра дополнительно вычитывает ещё 1 байт, видимо как \0 для char*
fs.ReadByte(); fs.ReadByte();
entry.Index = fs.ReadInt32LittleEndian(); var index = fs.ReadInt32LittleEndian();
entry.InnerCount = fs.ReadInt32LittleEndian(); var innerCount = fs.ReadInt32LittleEndian();
entry.Inners = []; List<ScrEntryInner> inners = [];
for (var i1 = 0; i1 < entry.InnerCount; i1++) for (var i1 = 0; i1 < innerCount; i1++)
{ {
var entryInner = new ScrEntryInner(); var scriptIndex = fs.ReadInt32LittleEndian();
entryInner.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(); for (var i2 = 0; i2 < argumentsCount; i2++)
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++)
{ {
entryInner.Arguments.Add(fs.ReadInt32LittleEndian()); arguments.Add(fs.ReadInt32LittleEndian());
} }
entryInner.UnkInner7 = fs.ReadInt32LittleEndian(); var unkInner7 = fs.ReadInt32LittleEndian();
entry.Inners.Add(entryInner); 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 return format switch
{ {
0x22B8 => 32, 0x0 => 8,
0x115C => 16,
0x235 => 16, 0x235 => 16,
0x22C => 16,
0x115C => 16,
0x58 => 16,
0x378 => 32, 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> /// <param name="Items">Элементы</param>
public record PageHeader(string Page, int Count, List<PageItem> Items); 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 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> public Task WriteToFolder(string folder)
/// Исходное имя файла текстуры 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)
{ {
if (Directory.Exists(folder)) if (!Directory.Exists(folder))
{ {
Directory.CreateDirectory(folder); Directory.CreateDirectory(folder);
} }
@@ -84,8 +71,8 @@ public class TexmFile
{ {
for (var i = 0; i < Header.MipmapCount; i++) for (var i = 0; i < Header.MipmapCount; i++)
{ {
var mipWidth = Header.Width / (int) Math.Pow(2, i); var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Header.Height / (int) Math.Pow(2, i); var mipHeight = Math.Max(1, Header.Height >> i);
var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors); var reinterpretedPixels = ReinterpretIndexedMipmap(MipmapBytes[i], LookupColors);
var image = Image.LoadPixelData<Rgba32>(reinterpretedPixels, mipWidth, mipHeight); 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"); 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++) for (var i = 0; i < Header.MipmapCount; i++)
{ {
var mipWidth = Header.Width / (int) Math.Pow(2, i); var mipWidth = Math.Max(1, Header.Width >> i);
var mipHeight = Header.Height / (int) Math.Pow(2, i); var mipHeight = Math.Max(1, Header.Height >> i);
var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32( var reinterpretedPixels = ReinterpretMipmapBytesAsRgba32(
MipmapBytes[i], MipmapBytes[i],
@@ -112,14 +99,16 @@ public class TexmFile
image.SaveAsPng(Path.Combine(outputDir, Path.GetFileName(FileName)) + $"_{Header.Format}_{mipWidth}x{mipHeight}.png"); 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) public byte[] GetRgba32BytesFromMipmap(int index, out int mipWidth, out int mipHeight)
{ {
var mipmapBytes = MipmapBytes[index]; var mipmapBytes = MipmapBytes[index];
mipWidth = Header.Width / (int) Math.Pow(2, index); mipWidth = Math.Max(1, Header.Width >> index);
mipHeight = Header.Height / (int) Math.Pow(2, index); mipHeight = Math.Max(1, Header.Height >> index);
if (IsIndexed) if (IsIndexed)
{ {
@@ -151,7 +140,7 @@ public class TexmFile
result[i * 4 + 0] = r; result[i * 4 + 0] = r;
result[i * 4 + 1] = g; result[i * 4 + 1] = g;
result[i * 4 + 2] = b; result[i * 4 + 2] = b;
result[i * 4 + 3] = 255; result[i * 4 + 3] = a;
} }
return result; return result;
@@ -165,6 +154,8 @@ public class TexmFile
888 => ReinterpretAs888(bytes, mipWidth, mipHeight), 888 => ReinterpretAs888(bytes, mipWidth, mipHeight),
4444 => ReinterpretAs4444(bytes, mipWidth, mipHeight), 4444 => ReinterpretAs4444(bytes, mipWidth, mipHeight),
565 => ReinterpretAs565(bytes, mipWidth, mipHeight), 565 => ReinterpretAs565(bytes, mipWidth, mipHeight),
556 => ReinterpretAs556(bytes, mipWidth, mipHeight),
88 => ReinterpretAs88(bytes, mipWidth, mipHeight),
_ => throw new InvalidOperationException($"Invalid format {format}") _ => throw new InvalidOperationException($"Invalid format {format}")
}; };
@@ -178,22 +169,38 @@ public class TexmFile
var result = new byte[bytes.Length * 2]; var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 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 var r = (byte)(((rawPixel >> 11) & 0b11111) * 255 / 31);
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]); var g = (byte)(((rawPixel >> 5) & 0b111111) * 255 / 63);
var b = (byte)((rawPixel & 0b11111) * 255 / 31);
var r = (byte)(((rawPixel[0] >> 3) & 0b11111) * 255 / 31); result[i / 2 * 4 + 0] = r;
var g = (byte)((((rawPixel[0] & 0b111) << 3) | ((rawPixel[1] >> 5) & 0b111)) * 255 / 63); result[i / 2 * 4 + 1] = g;
var b = (byte)((rawPixel[1] & 0b11111) * 255 / 31); result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 0] = (byte)(0xff - r);
result[i / 2 * 4 + 1] = (byte)(0xff - g);
result[i / 2 * 4 + 2] = (byte)(0xff - b);
result[i / 2 * 4 + 3] = 0xff; result[i / 2 * 4 + 3] = 0xff;
}
// swap endianess back return result;
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]); }
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; return result;
@@ -206,23 +213,36 @@ public class TexmFile
var result = new byte[bytes.Length * 2]; var result = new byte[bytes.Length * 2];
for (var i = 0; i < span.Length; i += 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 var a = (byte)(((rawPixel >> 12) & 0b1111) * 17);
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]); var r = (byte)(((rawPixel >> 8) & 0b1111) * 17);
var g = (byte)(((rawPixel >> 4) & 0b1111) * 17);
var a = (byte)(((rawPixel[0] >> 4) & 0b1111) * 17); var b = (byte)((rawPixel & 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);
result[i / 2 * 4 + 0] = r; result[i / 2 * 4 + 0] = r;
result[i / 2 * 4 + 1] = g; result[i / 2 * 4 + 1] = g;
result[i / 2 * 4 + 2] = b; result[i / 2 * 4 + 2] = b;
result[i / 2 * 4 + 3] = a; result[i / 2 * 4 + 3] = a;
}
// swap endianess back return result;
(rawPixel[0], rawPixel[1]) = (rawPixel[1], rawPixel[0]); }
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; return result;
@@ -237,14 +257,9 @@ public class TexmFile
{ {
var rawPixel = span.Slice(i, 4); var rawPixel = span.Slice(i, 4);
var r = rawPixel[0]; result[i + 0] = rawPixel[0];
var g = rawPixel[1]; result[i + 1] = rawPixel[1];
var b = rawPixel[2]; result[i + 2] = rawPixel[2];
var w = rawPixel[3];
result[i + 0] = r;
result[i + 1] = g;
result[i + 2] = b;
result[i + 3] = 255; result[i + 3] = 255;
} }
@@ -270,7 +285,7 @@ public class TexmFile
result[i + 0] = r; result[i + 0] = r;
result[i + 1] = g; result[i + 1] = g;
result[i + 2] = b; result[i + 2] = b;
result[i + 3] = a; result[i + 3] = 255;
// swap endianess back // swap endianess back
// (rawPixel[0], rawPixel[1], rawPixel[2], rawPixel[3]) = (rawPixel[3], rawPixel[2], rawPixel[1], rawPixel[0]); // (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 widthBytes = headerBytes[4..8];
var heightBytes = headerBytes[8..12]; var heightBytes = headerBytes[8..12];
var mipmapCountBytes = headerBytes[12..16]; var mipmapCountBytes = headerBytes[12..16];
var strideBytes = headerBytes[16..20]; var flags4Bytes = headerBytes[16..20];
var magic1Bytes = headerBytes[20..24]; var flags5Bytes = headerBytes[20..24];
var formatOptionFlagsBytes = headerBytes[24..28]; var unk6Bytes = headerBytes[24..28];
var formatBytes = headerBytes[28..32]; var formatBytes = headerBytes[28..32];
var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0'); var texmAscii = Encoding.ASCII.GetString(texmHeader).Trim('\0');
var width = BinaryPrimitives.ReadInt32LittleEndian(widthBytes); var width = BinaryPrimitives.ReadInt32LittleEndian(widthBytes);
var height = BinaryPrimitives.ReadInt32LittleEndian(heightBytes); var height = BinaryPrimitives.ReadInt32LittleEndian(heightBytes);
var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes); var mipmapCount = BinaryPrimitives.ReadInt32LittleEndian(mipmapCountBytes);
var stride = BinaryPrimitives.ReadInt32LittleEndian(strideBytes); var flags4 = BinaryPrimitives.ReadInt32LittleEndian(flags4Bytes);
var magic1 = BinaryPrimitives.ReadInt32LittleEndian(magic1Bytes); var flags5 = BinaryPrimitives.ReadInt32LittleEndian(flags5Bytes);
var formatOptionFlags = BinaryPrimitives.ReadInt32LittleEndian(formatOptionFlagsBytes); var unk6 = BinaryPrimitives.ReadInt32LittleEndian(unk6Bytes);
var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes); var format = BinaryPrimitives.ReadInt32LittleEndian(formatBytes);
if (texmAscii != "Texm") if (texmAscii != "Texm")
@@ -39,64 +39,56 @@ public class TexmParser
return new TexmParseResult(null, "Файл не начинается с Texm"); return new TexmParseResult(null, "Файл не начинается с Texm");
} }
var textureFile = new TexmFile()
{
FileName = file
};
var header = new TexmHeader( var header = new TexmHeader(
texmAscii, texmAscii,
width, width,
height, height,
mipmapCount, mipmapCount,
stride, flags4,
magic1, flags5,
formatOptionFlags, unk6,
format format
); );
textureFile.Header = header; List<byte[]> mipmapBytesList;
var isIndexed = false;
byte[] lookupColors = [];
if (format == 0) if (format == 0)
{ {
// если формат 0, то текстура использует lookup таблицу в первых 1024 байтах (256 разных цветов в формате ARGB 888) // если формат 0, то текстура использует lookup таблицу в первых 1024 байтах (256 разных цветов в формате ARGB 888)
var lookupColors = new byte[1024]; lookupColors = new byte[1024];
stream.ReadExactly(lookupColors, 0, lookupColors.Length); stream.ReadExactly(lookupColors, 0, lookupColors.Length);
textureFile.LookupColors = lookupColors; mipmapBytesList = ReadMipmapsAsIndexes(
var mipmapBytesList = ReadMipmapsAsIndexes(
stream, stream,
mipmapCount, mipmapCount,
width, width,
height height
); );
textureFile.MipmapBytes = mipmapBytesList; isIndexed = true;
textureFile.IsIndexed = true;
} }
else else
{ {
var mipmapBytesList = ReadMipmaps( mipmapBytesList = ReadMipmaps(
stream, stream,
format.AsStride(), format.AsStride(),
mipmapCount, mipmapCount,
width, width,
height height
); );
textureFile.MipmapBytes = mipmapBytesList;
} }
PageHeader? pages = null;
if (stream.Position < stream.Length) if (stream.Position < stream.Length)
{ {
// has PAGE data // has PAGE data
var pageHeader = ReadPage(stream); pages = ReadPage(stream);
textureFile.Pages = pageHeader;
} }
var textureFile = new TexmFile(file, header, mipmapBytesList, pages, isIndexed, lookupColors);
return new TexmParseResult(textureFile); 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) private static List<byte[]> ReadMipmaps(Stream stream, int stride, int mipmapCount, int topWidth, int topHeight)
{ {
if (stride == 0)
{
stride = 16;
}
List<int> mipmapByteLengths = []; List<int> mipmapByteLengths = [];
for (int i = 0; i < mipmapCount; i++) for (int i = 0; i < mipmapCount; i++)
{ {
var mipWidth = topWidth / (int) Math.Pow(2, i); var mipWidth = Math.Max(1, topWidth >> i);
var mipHeight = topHeight / (int) Math.Pow(2, i); var mipHeight = Math.Max(1, topHeight >> i);
var imageByteLength = mipWidth * mipHeight * (stride / 8); var imageByteLength = mipWidth * mipHeight * (stride / 8);
mipmapByteLengths.Add(imageByteLength); mipmapByteLengths.Add(imageByteLength);
@@ -178,8 +165,8 @@ public class TexmParser
for (int i = 0; i < mipmapCount; i++) for (int i = 0; i < mipmapCount; i++)
{ {
var mipWidth = topWidth / (int) Math.Pow(2, i); var mipWidth = Math.Max(1, topWidth >> i);
var mipHeight = topHeight / (int) Math.Pow(2, i); var mipHeight = Math.Max(1, topHeight >> i);
var imageByteLength = mipWidth * mipHeight; var imageByteLength = mipWidth * mipHeight;
mipmapByteLengths.Add(imageByteLength); mipmapByteLengths.Add(imageByteLength);