1
mirror of https://github.com/sampletext32/ParkanPlayground.git synced 2025-12-09 11:11:24 +04:00
This commit is contained in:
bird_egop
2025-12-09 02:32:32 +03:00
parent fcfb8d0e8a
commit a8536f938d
5 changed files with 911 additions and 36 deletions

View File

@@ -0,0 +1,255 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// Static reader methods for parsing FXID effect definition structures from binary streams.
/// </summary>
public static class FxidReader
{
/// <summary>
/// Reads a Vector3 (3 floats: X, Y, Z) from the binary stream.
/// </summary>
public static Vector3 ReadVector3(BinaryReader br)
{
float x = br.ReadSingle();
float y = br.ReadSingle();
float z = br.ReadSingle();
return new Vector3(x, y, z);
}
/// <summary>
/// Reads the 60-byte effect header from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a BillboardComponentData (type 1) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a SoundComponentData (type 2) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads an AnimParticleComponentData (type 3) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads an AnimBillboardComponentData (type 4) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a TrailComponentData (type 5) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a PointComponentData (type 6) from the binary stream.
/// Note: Point components have no payload beyond the typeAndFlags header.
/// </summary>
public static PointComponentData ReadPointComponent(uint typeAndFlags)
{
PointComponentData d;
d.TypeAndFlags = typeAndFlags;
return d;
}
/// <summary>
/// Reads a PlaneComponentData (type 7) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a ModelComponentData (type 8) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads an AnimModelComponentData (type 9) from the binary stream.
/// </summary>
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;
}
/// <summary>
/// Reads a CubeComponentData (type 10) from the binary stream.
/// </summary>
public static CubeComponentData ReadCubeComponent(BinaryReader br, uint typeAndFlags)
{
CubeComponentData d;
d.Base = ReadAnimBillboardComponent(br, typeAndFlags);
d.ExtraCubeParam0 = br.ReadUInt32();
return d;
}
}

View File

@@ -0,0 +1,237 @@
using Common;
namespace ParkanPlayground.Effects;
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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 uint TypeAndFlags; // component type and flags; definition block has no payload
}
/// <summary>
/// Plane component definition (type 7).
/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
/// followed by two dwords of plane-specific data.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>
/// Cube component definition (type 10).
/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
/// followed by one dword of cube-specific data.
/// </summary>
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
}

View File

@@ -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)

View File

@@ -11,6 +11,7 @@
<ProjectReference Include="..\ScrLib\ScrLib.csproj" />
<ProjectReference Include="..\VarsetLib\VarsetLib.csproj" />
<ProjectReference Include="..\MaterialLib\MaterialLib.csproj" />
<ProjectReference Include="..\TexmLib\TexmLib.csproj" />
</ItemGroup>
</Project>

View File

@@ -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 <effects-directory-or-fxid-file>");
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<byte, int>
{
[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
);
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<byte, int>();
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;
}
}