mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
fpopov thanks
This commit is contained in:
@@ -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 IntFloatValue(Span<byte> span)
|
||||||
|
: this(MemoryMarshal.Read<int>(span), MemoryMarshal.Read<float>(span))
|
||||||
{
|
{
|
||||||
public int AsInt { get; set; } = MemoryMarshal.Read<int>(span);
|
}
|
||||||
public float AsFloat { get; set; } = MemoryMarshal.Read<float>(span);
|
|
||||||
}
|
}
|
||||||
+107
-113
@@ -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) & 0xF. 0 = standard, 1 = special, 2 = particle.</param>
|
||||||
public int Magic1 { get; set; } // From NRes Magic1
|
/// <param name="SupportsBumpMapping">Производное от Version: (Version & 2) != 0.</param>
|
||||||
|
/// <param name="IsParticleEffect">Производное от Version: Version & 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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
;
|
||||||
@@ -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))
|
||||||
@@ -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++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user