Files
parkan-playground/MshLib/Msh0x01.cs
T

184 lines
6.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Buffers.Binary;
using NResLib;
namespace MshLib;
/// <summary>
/// MSH-компонент 0x01: таблица узлов модели.
/// Для обычного AniMesh node имеет size 0x26.
/// У ландшафта metadata/magic1 может иметь другой смысл, например grid_x_count.
/// </summary>
public static class Msh0x01
{
public const int NormalElementSize = 0x26;
public const int StateCount = 3;
public const int MaxLodCount = 5;
public const int SlotCount = StateCount * MaxLodCount;
public static Msh0x01Component ReadComponent(FileStream mshFs, NResArchive archive)
{
var entry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
if (entry is null)
{
throw new Exception("Archive doesn't contain node table component (0x01)");
}
if (entry.ElementSize <= 0)
{
throw new Exception("Node table component (0x01) has invalid element size");
}
if (entry.FileLength % entry.ElementSize != 0)
{
throw new Exception("Node table component (0x01) payload size is not divisible by element size");
}
if (entry.ElementSize < 8)
{
throw new Exception("Node table component (0x01) element size is too small");
}
var elementCount = entry.FileLength / entry.ElementSize;
var data = new byte[entry.FileLength];
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
mshFs.ReadExactly(data, 0, data.Length);
var dataSpan = data.AsSpan();
var nodes = new List<Node>(elementCount);
for (var i = 0; i < elementCount; i++)
{
var baseOffset = i * entry.ElementSize;
var elementSpan = dataSpan.Slice(baseOffset, entry.ElementSize);
var slotIndices = new ushort[SlotCount];
Array.Fill(slotIndices, ushort.MaxValue);
var slotWordCount = Math.Min(
slotIndices.Length,
Math.Max(0, (entry.ElementSize - 8) / 2));
for (var slotIndex = 0; slotIndex < slotWordCount; slotIndex++)
{
slotIndices[slotIndex] =
BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x08 + slotIndex * 2, 2));
}
nodes.Add(new Node(
Flags: (NodeFlags)BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x00, 2)),
ParentIndexOrLink: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x02, 2)),
AnimMapStart0x13: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x04, 2)),
FallbackKey0x08: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x06, 2)),
Msh02SlotIndicesByStateAndLOD: slotIndices));
}
return new Msh0x01Component(entry.ElementSize, nodes);
}
/// <summary>Результат чтения MSH-компонента 0x01.</summary>
/// <param name="ElementSize">Размер node entry из NRes metadata.</param>
/// <param name="Nodes">Узлы компонента 0x01.</param>
public sealed record Msh0x01Component(
int ElementSize,
List<Node> Nodes)
{
public bool IsNormalAniMeshNodeTable => ElementSize == NormalElementSize;
}
/// <summary>Узел MSH 0x01.</summary>
/// <param name="Flags">[0x00..0x02] Флаги узла.</param>
/// <param name="ParentIndexOrLink">[0x02..0x04] Parent node index. 0xFFFF обычно значит root/no parent.</param>
/// <param name="AnimMapStart0x13">[0x04..0x06] Начало блока в MSH 0x13 animation map или 0xFFFF.</param>
/// <param name="FallbackKey0x08">[0x06..0x08] Fallback key / index в MSH 0x08.</param>
/// <param name="Msh02SlotIndicesByStateAndLOD">
/// [0x08..0x26] Индексы geometry slot в MSH 0x02.
/// Формула: slot = lod * 5 + group. 0xFFFF значит отсутствует.
/// </param>
public sealed record Node(
NodeFlags Flags,
ushort ParentIndexOrLink,
ushort AnimMapStart0x13,
ushort FallbackKey0x08,
ushort[] Msh02SlotIndicesByStateAndLOD)
{
public ushort ResolveSlotIndex(int state, int lod = 0)
{
// MODEL_STATE_DEFAULT -1
// MODEL_STATE_REGULAR 0
// MODEL_STATE_COLLAPSED 1
// _MODEL_STATE_UNKNOWN_2 2
// LOD_LEVEL_MAX_0 0
// LOD_LEVEL_MINUS_1 1
// LOD_LEVEL_MINUS_2 2
// LOD_LEVEL_MINUS_3 3
// LOD_LEVEL_MINUS_4 4
var index = state * MaxLodCount + lod;
return index >= 0 && index < Msh02SlotIndicesByStateAndLOD.Length
? Msh02SlotIndicesByStateAndLOD[index]
: ushort.MaxValue;
}
}
}
[Flags]
public enum NodeFlags : ushort
{
None = 0,
/// <summary>
/// Still uncertain. In recursive bounds/intersection paths this can suppress/alter child recursion.
/// Seen as child_node.flags &amp; 0x04.
/// </summary>
MSH01_BOUNDS_MODE0_STOP = 0x0004,
/// <summary>
/// Stops recursive traversal into children for bounds/render/intersection helpers.
/// </summary>
MSH01_STOP_CHILD_BOUNDS_TRAVERSAL = 0x0010,
/// <summary>
/// Special lod-4 mode bit. In lod 4, selects alternate piece render mode.
/// </summary>
MSH01_HAS_SPECIAL_LOD_4 = 0x0020,
/// <summary>
/// Exclude from shadow / no shadow. CAniMesh tracks has_any_shadow_casting_piece when this bit is absent.
/// </summary>
MSH01_NO_SHADOW = 0x0040,
/// <summary>
/// If this MSH01 flag is set, depletion/restore of this NDP slot should not
/// participate in central-piece state updates. In practice it maps to
/// CONTROL_NDP_REF_FLAG_NO_CENTRAL_STATE_UPDATE and probably prevents this piece
/// from changing the "central" damage/mobility accounting.
/// </summary>
MSH01_NO_CENTRAL_STATE_UPDATE = 0x0100,
/// <summary>
/// If this MSH01 flag is set, the matching ControlNdpRef becomes fatal/critical.
/// When that NDP slot is depleted, higher-level life logic likely treats the owning
/// object as destroyed or forces a kill/failure path.
/// </summary>
MSH01_NDP_FATAL_ON_DEPLETION = 0x0200,
/// <summary>
/// If this MSH01 flag is set, this NDP slot derives or clamps its life ratio
/// from the parent piece/NDP slot. It maps to
/// CONTROL_NDP_REF_FLAG_INHERIT_PARENT_LIFE_RATIO and is likely used for
/// child/helper damage parts that should visually follow parent damage state.
/// </summary>
MSH01_NDP_INHERIT_PARENT_LIFE_RATIO = 0x0400,
/// <summary>
/// Used during attached MSH load: if parent/root description contains "central", piece gets hidden/excluded flag.
/// Exact semantic name still provisional.
/// </summary>
MSH01_CHECK_PARENT_DESCRIPTION_CENTRAL = 0x0800,
}