CTL CPT NDP initial reverse and dump using ai

This commit is contained in:
bird_egop
2026-06-02 01:49:46 +03:00
parent 52015f3314
commit a8c0348ee4
5 changed files with 1131 additions and 2 deletions
+3
View File
@@ -0,0 +1,3 @@
<Project Sdk="Microsoft.NET.Sdk">
</Project>
+561
View File
@@ -0,0 +1,561 @@
using System.Globalization;
namespace ControlLib;
public static class ControlResourceDump
{
public static void DumpFiles(string ctlPath, string? cptPath = null, string? ndpPath = null, TextWriter? writer = null)
{
var ctl = ControlResourceReader.ReadCtlFile(ctlPath);
var cpt = cptPath is null ? null : ControlResourceReader.ReadCptFile(cptPath);
var ndp = ndpPath is null ? null : ControlResourceReader.ReadNdpFile(ndpPath);
DumpAll(ctl, cpt, ndp, writer);
}
public static void DumpAll(CtlFile ctl, CptFile? cpt = null, NdpFile? ndp = null, TextWriter? writer = null)
{
var w = writer ?? Console.Out;
w.WriteLine("================================================================================");
w.WriteLine($"CTL: {ctl.SourceName}");
w.WriteLine("================================================================================");
DumpCtl(ctl, cpt, w);
if (cpt is not null)
{
w.WriteLine();
w.WriteLine("================================================================================");
w.WriteLine($"CPT: {cpt.SourceName}");
w.WriteLine("================================================================================");
DumpCpt(cpt, w);
}
if (ndp is not null)
{
w.WriteLine();
w.WriteLine("================================================================================");
w.WriteLine($"NDP: {ndp.SourceName}");
w.WriteLine("================================================================================");
DumpNdp(ndp, w);
}
}
private static void DumpCtl(CtlFile ctl, CptFile? cpt, TextWriter w)
{
var h = ctl.Header;
w.WriteLine("Header");
w.WriteLine($" states : {h.ControlStateCount}");
w.WriteLine($" link slots : {h.ControlStateLinkSlotCount}");
w.WriteLine($" bindings : {h.PieceBindingCount}");
w.WriteLine($" items : {h.ItemRecordCount}");
w.WriteLine($" command lists: {h.CommandListCount}");
DumpRootParams(ctl.RootParams, w);
DumpStates(ctl, w);
DumpPieceBindings(ctl, cpt, w);
DumpItems(ctl, cpt, w);
DumpCommandTable(ctl, w);
DumpCommandLists(ctl, cpt, w);
}
private static void DumpRootParams(object root, TextWriter w)
{
w.WriteLine();
w.WriteLine($"Root params @ {Off(GetLong(root, "Offset"))}");
w.WriteLine($" max tang accel : {Vec(Get(root, "MaxTangentialAccel"))}");
w.WriteLine($" max tang speed : {Vec(Get(root, "MaxTangentialSpeed"))}");
w.WriteLine($" max angular speed : {Vec(Get(root, "MaxAngularSpeed"))}");
w.WriteLine($" torque response scale : {Vec(Get(root, "ExternalTorqueResponseScale"))}");
w.WriteLine($" visual euler limit : {Vec(Get(root, "VisualEulerAngleLimit"))}");
w.WriteLine($" movement physics mode : {GetInt(root, "MovementPhysicsMode")}");
w.WriteLine($" slope limit angle : {F(GetFloat(root, "SlopeLimitAngle"))}");
w.WriteLine($" mobility integrity : {F(GetFloat(root, "MobilityIntegrityAmount"))}");
w.WriteLine($" detach cleanup ms : {GetInt(root, "DetachCleanupDelayMs", "DetachCleanupDelay")}");
w.WriteLine($" unknown 0x4C / 0x50 : {F(GetFloat(root, "Unknown4C"))}, {F(GetFloat(root, "Unknown50"))}");
w.WriteLine($" unknown 0x58 / 0x60/64: {F(GetFloat(root, "Unknown58"))}, {GetInt(root, "Unknown60")}, {F(GetFloat(root, "Unknown64"))}");
}
private static void DumpStates(CtlFile ctl, TextWriter w)
{
w.WriteLine();
w.WriteLine("Control states");
if (ctl.StateRecords.Count == 0)
{
w.WriteLine(" <none>");
return;
}
for (int i = 0; i < ctl.StateRecords.Count; i++)
{
var state = ctl.StateRecords[i];
w.WriteLine($" [{i}] @ {Off(state.Offset)} links={state.Links.Count}");
for (int j = 0; j < state.Links.Count; j++)
{
var link = state.Links[j];
w.WriteLine(
$" link[{j}] " +
$"w0={Hex(link.Word0)} ({UncheckedInt(link.Word0)}) " +
$"w1={Hex(link.Word1)} ({UncheckedInt(link.Word1)}) " +
$"w2={Hex(link.Word2)} ({UncheckedInt(link.Word2)}) " +
$"w3={Hex(link.Word3)} ({UncheckedInt(link.Word3)})");
}
}
if (ctl.EdgeWeightMatrix.Count != 0)
w.WriteLine($" edge weights: {string.Join(", ", ctl.EdgeWeightMatrix.Select(F))}");
}
private static void DumpPieceBindings(CtlFile ctl, CptFile? cpt, TextWriter w)
{
w.WriteLine();
w.WriteLine("Piece bindings");
for (int i = 0; i < ctl.PieceBindings.Count; i++)
{
var b = ctl.PieceBindings[i];
int primary = GetInt(b, "PrimaryCptRefIndex");
int secondary = GetInt(b, "SecondaryCptRefIndex");
uint flags = GetUInt(b, "Flags");
w.WriteLine(
$" [{i}] @ {Off(GetLong(b, "Offset"))} " +
$"piece={GetInt(b, "LocalPieceIndex"),3} " +
$"cptA={CptRef(cpt, primary),-24} " +
$"cptB={CptRef(cpt, secondary),-24} " +
$"default={F(GetFloat(b, "DefaultValue")),8} " +
$"flags={Hex(flags)} {PieceBindingFlags(flags)}");
w.WriteLine(
$" anim1=({F(GetFloat(b, "Anim1Start"))}, {F(GetFloat(b, "Anim1EndOrDuration"))}) " +
$"anim2=({F(GetFloat(b, "Anim2Start"))}, {F(GetFloat(b, "Anim2EndOrDuration"))}) " +
$"weight={F(GetFloat(b, "AnimBlendWeight"))} " +
$"p24={F(GetFloat(b, "BindingParam24", "Unknown24"))} " +
$"p28={F(GetFloat(b, "BindingParam28", "Unknown28"))}");
}
}
private static string ItemFlags(uint flags)
{
var names = new List<string>();
if ((flags & 0x01000000) != 0) names.Add("left_drive_group");
if ((flags & 0x02000000) != 0) names.Add("right_drive_group");
if ((flags & 0x04000000) != 0) names.Add("invert_motion_or_orientation");
return names.Count == 0 ? "" : $"[{string.Join("|", names)}]";
}
private static void DumpItems(CtlFile ctl, CptFile? cpt, TextWriter w)
{
w.WriteLine();
w.WriteLine("Items");
for (int i = 0; i < ctl.ItemRecords.Count; i++)
{
var item = ctl.ItemRecords[i];
int type = GetInt(item, "ItemType");
uint flags = GetUInt(item, "Flags");
var bindingIndices = ToIntList(Get(item, "PieceBindingIndices"));
string name = GetString(item, "ItemName");
string namePart = string.IsNullOrEmpty(name) ? "" : $" name='{name}'";
w.WriteLine(
$" [{i}] @ {Off(GetLong(item, "Offset"))}-{Off(GetLong(item, "EndOffset"))} " +
$"type=0x{type:X2} {ItemTypeName(type)} " +
$"piece={GetInt(item, "LocalPieceIndex"),3} " +
$"flags={Hex(flags)} {ItemFlags(flags)}{namePart}");
w.WriteLine(
$" idx0C/10/14={GetInt(item, "OptionalIndex0C", "Unknown0C")}, " +
$"{GetInt(item, "OptionalIndex10", "Unknown10")}, " +
$"{GetInt(item, "OptionalIndex14", "Unknown14")} " +
$"link={GetInt(item, "OptionalLinkIndex")} " +
$"mobility={GetInt(item, "MobilityContribution")}");
w.WriteLine(
$" p20/24/28/2C={F(GetFloat(item, "Param20", "Unknown20"))}, " +
$"{F(GetFloat(item, "Param24", "Unknown24"))}, " +
$"{F(GetFloat(item, "Param28", "Unknown28"))}, " +
$"{F(GetFloat(item, "Param2C", "Unknown2C"))}");
var resource = Get(item, "Resource");
if (resource is not null && !ResourceIsEmpty(resource))
w.WriteLine($" resource={ResourceToString(resource)}");
var commonParams = ToFloatList(Get(item, "CommonParams"));
var nonZeroParams = commonParams
.Select((v, idx) => (idx, v))
.Where(x => Math.Abs(x.v) > 0.00001f)
.Select(x => $"{x.idx}:{F(x.v)}")
.ToArray();
if (nonZeroParams.Length != 0)
w.WriteLine($" common params: {string.Join(", ", nonZeroParams)}");
if (bindingIndices.Count != 0)
{
w.WriteLine($" binding refs: {string.Join(", ", bindingIndices.Select(x => BindingRef(ctl, cpt, x)))}");
}
}
}
private static void DumpCommandTable(CtlFile ctl, TextWriter w)
{
w.WriteLine();
w.WriteLine($"Command hook table @ {Off(ctl.CommandTable.Offset)}");
w.WriteLine($" on_load : {ctl.CommandTable.OnLoad}");
w.WriteLine($" on_enter_critical_decay: {ctl.CommandTable.OnEnterCriticalDecay}");
w.WriteLine($" on_exit_critical_decay : {ctl.CommandTable.OnExitCriticalDecay}");
for (int i = 0; i < ctl.CommandTable.Indices.Count; i++)
{
int index = ctl.CommandTable.Indices[i];
if (index != -1)
w.WriteLine($" slot[{i,2}] -> list {index}");
}
}
private static void DumpCommandLists(CtlFile ctl, CptFile? cpt, TextWriter w)
{
w.WriteLine();
w.WriteLine("Command lists");
for (int listIndex = 0; listIndex < ctl.CommandLists.Count; listIndex++)
{
var list = ctl.CommandLists[listIndex];
w.WriteLine($" list[{listIndex}] @ {Off(list.Offset)} count={list.Count}");
for (int commandIndex = 0; commandIndex < list.Records.Count; commandIndex++)
{
var cmd = list.Records[commandIndex];
uint opcode = GetUInt(cmd, "Opcode");
var args = GetCommandArgs(cmd);
w.WriteLine(
$" [{commandIndex}] @ {Off(GetLong(cmd, "Offset"))} " +
$"op=0x{opcode:X2} {OpcodeName(opcode)}");
var condition = Get(cmd, "Condition");
if (condition is not null)
{
uint cf = GetUInt(condition, "ConditionFlags");
uint cm = GetUInt(condition, "ConditionMask");
uint ci = GetUInt(condition, "ConditionInvertMask");
if (cf != 0 || cm != 0 || ci != 0)
w.WriteLine($" cond flags={Hex(cf)} mask={Hex(cm)} invert={Hex(ci)}");
}
w.WriteLine(
$" args int : {string.Join(", ", args.Select(x => UncheckedInt(x).ToString(CultureInfo.InvariantCulture)))}");
w.WriteLine(
$" args float: {string.Join(", ", args.Select(x => F(U32AsFloat(x))))}");
var resource = Get(cmd, "Resource");
if (resource is not null && !ResourceIsEmpty(resource))
w.WriteLine($" resource : {ResourceToString(resource)}");
PrintCommandHints(cpt, opcode, args, w);
}
}
}
private static void DumpCpt(CptFile cpt, TextWriter w)
{
for (int i = 0; i < cpt.Records.Count; i++)
{
var r = cpt.Records[i];
string name = i < cpt.Names.Count ? cpt.Names[i] : "";
int piece = GetInt(r, "OwnerPieceIndex");
w.WriteLine(
$" [{i,2}] @ {Off(GetLong(r, "Offset"))} " +
$"name='{name,-16}' OwnerPieceIndex={piece,3} " +
$"TargetPieceIndex={GetInt(r, "TargetPieceIndex"),3} " +
$"pos={Vec(Get(r, "Position"))} dir={Vec(Get(r, "Direction"))}");
}
}
private static void DumpNdp(NdpFile ndp, TextWriter w)
{
float totalMax = 0;
for (int i = 0; i < ndp.Records.Count; i++)
{
var r = ndp.Records[i];
float max = GetFloat(r, "MaxVolume");
float mobility = GetFloat(r, "MobilityContributionPerVolume");
totalMax += max;
w.WriteLine(
$" [{i,2}] @ {Off(GetLong(r, "Offset"))} " +
$"max={F(max),8} mobility/vol={F(mobility),10} " +
$"unknown0={Hex(GetUInt(r, "Unknown0"))} " +
$"resource={ResourceToString(Get(r, "Resource")!)}");
}
w.WriteLine($" total max volume: {F(totalMax)}");
}
private static void PrintCommandHints(CptFile? cpt, uint opcode, IReadOnlyList<uint> args, TextWriter w)
{
if (cpt is null || args.Count < 5)
return;
switch (opcode)
{
case 0x03:
case 0x04:
case 0x05:
case 0x16:
w.WriteLine(
$" cpt hint : " +
$"a0={CptRef(cpt, UncheckedInt(args[0]))}, " +
$"a1={CptRef(cpt, UncheckedInt(args[1]))}, " +
$"a2={CptRef(cpt, UncheckedInt(args[2]))}, " +
$"id/local={UncheckedInt(args[3])}, " +
$"scale/range={F(U32AsFloat(args[4]))}");
break;
}
}
private static string BindingRef(CtlFile ctl, CptFile? cpt, int bindingIndex)
{
if (bindingIndex < 0 || bindingIndex >= ctl.PieceBindings.Count)
return $"{bindingIndex}:<bad>";
var b = ctl.PieceBindings[bindingIndex];
return $"{bindingIndex}:piece {GetInt(b, "LocalPieceIndex")} cptA={CptRef(cpt, GetInt(b, "PrimaryCptRefIndex"))}";
}
private static string CptRef(CptFile? cpt, int index)
{
if (index < 0)
return "-";
if (cpt is null)
return index.ToString(CultureInfo.InvariantCulture);
if (index >= cpt.Records.Count)
return $"{index}:<bad>";
string name = index < cpt.Names.Count ? cpt.Names[index] : "";
var r = cpt.Records[index];
string label = string.IsNullOrEmpty(name) ? $"cpt{index}" : name;
return $"{index}:{label}/owner {r.OwnerPieceIndex}/target {r.TargetPieceIndex}";
}
private static string PieceBindingFlags(uint flags)
{
var names = new List<string>();
if ((flags & 0x01) != 0) names.Add("wrap");
if ((flags & 0x02) != 0) names.Add("invert");
if ((flags & 0x04) != 0) names.Add("skip_anim_init");
if ((flags & 0x08) != 0) names.Add("auto_item_ref");
if ((flags & 0x100) != 0) names.Add("conditional_link_target");
return names.Count == 0 ? "" : $"[{string.Join("|", names)}]";
}
private static string ItemTypeName(int type)
{
return type switch
{
0x01 => "turret drive",
0x02 => "projectile weapon",
0x03 => "mobility support",
0x04 => "targeting system",
0x05 => "engine",
0x08 => "sensor-radar",
0x09 => "force shield",
0x0A => "detection shield",
0x0F => "repair system",
0x11 => "BULL guidance",
0x13 => "battery",
0x14 => "wheel mobility unit",
0x15 => "deflector",
0x16 => "weapon/actuator? type16",
0x1A => "engine alt",
0x1B => "armour",
0x1E => "projectile weapon alt",
_ => $"unknown item type 0x{type:X2}",
};
}
private static string OpcodeName(uint op)
{
return Enum.IsDefined(typeof(EControlCommandOpcode), op) ? ((EControlCommandOpcode)op).ToString() : $"UNKNOWN_0x{op:X2}";
}
private static IReadOnlyList<uint> GetCommandArgs(ControlCommandRecordRaw cmd)
{
return
[
GetUInt(cmd, "Arg0"),
GetUInt(cmd, "Arg1"),
GetUInt(cmd, "Arg2"),
GetUInt(cmd, "Arg3"),
GetUInt(cmd, "Arg4"),
];
}
private static object? Get(object? obj, params string[] names)
{
if (obj is null)
return null;
var t = obj.GetType();
foreach (string name in names)
{
var p = t.GetProperty(name);
if (p is not null)
return p.GetValue(obj);
}
return null;
}
private static int GetInt(object? obj, params string[] names)
{
var v = Get(obj, names);
return v switch
{
Enum e => Convert.ToInt32(e, CultureInfo.InvariantCulture),
int x => x,
uint x => unchecked((int)x),
long x => checked((int)x),
ulong x => checked((int)x),
short x => x,
ushort x => x,
byte x => x,
sbyte x => x,
float x => checked((int)x),
double x => checked((int)x),
_ => 0
};
}
private static uint GetUInt(object? obj, params string[] names)
{
var v = Get(obj, names);
return v switch
{
Enum e => Convert.ToUInt32(e, CultureInfo.InvariantCulture),
uint x => x,
int x => unchecked((uint)x),
long x => checked((uint)x),
ulong x => checked((uint)x),
short x => unchecked((uint)x),
ushort x => x,
byte x => x,
sbyte x => unchecked((uint)x),
_ => 0
};
}
private static long GetLong(object? obj, params string[] names)
{
var v = Get(obj, names);
return v switch
{
long x => x,
int x => x,
uint x => x,
ulong x => checked((long)x),
_ => 0
};
}
private static float GetFloat(object? obj, params string[] names)
{
var v = Get(obj, names);
return v switch
{
float x => x,
double x => (float)x,
int x when Math.Abs(x) > 1_000_000 => BitConverter.Int32BitsToSingle(x),
uint x when x > 1_000_000 => BitConverter.UInt32BitsToSingle(x),
int x => x,
uint x => x,
long x => x,
ulong x => x,
_ => 0
};
}
private static string GetString(object? obj, params string[] names)
{
return Get(obj, names) as string ?? "";
}
private static List<int> ToIntList(object? value)
{
if (value is IEnumerable<int> ints)
return ints.ToList();
if (value is IEnumerable<uint> uints)
return uints.Select(uncheckedInt => unchecked((int)uncheckedInt)).ToList();
return [];
}
private static List<float> ToFloatList(object? value)
{
if (value is IEnumerable<float> floats)
return floats.ToList();
if (value is IEnumerable<double> doubles)
return doubles.Select(x => (float)x).ToList();
return [];
}
private static string Vec(object? v)
{
if (v is null)
return "(0, 0, 0)";
return $"({F(GetFloat(v, "X"))}, {F(GetFloat(v, "Y"))}, {F(GetFloat(v, "Z"))})";
}
private static string ResourceToString(object resource)
{
var archive = GetString(resource, "ArchiveName");
var name = GetString(resource, "ResourceName");
return string.IsNullOrEmpty(archive) || string.IsNullOrEmpty(name)
? "<empty>"
: $"{archive}/{name}";
}
private static bool ResourceIsEmpty(object resource)
{
var archive = GetString(resource, "ArchiveName");
var name = GetString(resource, "ResourceName");
return string.IsNullOrEmpty(archive) || string.IsNullOrEmpty(name);
}
private static string Off(long offset) => $"0x{offset:X4}";
private static string Hex(uint value) => $"0x{value:X8}";
private static int UncheckedInt(uint value) => unchecked((int)value);
private static float U32AsFloat(uint value) => BitConverter.UInt32BitsToSingle(value);
private static string F(float value)
{
if (float.IsNaN(value))
return "NaN";
if (float.IsPositiveInfinity(value))
return "+Inf";
if (float.IsNegativeInfinity(value))
return "-Inf";
return value.ToString("0.######", CultureInfo.InvariantCulture);
}
}
+544
View File
@@ -0,0 +1,544 @@
using System.Text;
namespace ControlLib;
public sealed class ControlReadException : Exception
{
public ControlReadException(string message) : base(message) { }
}
public readonly record struct Vec3(float X, float Y, float Z);
public readonly record struct ResourceNamedRef(string ArchiveName, string ResourceName)
{
public bool IsEmpty => string.IsNullOrEmpty(ArchiveName) || string.IsNullOrEmpty(ResourceName);
public override string ToString() => IsEmpty ? "<empty>" : $"{ArchiveName} / {ResourceName}";
}
public static class ControlResourceReader
{
public static CptFile ReadCptFile(string path)
{
using var fs = File.OpenRead(path);
return ReadCptFile(fs, path);
}
public static CptFile ReadCptFile(Stream stream, string? sourceName = null)
{
var r = new LeReader(stream);
long start = stream.Position;
uint count = r.ReadUInt32();
var records = new List<CptRecordRaw>(checked((int)count));
for (int i = 0; i < count; i++)
{
long off = stream.Position - start;
records.Add(new CptRecordRaw(
Offset: off,
Unknown0: r.ReadInt32(),
OwnerPieceIndex: r.ReadInt32(),
TargetPieceIndex: r.ReadInt32(),
Position: r.ReadVec3(),
Direction: r.ReadVec3()
));
}
var names = new List<string>(checked((int)count));
for (int i = 0; i < count; i++)
{
names.Add(r.ReadFixedString(32));
}
r.ExpectEofOrThrow(sourceName ?? "CPT");
return new CptFile(sourceName, count, records, names);
}
public static NdpFile ReadNdpFile(string path)
{
using var fs = File.OpenRead(path);
return ReadNdpFile(fs, path);
}
public static NdpFile ReadNdpFile(Stream stream, string? sourceName = null)
{
var r = new LeReader(stream);
long start = stream.Position;
uint count = r.ReadUInt32();
var records = new List<NdpRecordRaw>(checked((int)count));
for (int i = 0; i < count; i++)
{
long off = stream.Position - start;
records.Add(new NdpRecordRaw(
Offset: off,
Unknown0: r.ReadUInt32(),
MaxVolume: r.ReadSingle(),
MobilityContributionPerVolume: r.ReadSingle(),
Resource: r.ReadResourceNamedRef()
));
}
r.ExpectEofOrThrow(sourceName ?? "NDP");
return new NdpFile(sourceName, count, records);
}
public static CtlFile ReadCtlFile(string path)
{
using var fs = File.OpenRead(path);
return ReadCtlFile(fs, path);
}
public static CtlFile ReadCtlFile(Stream stream, string? sourceName = null)
{
var r = new LeReader(stream);
long start = stream.Position;
var header = new CtlHeader(
ControlStateCount: r.ReadUInt32(),
ControlStateLinkSlotCount: r.ReadUInt32(),
PieceBindingCount: r.ReadUInt32(),
ItemRecordCount: r.ReadUInt32(),
CommandListCount: r.ReadUInt32()
);
var rootParams = ReadCtlRootControlParams(r, stream.Position - start);
var states = new List<CtlStateRecordRaw>(checked((int)header.ControlStateCount));
for (int i = 0; i < header.ControlStateCount; i++)
{
long off = stream.Position - start;
byte[] fixedBody = r.ReadBytesExact(0x9C);
var links = new List<NavNodeLinkSlotRaw>(checked((int)header.ControlStateLinkSlotCount));
for (int j = 0; j < header.ControlStateLinkSlotCount; j++)
{
links.Add(new NavNodeLinkSlotRaw(
Word0: r.ReadUInt32(),
Word1: r.ReadUInt32(),
Word2: r.ReadUInt32(),
Word3: r.ReadUInt32()
));
}
states.Add(new CtlStateRecordRaw(off, fixedBody, links));
}
int edgeCount = checked((int)(header.ControlStateCount * header.ControlStateCount));
var edgeWeightMatrix = new float[edgeCount];
for (int i = 0; i < edgeWeightMatrix.Length; i++)
edgeWeightMatrix[i] = r.ReadSingle();
var pieceBindings = new List<CtlPieceBindingRecordRaw>(checked((int)header.PieceBindingCount));
for (int i = 0; i < header.PieceBindingCount; i++)
{
long off = stream.Position - start;
pieceBindings.Add(ReadPieceBinding(r, off));
}
var items = new List<CtlItemRecordRaw>(checked((int)header.ItemRecordCount));
for (int i = 0; i < header.ItemRecordCount; i++)
{
long off = stream.Position - start;
items.Add(ReadCtlItemRecord(r, start, off));
}
long commandTableOffset = stream.Position - start;
var commandTable = new ControlCommandListIndexTable(commandTableOffset, r.ReadInt32Array(21));
var commandLists = new List<ControlCommandListRaw>(checked((int)header.CommandListCount));
for (int i = 0; i < header.CommandListCount; i++)
{
long listOffset = stream.Position - start;
uint count = r.ReadUInt32();
var records = new List<ControlCommandRecordRaw>(checked((int)count));
for (int j = 0; j < count; j++)
{
long recordOffset = stream.Position - start;
records.Add(ReadCommandRecord(r, recordOffset));
}
commandLists.Add(new ControlCommandListRaw(listOffset, count, records));
}
r.ExpectEofOrThrow(sourceName ?? "CTL");
return new CtlFile(
SourceName: sourceName,
Header: header,
RootParams: rootParams,
StateRecords: states,
EdgeWeightMatrix: edgeWeightMatrix,
PieceBindings: pieceBindings,
ItemRecords: items,
CommandTable: commandTable,
CommandLists: commandLists
);
}
private static CtlRootControlParams ReadCtlRootControlParams(LeReader r, long offset)
{
return new CtlRootControlParams(
Offset: offset,
MaxTangentialAccel: r.ReadVec3(),
Unknown0C: r.ReadUInt32(),
Unknown10: r.ReadUInt32(),
Unknown14: r.ReadUInt32(),
MaxTangentialSpeed: r.ReadVec3(),
MaxAngularSpeed: r.ReadVec3(),
ExternalTorqueResponseScale: r.ReadVec3(),
VisualEulerAngleLimit: r.ReadVec3(),
DetachCleanupDelayMs: r.ReadInt32(),
Unknown4C: r.ReadSingle(),
Unknown50: r.ReadSingle(),
MovementPhysicsMode: r.ReadInt32(),
Unknown58: r.ReadSingle(),
SlopeLimitAngle: r.ReadSingle(),
Unknown60: r.ReadUInt32(),
Unknown64: r.ReadSingle(),
MobilityIntegrityAmount: r.ReadSingle()
);
}
private static CtlPieceBindingRecordRaw ReadPieceBinding(LeReader r, long offset)
{
return new CtlPieceBindingRecordRaw(
Offset: offset,
LocalPieceIndex: r.ReadInt32(),
Anim1Start: r.ReadSingle(),
Anim1EndOrDuration: r.ReadSingle(),
Anim2Start: r.ReadSingle(),
Anim2EndOrDuration: r.ReadSingle(),
AnimBlendWeight: r.ReadSingle(),
DefaultValue: r.ReadSingle(),
PrimaryCptRefIndex: r.ReadInt32(),
SecondaryCptRefIndex: r.ReadInt32(),
Param24: r.ReadSingle(),
Param28: r.ReadSingle(),
Flags: r.ReadUInt32()
);
}
private static CtlItemRecordRaw ReadCtlItemRecord(LeReader r, long fileStart, long offset)
{
int itemType = r.ReadInt32();
int localPieceIndex = r.ReadInt32();
uint flags = r.ReadUInt32();
int unknown0C = r.ReadInt32();
int unknown10 = r.ReadInt32();
int unknown14 = r.ReadInt32();
int optionalLinkIndex = r.ReadInt32();
uint mobilityContribution = r.ReadUInt32();
float unknown20 = r.ReadSingle();
float unknown24 = r.ReadSingle();
float unknown28 = r.ReadSingle();
float unknown2C = r.ReadSingle();
var commonParams = new float[16];
for (int i = 0; i < commonParams.Length; i++)
commonParams[i] = r.ReadSingle();
string resourceArchiveName = r.ReadFixedString(32);
string resourceResourceName = r.ReadFixedString(32);
uint bindingCount = r.ReadUInt32();
var bindingIndices = new int[checked((int)bindingCount)];
for (int i = 0; i < bindingIndices.Length; i++)
bindingIndices[i] = r.ReadInt32();
int itemNameLength = r.ReadInt32();
string itemName = string.Empty;
if (itemNameLength < 0)
throw new ControlReadException($"Negative CTL item name length {itemNameLength} at 0x{offset:X}");
if (itemNameLength != 0)
{
// The engine allocates length+1 and copies length+1 bytes from the file.
byte[] raw = r.ReadBytesExact(itemNameLength + 1);
int nul = Array.IndexOf(raw, (byte)0);
int len = nul >= 0 ? nul : raw.Length;
itemName = Encoding.ASCII.GetString(raw, 0, len);
}
long endOffset = r.BaseStream.Position - fileStart;
return new CtlItemRecordRaw(
Offset: offset,
EndOffset: endOffset,
ItemType: itemType,
LocalPieceIndex: localPieceIndex,
Flags: flags,
Unknown0C: unknown0C,
Unknown10: unknown10,
Unknown14: unknown14,
OptionalLinkIndex: optionalLinkIndex,
MobilityContribution: mobilityContribution,
Param20: unknown20,
Param24: unknown24,
Param28: unknown28,
Param2C: unknown2C,
CommonParams: commonParams,
Resource: new ResourceNamedRef(resourceArchiveName, resourceResourceName),
PieceBindingIndices: bindingIndices,
ItemNameLength: itemNameLength,
ItemName: itemName
);
}
private static ControlCommandRecordRaw ReadCommandRecord(LeReader r, long offset)
{
var condition = new ControlCommandConditionRaw(
ConditionFlags: r.ReadUInt32(),
ConditionMask: r.ReadUInt32(),
ConditionInvertMask: r.ReadUInt32()
);
return new ControlCommandRecordRaw(
Offset: offset,
Condition: condition,
Opcode: (EControlCommandOpcode)r.ReadUInt32(),
Arg0: r.ReadUInt32(),
Arg1: r.ReadUInt32(),
Arg2: r.ReadUInt32(),
Arg3: r.ReadUInt32(),
Arg4: r.ReadUInt32(),
Resource: r.ReadResourceNamedRef()
);
}
}
public sealed record CptFile(
string? SourceName,
uint Count,
IReadOnlyList<CptRecordRaw> Records,
IReadOnlyList<string> Names);
public sealed record CptRecordRaw(
long Offset,
int Unknown0,
int OwnerPieceIndex,
int TargetPieceIndex,
Vec3 Position,
Vec3 Direction);
public sealed record NdpFile(
string? SourceName,
uint Count,
IReadOnlyList<NdpRecordRaw> Records);
public sealed record NdpRecordRaw(
long Offset,
uint Unknown0,
float MaxVolume,
float MobilityContributionPerVolume,
ResourceNamedRef Resource);
public sealed record CtlFile(
string? SourceName,
CtlHeader Header,
CtlRootControlParams RootParams,
IReadOnlyList<CtlStateRecordRaw> StateRecords,
IReadOnlyList<float> EdgeWeightMatrix,
IReadOnlyList<CtlPieceBindingRecordRaw> PieceBindings,
IReadOnlyList<CtlItemRecordRaw> ItemRecords,
ControlCommandListIndexTable CommandTable,
IReadOnlyList<ControlCommandListRaw> CommandLists);
public readonly record struct CtlHeader(
uint ControlStateCount,
uint ControlStateLinkSlotCount,
uint PieceBindingCount,
uint ItemRecordCount,
uint CommandListCount);
public sealed record CtlRootControlParams(
long Offset,
Vec3 MaxTangentialAccel,
uint Unknown0C,
uint Unknown10,
uint Unknown14,
Vec3 MaxTangentialSpeed,
Vec3 MaxAngularSpeed,
Vec3 ExternalTorqueResponseScale,
Vec3 VisualEulerAngleLimit,
int DetachCleanupDelayMs,
float Unknown4C,
float Unknown50,
int MovementPhysicsMode,
float Unknown58,
float SlopeLimitAngle,
uint Unknown60,
float Unknown64,
float MobilityIntegrityAmount);
public sealed record CtlStateRecordRaw(
long Offset,
byte[] FixedBody,
IReadOnlyList<NavNodeLinkSlotRaw> Links);
public readonly record struct NavNodeLinkSlotRaw(uint Word0, uint Word1, uint Word2, uint Word3);
public sealed record CtlPieceBindingRecordRaw(
long Offset,
int LocalPieceIndex,
float Anim1Start,
float Anim1EndOrDuration,
float Anim2Start,
float Anim2EndOrDuration,
float AnimBlendWeight,
float DefaultValue,
int PrimaryCptRefIndex,
int SecondaryCptRefIndex,
float Param24,
float Param28,
uint Flags);
public sealed record CtlItemRecordRaw(
long Offset,
long EndOffset,
int ItemType,
int LocalPieceIndex,
uint Flags,
int Unknown0C,
int Unknown10,
int Unknown14,
int OptionalLinkIndex,
uint MobilityContribution,
float Param20,
float Param24,
float Param28,
float Param2C,
IReadOnlyList<float> CommonParams,
ResourceNamedRef Resource,
IReadOnlyList<int> PieceBindingIndices,
int ItemNameLength,
string ItemName);
public sealed record ControlCommandListIndexTable(long Offset, IReadOnlyList<int> Indices)
{
public int OnLoad => Indices.Count > 0 ? Indices[0] : -1;
public int OnEnterCriticalDecay => Indices.Count > 6 ? Indices[6] : -1;
public int OnExitCriticalDecay => Indices.Count > 7 ? Indices[7] : -1;
}
public sealed record ControlCommandListRaw(
long Offset,
uint Count,
IReadOnlyList<ControlCommandRecordRaw> Records);
public sealed record ControlCommandRecordRaw(
long Offset,
ControlCommandConditionRaw Condition,
EControlCommandOpcode Opcode,
uint Arg0,
uint Arg1,
uint Arg2,
uint Arg3,
uint Arg4,
ResourceNamedRef Resource);
public enum EControlCommandOpcode : uint {
CONTROL_CMD_RESET_MOTION_COMMANDS = 0,
CONTROL_CMD_MARK_ROOT_CENTRAL = 1,
CONTROL_CMD_UNMARK_ROOT_CENTRAL = 2,
CONTROL_CMD_SPAWN_EFFECT_ON_JOINT_BOUNDS = 3,
CONTROL_CMD_SPAWN_ORIENTED_EFFECT_FROM_CPTS = 4,
CONTROL_CMD_SPAWN_EFFECT_ON_OBJECT_BOUNDS = 5,
CONTROL_CMD_KILL_PIECE = 7,
CONTROL_CMD_REMOVE_EFFECT = 8,
CONTROL_CMD_ITEM_LOAD_RESOURCE = 9,
CONTROL_CMD_START_EFFECT = 0xA,
CONTROL_CMD_REQUEST_STOP_EFFECT = 0xB,
CONTROL_CMD_SET_TANG_ACCEL = 0xC,
CONTROL_CMD_SET_NORM_SPEED = 0xD,
CONTROL_CMD_ATTACH_EFFECT_TO_PIECE = 0xE,
CONTROL_CMD_SCHEDULE_DETACH_OR_KILL = 0xF,
CONTROL_CMD_FORCE_KILL_IGNORE_INVULNERABILITY = 0x11,
CONTROL_CMD_ACTIVATE_EFFECT = 0x12,
CONTROL_CMD_DEACTIVATE_EFFECT = 0x13,
CONTROL_CMD_PLACE_BUILDING_ON_TERRAIN = 0x14,
CONTROL_CMD_DAMAGE_OBJECTS_IN_BOUNDS = 0x15,
CONTROL_CMD_SPAWN_OBJECT_FROM_3_CPTS = 0x16,
CONTROL_CMD_EXECUTE_ITEM_ACTION_FALSE = 0x17,
CONTROL_CMD_SET_ITEM_FLAG_0x100 = 0x18,
CONTROL_CMD_CLEAR_ITEM_FLAG_0x100 = 0x19,
CONTROL_CMD_SWAP_NDP_RESOURCE = 0x1B,
CONTROL_CMD_QUERY_PARENT_CEILING = 0x1D,
CONTROL_CMD_EXECUTE_ITEM_ACTION_TRUE = 0x1E,
CONTROL_CMD_SET_OBJECT_TRANSFORMATION = 0x1F,
CONTROL_CMD_DISTRIBUTE_DAMAGE = 0x23,
CONTROL_CMD_QUEUE_PATH_TO_CONTROL_STATE = 0x28,
CONTROL_CMD_QUEUE_PATH_TO_RANDOM_CONTROL_STATE = 0x29,
CONTROL_CMD_SET_NET_SYNC_FLAG_0x10000000 = 0x2A,
CONTROL_CMD_CLEAR_NET_SYNC_FLAG_0x10000000 = 0x2B,
CONTROL_CMD_UNKNOWN_0x2c = 0x2C,
CONTROL_CMD_UNKNOWN_0x2d = 0x2D,
CONTROL_CMD_UNKNOWN_0x2e = 0x2E,
CONTROL_CMD_SET_NET_SYNCHRO_MODE = 0x2F,
}
public readonly record struct ControlCommandConditionRaw(
uint ConditionFlags,
uint ConditionMask,
uint ConditionInvertMask);
internal sealed class LeReader
{
private readonly Stream _s;
private readonly byte[] _buf = new byte[8];
public LeReader(Stream stream) => _s = stream;
public Stream BaseStream => _s;
public byte[] ReadBytesExact(int count)
{
byte[] bytes = new byte[count];
_s.ReadExactly(bytes, 0, count);
return bytes;
}
public int ReadInt32()
{
_s.ReadExactly(_buf, 0, 4);
return BitConverter.ToInt32(_buf, 0);
}
public uint ReadUInt32()
{
_s.ReadExactly(_buf, 0, 4);
return BitConverter.ToUInt32(_buf, 0);
}
public float ReadSingle()
{
_s.ReadExactly(_buf, 0, 4);
return BitConverter.ToSingle(_buf, 0);
}
public Vec3 ReadVec3() => new(ReadSingle(), ReadSingle(), ReadSingle());
public string ReadFixedString(int byteCount)
{
byte[] raw = ReadBytesExact(byteCount);
int nul = Array.IndexOf(raw, (byte)0);
int len = nul >= 0 ? nul : raw.Length;
return Encoding.ASCII.GetString(raw, 0, len);
}
public ResourceNamedRef ReadResourceNamedRef()
{
return new ResourceNamedRef(
ArchiveName: ReadFixedString(32),
ResourceName: ReadFixedString(32));
}
public int[] ReadInt32Array(int count)
{
var arr = new int[count];
for (int i = 0; i < count; i++)
arr[i] = ReadInt32();
return arr;
}
public void ExpectEofOrThrow(string sourceName)
{
if (_s.CanSeek && _s.Position != _s.Length)
throw new ControlReadException($"{sourceName}: parser stopped at 0x{_s.Position:X}, file length is 0x{_s.Length:X}");
}
}