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