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; + } +}