diff --git a/ParkanPlayground/Effects/FxidReader.cs b/ParkanPlayground/Effects/FxidReader.cs
new file mode 100644
index 0000000..a3c0977
--- /dev/null
+++ b/ParkanPlayground/Effects/FxidReader.cs
@@ -0,0 +1,255 @@
+using Common;
+
+namespace ParkanPlayground.Effects;
+
+///
+/// Static reader methods for parsing FXID effect definition structures from binary streams.
+///
+public static class FxidReader
+{
+ ///
+ /// Reads a Vector3 (3 floats: X, Y, Z) from the binary stream.
+ ///
+ public static Vector3 ReadVector3(BinaryReader br)
+ {
+ float x = br.ReadSingle();
+ float y = br.ReadSingle();
+ float z = br.ReadSingle();
+ return new Vector3(x, y, z);
+ }
+
+ ///
+ /// Reads the 60-byte effect header from the binary stream.
+ ///
+ public static EffectHeader ReadEffectHeader(BinaryReader br)
+ {
+ EffectHeader h;
+ h.ComponentCount = br.ReadUInt32();
+ h.Unknown1 = br.ReadUInt32();
+ h.Duration = br.ReadSingle();
+ h.Unknown2 = br.ReadSingle();
+ h.Flags = br.ReadUInt32();
+ h.Unknown3 = br.ReadUInt32();
+ h.Reserved = br.ReadBytes(24);
+ h.ScaleX = br.ReadSingle();
+ h.ScaleY = br.ReadSingle();
+ h.ScaleZ = br.ReadSingle();
+ return h;
+ }
+
+ ///
+ /// Reads a BillboardComponentData (type 1) from the binary stream.
+ ///
+ public static BillboardComponentData ReadBillboardComponent(BinaryReader br, uint typeAndFlags)
+ {
+ BillboardComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.Unknown04 = br.ReadSingle();
+ d.ScalarAMin = br.ReadSingle();
+ d.ScalarAMax = br.ReadSingle();
+ d.ScalarAExp = br.ReadSingle();
+ d.ActiveTimeStart = br.ReadSingle();
+ d.ActiveTimeEnd = br.ReadSingle();
+ d.SampleSpreadParam = br.ReadSingle();
+ d.Unknown20 = br.ReadUInt32();
+ d.PrimarySampleCount = br.ReadUInt32();
+ d.SecondarySampleCount = br.ReadUInt32();
+ d.ExtentVec0 = ReadVector3(br);
+ d.ExtentVec1 = ReadVector3(br);
+ d.ExtentVec2 = ReadVector3(br);
+ d.ExponentTriplet0 = ReadVector3(br);
+ d.RadiusTriplet0 = ReadVector3(br);
+ d.RadiusTriplet1 = ReadVector3(br);
+ d.RadiusTriplet2 = ReadVector3(br);
+ d.ExponentTriplet1 = ReadVector3(br);
+ d.NoiseAmplitude = br.ReadSingle();
+ d.Reserved = br.ReadBytes(0x50);
+ return d;
+ }
+
+ ///
+ /// Reads a SoundComponentData (type 2) from the binary stream.
+ ///
+ public static SoundComponentData ReadSoundComponent(BinaryReader br, uint typeAndFlags)
+ {
+ SoundComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.PlayMode = br.ReadUInt32();
+ d.StartTime = br.ReadSingle();
+ d.EndTime = br.ReadSingle();
+ d.Pos0 = ReadVector3(br);
+ d.Pos1 = ReadVector3(br);
+ d.Offset0 = ReadVector3(br);
+ d.Offset1 = ReadVector3(br);
+ d.Scalar0Min = br.ReadSingle();
+ d.Scalar0Max = br.ReadSingle();
+ d.Scalar1Min = br.ReadSingle();
+ d.Scalar1Max = br.ReadSingle();
+ d.SoundFlags = br.ReadUInt32();
+ d.SoundNameAndReserved = br.ReadBytes(0x40);
+ return d;
+ }
+
+ ///
+ /// Reads an AnimParticleComponentData (type 3) from the binary stream.
+ ///
+ public static AnimParticleComponentData ReadAnimParticleComponent(BinaryReader br, uint typeAndFlags)
+ {
+ AnimParticleComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.Unknown04 = br.ReadSingle();
+ d.ScalarAMin = br.ReadSingle();
+ d.ScalarAMax = br.ReadSingle();
+ d.ScalarAExp = br.ReadSingle();
+ d.ActiveTimeStart = br.ReadSingle();
+ d.ActiveTimeEnd = br.ReadSingle();
+ d.SampleSpreadParam = br.ReadSingle();
+ d.Unknown20 = br.ReadUInt32();
+ d.PrimarySampleCount = br.ReadUInt32();
+ d.SecondarySampleCount = br.ReadUInt32();
+ d.ExtentVec0 = ReadVector3(br);
+ d.ExtentVec1 = ReadVector3(br);
+ d.ExtentVec2 = ReadVector3(br);
+ d.ExponentTriplet0 = ReadVector3(br);
+ d.RadiusTriplet0 = ReadVector3(br);
+ d.RadiusTriplet1 = ReadVector3(br);
+ d.RadiusTriplet2 = ReadVector3(br);
+ d.ExponentTriplet1 = ReadVector3(br);
+ d.NoiseAmplitude = br.ReadSingle();
+ d.Reserved = br.ReadBytes(0x38);
+ return d;
+ }
+
+ ///
+ /// Reads an AnimBillboardComponentData (type 4) from the binary stream.
+ ///
+ public static AnimBillboardComponentData ReadAnimBillboardComponent(BinaryReader br, uint typeAndFlags)
+ {
+ AnimBillboardComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.Unknown04 = br.ReadSingle();
+ d.ScalarAMin = br.ReadSingle();
+ d.ScalarAMax = br.ReadSingle();
+ d.ScalarAExp = br.ReadSingle();
+ d.ActiveTimeStart = br.ReadSingle();
+ d.ActiveTimeEnd = br.ReadSingle();
+ d.SampleSpreadParam = br.ReadSingle();
+ d.Unknown20 = br.ReadUInt32();
+ d.PrimarySampleCount = br.ReadUInt32();
+ d.SecondarySampleCount = br.ReadUInt32();
+ d.ExtentVec0 = ReadVector3(br);
+ d.ExtentVec1 = ReadVector3(br);
+ d.ExtentVec2 = ReadVector3(br);
+ d.ExponentTriplet0 = ReadVector3(br);
+ d.RadiusTriplet0 = ReadVector3(br);
+ d.RadiusTriplet1 = ReadVector3(br);
+ d.RadiusTriplet2 = ReadVector3(br);
+ d.ExponentTriplet1 = ReadVector3(br);
+ d.NoiseAmplitude = br.ReadSingle();
+ d.Reserved = br.ReadBytes(0x3C);
+ return d;
+ }
+
+ ///
+ /// Reads a TrailComponentData (type 5) from the binary stream.
+ ///
+ public static TrailComponentData ReadTrailComponent(BinaryReader br, uint typeAndFlags)
+ {
+ TrailComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.Unknown04To10 = br.ReadBytes(0x10);
+ d.SegmentCount = br.ReadUInt32();
+ d.Param0 = br.ReadSingle();
+ d.Param1 = br.ReadSingle();
+ d.Unknown20 = br.ReadUInt32();
+ d.Unknown24 = br.ReadUInt32();
+ d.ActiveTimeStart = br.ReadSingle();
+ d.ActiveTimeEnd = br.ReadSingle();
+ d.TextureNameAndReserved = br.ReadBytes(0x40);
+ return d;
+ }
+
+ ///
+ /// Reads a PointComponentData (type 6) from the binary stream.
+ /// Note: Point components have no payload beyond the typeAndFlags header.
+ ///
+ public static PointComponentData ReadPointComponent(uint typeAndFlags)
+ {
+ PointComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ return d;
+ }
+
+ ///
+ /// Reads a PlaneComponentData (type 7) from the binary stream.
+ ///
+ public static PlaneComponentData ReadPlaneComponent(BinaryReader br, uint typeAndFlags)
+ {
+ PlaneComponentData d;
+ d.Base = ReadAnimParticleComponent(br, typeAndFlags);
+ d.ExtraPlaneParam0 = br.ReadUInt32();
+ d.ExtraPlaneParam1 = br.ReadUInt32();
+ return d;
+ }
+
+ ///
+ /// Reads a ModelComponentData (type 8) from the binary stream.
+ ///
+ public static ModelComponentData ReadModelComponent(BinaryReader br, uint typeAndFlags)
+ {
+ ModelComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.Unk04 = br.ReadBytes(0x14);
+ d.ActiveTimeStart = br.ReadSingle();
+ d.ActiveTimeEnd = br.ReadSingle();
+ d.Unknown20 = br.ReadUInt32();
+ d.InstanceCount = br.ReadUInt32();
+ d.BasePos = ReadVector3(br);
+ d.OffsetPos = ReadVector3(br);
+ d.ScatterExtent = ReadVector3(br);
+ d.Axis0 = ReadVector3(br);
+ d.Axis1 = ReadVector3(br);
+ d.Axis2 = ReadVector3(br);
+ d.Reserved70 = br.ReadBytes(0x18);
+ d.RadiusTriplet0 = ReadVector3(br);
+ d.RadiusTriplet1 = ReadVector3(br);
+ d.ReservedA0 = br.ReadBytes(0x18);
+ d.TextureNameAndFlags = br.ReadBytes(0x40);
+ return d;
+ }
+
+ ///
+ /// Reads an AnimModelComponentData (type 9) from the binary stream.
+ ///
+ public static AnimModelComponentData ReadAnimModelComponent(BinaryReader br, uint typeAndFlags)
+ {
+ AnimModelComponentData d;
+ d.TypeAndFlags = typeAndFlags;
+ d.AnimSpeed = br.ReadSingle();
+ d.MinTime = br.ReadSingle();
+ d.MaxTime = br.ReadSingle();
+ d.Exponent = br.ReadSingle();
+ d.Reserved14 = br.ReadBytes(0x14);
+ d.DirVec0 = ReadVector3(br);
+ d.Reserved34 = br.ReadBytes(0x0C);
+ d.RadiusTriplet0 = ReadVector3(br);
+ d.DirVec1 = ReadVector3(br);
+ d.RadiusTriplet1 = ReadVector3(br);
+ d.ExtentVec0 = ReadVector3(br);
+ d.ExtentVec1 = ReadVector3(br);
+ d.Reserved7C = br.ReadBytes(0x0C);
+ d.TextureNameAndFlags = br.ReadBytes(0x48);
+ return d;
+ }
+
+ ///
+ /// Reads a CubeComponentData (type 10) from the binary stream.
+ ///
+ public static CubeComponentData ReadCubeComponent(BinaryReader br, uint typeAndFlags)
+ {
+ CubeComponentData d;
+ d.Base = ReadAnimBillboardComponent(br, typeAndFlags);
+ d.ExtraCubeParam0 = br.ReadUInt32();
+ return d;
+ }
+}
diff --git a/ParkanPlayground/Effects/FxidTypes.cs b/ParkanPlayground/Effects/FxidTypes.cs
new file mode 100644
index 0000000..42b85af
--- /dev/null
+++ b/ParkanPlayground/Effects/FxidTypes.cs
@@ -0,0 +1,237 @@
+using Common;
+
+namespace ParkanPlayground.Effects;
+
+///
+/// Effect-level header at the start of each FXID file (60 bytes total).
+/// Parsed from CEffect_InitFromDef: defines component count, global duration/flags,
+/// some unknown control fields, and the uniform scale vector applied to the effect.
+///
+public struct EffectHeader
+{
+ public uint ComponentCount;
+ public uint Unknown1;
+ public float Duration;
+ public float Unknown2;
+ public uint Flags;
+ public uint Unknown3;
+ public byte[] Reserved; // 24 bytes
+ public float ScaleX;
+ public float ScaleY;
+ public float ScaleZ;
+}
+
+///
+/// Shared on-disk definition layout for billboard-style components (type 1).
+/// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha
+/// curves and sample scattering within a 3D extent volume.
+///
+public struct BillboardComponentData
+{
+ public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
+ public float Unknown04; // mode / flag-like float, semantics not fully clear
+ public float ScalarAMin; // base scalar A (e.g. base radius)
+ public float ScalarAMax; // max scalar A
+ public float ScalarAExp; // exponent applied to scalar A curve
+ public float ActiveTimeStart; // activation window start (seconds)
+ public float ActiveTimeEnd; // activation window end (seconds)
+ public float SampleSpreadParam; // extra param used when scattering samples
+ public uint Unknown20; // used as integer param in billboard code, exact meaning unknown
+ public uint PrimarySampleCount; // number of samples along primary axis
+ public uint SecondarySampleCount; // number of samples along secondary axis
+ public Vector3 ExtentVec0; // base extent/origin vector
+ public Vector3 ExtentVec1; // extent center / offset
+ public Vector3 ExtentVec2; // extent size used for random boxing
+ public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
+ public Vector3 RadiusTriplet0; // radius curve key 0
+ public Vector3 RadiusTriplet1; // radius curve key 1
+ public Vector3 RadiusTriplet2; // radius curve key 2
+ public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
+ public float NoiseAmplitude; // per-sample noise amplitude
+ public byte[] Reserved; // 0x50-byte tail, currently not touched by billboard code
+}
+
+///
+/// 3D sound component definition (type 2).
+/// Used by CSoundComponent_Initialize/Update to drive positional audio, playback
+/// window, and scalar ranges (e.g. volume / pitch), plus a 0x40-byte sound name tail.
+///
+public struct SoundComponentData
+{
+ public uint TypeAndFlags; // component type and flags
+ public uint PlayMode; // playback mode (looping, one-shot, etc.)
+ public float StartTime; // playback window start (seconds)
+ public float EndTime; // playback window end (seconds)
+ public Vector3 Pos0; // base 3D position or path start
+ public Vector3 Pos1; // secondary position / path end
+ public Vector3 Offset0; // random offset range 0
+ public Vector3 Offset1; // random offset range 1
+ public float Scalar0Min; // scalar range 0 min (e.g. volume)
+ public float Scalar0Max; // scalar range 0 max
+ public float Scalar1Min; // scalar range 1 min (e.g. pitch)
+ public float Scalar1Max; // scalar range 1 max
+ public uint SoundFlags; // misc sound control flags
+ public byte[] SoundNameAndReserved; // 0x40-byte tail; sound name plus padding/unused
+}
+
+///
+/// Animated particle component definition (type 3).
+/// Prefix layout matches BillboardComponentData and is used to allocate a grid of
+/// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture.
+///
+public struct AnimParticleComponentData
+{
+ public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
+ public float Unknown04; // mode / flag-like float, semantics not fully clear
+ public float ScalarAMin; // base scalar A (e.g. base radius)
+ public float ScalarAMax; // max scalar A
+ public float ScalarAExp; // exponent applied to scalar A curve
+ public float ActiveTimeStart; // activation window start (seconds)
+ public float ActiveTimeEnd; // activation window end (seconds)
+ public float SampleSpreadParam; // extra param used when scattering particles
+ public uint Unknown20; // used as integer param in anim particle code, exact meaning unknown
+ public uint PrimarySampleCount; // number of particles along primary axis
+ public uint SecondarySampleCount; // number of particles along secondary axis
+ public Vector3 ExtentVec0; // base extent/origin vector
+ public Vector3 ExtentVec1; // extent center / offset
+ public Vector3 ExtentVec2; // extent size used for random boxing
+ public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
+ public Vector3 RadiusTriplet0; // radius curve key 0
+ public Vector3 RadiusTriplet1; // radius curve key 1
+ public Vector3 RadiusTriplet2; // radius curve key 2
+ public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
+ public float NoiseAmplitude; // per-particle noise amplitude
+ public byte[] Reserved; // 0x38-byte tail; forwarded to CFxManager_LoadTexture unchanged
+}
+
+///
+/// Animated billboard component definition (type 4).
+/// Shares the same prefix layout as BillboardComponentData, including extents and
+/// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture.
+///
+public struct AnimBillboardComponentData
+{
+ public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
+ public float Unknown04; // mode / flag-like float, semantics not fully clear
+ public float ScalarAMin; // base scalar A (e.g. base radius)
+ public float ScalarAMax; // max scalar A
+ public float ScalarAExp; // exponent applied to scalar A curve
+ public float ActiveTimeStart; // activation window start (seconds)
+ public float ActiveTimeEnd; // activation window end (seconds)
+ public float SampleSpreadParam; // extra param used when scattering animated billboards
+ public uint Unknown20; // used as integer param in anim billboard code, exact meaning unknown
+ public uint PrimarySampleCount; // number of samples along primary axis
+ public uint SecondarySampleCount; // number of samples along secondary axis
+ public Vector3 ExtentVec0; // base extent/origin vector
+ public Vector3 ExtentVec1; // extent center / offset
+ public Vector3 ExtentVec2; // extent size used for random boxing
+ public Vector3 ExponentTriplet0;// exponent triplet for size/color curve
+ public Vector3 RadiusTriplet0; // radius curve key 0
+ public Vector3 RadiusTriplet1; // radius curve key 1
+ public Vector3 RadiusTriplet2; // radius curve key 2
+ public Vector3 ExponentTriplet1;// second exponent triplet (e.g. alpha curve)
+ public float NoiseAmplitude; // per-sample noise amplitude
+ public byte[] Reserved; // 0x3C-byte tail; forwarded to CFxManager_LoadTexture unchanged
+}
+
+///
+/// Compact definition for trail / ribbon components (type 5).
+/// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
+/// ranges, timing, and a shared texture name at +0x30.
+///
+public struct TrailComponentData
+{
+ public uint TypeAndFlags; // component type and flags
+ public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown
+ public uint SegmentCount; // number of trail segments (particles)
+ public float Param0; // first width/alpha/UV control value (start)
+ public float Param1; // second width/alpha/UV control value (end)
+ public uint Unknown20; // extra integer parameter, purpose unknown
+ public uint Unknown24; // extra integer parameter, purpose unknown
+ public float ActiveTimeStart; // trail activation start time (>= 0)
+ public float ActiveTimeEnd; // trail activation end time
+ public byte[] TextureNameAndReserved; // 0x40-byte tail containing texture name and padding/flags
+}
+
+///
+/// Simple point component definition (type 6).
+/// Definition block is just the 4-byte typeAndFlags header; no extra data on disk.
+///
+public struct PointComponentData
+{
+ public uint TypeAndFlags; // component type and flags; definition block has no payload
+}
+
+///
+/// Plane component definition (type 7).
+/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
+/// followed by two dwords of plane-specific data.
+///
+public struct PlaneComponentData
+{
+ public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves
+ public uint ExtraPlaneParam0; // plane-specific parameter, semantics not yet reversed
+ public uint ExtraPlaneParam1; // plane-specific parameter, semantics not yet reversed
+}
+
+///
+/// Static model component definition (type 8).
+/// Layout fully matches the IDA typedef used by CModelComponent_Initialize:
+/// time window, instance count, spatial extents/axes, radius triplets, and a
+/// 0x40-byte texture name tail.
+///
+public struct ModelComponentData
+{
+ public uint TypeAndFlags; // component type and flags
+ public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear
+ public float ActiveTimeStart; // activation window start (seconds), +0x18
+ public float ActiveTimeEnd; // activation window end (seconds), +0x1C
+ public uint Unknown20; // extra flags/int parameter at +0x20
+ public uint InstanceCount; // number of model instances to spawn at +0x24
+ public Vector3 BasePos; // base position of the emitter / origin, +0x28
+ public Vector3 OffsetPos; // positional offset applied per-instance, +0x34
+ public Vector3 ScatterExtent; // extent volume used for random scattering, +0x40
+ public Vector3 Axis0; // local axis 0 (orientation / shape), +0x4C
+ public Vector3 Axis1; // local axis 1 (orientation / shape), +0x58
+ public Vector3 Axis2; // local axis 2 (orientation / shape), +0x64
+ public byte[] Reserved70; // 0x18 bytes at +0x70..+0x87, not directly used
+ public Vector3 RadiusTriplet0; // radius / extent triplet 0 at +0x88
+ public Vector3 RadiusTriplet1; // radius / extent triplet 1 at +0x94
+ public byte[] ReservedA0; // 0x18 bytes at +0xA0..+0xB7, not directly used
+ public byte[] TextureNameAndFlags; // 0x40-byte tail at +0xB8: texture name + padding/flags
+}
+
+///
+/// Animated model component definition (type 9).
+/// Layout derived from CAnimModelComponent_Initialize: time params, direction vectors,
+/// radius triplets, extent vectors, and a 0x48-byte texture name tail.
+///
+public struct AnimModelComponentData
+{
+ public uint TypeAndFlags; // component type and flags
+ public float AnimSpeed; // animation speed multiplier at +0x04
+ public float MinTime; // activation window start (clamped >= 0) at +0x08
+ public float MaxTime; // activation window end at +0x0C
+ public float Exponent; // exponent for time interpolation at +0x10
+ public byte[] Reserved14; // 0x14 bytes at +0x14..+0x27, padding
+ public Vector3 DirVec0; // normalized direction vector 0 at +0x28
+ public byte[] Reserved34; // 0x0C bytes at +0x34..+0x3F, padding
+ public Vector3 RadiusTriplet0; // radius triplet 0 at +0x40
+ public Vector3 DirVec1; // normalized direction vector 1 at +0x4C
+ public Vector3 RadiusTriplet1; // radius triplet 1 at +0x58
+ public Vector3 ExtentVec0; // extent vector 0 at +0x64
+ public Vector3 ExtentVec1; // extent vector 1 at +0x70
+ public byte[] Reserved7C; // 0x0C bytes at +0x7C..+0x87, padding
+ public byte[] TextureNameAndFlags; // 0x48-byte tail at +0x88: texture/model name + padding
+}
+
+///
+/// Cube component definition (type 10).
+/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
+/// followed by one dword of cube-specific data.
+///
+public struct CubeComponentData
+{
+ public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves
+ public uint ExtraCubeParam0; // cube-specific parameter, semantics not yet reversed
+}
diff --git a/ParkanPlayground/MSH_FORMAT.md b/ParkanPlayground/MSH_FORMAT.md
index 0841fcd..630e25f 100644
--- a/ParkanPlayground/MSH_FORMAT.md
+++ b/ParkanPlayground/MSH_FORMAT.md
@@ -26,20 +26,20 @@ MSH файлы — это NRes архивы, содержащие несколь
| Тип | Название | Размер элемента | Описание |
|:---:|----------|:---------------:|----------|
| 01 | Pieces | 38 (0x26) | Части меша / тайлы с LOD-ссылками |
-| 02 | Submeshes | 68 (0x44) | Сабмеши с баундинг-боксами |
+| 02 | Submeshes | 68 (0x44) | LOD части с баундинг-боксами |
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) |
-| 04 | VertexData1 | 4 | Данные на вершину (неизвестно) |
-| 05 | VertexData2 | 4 | Данные на вершину (неизвестно) |
+| 04 | неизвестно | 4 | (неизвестно) |
+| 05 | неизвестно | 4 | (неизвестно) |
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) |
-| 07 | TriangleData | 16 | Данные рендера на треугольник (только Модель) |
+| 07 | неизвестно | 16 | (только Модель) |
| 08 | Animations | 4 | Кейфреймы анимации меша |
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) |
-| 0B | MaterialData | 4 | Материалы на треугольник (только Ландшафт) |
-| 0D | DrawBatches | 20 (0x14) | Батчи отрисовки (только Модель) |
-| 0E | VertexData3 | 4 | Данные на вершину (только Ландшафт) |
-| 12 | MicrotextureMap | 4 | Микротекстурный маппинг на вершину |
+| 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
+| 0D | неизвестно | 20 (0x14) | неизвестно (только Модель) |
+| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
+| 12 | MicrotextureMap | 4 | неизвестно |
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
-| 15 | Triangles | 28 (0x1C) | Прямые определения треугольников |
+| 15 | неизвестно | 28 (0x1C) | неизвестно |
---
@@ -201,7 +201,7 @@ MSH файлы — это NRes архивы, содержащие несколь
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
-| 0x04 | 4 | uint32 | Magic04 | Неизвестно (часто 0xFFFFFF01) |
+| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) |
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
@@ -210,6 +210,24 @@ MSH файлы — это NRes архивы, содержащие несколь
| 0x16 | 4 | uint32 | Magic16 | Неизвестно |
| 0x1A | 2 | ushort | Magic1A | Неизвестно |
+#### MaterialData (0x04) - Структура материала
+
+```
+MaterialData = 0xFFFF_SSPP
+ │ │└─ PP: Основной материал (byte 0)
+ │ └─── SS: Вторичный материал для блендинга (byte 1)
+ └────── Всегда 0xFFFF (байты 2-3)
+```
+
+| Значение SS | Описание |
+|:-----------:|----------|
+| 0xFF | Сплошной материал (без блендинга) |
+| 0x01-0xFE | Индекс вторичного материала для блендинга |
+
+Примеры:
+- `0xFFFFFF01` = Сплошной материал 1
+- `0xFFFF0203` = Материал 3 с блендингом в материал 2
+
---
### Компонент 0A - External References (переменный размер)
@@ -261,6 +279,71 @@ var type = MshConverter.DetectMeshType(archive);
---
+## Формат WEA - Файлы материалов ландшафта
+
+Файлы `.wea` — текстовые файлы, определяющие таблицу материалов для ландшафта.
+
+### Формат
+
+```
+{count}
+{index} {material_name}
+{index} {material_name}
+...
+```
+
+### Связь с Land.msh
+
+Каждая карта имеет два файла материалов:
+
+| Файл | Используется для | Треугольники в Comp15 |
+|------|------------------|----------------------|
+| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) |
+| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
+
+### Пример (SC_1)
+
+**Land1.wea:**
+```
+4
+0 B_S0
+1 L04
+2 L02
+3 L00
+```
+
+**Land2.wea:**
+```
+4
+0 DEFAULT
+1 L05
+2 L03
+3 L01
+```
+
+### Маппинг материалов
+
+Индекс материала в `Comp15.MaterialData & 0xFF` → строка в `.wea` файле.
+
+```
+Треугольник с MaterialData = 0xFFFF0102
+ └─ Основной материал = 02 → Land1.wea[2] = "L02"
+ └─ Блендинг с материалом = 01 → Land1.wea[1] = "L04"
+```
+
+### Типичные имена материалов
+
+| Префикс | Назначение |
+|---------|------------|
+| L00-L05 | Текстуры ландшафта (grass, dirt, etc.) |
+| B_S0 | Базовая текстура |
+| DEFAULT | Фолбэк для LOD1 |
+| WATER | Вода (поверхность) |
+| WATER_BOT | Вода (дно) |
+| WATER_M | Вода LOD1 |
+
+---
+
## Источники
- Реверс-инжиниринг `Terrain.dll` (класс CLandscape)
diff --git a/ParkanPlayground/ParkanPlayground.csproj b/ParkanPlayground/ParkanPlayground.csproj
index 8dc9cf8..4eedae8 100644
--- a/ParkanPlayground/ParkanPlayground.csproj
+++ b/ParkanPlayground/ParkanPlayground.csproj
@@ -11,6 +11,7 @@
+
diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs
index c2ed923..9516d29 100644
--- a/ParkanPlayground/Program.cs
+++ b/ParkanPlayground/Program.cs
@@ -1,33 +1,332 @@
-using ParkanPlayground;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using ParkanPlayground.Effects;
+using static ParkanPlayground.Effects.FxidReader;
-// ========== MSH CONVERTER TEST - AUTO-DETECTING MODEL VS LANDSCAPE ==========
+Console.OutputEncoding = Encoding.UTF8;
-var converter = new MshConverter();
+if (args.Length == 0)
+{
+ Console.WriteLine("Usage: ParkanPlayground ");
+ return;
+}
-// Test with landscape
-Console.WriteLine("=".PadRight(60, '='));
-converter.Convert(
- @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land.msh",
- "landscape_lod0.obj",
- lodLevel: 0
-);
+var path = args[0];
+bool anyError = false;
+var sizeByType = new Dictionary
+{
+ [1] = 0xE0, // 1: Billboard
+ [2] = 0x94, // 2: Sound
+ [3] = 0xC8, // 3: AnimParticle
+ [4] = 0xCC, // 4: AnimBillboard
+ [5] = 0x70, // 5: Trail
+ [6] = 0x04, // 6: Point
+ [7] = 0xD0, // 7: Plane
+ [8] = 0xF8, // 8: Model
+ [9] = 0xD0, // 9: AnimModel
+ [10] = 0xD0, // 10: Cube
+};
-Console.WriteLine();
+// Check for --dump-headers flag
+bool dumpHeaders = args.Length > 1 && args[1] == "--dump-headers";
-// Test with landscape LOD 1 (lower detail)
-Console.WriteLine("=".PadRight(60, '='));
-converter.Convert(
- @"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land.msh",
- "landscape_lod1.obj",
- lodLevel: 1
-);
+if (Directory.Exists(path))
+{
+ var files = Directory.EnumerateFiles(path, "*.bin").ToList();
+
+ if (dumpHeaders)
+ {
+ // Collect all headers for analysis
+ var headers = new List<(string name, EffectHeader h)>();
+ foreach (var file in files)
+ {
+ try
+ {
+ using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
+ if (fs.Length >= 60)
+ {
+ headers.Add((Path.GetFileName(file), ReadEffectHeader(br)));
+ }
+ }
+ catch { }
+ }
+
+ // Analyze unique values
+ Console.WriteLine("=== UNIQUE VALUES ANALYSIS ===\n");
+
+ var uniqueUnk1 = headers.Select(x => x.h.Unknown1).Distinct().OrderBy(x => x).ToList();
+ Console.WriteLine($"Unknown1 unique values ({uniqueUnk1.Count}): {string.Join(", ", uniqueUnk1)}");
+
+ var uniqueUnk2 = headers.Select(x => x.h.Unknown2).Distinct().OrderBy(x => x).ToList();
+ Console.WriteLine($"Unknown2 unique values ({uniqueUnk2.Count}): {string.Join(", ", uniqueUnk2.Select(x => x.ToString("F2")))}");
+
+ var uniqueFlags = headers.Select(x => x.h.Flags).Distinct().OrderBy(x => x).ToList();
+ Console.WriteLine($"Flags unique values ({uniqueFlags.Count}): {string.Join(", ", uniqueFlags.Select(x => $"0x{x:X4}"))}");
+
+ var uniqueUnk3 = headers.Select(x => x.h.Unknown3).Distinct().OrderBy(x => x).ToList();
+ Console.WriteLine($"Unknown3 unique values ({uniqueUnk3.Count}): {string.Join(", ", uniqueUnk3)}");
+ Console.WriteLine($"Unknown3 as hex: {string.Join(", ", uniqueUnk3.Select(x => $"0x{x:X3}"))}");
+ Console.WriteLine($"Unknown3 decoded (hi.lo): {string.Join(", ", uniqueUnk3.Select(x => $"{x >> 8}.{x & 0xFF}"))}");
+
+ // Check reserved bytes
+ var nonZeroReserved = headers.Where(x => x.h.Reserved.Any(b => b != 0)).ToList();
+ Console.WriteLine($"\nFiles with non-zero Reserved bytes: {nonZeroReserved.Count} / {headers.Count}");
+
+ // Check scales
+ var uniqueScales = headers.Select(x => (x.h.ScaleX, x.h.ScaleY, x.h.ScaleZ)).Distinct().ToList();
+ Console.WriteLine($"Unique scale combinations: {string.Join(", ", uniqueScales.Select(s => $"({s.ScaleX:F2},{s.ScaleY:F2},{s.ScaleZ:F2})"))}");
+
+ Console.WriteLine("\n=== SAMPLE HEADERS (first 30) ===");
+ Console.WriteLine($"{"File",-40} | {"Cnt",3} | {"U1",2} | {"Duration",8} | {"U2",6} | {"Flags",6} | {"U3",4} | Scale");
+ Console.WriteLine(new string('-', 100));
+
+ foreach (var (name, h) in headers.Take(30))
+ {
+ Console.WriteLine($"{name,-40} | {h.ComponentCount,3} | {h.Unknown1,2} | {h.Duration,8:F2} | {h.Unknown2,6:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | ({h.ScaleX:F1},{h.ScaleY:F1},{h.ScaleZ:F1})");
+ }
+ }
+ else
+ {
+ foreach (var file in files)
+ {
+ if (!ValidateFxidFile(file))
+ {
+ anyError = true;
+ }
+ }
-Console.WriteLine();
+ Console.WriteLine(anyError
+ ? "Validation finished with errors."
+ : "All FXID files parsed successfully.");
+ }
+}
+else if (File.Exists(path))
+{
+ anyError = !ValidateFxidFile(path);
+ Console.WriteLine(anyError ? "Validation failed." : "Validation OK.");
+}
+else
+{
+ Console.WriteLine($"Path not found: {path}");
+}
-// Test with model
-Console.WriteLine("=".PadRight(60, '='));
-converter.Convert(
- @"E:\ParkanUnpacked\fortif.rlb\133_fr_m_bunker.msh",
- "bunker_lod0.obj",
- lodLevel: 0
-);
\ No newline at end of file
+void DumpEffectHeader(string path)
+{
+ try
+ {
+ using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
+
+ if (fs.Length < 60)
+ {
+ Console.WriteLine($"{Path.GetFileName(path)}: file too small");
+ return;
+ }
+
+ var h = ReadEffectHeader(br);
+
+ // Format reserved bytes as hex (show first 8 bytes for brevity)
+ var reservedHex = BitConverter.ToString(h.Reserved, 0, Math.Min(8, h.Reserved.Length)).Replace("-", " ");
+ if (h.Reserved.Length > 8) reservedHex += "...";
+
+ // Check if reserved has any non-zero bytes
+ bool reservedAllZero = h.Reserved.All(b => b == 0);
+
+ Console.WriteLine($"{Path.GetFileName(path),-40} | {h.ComponentCount,7} | {h.Unknown1,4} | {h.Duration,8:F2} | {h.Unknown2,8:F2} | 0x{h.Flags:X4} | {h.Unknown3,4} | {(reservedAllZero ? "(all zero)" : reservedHex),-20} | ({h.ScaleX:F2}, {h.ScaleY:F2}, {h.ScaleZ:F2})");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"{Path.GetFileName(path)}: {ex.Message}");
+ }
+}
+
+bool ValidateFxidFile(string path)
+{
+ try
+ {
+ using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var br = new BinaryReader(fs, Encoding.ASCII, leaveOpen: false);
+
+ const int headerSize = 60; // sizeof(EffectHeader) on disk
+ if (fs.Length < headerSize)
+ {
+ Console.WriteLine($"{path}: file too small ({fs.Length} bytes).");
+ return false;
+ }
+
+ var header = ReadEffectHeader(br);
+
+ var typeCounts = new Dictionary();
+
+ for (int i = 0; i < header.ComponentCount; i++)
+ {
+ long blockStart = fs.Position;
+ if (fs.Position + 4 > fs.Length)
+ {
+ Console.WriteLine($"{path}: component {i}: unexpected EOF before type (offset 0x{fs.Position:X}, size 0x{fs.Length:X}).");
+ return false;
+ }
+
+ uint typeAndFlags = br.ReadUInt32();
+ byte type = (byte)(typeAndFlags & 0xFF);
+
+ if (!typeCounts.TryGetValue(type, out var count))
+ {
+ count = 0;
+ }
+ typeCounts[type] = count + 1;
+
+ if (!sizeByType.TryGetValue(type, out int blockSize))
+ {
+ Console.WriteLine($"{path}: component {i}: unknown type {type} (typeAndFlags=0x{typeAndFlags:X8}).");
+ return false;
+ }
+
+ int remaining = blockSize - 4;
+ if (fs.Position + remaining > fs.Length)
+ {
+ Console.WriteLine($"{path}: component {i}: block size 0x{blockSize:X} runs past EOF (blockStart=0x{blockStart:X}, fileSize=0x{fs.Length:X}).");
+ return false;
+ }
+
+ if (type == 1)
+ {
+ var def = ReadBillboardComponent(br, typeAndFlags);
+
+ if (def.Reserved.Length != 0x50)
+ {
+ Console.WriteLine($"{path}: component {i}: type 1 reserved length {def.Reserved.Length}, expected 0x50.");
+ return false;
+ }
+ }
+ else if (type == 2)
+ {
+ var def = ReadSoundComponent(br, typeAndFlags);
+
+ if (def.SoundNameAndReserved.Length != 0x40)
+ {
+ Console.WriteLine($"{path}: component {i}: type 2 reserved length {def.SoundNameAndReserved.Length}, expected 0x40.");
+ return false;
+ }
+ }
+ else if (type == 3)
+ {
+ var def = ReadAnimParticleComponent(br, typeAndFlags);
+
+ if (def.Reserved.Length != 0x38)
+ {
+ Console.WriteLine($"{path}: component {i}: type 3 reserved length {def.Reserved.Length}, expected 0x38.");
+ return false;
+ }
+ }
+ else if (type == 4)
+ {
+ var def = ReadAnimBillboardComponent(br, typeAndFlags);
+
+ if (def.Reserved.Length != 0x3C)
+ {
+ Console.WriteLine($"{path}: component {i}: type 4 reserved length {def.Reserved.Length}, expected 0x3C.");
+ return false;
+ }
+ }
+ else if (type == 5)
+ {
+ var def = ReadTrailComponent(br, typeAndFlags);
+
+ if (def.Unknown04To10.Length != 0x10)
+ {
+ Console.WriteLine($"{path}: component {i}: type 5 prefix length {def.Unknown04To10.Length}, expected 0x10.");
+ return false;
+ }
+
+ if (def.TextureNameAndReserved.Length != 0x40)
+ {
+ Console.WriteLine($"{path}: component {i}: type 5 tail length {def.TextureNameAndReserved.Length}, expected 0x40.");
+ return false;
+ }
+ }
+ else if (type == 6)
+ {
+ // Point components have no extra bytes beyond the 4-byte typeAndFlags header.
+ var def = ReadPointComponent(typeAndFlags);
+ }
+ else if (type == 7)
+ {
+ var def = ReadPlaneComponent(br, typeAndFlags);
+
+ if (def.Base.Reserved.Length != 0x38)
+ {
+ Console.WriteLine($"{path}: component {i}: type 7 base reserved length {def.Base.Reserved.Length}, expected 0x38.");
+ return false;
+ }
+ }
+ else if (type == 8)
+ {
+ var def = ReadModelComponent(br, typeAndFlags);
+
+ if (def.TextureNameAndFlags.Length != 0x40)
+ {
+ Console.WriteLine($"{path}: component {i}: type 8 tail length {def.TextureNameAndFlags.Length}, expected 0x40.");
+ return false;
+ }
+ }
+ else if (type == 9)
+ {
+ var def = ReadAnimModelComponent(br, typeAndFlags);
+
+ if (def.TextureNameAndFlags.Length != 0x48)
+ {
+ Console.WriteLine($"{path}: component {i}: type 9 tail length {def.TextureNameAndFlags.Length}, expected 0x48.");
+ return false;
+ }
+ }
+ else if (type == 10)
+ {
+ var def = ReadCubeComponent(br, typeAndFlags);
+
+ if (def.Base.Reserved.Length != 0x3C)
+ {
+ Console.WriteLine($"{path}: component {i}: type 10 base reserved length {def.Base.Reserved.Length}, expected 0x3C.");
+ return false;
+ }
+ }
+ else
+ {
+ // Skip the remaining bytes for other component types.
+ fs.Position += remaining;
+ }
+ }
+
+ // Dump a compact per-file summary of component types and counts.
+ var sb = new StringBuilder();
+ bool first = true;
+ foreach (var kv in typeCounts)
+ {
+ if (!first)
+ {
+ sb.Append(", ");
+ }
+ sb.Append(kv.Key);
+ sb.Append('x');
+ sb.Append(kv.Value);
+ first = false;
+ }
+ Console.WriteLine($"{path}: components={header.ComponentCount}, types=[{sb}]");
+
+ if (fs.Position != fs.Length)
+ {
+ Console.WriteLine($"{path}: parsed to 0x{fs.Position:X}, but file size is 0x{fs.Length:X} (leftover {fs.Length - fs.Position} bytes).");
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"{path}: exception while parsing: {ex.Message}");
+ return false;
+ }
+}