using Collections.Pooled; using SharpDX; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Xml; using static CodeWalker.GameFiles.MetaXmlBase; /* Copyright(c) 2016 Neodymium Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //ruthlessly stolen namespace CodeWalker.GameFiles { [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipDictionary : ResourceFileBase { public override long BlockLength => 64; // structure data public uint Unknown_10h { get; set; } // 0x00000000 public uint Unknown_14h { get; set; } // 0x00000000 public ulong AnimationsPointer { get; set; } public uint Unknown_20h { get; set; } = 0x00000101; public uint Unknown_24h { get; set; } // 0x00000000 public ulong ClipsPointer { get; set; } public ushort ClipsMapCapacity { get; set; } public ushort ClipsMapEntries { get; set; } public uint Unknown_34h { get; set; } = 0x01000000; public uint Unknown_38h { get; set; } // 0x00000000 public uint Unknown_3Ch { get; set; } // 0x00000000 // reference data public AnimationMap Animations { get; set; } public ResourcePointerArray64 Clips { get; set; } //data used by CW for loading/saving public Dictionary ClipMap { get; set; } public Dictionary AnimMap { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Unknown_10h = reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.AnimationsPointer = reader.ReadUInt64(); this.Unknown_20h = reader.ReadUInt32(); this.Unknown_24h = reader.ReadUInt32(); this.ClipsPointer = reader.ReadUInt64(); this.ClipsMapCapacity = reader.ReadUInt16(); this.ClipsMapEntries = reader.ReadUInt16(); this.Unknown_34h = reader.ReadUInt32(); this.Unknown_38h = reader.ReadUInt32(); this.Unknown_3Ch = reader.ReadUInt32(); // read reference data this.Animations = reader.ReadBlockAt( this.AnimationsPointer // offset ); this.Clips = reader.ReadBlockAt>( this.ClipsPointer, // offset this.ClipsMapCapacity ); BuildMaps(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // update structure data this.AnimationsPointer = (ulong)(this.Animations != null ? this.Animations.FilePosition : 0); this.ClipsPointer = (ulong)(this.Clips != null ? this.Clips.FilePosition : 0); this.ClipsMapCapacity = (ushort)((Clips != null) ? Clips.Count : 0); this.ClipsMapEntries = (ushort)((ClipMap != null) ? ClipMap.Count : 0); // write structure data writer.Write(this.Unknown_10h); writer.Write(this.Unknown_14h); writer.Write(this.AnimationsPointer); writer.Write(this.Unknown_20h); writer.Write(this.Unknown_24h); writer.Write(this.ClipsPointer); writer.Write(this.ClipsMapCapacity); writer.Write(this.ClipsMapEntries); writer.Write(this.Unknown_34h); writer.Write(this.Unknown_38h); writer.Write(this.Unknown_3Ch); } public override IResourceBlock[] GetReferences() { var list = new List(base.GetReferences()); if (Animations != null) { list.Add(Animations); Animations.AnimationsMapEntries = (ushort)(AnimMap?.Count ?? 0); } if (Clips != null) list.Add(Clips); return list.ToArray(); } public void BuildMaps() { ClipMap = new Dictionary(); AnimMap = new Dictionary(); if (Clips?.data_items is not null) { foreach (var cme in Clips.data_items) { if (cme is not null) { ClipMap[cme.Hash] = cme; var nxt = cme.Next; while (nxt != null) { ClipMap[nxt.Hash] = nxt; nxt = nxt.Next; } } } } if (Animations?.Animations?.data_items is not null) { foreach (var ame in Animations.Animations.data_items) { if (ame is not null) { AnimMap[ame.Hash] = ame; var nxt = ame.NextEntry; while (nxt != null) { AnimMap[nxt.Hash] = nxt; nxt = nxt.NextEntry; } } } } //foreach (var cme in ClipMap.Values) //{ // var clip = cme.Clip; // if (clip == null) // continue; // var name = clip.ShortName; //just to make sure ShortName is generated and in JenkIndex... // //if (name.EndsWith("_uv_0")) //hash for these entries match string with this removed, +1 // //{ // //} // //if (name.EndsWith("_uv_1")) //same as above, but +2 // //{ // //} //} //foreach (var ame in AnimMap.Values) //{ // var anim = ame.Animation; // if (anim == null) continue; //} } public void UpdateUsageCounts() { var usages = new Dictionary(); void addUsage(MetaHash h) { uint u = 0; usages.TryGetValue(h, out u); u++; usages[h] = u; } if ((Animations != null) && (Animations.Animations != null) && (Animations.Animations.data_items != null)) { foreach (var ame in Animations.Animations.data_items) { if (ame != null) { addUsage(ame.Hash); var nxt = ame.NextEntry; while (nxt != null) { addUsage(nxt.Hash); nxt = nxt.NextEntry; } } } } foreach (var cme in ClipMap.Values) { var ca = cme.Clip as ClipAnimation; var cal = cme.Clip as ClipAnimationList; if (ca?.Animation != null) { addUsage(ca.Animation.Hash); } if (cal?.Animations != null) { foreach (var cae in cal.Animations) { if (cae?.Animation != null) { addUsage(cae.Animation.Hash); } } } } foreach (var ame in AnimMap.Values) { if (ame.Animation != null) { uint u = 0; if (usages.TryGetValue(ame.Animation.Hash, out u)) { if (ame.Animation.UsageCount != u) { } ame.Animation.UsageCount = u; } else { } } } } public void WriteXml(StringBuilder sb, int indent) { var clips = new List(); if (ClipMap != null) { foreach (var cme in ClipMap.Values) { if (cme?.Clip == null) continue; clips.Add(cme.Clip); } } var anims = new List(); if (AnimMap != null) { foreach (var ame in AnimMap.Values) { if (ame?.Animation == null) continue; anims.Add(ame.Animation); } } YcdXml.WriteItemArray(sb, clips.ToArray(), indent, "Clips"); YcdXml.WriteItemArray(sb, anims.ToArray(), indent, "Animations"); } public void ReadXml(XmlNode node) { var clipList = new List(); var clipsNode = node.SelectSingleNode("Clips"); if (clipsNode != null) { var inodes = clipsNode.SelectNodes("Item"); if (inodes?.Count > 0) { foreach (XmlNode inode in inodes) { var type = Xml.GetEnumValue(Xml.GetChildStringAttribute(inode, "Type", "value")); var c = ClipBase.ConstructClip(type); c.ReadXml(inode); var cme = new ClipMapEntry(); cme.Hash = c.Hash; cme.Clip = c; clipList.Add(cme); } } } var animDict = new Dictionary(); var animList = new List(); var anims = XmlMeta.ReadItemArrayNullable(node, "Animations"); if (anims != null) { foreach (var anim in anims) { animDict[anim.Hash] = anim; var ame = new AnimationMapEntry(); ame.Hash = anim.Hash; ame.Animation = anim; animList.Add(ame); } } foreach (var cme in clipList) { var cb = cme?.Clip; if (cb is ClipAnimation clipanim) { animDict.TryGetValue(clipanim.AnimationHash, out Animation a); clipanim.Animation = a; } var clipanimlist = cb as ClipAnimationList; if (clipanimlist?.Animations?.Data != null) { foreach (var cae in clipanimlist.Animations.Data) { animDict.TryGetValue(cae.AnimationHash, out Animation a); cae.Animation = a; } } } CreateAnimationsMap(animList.ToArray()); CreateClipsMap(clipList.ToArray()); BuildMaps(); UpdateUsageCounts(); } public void CreateClipsMap(ClipMapEntry[] clips) { var numClipBuckets = GetNumHashBuckets(clips?.Length ?? 0); var clipBuckets = new List[numClipBuckets]; if (clips is not null) { foreach (var cme in clips) { var b = cme.Hash % numClipBuckets; ref var bucket = ref clipBuckets[b]; if (bucket is null) { bucket = new List(); } bucket.Add(cme); } } var newClips = new List(); foreach (var b in clipBuckets) { if ((b?.Count ?? 0) == 0) newClips.Add(null); else { newClips.Add(b[0]); var p = b[0]; for (int i = 1; i < b.Count; i++) { var c = b[i]; c.Next = null; p.Next = c; p = c; } } } Clips = new ResourcePointerArray64(); Clips.data_items = newClips.ToArray(); } public void CreateAnimationsMap(AnimationMapEntry[] anims) { var numAnimBuckets = GetNumHashBuckets(anims?.Length ?? 0); var animBuckets = new List[numAnimBuckets]; if (anims is not null) { foreach (var ame in anims) { var b = ame.Hash % numAnimBuckets; ref var bucket = ref animBuckets[b]; if (bucket is null) { bucket = new List(); } bucket.Add(ame); } } var newAnims = new List(); foreach (var b in animBuckets) { if (b is null || b.Count == 0) newAnims.Add(null); else { newAnims.Add(b[0]); var p = b[0]; for (int i = 1; i < b.Count; i++) { var c = b[i]; c.NextEntry = null; p.NextEntry = c; p = c; } } } Animations = new AnimationMap(); Animations.Animations = new ResourcePointerArray64(); Animations.Animations.data_items = newAnims.ToArray(); } public static uint GetNumHashBuckets(int nHashes) { if (nHashes < 11) return 11; else if (nHashes < 29) return 29; else if (nHashes < 59) return 59; else if (nHashes < 107) return 107; else if (nHashes < 191) return 191; else if (nHashes < 331) return 331; else if (nHashes < 563) return 563; else if (nHashes < 953) return 953; else if (nHashes < 1609) return 1609; else if (nHashes < 2729) return 2729; else if (nHashes < 4621) return 4621; else if (nHashes < 7841) return 7841; else if (nHashes < 13297) return 13297; else if (nHashes < 22571) return 22571; else if (nHashes < 38351) return 38351; else if (nHashes < 65167) return 65167; else /*if (nHashes < 65521)*/ return 65521; //return ((uint)nHashes / 4) * 4 + 3; } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimationMap : ResourceSystemBlock { public override long BlockLength => 48; // structure data public uint VFT { get; set; } public uint Unknown_04h { get; set; } = 1; // 0x00000001 public uint Unknown_08h { get; set; } = 0; // 0x00000000 public uint Unknown_0Ch { get; set; } = 0; // 0x00000000 public uint Unknown_10h { get; set; } = 0; // 0x00000000 public uint Unknown_14h { get; set; } = 0; // 0x00000000 public ulong AnimationsPointer { get; set; } public ushort AnimationsMapCapacity { get; set; } public ushort AnimationsMapEntries { get; set; } public uint Unknown_24h { get; set; } = 16777216; public uint Unknown_28h { get; set; } = 1; // 0x00000001 public uint Unknown_2Ch { get; set; } = 0; // 0x00000000 // reference data public ResourcePointerArray64 Animations { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.VFT = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); this.Unknown_08h = reader.ReadUInt32(); this.Unknown_0Ch = reader.ReadUInt32(); this.Unknown_10h = reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.AnimationsPointer = reader.ReadUInt64(); this.AnimationsMapCapacity = reader.ReadUInt16(); this.AnimationsMapEntries = reader.ReadUInt16(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28h = reader.ReadUInt32(); this.Unknown_2Ch = reader.ReadUInt32(); // read reference data this.Animations = reader.ReadBlockAt>( this.AnimationsPointer, // offset this.AnimationsMapCapacity ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.AnimationsPointer = (ulong)(this.Animations != null ? this.Animations.FilePosition : 0); this.AnimationsMapCapacity = (ushort)(this.Animations != null ? this.Animations.Count : 0); //this.AnimationsMapEntries //this is already set by ClipDictionary // write structure data writer.Write(this.VFT); writer.Write(this.Unknown_04h); writer.Write(this.Unknown_08h); writer.Write(this.Unknown_0Ch); writer.Write(this.Unknown_10h); writer.Write(this.Unknown_14h); writer.Write(this.AnimationsPointer); writer.Write(this.AnimationsMapCapacity); writer.Write(this.AnimationsMapEntries); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28h); writer.Write(this.Unknown_2Ch); } public override IResourceBlock[] GetReferences() { if (Animations is null) { return []; } return [ Animations ]; } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimationMapEntry : ResourceSystemBlock { public override long BlockLength => 32; // structure data public MetaHash Hash { get; set; } public uint Unknown_04h { get; set; } // 0x00000000 public ulong AnimationPtr { get; set; } public ulong NextEntryPtr { get; set; } public uint Unknown_18h { get; set; } // 0x00000000 public uint Unknown_1Ch { get; set; } // 0x00000000 // reference data public Animation? Animation { get; set; } public AnimationMapEntry? NextEntry { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.Hash = new MetaHash(reader.ReadUInt32()); this.Unknown_04h = reader.ReadUInt32(); this.AnimationPtr = reader.ReadUInt64(); this.NextEntryPtr = reader.ReadUInt64(); this.Unknown_18h = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); // read reference data this.Animation = reader.ReadBlockAt( this.AnimationPtr // offset ); this.NextEntry = reader.ReadBlockAt( this.NextEntryPtr // offset ); if (Animation is not null) { Animation.Hash = Hash; } } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.AnimationPtr = (ulong)(this.Animation != null ? this.Animation.FilePosition : 0); this.NextEntryPtr = (ulong)(this.NextEntry != null ? this.NextEntry.FilePosition : 0); // write structure data writer.Write(this.Hash); writer.Write(this.Unknown_04h); writer.Write(this.AnimationPtr); writer.Write(this.NextEntryPtr); writer.Write(this.Unknown_18h); writer.Write(this.Unknown_1Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Animation is not null) list.Add(Animation); if (NextEntry is not null) list.Add(NextEntry); return list.ToArray(); } public override string ToString() => Hash.ToString(); } [TypeConverter(typeof(ExpandableObjectConverter))] public class Animation : ResourceSystemBlock, IMetaXmlItem { public override long BlockLength => 96; // structure data public uint VFT { get; set; } public uint Unknown_04h { get; set; } = 1; // 0x00000001 public uint Unused_08h { get; set; } // 0x00000000 public uint Unused_0Ch { get; set; } // 0x00000000 public byte Unknown_10h { get; set; } public byte Unknown_11h { get; set; } = 1; // 0x01 public ushort Unused_12h { get; set; } // 0x0000 public ushort Frames { get; set; } public ushort SequenceFrameLimit { get; set; } public float Duration { get; set; } public MetaHash Unknown_1Ch { get; set; } public uint Unused_20h { get; set; } // 0x00000000 public uint Unused_24h { get; set; } // 0x00000000 public uint Unused_28h { get; set; } // 0x00000000 public uint Unused_2Ch { get; set; } // 0x00000000 public uint Unused_30h { get; set; } // 0x00000000 public uint Unused_34h { get; set; } // 0x00000000 public uint MaxSeqBlockLength { get; set; } public uint UsageCount { get; set; } public ResourcePointerList64 Sequences { get; set; } public ResourceSimpleList64_s BoneIds { get; set; } public YcdFile Ycd { get; set; } public MetaHash Hash { get; set; } //updated by CW, for use when reading/writing files public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.VFT = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); //1 1 1 1 this.Unused_08h = reader.ReadUInt32(); //0 0 0 0 this.Unused_0Ch = reader.ReadUInt32(); //0 0 0 0 this.Unknown_10h = reader.ReadByte(); //1 1 1 1 this.Unknown_11h = reader.ReadByte(); //1 this.Unused_12h = reader.ReadUInt16(); //0 0 0 0 this.Frames = reader.ReadUInt16(); //221 17 151 201 frames this.SequenceFrameLimit = reader.ReadUInt16(); //223 31 159 207 sequence limit this.Duration = reader.ReadSingle(); //7.34 0.53 5.0 6.66 duration this.Unknown_1Ch = reader.ReadUInt32(); this.Unused_20h = reader.ReadUInt32(); //0 0 0 0 this.Unused_24h = reader.ReadUInt32(); //0 0 0 0 this.Unused_28h = reader.ReadUInt32(); //0 0 0 0 this.Unused_2Ch = reader.ReadUInt32(); //0 0 0 0 this.Unused_30h = reader.ReadUInt32(); //0 0 0 0 this.Unused_34h = reader.ReadUInt32(); //0 0 0 0 this.MaxSeqBlockLength = reader.ReadUInt32(); //314 174 1238 390 maximum sequence block size this.UsageCount = reader.ReadUInt32(); //2 2 2 2 this.Sequences = reader.ReadBlock>(); this.BoneIds = reader.ReadBlock>(); AssignSequenceBoneIds(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { //BuildSequencesData(); // write structure data writer.Write(this.VFT); writer.Write(this.Unknown_04h); writer.Write(this.Unused_08h); writer.Write(this.Unused_0Ch); writer.Write(this.Unknown_10h); writer.Write(this.Unknown_11h); writer.Write(this.Unused_12h); writer.Write(this.Frames); writer.Write(this.SequenceFrameLimit); writer.Write(this.Duration); writer.Write(this.Unknown_1Ch); writer.Write(this.Unused_20h); writer.Write(this.Unused_24h); writer.Write(this.Unused_28h); writer.Write(this.Unused_2Ch); writer.Write(this.Unused_30h); writer.Write(this.Unused_34h); writer.Write(this.MaxSeqBlockLength); writer.Write(this.UsageCount); writer.WriteBlock(this.Sequences); writer.WriteBlock(this.BoneIds); } public override (long, IResourceBlock)[] GetParts() { BuildSequencesData();//TODO: move this somewhere better? return new (long, IResourceBlock)[] { (0x40, Sequences), (0x50, BoneIds) }; } public void AssignSequenceBoneIds() { if (Sequences?.data_items != null) { foreach (var seq in Sequences.data_items) { for (int i = 0; i < seq?.Sequences?.Length; i++) { if (i < BoneIds?.data_items?.Length) { seq.Sequences[i].BoneId = BoneIds.data_items[i]; } } } } } public void CalculateMaxSeqBlockLength() { if (Sequences?.data_items != null) { uint maxSize = 0; foreach (var seq in Sequences.data_items) { maxSize = Math.Max(maxSize, (uint)seq.BlockLength); } MaxSeqBlockLength = maxSize; } } public void BuildSequencesData() { AssignSequenceBoneIds(); if (Sequences?.data_items != null) { foreach (var seq in Sequences.data_items) { seq.BuildData(); } } CalculateMaxSeqBlockLength(); } public void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "Hash", YcdXml.HashString(Hash)); YcdXml.ValueTag(sb, indent, "Unknown10", Unknown_10h.ToString()); YcdXml.ValueTag(sb, indent, "FrameCount", Frames.ToString()); YcdXml.ValueTag(sb, indent, "SequenceFrameLimit", SequenceFrameLimit.ToString());//sequences should be transparent to this! YcdXml.ValueTag(sb, indent, "Duration", FloatUtil.ToString(Duration)); YcdXml.StringTag(sb, indent, "Unknown1C", YcdXml.HashString(Unknown_1Ch)); YcdXml.WriteItemArray(sb, BoneIds?.data_items, indent, "BoneIds"); YcdXml.WriteItemArray(sb, Sequences?.data_items, indent, "Sequences"); } public void ReadXml(XmlNode node) { Hash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "Hash")); Unknown_10h = (byte)Xml.GetChildUIntAttribute(node, "Unknown10", "value"); Frames = (ushort)Xml.GetChildUIntAttribute(node, "FrameCount", "value"); SequenceFrameLimit = (ushort)Xml.GetChildUIntAttribute(node, "SequenceFrameLimit", "value"); Duration = Xml.GetChildFloatAttribute(node, "Duration", "value"); Unknown_1Ch = XmlMeta.GetHash(Xml.GetChildInnerText(node, "Unknown1C")); BoneIds = new ResourceSimpleList64_s(); BoneIds.data_items = XmlMeta.ReadItemArray(node, "BoneIds"); Sequences = new ResourcePointerList64(); Sequences.data_items = XmlMeta.ReadItemArrayNullable(node, "Sequences"); AssignSequenceBoneIds(); } public readonly struct FramePosition { public int Frame0 { get; init; } public int Frame1 { get; init; } public float Alpha0 { get; init; } public float Alpha1 { get; init; } } public FramePosition GetFramePosition(float t) { bool ignoreLastFrame = true;//if last frame is equivalent to the first one, eg rollercoaster small light "globes" don't var nframes = (ignoreLastFrame) ? (Frames - 1) : Frames; var curPos = (t / Duration) * nframes; var frame0 = ((ushort)curPos) % Frames; var alpha1 = (float)(curPos - Math.Floor(curPos)); return new FramePosition { Frame0 = frame0, Frame1 = frame0 + 1, Alpha0 = 1.0f - alpha1, Alpha1 = alpha1, }; } public Vector4 EvaluateVector4(in FramePosition frame, int boneIndex, bool interpolate) { var s = frame.Frame0 / SequenceFrameLimit; int f0 = frame.Frame0 % SequenceFrameLimit; int f1 = f0 + 1; var seq = Sequences.data_items[s]; var aseq = seq.Sequences[boneIndex]; var v0 = aseq.EvaluateVector(f0); var v1 = aseq.EvaluateVector(f1); var v = interpolate ? (v0 * frame.Alpha0) + (v1 * frame.Alpha1) : v0; return v; } public Quaternion EvaluateQuaternion(in FramePosition frame, int boneIndex, bool interpolate) { var s = frame.Frame0 / SequenceFrameLimit; int f0 = frame.Frame0 % SequenceFrameLimit; int f1 = f0 + 1; var seq = Sequences.data_items[s]; var aseq = seq.Sequences[boneIndex]; var q0 = aseq.EvaluateQuaternion(f0); var q1 = aseq.EvaluateQuaternion(f1); var q = interpolate ? QuaternionExtension.FastLerp(in q0, in q1, frame.Alpha1) : q0; return q; } public int FindBoneIndex(ushort boneTag, byte track) { //TODO: make this use a dict?? if (BoneIds?.data_items != null) { for (int i = 0; i < BoneIds.data_items.Length; i++) { var b = BoneIds.data_items[i]; if ((b.BoneId == boneTag) && (b.Track == track)) return i; } } return -1; } } [TypeConverter(typeof(ExpandableObjectConverter))] public struct AnimationBoneId : IMetaXmlItem { public ushort BoneId { get; set; } public byte Unk0 { get; set; } public byte Track { get; set; } public readonly override string ToString() => $"{BoneId}: {Unk0}, {Track}"; public readonly void WriteXml(StringBuilder sb, int indent) { YcdXml.ValueTag(sb, indent, "BoneId", BoneId.ToString()); YcdXml.ValueTag(sb, indent, "Track", Track.ToString()); YcdXml.ValueTag(sb, indent, "Unk0", Unk0.ToString()); } public void ReadXml(XmlNode node) { BoneId = (ushort)Xml.GetChildUIntAttribute(node, "BoneId", "value"); Track = (byte)Xml.GetChildUIntAttribute(node, "Track", "value"); Unk0 = (byte)Xml.GetChildUIntAttribute(node, "Unk0", "value"); } } public enum AnimChannelType : int { StaticQuaternion = 0, StaticVector3 = 1, StaticFloat = 2, RawFloat = 3, QuantizeFloat = 4, IndirectQuantizeFloat = 5, LinearFloat = 6, CachedQuaternion1 = 7, CachedQuaternion2 = 8, } [TypeConverter(typeof(ExpandableObjectConverter))] public abstract class AnimChannel : IMetaXmlItem { public AnimChannelType Type { get; set; } public int Sequence { get; set; } public int Index { get; set; } public int DataOffset { get; set; } public int FrameOffset { get; set; } public abstract void Read(AnimChannelDataReader reader); public virtual void Write(AnimChannelDataWriter writer) { } public virtual void ReadFrame(AnimChannelDataReader reader) { } public virtual void WriteFrame(AnimChannelDataWriter writer) { } public virtual int GetReferenceIndex() { return Index; } public virtual int GetFrameBits() { return 0; } public virtual float EvaluateFloat(int frame) => 0.0f; public void Associate(int sequence, int index) { Sequence = sequence; Index = index; } public virtual void WriteXml(StringBuilder sb, int indent) { YcdXml.ValueTag(sb, indent, "Type", Type.ToString()); //YcdXml.ValueTag(sb, indent, "Sequence", Sequence.ToString()); //YcdXml.ValueTag(sb, indent, "Index", Index.ToString()); } public virtual void ReadXml(XmlNode node) { //not necessary to read Type as it's already read and set in constructor //Type = Xml.GetEnumValue(Xml.GetChildStringAttribute(node, "Type", "value")); //Sequence = Xml.GetChildIntAttribute(node, "Sequence", "value"); //Index = Xml.GetChildIntAttribute(node, "Index", "value"); } public static AnimChannel ConstructChannel(AnimChannelType type) { switch (type) { case AnimChannelType.StaticQuaternion: return new AnimChannelStaticQuaternion(); case AnimChannelType.StaticVector3: return new AnimChannelStaticVector3(); case AnimChannelType.StaticFloat: return new AnimChannelStaticFloat(); case AnimChannelType.RawFloat: return new AnimChannelRawFloat(); case AnimChannelType.QuantizeFloat: return new AnimChannelQuantizeFloat(); case AnimChannelType.IndirectQuantizeFloat: return new AnimChannelIndirectQuantizeFloat(); case AnimChannelType.LinearFloat: return new AnimChannelLinearFloat(); case AnimChannelType.CachedQuaternion1: // normalized W from quaternion (evaluate first three channels, calculate W) return new AnimChannelCachedQuaternion(AnimChannelType.CachedQuaternion1); case AnimChannelType.CachedQuaternion2: // unknown extra // kind of the same as above but different at runtime? return new AnimChannelCachedQuaternion(AnimChannelType.CachedQuaternion2); default: return null; } } public override string ToString() { return Sequence.ToString() + ": " + Index.ToString() + ": " + Type.ToString() + " DataOffset: " + DataOffset.ToString() + " FrameOffset: " + FrameOffset.ToString(); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelStaticFloat : AnimChannel { public float Value { get; set; } public AnimChannelStaticFloat() { Type = AnimChannelType.StaticFloat; } public override void Read(AnimChannelDataReader reader) { Value = reader.ReadSingle(); } public override void Write(AnimChannelDataWriter writer) { writer.Write(Value); } public override float EvaluateFloat(int frame) { return Value; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Value", FloatUtil.ToString(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildFloatAttribute(node, "Value", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelStaticVector3 : AnimChannel { public Vector3 Value { get; set; } public AnimChannelStaticVector3() { Type = AnimChannelType.StaticVector3; } public override void Read(AnimChannelDataReader reader) { Value = reader.ReadVector3(); } public override void Write(AnimChannelDataWriter writer) { writer.Write(Value); } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.SelfClosingTag(sb, indent, "Value " + FloatUtil.GetVector3XmlString(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildVector3Attributes(node, "Value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelStaticQuaternion : AnimChannel { public Quaternion Value { get; set; } public AnimChannelStaticQuaternion() { Type = AnimChannelType.StaticQuaternion; } public override void Read(AnimChannelDataReader reader) { var vec = reader.ReadVector3(); Value = new Quaternion( vec, (float)Math.Sqrt(Math.Max(1.0f - vec.LengthSquared(), 0.0)) ); } public override void Write(AnimChannelDataWriter writer) { writer.Write(Value.ToVector4().XYZ());//heh } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.SelfClosingTag(sb, indent, "Value " + FloatUtil.GetVector4XmlString(Value.ToVector4())); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = new Quaternion(Xml.GetChildVector4Attributes(node, "Value")); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelIndirectQuantizeFloat : AnimChannel { public int FrameBits { get; set; } public int ValueBits { get; set; } public int NumInts { get; set; } public float Quantum { get; set; } public float Offset { get; set; } public float[] Values { get; set; } public uint[] ValueList { get; set; } public uint[] Frames { get; set; } public AnimChannelIndirectQuantizeFloat() { Type = AnimChannelType.IndirectQuantizeFloat; } public override void Read(AnimChannelDataReader reader) { FrameBits = reader.ReadInt32(); ValueBits = reader.ReadInt32(); NumInts = reader.ReadInt32(); Quantum = reader.ReadSingle(); Offset = reader.ReadSingle(); Frames = new uint[reader.NumFrames]; var numValues0 = (NumInts * 32) / ValueBits; var numValues1 = (1u << FrameBits) - 1; var numValues = Math.Min(numValues0, numValues1); //any better way to calculate this? Values = new float[numValues]; ValueList = new uint[numValues]; reader.BitPosition = reader.Position * 8; for (int i = 0; i < numValues; i++) { uint bits = reader.ReadBits(ValueBits); Values[i] = (bits * Quantum) + Offset; ValueList[i] = bits; } reader.Position += NumInts * 4; //var endBit = bit + (NumInts * 32); //var valueList = new List(); //while (bit < endBit) // this actually seems to be reading too far..... //{ // valueList.Add((reader.GetBit(bit, ValueBits) * Quantum) + Offset); // bit += ValueBits; //} //Values = valueList.ToArray(); if (FrameBits < 2) { } if (ValueBits < 3) { } } public override void Write(AnimChannelDataWriter writer) { var frameBits = Math.Max(writer.BitCount((uint)((Values?.Length ?? 1))), 2);// Math.Max(writer.BitCount(Frames), 2); //if ((frameBits != FrameBits)&&(ValueList!=null)) //{ } // ######### DEBUG TEST FrameBits = frameBits; var valueCount = Values?.Length ?? 0; var valueList = new uint[valueCount]; for (int i = 0; i < valueCount; i++) { var bits = GetQuanta(Values[i]); valueList[i] = bits; //if (ValueList != null) // ######### DEBUG TEST //{ // var testbits = ValueList[i]; // if (bits != testbits) // { } //} } var valueBits = Math.Max(writer.BitCount(valueList), 3); //if ((valueBits != ValueBits)&&(ValueList!=null)) //{ }// ######### DEBUG TEST ValueBits = valueBits; writer.ResetBitstream(); for (int i = 0; i < valueCount; i++) { var u = valueList[i]; writer.WriteBits(u, ValueBits); } NumInts = writer.Bitstream.Count; writer.Write(FrameBits); writer.Write(ValueBits); writer.Write(NumInts); writer.Write(Quantum); writer.Write(Offset); writer.WriteBitstream(); } public override void ReadFrame(AnimChannelDataReader reader) { Frames[reader.Frame] = reader.ReadFrameBits(FrameBits); } public override void WriteFrame(AnimChannelDataWriter writer) { writer.WriteFrameBits(Frames[writer.Frame], FrameBits); } public override int GetFrameBits() { return FrameBits; } private uint GetQuanta(float v) { var q = (v - Offset) / Quantum; return (uint)(q + 0.5f); //return (uint)Math.Round(q, 0);//any better way? } public override float EvaluateFloat(int frame) { if (Frames?.Length > 0) return Values[Frames[frame % Frames.Length]]; return Offset; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Quantum", FloatUtil.ToString(Quantum)); YcdXml.ValueTag(sb, indent, "Offset", FloatUtil.ToString(Offset)); YcdXml.WriteRawArray(sb, Values, indent, "Values", "", FloatUtil.ToString, 10);// (Values?.Length ?? 0) + 1); YcdXml.WriteRawArray(sb, Frames, indent, "Frames", "", (FormatterRef?)null, 10);// (Frames?.Length ?? 0) + 1); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Quantum = Xml.GetChildFloatAttribute(node, "Quantum", "value"); Offset = Xml.GetChildFloatAttribute(node, "Offset", "value"); Values = Xml.GetChildRawFloatArray(node, "Values"); Frames = Xml.GetChildRawUintArray(node, "Frames"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelQuantizeFloat : AnimChannel { public int ValueBits { get; set; } public float Quantum { get; set; } public float Offset { get; set; } public float[] Values { get; set; } public uint[] ValueList { get; set; } public AnimChannelQuantizeFloat() { Type = AnimChannelType.QuantizeFloat; } public override void Read(AnimChannelDataReader reader) { ValueBits = reader.ReadInt32(); Quantum = reader.ReadSingle(); Offset = reader.ReadSingle(); Values = new float[reader.NumFrames]; ValueList = new uint[reader.NumFrames]; if (ValueBits < 1) { } } public override void Write(AnimChannelDataWriter writer) { var valueCount = Values?.Length ?? 0; var valueList = new uint[valueCount]; for (int i = 0; i < valueCount; i++) { var bits = GetQuanta(Values[i]); valueList[i] = bits; //if (ValueList != null) // ######### DEBUG TEST //{ // var testbits = ValueList[i]; // if (bits != testbits) // { } //} } var valueBits = Math.Max(writer.BitCount(valueList), 1); //if ((valueBits != ValueBits)&&(ValueList!=null)) //{ } // ######### DEBUG TEST ValueBits = valueBits; writer.Write(ValueBits); writer.Write(Quantum); writer.Write(Offset); } public override void ReadFrame(AnimChannelDataReader reader) { uint bits = reader.ReadFrameBits(ValueBits); float val = (bits * Quantum) + Offset; Values[reader.Frame] = val; ValueList[reader.Frame] = bits; } public override void WriteFrame(AnimChannelDataWriter writer) { uint bits = GetQuanta(Values[writer.Frame]); writer.WriteFrameBits(bits, ValueBits); } public override int GetFrameBits() { return ValueBits; } private uint GetQuanta(float v) { var q = (v - Offset) / Quantum; return (uint)(q + 0.5f); //return (uint)Math.Round(q, 0);//any better way? } public override float EvaluateFloat(int frame) { if (Values?.Length > 0) return Values[frame%Values.Length]; return Offset; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Quantum", FloatUtil.ToString(Quantum)); YcdXml.ValueTag(sb, indent, "Offset", FloatUtil.ToString(Offset)); YcdXml.WriteRawArray(sb, Values, indent, "Values", "", FloatUtil.ToString, 10);// (Values?.Length ?? 0) + 1); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Quantum = Xml.GetChildFloatAttribute(node, "Quantum", "value"); Offset = Xml.GetChildFloatAttribute(node, "Offset", "value"); Values = Xml.GetChildRawFloatArray(node, "Values"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelLinearFloat : AnimChannel { private int NumInts { get; set; } private int Counts { get; set; } public float Quantum { get; set; } public float Offset { get; set; } private int Bit { get; set; } //chunks start bit private int Count1 { get; set; } //number of offset bits for each chunk private int Count2 { get; set; } //number of value bits for each chunk private int Count3 { get; set; } //number of delta bits for each frame public float[] Values { get; set; } public int[] ValueList { get; set; } public AnimChannelLinearFloat() { Type = AnimChannelType.LinearFloat; } public override void Read(AnimChannelDataReader reader) { NumInts = reader.ReadInt32(); Counts = reader.ReadInt32(); Quantum = reader.ReadSingle(); Offset = reader.ReadSingle(); Bit = (reader.Position * 8); //chunks start bit Count1 = Counts & 0xFF; //number of offset bits for each chunk Count2 = (Counts >> 8) & 0xFF; //number of value bits for each chunk Count3 = (Counts >> 16) & 0xFF; //number of delta bits for each frame var streamLength = (reader.Data?.Length ?? 0) * 8; var numFrames = reader.NumFrames; var chunkSize = reader.ChunkSize;//64 or 255(-1?) var numChunks = (ushort)((chunkSize + numFrames - 1) / chunkSize); var deltaOffset = Bit + (numChunks * (Count1 + Count2));//base offset to delta bits reader.BitPosition = Bit; var chunkOffsets = new int[numChunks]; var chunkValues = new int[numChunks]; var frameValues = new float[numFrames]; var frameBits = new int[numFrames]; for (int i = 0; i < numChunks; i++) { chunkOffsets[i] = (Count1 > 0) ? (int)reader.ReadBits(Count1) : 0; } for (int i = 0; i < numChunks; i++) { chunkValues[i] = (Count2 > 0) ? (int)reader.ReadBits(Count2) : 0; } for (int i = 0; i < numChunks; i++) { var doffs = chunkOffsets[i] + deltaOffset;//bit offset for chunk deltas var value = chunkValues[i];//chunk start frame value var cframe = (i * chunkSize);//chunk start frame ////if ((reader.BitPosition != doffs)) ////{ } reader.BitPosition = doffs; var inc = 0; for (int j = 0; j < chunkSize; j++) { int frame = cframe + j; if (frame >= numFrames) break; frameValues[frame] = (value * Quantum) + Offset; frameBits[frame] = value; if ((j + 1) >= chunkSize) break;//that's the last frame in the chunk, don't go further var delta = (Count3 != 0) ? (int)reader.ReadBits(Count3) : 0; var so = reader.BitPosition; var maxso = streamLength;//Math.Min(so + 32 - Count3, streamLength); // uint b = 0; while (b == 0) // scan for a '1' bit { b = reader.ReadBits(1); if (reader.BitPosition >= maxso) { break; } //trying to read more than 32 bits, or end of data... don't get into an infinite loop..! } delta |= ((reader.BitPosition - so - 1) << Count3); //add the found bit onto the delta. (position-so-1) is index of found bit if (delta != 0) { var sign = reader.ReadBits(1); if (sign == 1) { delta = -delta; } } inc += delta; value += inc; } } Values = frameValues; ValueList = frameBits; //for (int i = 1; i < numChunks; i++) //{ // if ((chunkOffsets[i] <= chunkOffsets[i - 1]) && (i != numChunks - 1)) // { break; }//what's going on here? chunks not in order..? only seems to affect the final chunks? //} reader.Position -= 16;//TODO: fix this? reader.Position += NumInts * 4; } public override void Write(AnimChannelDataWriter writer) { //TODO: fix this! var numFrames = writer.NumFrames; var numChunks = (ushort)((64 + numFrames - 1) / 64);//default value, if chunks used, chunkSize is always 64! byte chunkSize = 64; //seems to always be 64 for this if (writer.ChunkSize != chunkSize) { writer.ChunkSize = chunkSize; } var valueCount = Values?.Length ?? 0; var valueList = new int[valueCount]; for (int i = 0; i < valueCount; i++) { var bits = GetQuanta(Values[i]); valueList[i] = bits; //if (ValueList != null) // ######### DEBUG TEST //{ // var testbits = ValueList[i]; // if (bits != testbits) // { } //} } var chunkOffsets = new uint[numChunks]; var chunkValues = new uint[numChunks]; var chunkDeltas = new int[numChunks][]; var chunkDeltaBits = new uint[numFrames]; for (int i = 0; i < numChunks; i++) { var cframe = (i * chunkSize);//chunk start frame var cvalue = (cframe < valueCount) ? valueList[cframe] : valueList[0]; var cdeltas = new int[chunkSize]; var cinc = 0; chunkValues[i] = (uint)cvalue; chunkDeltas[i] = cdeltas; for (int j = 1; j < chunkSize; j++) { int frame = cframe + j; if (frame >= numFrames) break; var value = valueList[frame]; var inc = value - cvalue; var delta = inc - cinc; var deltaa = (uint)Math.Abs(delta); cinc = inc; cvalue = value; cdeltas[j] = delta; chunkDeltaBits[frame] = deltaa; } } Count3 = writer.BitCount(chunkDeltaBits); //number of delta bits for each frame uint coffset = 0; for (int i = 0; i < numChunks; i++) { chunkOffsets[i] = coffset; var cdeltas = chunkDeltas[i]; for (int j = 1; j < chunkSize; j++) { var delta = cdeltas[j]; coffset += (uint)Count3 + ((delta < 0) ? 2u : 1u); } } Count1 = writer.BitCount(chunkOffsets); //number of offset bits for each chunk Count2 = writer.BitCount(chunkValues); //number of value bits for each chunk writer.ResetBitstream(); if (Count1 > 0) ////write chunk delta offsets { for (int i = 0; i < numChunks; i++) { writer.WriteBits(chunkOffsets[i], Count1); } } if (Count2 > 0) ////write chunk start values { for (int i = 0; i < numChunks; i++) { writer.WriteBits(chunkValues[i], Count2); } } for (int i = 0; i < numChunks; i++) ////write chunk frame deltas { var cdeltas = chunkDeltas[i]; for (int j = 1; j < chunkSize; j++) { var delta = cdeltas[j]; var deltaa = (uint)Math.Abs(delta); writer.WriteBits(deltaa, Count3); writer.WriteBits(1, 1);//"stop" bit if (delta < 0) { writer.WriteBits(1, 1);//sign bit } } } Counts = Count1 & 0xFF; Counts += (Count2 & 0xFF) << 8; Counts += (Count3 & 0xFF) << 16; NumInts = 4 + writer.Bitstream.Count; writer.Write(NumInts); writer.Write(Counts); writer.Write(Quantum); writer.Write(Offset); writer.WriteBitstream(); } public override float EvaluateFloat(int frame) { if (Values?.Length > 0) return Values[frame % Values.Length]; return Offset; } private int GetQuanta(float v) { var q = (v - Offset) / Quantum; return (int)(q + 0.5f); //return (uint)Math.Round(Math.Max(q, 0));//any better way? } public override void WriteXml(StringBuilder sb, int indent) { //base.WriteXml(sb, indent); Type = AnimChannelType.QuantizeFloat;//TODO - FIX! temporary: just export this as a quantize float to avoid import issues.. base.WriteXml(sb, indent); Type = AnimChannelType.LinearFloat; float minVal = float.MaxValue; for (int i = 0; i < Values.Length; i++) { minVal = Math.Min(minVal, Values[i]); } if (minVal != Offset) { Offset = minVal; } YcdXml.ValueTag(sb, indent, "Quantum", FloatUtil.ToString(Quantum)); YcdXml.ValueTag(sb, indent, "Offset", FloatUtil.ToString(Offset)); YcdXml.WriteRawArray(sb, Values, indent, "Values", "", FloatUtil.ToString, 10);// (Values?.Length ?? 0) + 1); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Quantum = Xml.GetChildFloatAttribute(node, "Quantum", "value"); Offset = Xml.GetChildFloatAttribute(node, "Offset", "value"); Values = Xml.GetChildRawFloatArray(node, "Values"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelRawFloat : AnimChannel { public float[] Values { get; set; } public AnimChannelRawFloat() { Type = AnimChannelType.RawFloat; } public override void Read(AnimChannelDataReader reader) { Values = new float[reader.NumFrames]; } public override void Write(AnimChannelDataWriter writer) { //nothing to do here } public override void ReadFrame(AnimChannelDataReader reader) { uint bits = reader.ReadFrameBits(32); float v = MetaTypes.ConvertData(MetaTypes.ConvertToBytes(in bits)); Values[reader.Frame] = v; } public override void WriteFrame(AnimChannelDataWriter writer) { float v = Values[writer.Frame]; var b = BitConverter.GetBytes(v); var bits = BitConverter.ToUInt32(b, 0); writer.WriteFrameBits(bits, 32); } public override int GetFrameBits() { return 32; } public override float EvaluateFloat(int frame) { if (Values?.Length > 0) return Values[frame % Values.Length]; return base.EvaluateFloat(frame); } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.WriteRawArray(sb, Values, indent, "Values", "", FloatUtil.ToString, 10);// (Values?.Length ?? 0) + 1); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Values = Xml.GetChildRawFloatArray(node, "Values"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimChannelCachedQuaternion : AnimChannel { private AnimChannelDataReader blockStream; private float[] valueCache; public float[] Values { get { if (valueCache != null) { return valueCache; } valueCache = new float[blockStream.NumFrames]; var channels = new AnimChannel[3]; var ch = 0; for (int i = 0; i < 4; i++) { if (i != 3) { channels[ch] = blockStream.Sequences[Sequence].Channels[i]; ch++; } } for (int i = 0; i < valueCache.Length; i++) { var vec = new Vector3( channels[0].EvaluateFloat(i), channels[1].EvaluateFloat(i), channels[2].EvaluateFloat(i) ); valueCache[i] = (float)Math.Sqrt(Math.Max(1.0f - vec.LengthSquared(), 0.0)); } return valueCache; } } public int QuatIndex { get; set; } public AnimChannelCachedQuaternion(AnimChannelType type) { Type = type; } public override void Read(AnimChannelDataReader reader) { this.blockStream = reader; } public override int GetReferenceIndex() { return QuatIndex; } public override float EvaluateFloat(int frame) { if (Values?.Length > 0) return Values[frame % Values.Length]; return 0.0f; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "QuatIndex", QuatIndex.ToString()); //data is already written in other channels... } public override void ReadXml(XmlNode node) { base.ReadXml(node); QuatIndex = Xml.GetChildIntAttribute(node, "QuatIndex", "value"); //data was already read in other channels... } } public class AnimChannelDataReader { public byte[] Data { get; set; } public ushort NumFrames { get; set; } public byte ChunkSize { get; set; } //stride of channel frame items (in frames) public int Position { get; set; } //current byte that the main data reader is on public int Frame { get; set; } //current frame that the reader is on public uint FrameOffset { get; set; } //offset to frame data items / bytes public ushort FrameLength { get; set; } //stride of frame data item public int ChannelListOffset { get; set; }//offset to channel counts public int ChannelDataOffset { get; set; }//offset to channel data/info ushorts public int ChannelFrameOffset { get; set; }//offset to channel current frame data (in bits!) public AnimSequence[] Sequences { get; set; }//used by AnimChannelCachedQuaternion when accessing values (when evaluating) public int BitPosition { get; set; } //for use with ReadBits() public AnimChannelDataReader(byte[] data, ushort numFrames, byte chunkSize, uint frameOffset, ushort frameLength) { Data = data; NumFrames = numFrames; ChunkSize = chunkSize; Position = 0; Frame = 0; FrameOffset = frameOffset; FrameLength = frameLength; ChannelListOffset = (int)FrameOffset + (FrameLength * NumFrames); ChannelDataOffset = ChannelListOffset + (9 * 2); ChannelFrameOffset = 0; } public int ReadInt32() { int i = BitConverter.ToInt32(Data, Position); Position += 4; return i; } public float ReadSingle() { float f = BitConverter.ToSingle(Data, Position); Position += 4; return f; } public Vector3 ReadVector3() { var v = new Vector3(); v.X = BitConverter.ToSingle(Data, Position); v.Y = BitConverter.ToSingle(Data, Position + 4); v.Z = BitConverter.ToSingle(Data, Position + 8); Position += 12; return v; } public uint GetBits(int startBit, int length) { //dexyfex version that won't read too many bytes - probably won't perform as well if (startBit < 0) { return 0; } //something must have went wrong reading other data... happening in fos_ep_1_p6-35.ycd int startByte = startBit / 8; int bitOffset = startBit % 8; uint result = 0; int shift = -bitOffset; int curByte = startByte; int bitsRemaining = length; while (bitsRemaining > 0) { var b = (curByte < Data.Length) ? (uint)Data[curByte++] : 0; var sb = (shift < 0) ? (b >> -shift) : (b << shift); var bm = ((1u << Math.Min(bitsRemaining, 8)) - 1u) << (Math.Max(shift, 0)); var mb = (sb & bm); result += mb; bitsRemaining -= (8 + Math.Min(shift, 0)); shift += 8; } return result; ////original calcium version - has issues near the end of the data from trying to read too many bytes.// //var mask = MaskTable[length]; //var lowByte = BitConverter.ToUInt32(Data, (startBit / 32) * 4); //var highByte = BitConverter.ToUInt32(Data, ((startBit / 32) + 1) * 4); //var pair = ((ulong)highByte << 32) | lowByte; //var res = (uint)((pair >> (startBit % 32)) & mask); //if (result != res)//dexyfex sanity check //{ } //return res; //private static uint[] MaskTable = new uint[] //{ // 0, 1, 3, 7, 0xF, 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, // 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, 0x1FFFF, // 0x3FFFF, 0x7FFFF, 0xFFFFF, 0x1FFFFF, 0x3FFFFF, 0x7FFFFF, // 0xFFFFFF, 0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF, // 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF //}; } public uint ReadBits(int length) { uint bits = GetBits(BitPosition, length); BitPosition += length; return bits; } public ushort ReadChannelCount() { ushort channelCount = BitConverter.ToUInt16(Data, ChannelListOffset); ChannelListOffset += 2; return channelCount; } public ushort ReadChannelDataBits() { ushort channelDataBit = BitConverter.ToUInt16(Data, ChannelDataOffset); ChannelDataOffset += 2; return channelDataBit; } public byte[] ReadChannelDataBytes(int n) { var r = new byte[n]; Buffer.BlockCopy(Data, ChannelDataOffset, r, 0, n); ChannelDataOffset += n; return r; } public void AlignChannelDataOffset(int channelCount) { int remainder = channelCount % 4; if (remainder > 0) { int addamt = (4 - remainder) * 2; ChannelDataOffset += addamt; } } public void BeginFrame(int f) { Frame = f; ChannelFrameOffset = (int)((FrameOffset + (FrameLength * f)) * 8); } public uint ReadFrameBits(int n) { uint b = GetBits(ChannelFrameOffset, n); ChannelFrameOffset += n; return b; } } public class AnimChannelDataWriter { public int ChannelListOffset { get; set; }//offset to channel counts public int ChannelItemOffset { get; set; }//offset to channel data/info ushorts public int ChannelFrameOffset { get; set; }//offset to channel current frame data (in bits!) public int Position { get; set; } //current byte that the main data reader is on public int Frame { get; set; } //current frame that the reader is on public ushort NumFrames { get; set; } public byte ChunkSize { get; set; } //stride of channel frame items - starts at 0 and will be set to 64 if need be public ushort FrameLength { get; set; } = 0; //stride of frame data item, calculated when ending frames MemoryStream ChannelListStream = new MemoryStream(); MemoryStream ChannelItemStream = new MemoryStream(); MemoryStream MainStream = new MemoryStream(); BinaryWriter ChannelListWriter = null; BinaryWriter ChannelItemWriter = null; BinaryWriter MainWriter = null; public List ChannelFrameStream { get; private set; } = new List(); //frame bits stream. public List ChannelFrames { get; private set; } = new List();//bitstreams for each frame public List Bitstream { get; private set; } = new List(); //temporary bitstream, used from WriteBits() public int BitstreamPos { get; set; } = 0; public AnimChannelDataWriter(ushort numFrames) { Position = 0; Frame = 0; NumFrames = numFrames; ChunkSize = 0; //default 0 value means chunks not used ChannelListWriter = new BinaryWriter(ChannelListStream); ChannelItemWriter = new BinaryWriter(ChannelItemStream); MainWriter = new BinaryWriter(MainStream); } public void WriteChannelListData(ushort c) { ChannelListWriter.Write(c); ChannelListOffset += 2; } public void WriteChannelItemData(ushort c) { ChannelItemWriter.Write(c); ChannelItemOffset += 2; } public void AlignChannelItemData(int channelCount, int sequenceCount) { int remainder = channelCount % 4; if (remainder > 0) { ushort writeval = (ushort)(sequenceCount << 2); int addamt = (4 - remainder); for (int i = 0; i < addamt; i++) { WriteChannelItemData(writeval); } } } public void WriteChannelItemDataBytes(byte[] data) { if (data?.Length > 0) { ChannelItemWriter.Write(data); ChannelItemOffset += data.Length; } } public void Write(int i) { MainWriter.Write(i); Position += 4; } public void Write(float f) { MainWriter.Write(f); Position += 4; } public void Write(Vector3 v) { MainWriter.Write(v.X); MainWriter.Write(v.Y); MainWriter.Write(v.Z); Position += 12; } public void BeginFrame(int f) { Frame = f; ChannelFrameStream.Clear(); ChannelFrameOffset = 0; } public void WriteFrameBits(uint bits, int n) { WriteToBitstream(ChannelFrameStream, ChannelFrameOffset, bits, n); ChannelFrameOffset += n; } public void EndFrame() { FrameLength = Math.Max(FrameLength, (ushort)(ChannelFrameStream.Count * 4)); ChannelFrames.Add(ChannelFrameStream.ToArray()); ChannelFrameStream.Clear(); } public int BitCount(uint bits)//could be static, but not for convenience { int bc = 0; for (int i = 0; i < 32; i++) { uint mask = 1u << i; if ((bits & mask) > 0) bc = (i + 1); } return bc; } public int BitCount(uint[] values) { uint maxValue = 0; for (int i = 0; i < values?.Length; i++) { maxValue = Math.Max(maxValue, values[i]); } return BitCount(maxValue); } public void ResetBitstream() { Bitstream.Clear(); BitstreamPos = 0; } public void WriteBits(uint bits, int n)//write n bits to the bitstream. { WriteToBitstream(Bitstream, BitstreamPos, bits, n); BitstreamPos += n; } public void WriteBitstream()//write the contents of the bitstream (as uints) to the main writer { for (int i = 0; i < Bitstream.Count; i++) { MainWriter.Write(Bitstream[i]); } Position += (Bitstream.Count * 4); } private void WriteToBitstream(List stream, int offset, uint bits, int n) { if (stream == null) return; uint mask = (uint)((1L << n) - 1); uint masked = bits & mask; if (bits != masked) { } int soffset = offset % 32; int sindex = offset / 32; while (sindex >= stream.Count) stream.Add(0); //pad beginning of the stream uint sval = stream[sindex]; uint sbits = bits << soffset; stream[sindex] = sval + sbits; int endbit = (soffset + n) - 32; if (endbit > 0) { int eindex = sindex + 1; while (eindex >= stream.Count) stream.Add(0);//pad end of stream int eoffset = 32 - soffset; uint eval = stream[eindex]; uint ebits = bits >> eoffset; stream[eindex] = eval + ebits; } } public byte[] GetStreamData(MemoryStream ms) { var length = (int)ms.Length; var data = new byte[length]; ms.Flush(); ms.Position = 0; ms.Read(data, 0, length); return data; } public byte[] GetChannelListDataBytes() { return GetStreamData(ChannelListStream); } public byte[] GetChannelItemDataBytes() { return GetStreamData(ChannelItemStream); } public byte[] GetMainDataBytes() { return GetStreamData(MainStream); } public byte[] GetFrameDataBytes() { var ms = new MemoryStream(); var bw = new BinaryWriter(ms); var frameUintCount = FrameLength / 4; for (int i = 0; i < ChannelFrames.Count; i++) { var frameData = ChannelFrames[i]; for (int f = 0; f < frameUintCount; f++) { bw.Write((f < frameData.Length) ? frameData[f] : 0); } } return GetStreamData(ms); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class AnimSequence : IMetaXmlItem { public AnimChannel[] Channels { get; set; } public bool IsType7Quat { get; internal set; } public AnimationBoneId BoneId { get; set; }//for convenience public Quaternion EvaluateQuaternionType7(int frame) { if (!IsType7Quat) { return new Quaternion( Channels[0].EvaluateFloat(frame), Channels[1].EvaluateFloat(frame), Channels[2].EvaluateFloat(frame), Channels[3].EvaluateFloat(frame) ); } //type 1 if (Channels[3] is not AnimChannelCachedQuaternion t7) t7 = Channels[4] as AnimChannelCachedQuaternion;//type 2 var x = Channels[0].EvaluateFloat(frame); var y = Channels[1].EvaluateFloat(frame); var z = Channels[2].EvaluateFloat(frame); var normalized = t7.EvaluateFloat(frame); switch (t7.QuatIndex) { case 0: return new Quaternion(normalized, x, y, z); case 1: return new Quaternion(x, normalized, y, z); case 2: return new Quaternion(x, y, normalized, z); case 3: return new Quaternion(x, y, z, normalized); default: return Quaternion.Identity; } } public Quaternion EvaluateQuaternion(int frame) { if (IsType7Quat) return EvaluateQuaternionType7(frame); return EvaluateVector(frame).ToQuaternion(); } public Vector4 EvaluateVector(int frame) { if (Channels == null) return Vector4.Zero; if (IsType7Quat) return Quaternion.Normalize(EvaluateQuaternionType7(frame)).ToVector4();//normalization shouldn't be necessary, but saves explosions in case of incorrectness var v = Vector4.Zero; int c = 0; for (int i = 0; i < Channels.Length; i++) { if (c >= 4) break; var channel = Channels[i]; if (channel is AnimChannelStaticVector3 sv3c) { for (int n = 0; n < 3; n++) { if ((c + n) >= 4) break; v[c + n] = sv3c.Value[n]; } c += 3; } else if (channel is AnimChannelStaticQuaternion ssqc) { for (int n = 0; n < 4; n++) { if ((c + n) >= 4) break; v[c + n] = ssqc.Value[n]; } c += 4; } else { v[c] = channel.EvaluateFloat(frame); c++; } } return v; } public void WriteXml(StringBuilder sb, int indent) { //YcdXml.ValueTag(sb, indent, "BoneId", BoneId.BoneId.ToString()); //just for convenience really..... YcdXml.WriteItemArray(sb, Channels, indent, "Channels"); } public void ReadXml(XmlNode node) { //AnimationBoneId b = new AnimationBoneId(); //b.BoneId = (ushort)Xml.GetChildUIntAttribute(node, "BoneId", "value"); //BoneId = b; //Channels = XmlMeta.ReadItemArrayNullable(node, "Channels"); var chansNode = node.SelectSingleNode("Channels"); if (chansNode != null) { var inodes = chansNode.SelectNodes("Item"); if (inodes?.Count > 0) { var clist = new List(); foreach (XmlNode inode in inodes) { var type = Xml.GetEnumValue(Xml.GetChildStringAttribute(inode, "Type", "value")); var c = AnimChannel.ConstructChannel(type); c.ReadXml(inode); clist.Add(c); } Channels = clist.ToArray(); } } } public override string ToString() { return "AnimSequence: " + (Channels?.Length??0).ToString() + " channels"; } } [TypeConverter(typeof(ExpandableObjectConverter))] public class SequenceRootChannelRef : IMetaXmlItem { public byte[] Bytes { get; set; } public byte ChannelType { get { return Bytes[0]; } set { Bytes[0] = value; } } public byte ChannelIndex { get { return Bytes[1]; } set { Bytes[1] = value; } } public ushort DataIntOffset { get { return (ushort)(Bytes[2] + (Bytes[3] << 8)); } set { Bytes[2] = (byte)(value & 0xFF); Bytes[3] = (byte)((value >> 8) & 0xFF); } } public ushort FrameBitOffset { get { return (ushort)(Bytes[4] + (Bytes[5] << 8)); } set { Bytes[4] = (byte)(value & 0xFF); Bytes[5] = (byte)((value >> 8) & 0xFF); } } public SequenceRootChannelRef() { Bytes = new byte[6]; } public SequenceRootChannelRef(AnimChannelType type, int channelIndex) { Bytes = new byte[6]; ChannelType = (byte)type; ChannelIndex = (byte)channelIndex; } public SequenceRootChannelRef(byte[] bytes) { Bytes = bytes; } public override string ToString() { if (Bytes?.Length >= 6) { return ChannelType.ToString() + ", " + ChannelIndex.ToString() + ", " + DataIntOffset.ToString() + ", " + FrameBitOffset.ToString(); } return "(empty)"; } public void WriteXml(StringBuilder sb, int indent) { YcdXml.WriteRawArray(sb, Bytes, indent, "Bytes", "", YcdXml.FormatHexByte, 6); } public void ReadXml(XmlNode node) { Bytes = Xml.GetChildRawByteArray(node, "Bytes"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class Sequence : ResourceSystemBlock, IMetaXmlItem { public override long BlockLength { get { return 32 + (Data?.Length ?? 0); } } // structure data public MetaHash Unknown_00h { get; set; } //identifier / name? public uint DataLength { get; set; } public uint Unused_08h { get; set; } // 0x00000000 public uint FrameOffset { get; set; } //offset to frame data items / bytes public uint RootMotionRefsOffset { get; set; } //offset to root motion items (relative to start of the chunk, -32), ==BlockLength when no root motion public ushort Unused_14h { get; set; } //0x0000 public ushort NumFrames { get; set; } // count of frame data items public ushort FrameLength { get; set; } //stride of frame data item public ushort IndirectQuantizeFloatNumInts { get; set; } //total number of ints that the indirect quantize float channels take (not the frames data though!) public ushort QuantizeFloatValueBits { get; set; } //total number of quantize float value bits per frame? public byte ChunkSize { get; set; } //64|255 0x40|0xFF public byte RootMotionRefCounts { get; set; } //0|17|20|21|49|52|53 0x11|0x14|0x15|0x31|0x34|0x35 public byte[] Data { get; set; } // parsed data public AnimSequence[] Sequences { get; set; } public SequenceRootChannelRef[] RootPositionRefs { get; set; } public SequenceRootChannelRef[] RootRotationRefs { get; set; } public int RootPositionRefCount { get { return (RootMotionRefCounts >> 4) & 0xF; } set { var rrc = RootMotionRefCounts & 0xF; RootMotionRefCounts = (byte)(rrc + ((value & 0xF) << 4)); } } public int RootRotationRefCount { get { return RootMotionRefCounts & 0xF; } set { var rpc = (RootMotionRefCounts >> 4) & 0xF; RootMotionRefCounts = (byte)(rpc + (value & 0xF)); } } class AnimChannelListItem { public int Sequence; public int Index; public AnimChannel Channel; public AnimChannelListItem(int seq, int ind, AnimChannel channel) { Sequence = seq; Index = ind; Channel = channel; } } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.Unknown_00h = reader.ReadUInt32(); //2965995365 2837183178 this.DataLength = reader.ReadUInt32(); //282 142 1206 358 this.Unused_08h = reader.ReadUInt32(); //0 0 0 0 this.FrameOffset = reader.ReadUInt32(); //224 (E0) 32 (20) 536 (218) 300 this.RootMotionRefsOffset = reader.ReadUInt32(); //314 174 1238 390 (=Length) this.Unused_14h = reader.ReadUInt16(); //0 0 0 0 this.NumFrames = reader.ReadUInt16(); //221 (DD) 17 (11) 151 (97) 201 this.FrameLength = reader.ReadUInt16(); //0 4 4 0 this.IndirectQuantizeFloatNumInts = reader.ReadUInt16();//0 0 106 0 this.QuantizeFloatValueBits = reader.ReadUInt16(); //0 17 0 0 this.ChunkSize = reader.ReadByte(); //64 255 255 64 this.RootMotionRefCounts = reader.ReadByte(); //0 0 0 0 this.Data = reader.ReadBytes((int)DataLength); ParseData(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { //BuildData should be called before this // write structure data writer.Write(this.Unknown_00h); writer.Write(this.DataLength); writer.Write(this.Unused_08h); writer.Write(this.FrameOffset); writer.Write(this.RootMotionRefsOffset); writer.Write(this.Unused_14h); writer.Write(this.NumFrames); writer.Write(this.FrameLength); writer.Write(this.IndirectQuantizeFloatNumInts); writer.Write(this.QuantizeFloatValueBits); writer.Write(this.ChunkSize); writer.Write(this.RootMotionRefCounts); writer.Write(this.Data); } public override string ToString() { return Unknown_00h.ToString() + ": " + DataLength.ToString(); } public void ParseData() { var reader = new AnimChannelDataReader(Data, NumFrames, ChunkSize, FrameOffset, FrameLength); var channelList = new List(); var channelLists = new AnimChannel[9][]; var frameOffset = 0; for (int i = 0; i < 9; i++)//iterate through anim channel types { var ctype = (AnimChannelType)i; int channelCount = reader.ReadChannelCount(); var channels = new AnimChannel[channelCount]; for (int c = 0; c < channelCount; c++) //construct and read channels { var channel = AnimChannel.ConstructChannel(ctype); var channelDataBit = reader.ReadChannelDataBits(); if (channel != null)//read channel sequences and indexes { channel.DataOffset = reader.Position / 4; channel.Read(reader); channels[c] = channel; var sequence = channelDataBit >> 2; var index = channelDataBit & 3; if (channel is AnimChannelCachedQuaternion t7) { t7.QuatIndex = index; index = (channel.Type == AnimChannelType.CachedQuaternion1) ? 3 : 4; } channel.Associate(sequence, index); channelList.Add(new AnimChannelListItem(sequence, index, channel)); channel.FrameOffset = frameOffset; frameOffset += channel.GetFrameBits(); } } reader.AlignChannelDataOffset(channelCount); channelLists[i] = channels; } for (int f = 0; f < NumFrames; f++)//read channel frame data { reader.BeginFrame(f); for (int i = 0; i < 9; i++) { var channels = channelLists[i]; for (int c = 0; c < channels.Length; c++) { var channel = channels[c]; channel?.ReadFrame(reader); } } } Sequences = new AnimSequence[channelList.Max(a => a.Sequence) + 1]; for (int i = 0; i < Sequences.Length; i++) //assign channels to sequences according to read indices { Sequences[i] = new AnimSequence(); var thisSeq = channelList.Where(a => a.Sequence == i); if (thisSeq.Count() == 0) { continue; } Sequences[i].Channels = new AnimChannel[thisSeq.Max(a => a.Index) + 1]; for (int j = 0; j < Sequences[i].Channels.Length; j++) { Sequences[i].Channels[j] = thisSeq.FirstOrDefault(a => a.Index == j)?.Channel; if (Sequences[i].Channels[j].Type == AnimChannelType.CachedQuaternion1)// is AnimChannelCachedQuaternion) { Sequences[i].IsType7Quat = true; } } } reader.Sequences = Sequences; int numPosRefs = RootPositionRefCount; int numRotRefs = RootRotationRefCount; if (numPosRefs > 0) { RootPositionRefs = new SequenceRootChannelRef[numPosRefs]; for (int i = 0; i < numPosRefs; i++) { var pref = new SequenceRootChannelRef(reader.ReadChannelDataBytes(6)); RootPositionRefs[i] = pref; } } if (numRotRefs > 0) { RootRotationRefs = new SequenceRootChannelRef[numRotRefs]; for (int i = 0; i < numRotRefs; i++) { var rref = new SequenceRootChannelRef(reader.ReadChannelDataBytes(6)); RootRotationRefs[i] = rref; } } if (reader.ChannelDataOffset != Data.Length) { var brem = Data.Length - reader.ChannelDataOffset; } } public void BuildData() { // convert parsed sequences into Data byte array............ if (Sequences == null) return;//this shouldn't happen... var writer = new AnimChannelDataWriter(NumFrames); var channelLists = new List[9]; for (int s = 0; s < Sequences.Length; s++) { var seq = Sequences[s]; if (seq?.Channels == null) continue; for (int c = 0; c < seq.Channels.Length; c++) { var chan = seq.Channels[c]; if (chan == null) continue; int typeid = (int)chan.Type; if ((typeid < 0) || (typeid >= 9)) { continue; } var chanList = channelLists[typeid]; if (chanList == null) { chanList = new List(); channelLists[typeid] = chanList; } if (chan is AnimChannelCachedQuaternion accq) { chan.Index = accq.QuatIndex;//seems to have QuatIndex stored in there (for channelDataBit below) } chanList.Add(chan); } } for (int i = 0; i < 9; i++) { var channelList = channelLists[i]; var channelCount = (ushort)(channelList?.Count ?? 0); writer.WriteChannelListData(channelCount); for (int c = 0; c < channelCount; c++) { var channel = channelList[c]; var channelDataBit = (ushort)((channel?.Index ?? 0) + ((channel?.Sequence ?? 0) << 2)); writer.WriteChannelItemData(channelDataBit); if (channel != null) { channel.DataOffset = writer.Position / 4; channel?.Write(writer); } } writer.AlignChannelItemData(channelCount, Sequences.Length); } for (int f = 0; f < NumFrames; f++)//write channel frame data { writer.BeginFrame(f); for (int i = 0; i < 9; i++) { var channelList = channelLists[i]; var channelCount = (ushort)(channelList?.Count ?? 0); for (int c = 0; c < channelCount; c++) { var channel = channelList[c]; channel?.WriteFrame(writer); } } writer.EndFrame(); } var frameOffset = 0; for (int i = 0; i < channelLists.Length; i++) { var chanList = channelLists[i]; if (chanList == null) continue; for (int c = 0; c < chanList.Count; c++) { var chan = chanList[c]; if (chan == null) continue; chan.FrameOffset = frameOffset; frameOffset += chan.GetFrameBits(); } } UpdateRootMotionRefs(); if (RootPositionRefs != null) { for (int i = 0; i < RootPositionRefs.Length; i++) { writer.WriteChannelItemDataBytes(RootPositionRefs[i].Bytes); } } if (RootRotationRefs != null) { for (int i = 0; i < RootRotationRefs.Length; i++) { writer.WriteChannelItemDataBytes(RootRotationRefs[i].Bytes); } } var mainData = writer.GetMainDataBytes(); var frameData = writer.GetFrameDataBytes(); var channelListData = writer.GetChannelListDataBytes(); var channelItemData = writer.GetChannelItemDataBytes(); var dataLen = mainData.Length + frameData.Length + channelListData.Length + channelItemData.Length; var data = new byte[dataLen]; var curpos = 0; Buffer.BlockCopy(mainData, 0, data, 0, mainData.Length); curpos += mainData.Length; Buffer.BlockCopy(frameData, 0, data, curpos, frameData.Length); curpos += frameData.Length; Buffer.BlockCopy(channelListData, 0, data, curpos, channelListData.Length); curpos += channelListData.Length; Buffer.BlockCopy(channelItemData, 0, data, curpos, channelItemData.Length); //if (FrameLength != writer.FrameLength) //{ } //else if (Data != null) //{ // if (Data.Length != data.Length) // { // if ((channelLists[6]?.Count ?? 0) == 0) // { } // } // else // { // for (int b = 0; b < Data.Length; b++) // { // if (Data[b] != data[b]) // { // if ((channelLists[6]?.Count ?? 0) == 0) // { } // break; // } // } // } //} Data = data; DataLength = (uint)data.Length; FrameOffset = (uint)mainData.Length; FrameLength = writer.FrameLength; ChunkSize = (writer.ChunkSize > 0) ? writer.ChunkSize : (byte)255; QuantizeFloatValueBits = GetQuantizeFloatValueBits(); IndirectQuantizeFloatNumInts = GetIndirectQuantizeFloatNumInts(); RootMotionRefCounts = (byte)((((uint)(RootPositionRefs?.Length??0))<<4) | ((uint)(RootRotationRefs?.Length ?? 0))); RootMotionRefsOffset = (uint)(BlockLength - ((RootPositionRefCount + RootRotationRefCount) * 6)); } public void AssociateSequenceChannels()//assigns Sequence and Index to all channels { if (Sequences == null) return;//this shouldn't happen... for (int s = 0; s < Sequences.Length; s++) { var seq = Sequences[s]; if (seq?.Channels == null) continue; for (int c = 0; c < seq.Channels.Length; c++) { var chan = seq.Channels[c]; chan?.Associate(s, c); } } } public ushort GetQuantizeFloatValueBits() { int b = 0; foreach (var seq in Sequences) { foreach (var chan in seq.Channels) { if (chan.Type == AnimChannelType.QuantizeFloat) { var acqf = chan as AnimChannelQuantizeFloat; b += acqf.ValueBits; } } } return (ushort)b; } public ushort GetIndirectQuantizeFloatNumInts() { int b = 0; foreach (var seq in Sequences) { foreach (var chan in seq.Channels) { if (chan.Type == AnimChannelType.IndirectQuantizeFloat) { var acif = chan as AnimChannelIndirectQuantizeFloat; b += acif.NumInts+5; } } } return (ushort)b; } public void UpdateRootMotionRefs() { // OFFSETS - [ChannelType], [Index], [DataIntOffset 0xFFFF], [FrameBitOffset 0xFFFF] var newPosRefs = new List(); var newRotRefs = new List(); for (int i = 0; i < Sequences.Length; i++) { var seq = Sequences[i]; if (seq == null) continue; if (seq.BoneId.Track == 5) //root position { for (var c = 0; c < seq.Channels?.Length; c++) { var chan = seq.Channels[c]; var newPosRef = new SequenceRootChannelRef(chan.Type, chan.GetReferenceIndex()); newPosRef.DataIntOffset = (ushort)chan.DataOffset; newPosRef.FrameBitOffset = (ushort)chan.FrameOffset; newPosRefs.Add(newPosRef); } } if (seq.BoneId.Track == 6) //root rotation { for (var c = 0; c < seq.Channels?.Length; c++) { var chan = seq.Channels[c]; var newRotRef = new SequenceRootChannelRef(chan.Type, chan.GetReferenceIndex()); newRotRef.DataIntOffset = (ushort)chan.DataOffset; newRotRef.FrameBitOffset = (ushort)chan.FrameOffset; newRotRefs.Add(newRotRef); } } } int compare(SequenceRootChannelRef a, SequenceRootChannelRef b) { var v1 = a.ChannelType.CompareTo(b.ChannelType); if (v1 != 0) return v1; var v2 = a.ChannelIndex.CompareTo(b.ChannelIndex); if (v2 != 0) return v2; return 0; } newPosRefs.Sort((a, b) => { return compare(a, b); }); newRotRefs.Sort((a, b) => { return compare(a, b); }); //if (RootPositionRefs != null) //{ // if (RootPositionRefs.Length != newPosRefs.Count) // { } // else // { // for (int i = 0; i < RootPositionRefs.Length; i++) // { // var oldRef = RootPositionRefs[i]; // var newRef = newPosRefs[i]; // for (int b = 0; b < 6; b++) // { // if (oldRef.Bytes[b] != newRef.Bytes[b]) // { } // } // } // } //} //if (RootRotationRefs != null) //{ // if (RootRotationRefs.Length != newRotRefs.Count) // { } // else // { // for (int i = 0; i < RootRotationRefs.Length; i++) // { // var oldRef = RootRotationRefs[i]; // var newRef = newRotRefs[i]; // for (int b = 0; b < 6; b++) // { // if (oldRef.Bytes[b] != newRef.Bytes[b]) // { } // } // } // } //} RootPositionRefs = (newPosRefs.Count > 0) ? newPosRefs.ToArray() : null; RootRotationRefs = (newRotRefs.Count > 0) ? newRotRefs.ToArray() : null; } public void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "Hash", YcdXml.HashString(Unknown_00h)); YcdXml.ValueTag(sb, indent, "FrameCount", NumFrames.ToString()); YcdXml.WriteItemArray(sb, Sequences, indent, "SequenceData"); } public void ReadXml(XmlNode node) { Unknown_00h = XmlMeta.GetHash(Xml.GetChildInnerText(node, "Hash")); NumFrames = (ushort)Xml.GetChildUIntAttribute(node, "FrameCount", "value"); Sequences = XmlMeta.ReadItemArray(node, "SequenceData"); AssociateSequenceChannels(); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipMapEntry : ResourceSystemBlock { public override long BlockLength { get { return 32; } } // structure data public MetaHash Hash { get; set; } public uint Unknown_04h { get; set; } // 0x00000000 public ulong ClipPointer { get; set; } public ulong NextPointer { get; set; } public uint Unknown_18h { get; set; } // 0x00000000 public uint Unknown_1Ch { get; set; } // 0x00000000 // reference data public ClipBase? Clip { get; set; } public ClipMapEntry? Next { get; set; } public bool EnableRootMotion { get; set; } = false; //used by CW to toggle whether or not to include root motion when playing animations public bool OverridePlayTime { get; set; } = false; //used by CW to manually override the animation playback time public float PlayTime { get; set; } = 0.0f; public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.Hash = new MetaHash(reader.ReadUInt32()); this.Unknown_04h = reader.ReadUInt32(); this.ClipPointer = reader.ReadUInt64(); this.NextPointer = reader.ReadUInt64(); this.Unknown_18h = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); // read reference data this.Clip = reader.ReadBlockAt( this.ClipPointer // offset ); this.Next = reader.ReadBlockAt( this.NextPointer // offset ); if (Clip is not null) { Clip.Hash = Hash; } } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.ClipPointer = (ulong)(this.Clip != null ? this.Clip.FilePosition : 0); this.NextPointer = (ulong)(this.Next != null ? this.Next.FilePosition : 0); // write structure data writer.Write(this.Hash); writer.Write(this.Unknown_04h); writer.Write(this.ClipPointer); writer.Write(this.NextPointer); writer.Write(this.Unknown_18h); writer.Write(this.Unknown_1Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Clip != null) list.Add(Clip); if (Next != null) list.Add(Next); return list.ToArray(); } public override string ToString() => Clip?.Name ?? Hash.ToString(); } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipBase : ResourceSystemBlock, IResourceXXSystemBlock, IMetaXmlItem { public override long BlockLength => 112; // structure data public uint VFT { get; set; } public uint Unknown_04h { get; set; } = 1; // 0x00000001 public uint Unknown_08h { get; set; } // 0x00000000 public uint Unknown_0Ch { get; set; } // 0x00000000 public ClipType Type { get; set; } // 1, 2 public uint Unknown_14h { get; set; } // 0x00000000 public ulong NamePointer { get; set; } public ushort NameLength { get; set; } // short, name length public ushort NameCapacity { get; set; } // short, name length +1 public uint Unknown_24h { get; set; } // 0x00000000 public ulong Unknown_28hPtr { get; set; } = 0x50000000; // 0x50000000 public uint Unknown_30h { get; set; } // 0, 1 public uint Unknown_34h { get; set; } // 0x00000000 public ulong TagsPointer { get; set; } public ulong PropertiesPointer { get; set; } public uint Unknown_48h { get; set; } = 1; // 0x00000001 public uint Unknown_4Ch { get; set; } // 0x00000000 // reference data public string Name { get; set; } public ClipTagList Tags { get; set; } public ClipPropertyMap Properties { get; set; } private string_r NameBlock = null; public YcdFile Ycd { get; set; } public string ShortName { get { if (!string.IsNullOrEmpty(_ShortName)) return _ShortName; if (string.IsNullOrEmpty(Name)) return null; string name = Name.Replace('\\', '/'); var slidx = name.LastIndexOf('/'); if ((slidx >= 0) && (slidx < name.Length - 1)) { name = name.Substring(slidx + 1); } var didx = name.IndexOf('.'); if ((didx > 0) && (didx < name.Length)) { name = name.Substring(0, didx); } _ShortName = name.ToLowerInvariant(); JenkIndex.Ensure(_ShortName); return _ShortName; } } private string _ShortName; public MetaHash Hash { get; set; } //used by CW when reading/writing public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.VFT = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); this.Unknown_08h = reader.ReadUInt32(); this.Unknown_0Ch = reader.ReadUInt32(); this.Type = (ClipType)reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.NamePointer = reader.ReadUInt64(); this.NameLength = reader.ReadUInt16(); this.NameCapacity = reader.ReadUInt16(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28hPtr = reader.ReadUInt64(); this.Unknown_30h = reader.ReadUInt32(); this.Unknown_34h = reader.ReadUInt32(); this.TagsPointer = reader.ReadUInt64(); this.PropertiesPointer = reader.ReadUInt64(); this.Unknown_48h = reader.ReadUInt32(); this.Unknown_4Ch = reader.ReadUInt32(); this.Name = reader.ReadStringAt(this.NamePointer); this.Tags = reader.ReadBlockAt( this.TagsPointer // offset ); this.Properties = reader.ReadBlockAt( this.PropertiesPointer // offset ); switch (VFT)//some examples { case 1079664808: case 1079656584: case 1079607128: break; default: break; } switch (Unknown_30h) { case 0: case 1: break; default: break; } } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.NamePointer = (ulong)(this.NameBlock != null ? this.NameBlock.FilePosition : 0); this.NameLength = (ushort)(Name?.Length ?? 0); this.NameCapacity = (ushort)((Name != null) ? Name.Length + 1 : 0); this.TagsPointer = (ulong)(this.Tags != null ? this.Tags.FilePosition : 0); this.PropertiesPointer = (ulong)(this.Properties != null ? this.Properties.FilePosition : 0); // write structure data writer.Write(this.VFT); writer.Write(this.Unknown_04h); writer.Write(this.Unknown_08h); writer.Write(this.Unknown_0Ch); writer.Write((uint)this.Type); writer.Write(this.Unknown_14h); writer.Write(this.NamePointer); writer.Write(this.NameLength); writer.Write(this.NameCapacity); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28hPtr); writer.Write(this.Unknown_30h); writer.Write(this.Unknown_34h); writer.Write(this.TagsPointer); writer.Write(this.PropertiesPointer); writer.Write(this.Unknown_48h); writer.Write(this.Unknown_4Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Name != null) { NameBlock = (string_r)Name; list.Add(NameBlock); } if (Tags is not null) list.Add(Tags); if (Properties is not null) list.Add(Properties); return list.ToArray(); } public IResourceSystemBlock GetType(ResourceDataReader reader, params object[] parameters) { reader.Position += 16; var type = reader.ReadByte(); reader.Position -= 17; return ConstructClip((ClipType)type); } public static ClipBase? ConstructClip(ClipType type) { return type switch { ClipType.Animation => new ClipAnimation(), ClipType.AnimationList => new ClipAnimationList(), _ => null,// throw new Exception("Unknown type"); }; } public override string ToString() => Name; public virtual void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "Hash", MetaXml.XmlEscape(YcdXml.HashString(Hash))); YcdXml.StringTag(sb, indent, "Name", MetaXml.XmlEscape(Name)); YcdXml.ValueTag(sb, indent, "Type", Type.ToString()); YcdXml.ValueTag(sb, indent, "Unknown30", Unknown_30h.ToString()); YcdXml.WriteItemArray(sb, Tags?.Tags?.data_items, indent, "Tags"); YcdXml.WriteItemArray(sb, Properties?.AllProperties, indent, "Properties"); } public virtual void ReadXml(XmlNode node) { Hash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "Hash")); Name = Xml.GetChildInnerText(node, "Name"); Unknown_30h = Xml.GetChildUIntAttribute(node, "Unknown30", "value"); var tags = XmlMeta.ReadItemArrayNullable(node, "Tags"); Tags = new ClipTagList(); if (tags != null) { Tags.Tags = new ResourcePointerArray64(); Tags.Tags.data_items = tags; Tags.BuildAllTags(); Tags.AssignTagOwners(); } var props = XmlMeta.ReadItemArrayNullable(node, "Properties"); Properties = new ClipPropertyMap(); Properties.CreatePropertyMap(props); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipAnimation : ClipBase { public override long BlockLength => 112; // structure data public ulong AnimationPointer { get; set; } public float StartTime { get; set; } //start time public float EndTime { get; set; } //end time public float Rate { get; set; } //1.0 rate..? public uint Unknown_64h { get; set; } // 0x00000000 public uint Unknown_68h { get; set; } // 0x00000000 public uint Unknown_6Ch { get; set; } // 0x00000000 // reference data public Animation? Animation { get; set; } public MetaHash AnimationHash { get; set; } //used when reading XML. public ClipAnimation() { Type = ClipType.Animation; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); this.AnimationPointer = reader.ReadUInt64(); this.StartTime = reader.ReadSingle(); this.EndTime = reader.ReadSingle(); this.Rate = reader.ReadSingle(); this.Unknown_64h = reader.ReadUInt32(); this.Unknown_68h = reader.ReadUInt32(); this.Unknown_6Ch = reader.ReadUInt32(); this.Animation = reader.ReadBlockAt( this.AnimationPointer // offset ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); this.AnimationPointer = (ulong)(this.Animation != null ? this.Animation.FilePosition : 0); writer.Write(this.AnimationPointer); writer.Write(this.StartTime); writer.Write(this.EndTime); writer.Write(this.Rate); writer.Write(this.Unknown_64h); writer.Write(this.Unknown_68h); writer.Write(this.Unknown_6Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); list.AddRange(base.GetReferences()); if (Animation is not null) list.Add(Animation); return list.ToArray(); } public float GetPlaybackTime(double currentTime) { double scaledTime = currentTime * Rate; double duration = EndTime - StartTime; double curpos = scaledTime % duration; return StartTime + (float)curpos; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.StringTag(sb, indent, "AnimationHash", YcdXml.HashString(Animation?.Hash ?? 0)); YcdXml.ValueTag(sb, indent, "StartTime", FloatUtil.ToString(StartTime)); YcdXml.ValueTag(sb, indent, "EndTime", FloatUtil.ToString(EndTime)); YcdXml.ValueTag(sb, indent, "Rate", FloatUtil.ToString(Rate)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); AnimationHash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "AnimationHash")); StartTime = Xml.GetChildFloatAttribute(node, "StartTime", "value"); EndTime = Xml.GetChildFloatAttribute(node, "EndTime", "value"); Rate = Xml.GetChildFloatAttribute(node, "Rate", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipAnimationList : ClipBase { public override long BlockLength => 112; // structure data public ulong AnimationsPointer { get; set; } public ushort AnimationsCount1 { get; set; } public ushort AnimationsCount2 { get; set; } public uint Unknown_5Ch { get; set; } // 0x00000000 public float Duration { get; set; } public uint Unknown_64h { get; set; } = 1; // 0x00000001 public uint Unknown_68h { get; set; } // 0x00000000 public uint Unknown_6Ch { get; set; } // 0x00000000 // reference data public ResourceSimpleArray? Animations { get; set; } public ClipAnimationList() { Type = ClipType.AnimationList; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); this.AnimationsPointer = reader.ReadUInt64(); this.AnimationsCount1 = reader.ReadUInt16(); this.AnimationsCount2 = reader.ReadUInt16(); this.Unknown_5Ch = reader.ReadUInt32(); this.Duration = reader.ReadSingle(); this.Unknown_64h = reader.ReadUInt32(); this.Unknown_68h = reader.ReadUInt32(); this.Unknown_6Ch = reader.ReadUInt32(); this.Animations = reader.ReadBlockAt>( this.AnimationsPointer, // offset this.AnimationsCount1 ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); this.AnimationsPointer = (ulong)(this.Animations != null ? this.Animations.FilePosition : 0); this.AnimationsCount1 = (ushort)(this.Animations != null ? this.Animations.Count : 0); this.AnimationsCount2 = this.AnimationsCount1; writer.Write(this.AnimationsPointer); writer.Write(this.AnimationsCount1); writer.Write(this.AnimationsCount2); writer.Write(this.Unknown_5Ch); writer.Write(this.Duration); writer.Write(this.Unknown_64h); writer.Write(this.Unknown_68h); writer.Write(this.Unknown_6Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); list.AddRange(base.GetReferences()); if (Animations is not null) list.Add(Animations); return list.ToArray(); } public float GetPlaybackTime(double currentTime) { double scaledTime = currentTime;// * Rate; double duration = Duration;// EndTime - StartTime; double curpos = scaledTime % duration; return /*StartTime +*/ (float)curpos; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Duration", FloatUtil.ToString(Duration)); YcdXml.WriteItemArray(sb, Animations?.Data.ToArray(), indent, "Animations"); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Duration = Xml.GetChildFloatAttribute(node, "Duration", "value"); var anims = XmlMeta.ReadItemArrayNullable(node, "Animations"); Animations = new ResourceSimpleArray(); if (anims != null) { Animations.Data = anims.ToList(); } else { Animations.Data = new List(); } } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipAnimationsEntry : ResourceSystemBlock, IMetaXmlItem { public override long BlockLength => 24; // structure data public float StartTime { get; set; } public float EndTime { get; set; } public float Rate { get; set; } public uint Unknown_0Ch { get; set; } // 0x00000000 public ulong AnimationPointer { get; set; } // reference data public Animation? Animation { get; set; } public MetaHash AnimationHash { get; set; } //used when reading XML. public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.StartTime = reader.ReadSingle(); this.EndTime = reader.ReadSingle(); this.Rate = reader.ReadSingle(); this.Unknown_0Ch = reader.ReadUInt32(); this.AnimationPointer = reader.ReadUInt64(); // read reference data this.Animation = reader.ReadBlockAt( this.AnimationPointer // offset ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data AnimationPointer = (ulong)(Animation?.FilePosition ?? 0); // write structure data writer.Write(this.StartTime); writer.Write(this.EndTime); writer.Write(this.Rate); writer.Write(this.Unknown_0Ch); writer.Write(this.AnimationPointer); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Animation != null) list.Add(Animation); return list.ToArray(); } public float GetPlaybackTime(double currentTime) { double scaledTime = currentTime * Rate; double duration = EndTime - StartTime; double curpos = scaledTime % duration; return StartTime + (float)curpos; } public void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "AnimationHash", YcdXml.HashString(Animation?.Hash ?? 0)); YcdXml.ValueTag(sb, indent, "StartTime", FloatUtil.ToString(StartTime)); YcdXml.ValueTag(sb, indent, "EndTime", FloatUtil.ToString(EndTime)); YcdXml.ValueTag(sb, indent, "Rate", FloatUtil.ToString(Rate)); } public void ReadXml(XmlNode node) { AnimationHash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "AnimationHash")); StartTime = Xml.GetChildFloatAttribute(node, "StartTime", "value"); EndTime = Xml.GetChildFloatAttribute(node, "EndTime", "value"); Rate = Xml.GetChildFloatAttribute(node, "Rate", "value"); } } public enum ClipType : uint { Animation = 1, AnimationList = 2, } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyMap : ResourceSystemBlock { public override long BlockLength => 16; // structure data public ulong PropertyEntriesPointer { get; set; } public ushort PropertyEntriesCapacity { get; set; } public ushort PropertyEntriesCount { get; set; } public uint Unknown_0Ch { get; set; } = 0x01000000; // 0x01000000 // reference data public ResourcePointerArray64? Properties { get; set; } public ClipProperty[]? AllProperties { get; set; } public Dictionary? PropertyMap { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.PropertyEntriesPointer = reader.ReadUInt64(); this.PropertyEntriesCapacity = reader.ReadUInt16(); this.PropertyEntriesCount = reader.ReadUInt16(); this.Unknown_0Ch = reader.ReadUInt32(); // read reference data this.Properties = reader.ReadBlockAt>( this.PropertyEntriesPointer, // offset this.PropertyEntriesCapacity ); BuildPropertyMap(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.PropertyEntriesPointer = (ulong)(this.Properties != null ? this.Properties.FilePosition : 0); this.PropertyEntriesCapacity = (ushort)(this.Properties != null ? this.Properties.Count : 0); this.PropertyEntriesCount = (ushort)(this.AllProperties?.Length ?? 0); // write structure data writer.Write(this.PropertyEntriesPointer); writer.Write(this.PropertyEntriesCapacity); writer.Write(this.PropertyEntriesCount); writer.Write(this.Unknown_0Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Properties != null) list.Add(Properties); return list.ToArray(); } public override string ToString() => $"Count: {AllProperties?.Length ?? 0}"; public void BuildPropertyMap() { if (Properties?.data_items is not null) { using PooledList pl = new PooledList(); foreach (var pme in Properties.data_items) { ClipPropertyMapEntry cpme = pme; while (cpme?.Data != null) { pl.Add(cpme.Data); cpme = cpme.Next; } } AllProperties = pl.ToArray(); PropertyMap = new Dictionary(); foreach (var cp in AllProperties) { PropertyMap[cp.NameHash] = cp; } } } public void CreatePropertyMap(ClipProperty[] properties) { var numBuckets = ClipDictionary.GetNumHashBuckets(properties?.Length ?? 0); var buckets = new List[numBuckets]; if (properties != null) { foreach (var prop in properties) { var b = prop.NameHash % numBuckets; var bucket = buckets[b]; if (bucket == null) { bucket = new List(); buckets[b] = bucket; } var pme = new ClipPropertyMapEntry(); pme.PropertyNameHash = prop.NameHash; pme.Data = prop; bucket.Add(pme); } } var newProperties = new List(); foreach (var b in buckets) { if (b is null || b.Count == 0) newProperties.Add(null); else { newProperties.Add(b[0]); var p = b[0]; for (int i = 1; i < b.Count; i++) { var c = b[i]; c.Next = null; p.Next = c; p = c; } } } Properties = new ResourcePointerArray64(); Properties.data_items = newProperties.ToArray(); AllProperties = properties; } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyMapEntry : ResourceSystemBlock { public override long BlockLength => 32; // structure data public MetaHash PropertyNameHash { get; set; } public uint Unknown_04h { get; set; } // 0x00000000 public ulong DataPointer { get; set; } public ulong NextPointer { get; set; } public uint Unknown_18h { get; set; } // 0x00000000 public uint Unknown_1Ch { get; set; } // 0x00000000 // reference data public ClipProperty Data { get; set; } public ClipPropertyMapEntry Next { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.PropertyNameHash = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); this.DataPointer = reader.ReadUInt64(); this.NextPointer = reader.ReadUInt64(); this.Unknown_18h = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); // read reference data this.Data = reader.ReadBlockAt( this.DataPointer // offset ); this.Next = reader.ReadBlockAt( this.NextPointer // offset ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.DataPointer = (ulong)(this.Data != null ? this.Data.FilePosition : 0); this.NextPointer = (ulong)(this.Next != null ? this.Next.FilePosition : 0); // write structure data writer.Write(this.PropertyNameHash); writer.Write(this.Unknown_04h); writer.Write(this.DataPointer); writer.Write(this.NextPointer); writer.Write(this.Unknown_18h); writer.Write(this.Unknown_1Ch); } public override IResourceBlock[] GetReferences() { var list = new List(); if (Data != null) list.Add(Data); if (Next != null) list.Add(Next); return list.ToArray(); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipProperty : ResourceSystemBlock, IMetaXmlItem { public override long BlockLength => 64; // structure data public uint VFT { get; set; } public uint Unknown_04h { get; set; } = 1; // 0x00000001 public uint Unknown_08h { get; set; } // 0x00000000 public uint Unknown_0Ch { get; set; } // 0x00000000 public uint Unknown_10h { get; set; } // 0x00000000 public uint Unknown_14h { get; set; } // 0x00000000 public MetaHash NameHash { get; set; } public uint Unknown_1Ch { get; set; } // 0x00000000 public ulong AttributesPointer { get; set; } public ushort AttributesCount { get; set; } public ushort AttributesCapacity { get; set; } public uint Unknown_2Ch { get; set; } // 0x00000000 public uint Unknown_30h { get; set; } // 0x00000000 public uint Unknown_34h { get; set; } // 0x00000000 public MetaHash UnkHash { get; set; } public uint Unknown_3Ch { get; set; } // 0x00000000 // reference data public ResourcePointerArray64 Attributes { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.VFT = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); this.Unknown_08h = reader.ReadUInt32(); this.Unknown_0Ch = reader.ReadUInt32(); this.Unknown_10h = reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.NameHash = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); this.AttributesPointer = reader.ReadUInt64(); this.AttributesCount = reader.ReadUInt16(); this.AttributesCapacity = reader.ReadUInt16(); this.Unknown_2Ch = reader.ReadUInt32(); this.Unknown_30h = reader.ReadUInt32(); this.Unknown_34h = reader.ReadUInt32(); this.UnkHash = reader.ReadUInt32(); this.Unknown_3Ch = reader.ReadUInt32(); // read reference data this.Attributes = reader.ReadBlockAt>( this.AttributesPointer, // offset this.AttributesCount ); switch (VFT)//some examples { case 1080111464: case 1080103160: case 1080119200: case 1080069168: case 1080053176: break; default: break; } } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.AttributesPointer = (ulong)(this.Attributes != null ? this.Attributes.FilePosition : 0); this.AttributesCount = (ushort)(this.Attributes != null ? this.Attributes.Count : 0); this.AttributesCapacity = this.AttributesCount; // write structure data writer.Write(this.VFT); writer.Write(this.Unknown_04h); writer.Write(this.Unknown_08h); writer.Write(this.Unknown_0Ch); writer.Write(this.Unknown_10h); writer.Write(this.Unknown_14h); writer.Write(this.NameHash); writer.Write(this.Unknown_1Ch); writer.Write(this.AttributesPointer); writer.Write(this.AttributesCount); writer.Write(this.AttributesCapacity); writer.Write(this.Unknown_2Ch); writer.Write(this.Unknown_30h); writer.Write(this.Unknown_34h); writer.Write(this.UnkHash); writer.Write(this.Unknown_3Ch); } public override IResourceBlock[] GetReferences() { if (Attributes is null) return []; return [Attributes]; } public override string ToString() { if (Attributes?.data_items is not null) { StringBuilder sb = new StringBuilder(); foreach (var item in Attributes.data_items) { if (sb.Length > 0) sb.Append(", "); sb.Append(item.ToString()); } return $"{NameHash}: {UnkHash}: {sb}"; } return $"{NameHash}: {UnkHash}"; } public virtual void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "NameHash", YcdXml.HashString(NameHash)); YcdXml.StringTag(sb, indent, "UnkHash", YcdXml.HashString(UnkHash)); YcdXml.WriteItemArray(sb, Attributes?.data_items, indent, "Attributes"); } public virtual void ReadXml(XmlNode node) { NameHash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "NameHash")); UnkHash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "UnkHash")); var attrsNode = node.SelectSingleNode("Attributes"); if (attrsNode != null) { var inodes = attrsNode.SelectNodes("Item"); if (inodes?.Count > 0) { var alist = new List(); foreach (XmlNode inode in inodes) { var item = new ClipPropertyAttribute(); item.ReadXml(inode); var v = ClipPropertyAttribute.ConstructItem(item.Type); v.ReadXml(inode);//slightly wasteful but meh alist.Add(v); } Attributes = new ResourcePointerArray64(); Attributes.data_items = alist.ToArray(); } } } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttribute : ResourceSystemBlock, IResourceXXSystemBlock, IMetaXmlItem { public override long BlockLength => 16; public uint VFT { get; set; } public uint Unknown_04h { get; set; } = 1; // 0x00000001 public ClipPropertyAttributeType Type { get; set; } public byte Unknown_09h { get; set; } // 0x00 public ushort Unknown_Ah { get; set; } // 0x0000 public uint Unknown_Ch { get; set; } // 0x00000000 public uint Unknown_10h { get; set; } // 0x00000000 public uint Unknown_14h { get; set; } // 0x00000000 public MetaHash NameHash { get; set; } public uint Unknown_1Ch { get; set; } // 0x00000000 public override void Read(ResourceDataReader reader, params object[] parameters) { this.VFT = reader.ReadUInt32(); this.Unknown_04h = reader.ReadUInt32(); this.Type = (ClipPropertyAttributeType)reader.ReadByte(); this.Unknown_09h = reader.ReadByte(); this.Unknown_Ah = reader.ReadUInt16(); this.Unknown_Ch = reader.ReadUInt32(); this.Unknown_10h = reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.NameHash = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); switch (VFT)//some examples { case 1080119416://type 1 case 1080119528://type 2 case 1080119640://type 3 case 1080119752://type 4 case 1080119832://type 6 case 1080120088://type 8 case 1080120472://type 12 case 1080069384://type 1 case 1080069496://type 2 case 1080127976: case 1080069800://type 6 break; default: break; } } public override void Write(ResourceDataWriter writer, params object[] parameters) { writer.Write(this.VFT); writer.Write(this.Unknown_04h); writer.Write((byte)this.Type); writer.Write(this.Unknown_09h); writer.Write(this.Unknown_Ah); writer.Write(this.Unknown_Ch); writer.Write(this.Unknown_10h); writer.Write(this.Unknown_14h); writer.Write(this.NameHash); writer.Write(this.Unknown_1Ch); } public IResourceSystemBlock GetType(ResourceDataReader reader, params object[] parameters) { reader.Position += 8; var type = (ClipPropertyAttributeType)reader.ReadByte(); reader.Position -= 9; return ConstructItem(type); } public static ClipPropertyAttribute ConstructItem(ClipPropertyAttributeType type) { switch (type) { case ClipPropertyAttributeType.Float: return new ClipPropertyAttributeFloat(); case ClipPropertyAttributeType.Int: return new ClipPropertyAttributeInt(); case ClipPropertyAttributeType.Bool: return new ClipPropertyAttributeBool(); case ClipPropertyAttributeType.String: return new ClipPropertyAttributeString(); case ClipPropertyAttributeType.Vector3: return new ClipPropertyAttributeVector3(); case ClipPropertyAttributeType.Vector4: return new ClipPropertyAttributeVector4(); case ClipPropertyAttributeType.HashString: return new ClipPropertyAttributeHashString(); default: return null;// throw new Exception("Unknown type"); } } public virtual void WriteXml(StringBuilder sb, int indent) { YcdXml.StringTag(sb, indent, "NameHash", YcdXml.HashString(NameHash)); YcdXml.ValueTag(sb, indent, "Type", Type.ToString()); } public virtual void ReadXml(XmlNode node) { NameHash = XmlMeta.GetHash(Xml.GetChildInnerText(node, "NameHash")); Type = Xml.GetEnumValue(Xml.GetChildStringAttribute(node, "Type", "value")); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeFloat : ClipPropertyAttribute { public override long BlockLength => 48; public float Value { get; set; } public uint Unknown_24h { get; set; } // 0x00000000 public uint Unknown_28h { get; set; } // 0x00000000 public uint Unknown_2Ch { get; set; } // 0x00000000 public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Value = reader.ReadSingle(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28h = reader.ReadUInt32(); this.Unknown_2Ch = reader.ReadUInt32(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28h); writer.Write(this.Unknown_2Ch); } public override string ToString() { return $"Float:{FloatUtil.ToString(Value)}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Value", FloatUtil.ToString(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildFloatAttribute(node, "Value", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeInt : ClipPropertyAttribute { public override long BlockLength => 48; public int Value { get; set; } public uint Unknown_24h { get; set; } // 0x00000000 public uint Unknown_28h { get; set; } // 0x00000000 public uint Unknown_2Ch { get; set; } // 0x00000000 public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Value = reader.ReadInt32(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28h = reader.ReadUInt32(); this.Unknown_2Ch = reader.ReadUInt32(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28h); writer.Write(this.Unknown_2Ch); } public override string ToString() { return $"Int:{Value}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Value", Value.ToString()); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildIntAttribute(node, "Value", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeBool : ClipPropertyAttribute { public override long BlockLength => 48; public uint Value { get; set; } public uint Unknown_24h { get; set; } // 0x00000000 public uint Unknown_28h { get; set; } // 0x00000000 public uint Unknown_2Ch { get; set; } // 0x00000000 public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Value = reader.ReadUInt32(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28h = reader.ReadUInt32(); this.Unknown_2Ch = reader.ReadUInt32(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28h); writer.Write(this.Unknown_2Ch); } public override string ToString() { return $"Uint:{Value}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "Value", Value.ToString()); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildUIntAttribute(node, "Value", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeString : ClipPropertyAttribute { public override long BlockLength => 48; public ulong ValuePointer { get; set; } public ushort ValueLength { get; set; } public ushort ValueCapacity { get; set; } public uint Unknown_02Ch { get; set; } // 0x00000000 public string Value; private string_r ValueBlock = null; public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.ValuePointer = reader.ReadUInt64(); this.ValueLength = reader.ReadUInt16(); this.ValueCapacity = reader.ReadUInt16(); this.Unknown_02Ch = reader.ReadUInt32(); //// read reference data Value = reader.ReadStringAt(ValuePointer); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // update structure data this.ValuePointer = (ulong)(this.ValueBlock != null ? this.ValueBlock.FilePosition : 0); this.ValueLength = (ushort)(Value?.Length ?? 0); this.ValueCapacity = (ushort)((Value != null) ? Value.Length + 1 : 0); // write structure data writer.Write(this.ValuePointer); writer.Write(this.ValueLength); writer.Write(this.ValueCapacity); writer.Write(this.Unknown_02Ch); } public override IResourceBlock[] GetReferences() { var list = new List(base.GetReferences()); if (Value is not null) { ValueBlock = (string_r)Value; list.Add(ValueBlock); } return list.ToArray(); } public override string ToString() { return $"String:{Value}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.StringTag(sb, indent, "Value", MetaXml.XmlEscape(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildInnerText(node, "Value"); ValueLength = (ushort)(Value?.Length??0); ValueCapacity = ValueLength; } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeVector3 : ClipPropertyAttribute { public override long BlockLength => 48; public Vector3 Value { get; set; } public float Unknown_02Ch { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data Value = reader.ReadVector3(); this.Unknown_02Ch = reader.ReadSingle(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); writer.Write(this.Unknown_02Ch); } public override string ToString() { return $"Vector3:{FloatUtil.GetVector3String(Value)}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.SelfClosingTag(sb, indent, "Value " + FloatUtil.GetVector3XmlString(Value)); YcdXml.ValueTag(sb, indent, "Unknown2C", FloatUtil.ToString(Unknown_02Ch)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildVector3Attributes(node, "Value"); Unknown_02Ch = Xml.GetChildFloatAttribute(node, "Unknown2C", "value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeVector4 : ClipPropertyAttribute { public override long BlockLength => 48; public Vector4 Value { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Value = reader.ReadVector4(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); } public override string ToString() { return $"Vector4:{FloatUtil.GetVector4String(Value)}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.SelfClosingTag(sb, indent, "Value " + FloatUtil.GetVector4XmlString(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = Xml.GetChildVector4Attributes(node, "Value"); } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipPropertyAttributeHashString : ClipPropertyAttribute { public override long BlockLength => 0x30; public MetaHash Value { get; set; } public uint Unknown_24h { get; set; } // 0x00000000 public uint Unknown_28h { get; set; } // 0x00000000 public uint Unknown_2Ch { get; set; } // 0x00000000 public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.Value = reader.ReadUInt32(); this.Unknown_24h = reader.ReadUInt32(); this.Unknown_28h = reader.ReadUInt32(); this.Unknown_2Ch = reader.ReadUInt32(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // write structure data writer.Write(this.Value); writer.Write(this.Unknown_24h); writer.Write(this.Unknown_28h); writer.Write(this.Unknown_2Ch); } public override string ToString() { return $"Hash:{Value}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.StringTag(sb, indent, "Value", YcdXml.HashString(Value)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); Value = XmlMeta.GetHash(Xml.GetChildInnerText(node, "Value")); } } public enum ClipPropertyAttributeType : byte { Float = 1, Int = 2, Bool = 3, String = 4, Vector3 = 6, Vector4 = 8, HashString = 12, } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipTagList : ResourceSystemBlock { public override long BlockLength { get { return 32; } } // structure data public ulong TagsPointer { get; set; } public ushort TagCount1 { get; set; } public ushort TagCount2 { get; set; } public uint Unknown_0Ch { get; set; } // 0x00000000 public uint HasBlockTag { get; set; } // 0, 1 public uint Unknown_14h { get; set; } // 0x00000000 public uint Unknown_18h { get; set; } // 0x00000000 public uint Unknown_1Ch { get; set; } // 0x00000000 // reference data public ResourcePointerArray64 Tags { get; set; } public ClipTag[] AllTags { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { // read structure data this.TagsPointer = reader.ReadUInt64(); this.TagCount1 = reader.ReadUInt16(); this.TagCount2 = reader.ReadUInt16(); this.Unknown_0Ch = reader.ReadUInt32(); this.HasBlockTag = reader.ReadUInt32(); this.Unknown_14h = reader.ReadUInt32(); this.Unknown_18h = reader.ReadUInt32(); this.Unknown_1Ch = reader.ReadUInt32(); // read reference data this.Tags = reader.ReadBlockAt>( this.TagsPointer, // offset this.TagCount1 ); BuildAllTags(); } public override void Write(ResourceDataWriter writer, params object[] parameters) { // update structure data this.TagsPointer = (ulong)(this.Tags != null ? this.Tags.FilePosition : 0); this.TagCount1 = (ushort)(this.Tags != null ? this.Tags.Count : 0); this.TagCount2 = this.TagCount1; BuildAllTags(); //just in case? updates HasBlockTag // write structure data writer.Write(this.TagsPointer); writer.Write(this.TagCount1); writer.Write(this.TagCount2); writer.Write(this.Unknown_0Ch); writer.Write(this.HasBlockTag); writer.Write(this.Unknown_14h); writer.Write(this.Unknown_18h); writer.Write(this.Unknown_1Ch); } public override IResourceBlock[] GetReferences() { if (Tags is null) return []; return [Tags]; } public override string ToString() { return $"Count: {AllTags?.Length ?? 0}"; } public void BuildAllTags() { if (Tags?.data_items is not null) { using var tl = new PooledList(); foreach (var te in Tags.data_items) { if (te is null) continue; tl.Add(te); } AllTags = tl.ToArray(); } uint hasBlock = 0; if (AllTags is not null) { foreach (var tag in AllTags) { if (tag.NameHash == (uint)MetaName.block) { hasBlock = 1; break; } } } HasBlockTag = hasBlock; } public void AssignTagOwners() { if (Tags?.data_items is null) return; foreach (var tag in Tags.data_items) { tag.Tags = this; } } } [TypeConverter(typeof(ExpandableObjectConverter))] public class ClipTag : ClipProperty { public override long BlockLength => 80; public float StartPhase { get; set; } public float EndPhase { get; set; } public ulong TagsPointer { get; set; } // reference data public ClipTagList? Tags { get; set; } public override void Read(ResourceDataReader reader, params object[] parameters) { base.Read(reader, parameters); // read structure data this.StartPhase = reader.ReadSingle(); this.EndPhase = reader.ReadSingle(); this.TagsPointer = reader.ReadUInt64(); // read reference data this.Tags = reader.ReadBlockAt( this.TagsPointer // offset ); } public override void Write(ResourceDataWriter writer, params object[] parameters) { base.Write(writer, parameters); // update structure data this.TagsPointer = (ulong)(this.Tags != null ? this.Tags.FilePosition : 0); // write structure data writer.Write(this.StartPhase); writer.Write(this.EndPhase); writer.Write(this.TagsPointer); } public override IResourceBlock[] GetReferences() { var list = new List(base.GetReferences()); if (Tags is not null) list.Add(Tags); return list.ToArray(); } public override string ToString() { return $"{base.ToString()}: {StartPhase}, {EndPhase}"; } public override void WriteXml(StringBuilder sb, int indent) { base.WriteXml(sb, indent); YcdXml.ValueTag(sb, indent, "StartPhase", FloatUtil.ToString(StartPhase)); YcdXml.ValueTag(sb, indent, "EndPhase", FloatUtil.ToString(EndPhase)); } public override void ReadXml(XmlNode node) { base.ReadXml(node); StartPhase = Xml.GetChildFloatAttribute(node, "StartPhase", "value"); EndPhase = Xml.GetChildFloatAttribute(node, "EndPhase", "value"); } } }