diff --git a/ControlLib/ControlLib.csproj b/ControlLib/ControlLib.csproj new file mode 100644 index 0000000..c632161 --- /dev/null +++ b/ControlLib/ControlLib.csproj @@ -0,0 +1,3 @@ + + + diff --git a/ControlLib/ControlResourceDump.cs b/ControlLib/ControlResourceDump.cs new file mode 100644 index 0000000..d538a56 --- /dev/null +++ b/ControlLib/ControlResourceDump.cs @@ -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(" "); + 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(); + + 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 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}:"; + + 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}:"; + + 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(); + + 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 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 ToIntList(object? value) + { + if (value is IEnumerable ints) + return ints.ToList(); + + if (value is IEnumerable uints) + return uints.Select(uncheckedInt => unchecked((int)uncheckedInt)).ToList(); + + return []; + } + + private static List ToFloatList(object? value) + { + if (value is IEnumerable floats) + return floats.ToList(); + + if (value is IEnumerable 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) + ? "" + : $"{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); + } +} \ No newline at end of file diff --git a/ControlLib/ControlResourceReaders.cs b/ControlLib/ControlResourceReaders.cs new file mode 100644 index 0000000..d746025 --- /dev/null +++ b/ControlLib/ControlResourceReaders.cs @@ -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 ? "" : $"{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(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(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(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(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(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(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(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(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(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 Records, + IReadOnlyList 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 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 StateRecords, + IReadOnlyList EdgeWeightMatrix, + IReadOnlyList PieceBindings, + IReadOnlyList ItemRecords, + ControlCommandListIndexTable CommandTable, + IReadOnlyList 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 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 CommonParams, + ResourceNamedRef Resource, + IReadOnlyList PieceBindingIndices, + int ItemNameLength, + string ItemName); + +public sealed record ControlCommandListIndexTable(long Offset, IReadOnlyList 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 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}"); + } +} diff --git a/ParkanPlayground.slnx b/ParkanPlayground.slnx index 449019b..76167bb 100644 --- a/ParkanPlayground.slnx +++ b/ParkanPlayground.slnx @@ -6,6 +6,7 @@ + diff --git a/ParkanPlayground/Program.cs b/ParkanPlayground/Program.cs index d8d9dcb..d1eba72 100644 --- a/ParkanPlayground/Program.cs +++ b/ParkanPlayground/Program.cs @@ -1,8 +1,28 @@ +using System.Text.Json; using Common; +using ControlLib; using MshLib; System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); +// +var ctl = ControlResourceReader.ReadCtlFile("E:\\ParkanUnpacked\\bases.rlb\\2_CTLD_r_l_03.ctl"); +var cpt = ControlResourceReader.ReadCptFile("E:\\ParkanUnpacked\\bases.rlb\\18_CTPT_R_L_03.CPT"); +var ndp = ControlResourceReader.ReadNdpFile("E:\\ParkanUnpacked\\bases.rlb\\14_NDPR_r_l_03.ndp"); -var converter = new MshConverter(); +// var ctl = ControlResourceReader.ReadCtlFile("E:\\ParkanUnpacked\\turrets.rlb\\30_CTLD_o_tur_lt_02.ctl"); +// var cpt = ControlResourceReader.ReadCptFile("E:\\ParkanUnpacked\\turrets.rlb\\162_CTPT_o_tur_lt_02.cpt"); +// var ndp = ControlResourceReader.ReadNdpFile("E:\\ParkanUnpacked\\turrets.rlb\\119_NDPR_o_tur_la_02.ndp"); -converter.Convert("E:\\ParkanUnpacked\\fortif.rlb\\130_fr_b_plant.msh"); \ No newline at end of file +// var ctl = ControlResourceReader.ReadCtlFile("E:\\ParkanUnpacked\\guns.rlb\\86_CTLD_o_gun_la_01.ctl"); +// var cpt = ControlResourceReader.ReadCptFile("E:\\ParkanUnpacked\\guns.rlb\\364_CTPT_o_gun_la_01.cpt"); +// var ndp = ControlResourceReader.ReadNdpFile("E:\\ParkanUnpacked\\guns.rlb\\309_NDPR_o_gun_la_01.ndp"); + +ControlResourceDump.DumpAll(ctl, cpt, ndp, Console.Out); + +// foreach (var item in ctl.ItemRecords) +// { +// Console.WriteLine( +// $"0x{item.Offset:X4}-0x{item.EndOffset:X4} " + +// $"type={item.ItemType:X} piece={item.LocalPieceIndex} " + +// $"name='{item.ItemName}' bindings=[{string.Join(", ", item.PieceBindingIndices)}]"); +// } \ No newline at end of file