mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
CTL CPT NDP initial reverse and dump using ai
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
</Project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user