using System; using System.Collections.Generic; using Oni.Xml; using Oni.Metadata; namespace Oni.Totoro { internal class AnimationDatWriter { private Animation animation; private List extents; private DatExtentInfo extentInfo; private Importer importer; private BinaryWriter dat; private BinaryWriter raw; #region private class DatExtent private class DatExtent { public readonly int Frame; public readonly AttackExtent Extent; public DatExtent(int frame, AttackExtent extent) { this.Frame = frame; this.Extent = extent; } } #endregion #region private class DatExtentInfo private class DatExtentInfo { public float MaxDistance; public float MinY = 1e09f; public float MaxY = -1e09f; public readonly DatExtentInfoFrame FirstExtent = new DatExtentInfoFrame(); public readonly DatExtentInfoFrame MaxExtent = new DatExtentInfoFrame(); } #endregion #region private class DatExtentInfoFrame private class DatExtentInfoFrame { public int Frame = -1; public int Attack; public int AttackOffset; public Vector2 Location; public float Height; public float Length; public float MinY; public float MaxY; public float Angle; } #endregion private AnimationDatWriter() { } public static void Write(Animation animation, Importer importer, BinaryWriter dat) { var writer = new AnimationDatWriter { animation = animation, importer = importer, dat = dat, raw = importer.RawWriter }; writer.WriteAnimation(); } private void WriteAnimation() { extentInfo = new DatExtentInfo(); extents = new List(); if (animation.Attacks.Count > 0) { if (animation.Attacks[0].Extents.Count == 0) GenerateExtentInfo(); foreach (var attack in animation.Attacks) { int frame = attack.Start; foreach (var extent in attack.Extents) extents.Add(new DatExtent(frame++, extent)); } GenerateExtentSummary(); } var rotations = animation.Rotations; int frameSize = animation.FrameSize; if (frameSize == 16 && (animation.Flags & AnimationFlags.Overlay) == 0) { rotations = CompressFrames(rotations); frameSize = 6; } dat.Write(0); WriteRawArray(animation.Heights, x => raw.Write(x)); WriteRawArray(animation.Velocities, x => raw.Write(x)); WriteRawArray(animation.Attacks, Write); WriteRawArray(animation.SelfDamage, Write); WriteRawArray(animation.MotionBlur, Write); WriteRawArray(animation.Shortcuts, Write); WriteThrowInfo(); WriteRawArray(animation.Footsteps, Write); WriteRawArray(animation.Particles, Write); WriteRawArray(animation.Positions, Write); WriteRotations(rotations, frameSize); WriteRawArray(animation.Sounds, Write); dat.Write((int)animation.Flags); if (!string.IsNullOrEmpty(animation.DirectAnimations[0])) dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[0])); else dat.Write(0); if (!string.IsNullOrEmpty(animation.DirectAnimations[1])) dat.Write(importer.CreateInstance(TemplateTag.TRAM, animation.DirectAnimations[1])); else dat.Write(0); dat.Write((int)animation.OverlayUsedBones); dat.Write((int)animation.OverlayReplacedBones); dat.Write(animation.FinalRotation); dat.Write((ushort)animation.Direction); dat.WriteUInt16(animation.Vocalization); WriteExtentInfo(); dat.Write(animation.Impact, 16); dat.WriteUInt16(animation.HardPause); dat.WriteUInt16(animation.SoftPause); dat.Write(animation.Sounds.Count); dat.Skip(6); dat.WriteUInt16(60); dat.WriteUInt16(frameSize); dat.WriteUInt16((ushort)animation.Type); dat.WriteUInt16((ushort)animation.AimingType); dat.WriteUInt16((ushort)animation.FromState); dat.WriteUInt16((ushort)animation.ToState); dat.WriteUInt16(rotations.Count); dat.WriteUInt16(animation.Velocities.Count); dat.WriteUInt16(animation.Velocities.Count); dat.WriteUInt16((ushort)animation.Varient); dat.Skip(2); dat.WriteUInt16(animation.AtomicStart); dat.WriteUInt16(animation.AtomicEnd); dat.WriteUInt16(animation.InterpolationEnd); dat.WriteUInt16(animation.InterpolationMax); dat.WriteUInt16(animation.ActionFrame); dat.WriteUInt16(animation.FirstLevelAvailable); dat.WriteByte(animation.InvulnerableStart); dat.WriteByte(animation.InvulnerableEnd); dat.WriteByte(animation.Attacks.Count); dat.WriteByte(animation.SelfDamage.Count); dat.WriteByte(animation.MotionBlur.Count); dat.WriteByte(animation.Shortcuts.Count); dat.WriteByte(animation.Footsteps.Count); dat.WriteByte(animation.Particles.Count); } private void WriteRotations(List> rotations, int frameSize) { dat.Write(raw.Align32()); var offsets = new ushort[rotations.Count]; offsets[0] = (ushort)(rotations.Count * 2); for (int i = 1; i < offsets.Length; i++) offsets[i] = (ushort)(offsets[i - 1] + rotations[i - 1].Count * (frameSize + 1) - 1); raw.Write(offsets); foreach (var keys in rotations) { foreach (var key in keys) { switch (frameSize) { case 6: raw.WriteInt16((short)(Math.Round(key.Rotation.X / 180.0f * 32767.5f))); raw.WriteInt16((short)(Math.Round(key.Rotation.Y / 180.0f * 32767.5f))); raw.WriteInt16((short)(Math.Round(key.Rotation.Z / 180.0f * 32767.5f))); break; case 16: raw.Write(new Quaternion(key.Rotation)); break; } if (key != keys.Last()) raw.WriteByte(key.Duration); } } } private void WriteThrowInfo() { if (animation.ThrowSource == null) { dat.Write(0); return; } dat.Write(raw.Align32()); raw.Write(animation.ThrowSource.Position); raw.Write(animation.ThrowSource.Angle); raw.Write(animation.ThrowSource.Distance); raw.WriteUInt16((ushort)animation.ThrowSource.Type); } private void WriteExtentInfo() { dat.Write(extentInfo.MaxDistance); dat.Write(extentInfo.MinY); dat.Write(extentInfo.MaxY); dat.Write(animation.AttackRing); Write(extentInfo.FirstExtent); Write(extentInfo.MaxExtent); dat.Write(0); dat.Write(extents.Count); WriteRawArray(extents, Write); } private void Write(DatExtentInfoFrame info) { dat.WriteInt16(info.Frame); dat.WriteByte(info.Attack); dat.WriteByte(info.AttackOffset); dat.Write(info.Location); dat.Write(info.Height); dat.Write(info.Length); dat.Write(info.MinY); dat.Write(info.MaxY); dat.Write(info.Angle); } private void Write(Position position) { raw.Write((short)Math.Round(position.X * 100.0f)); raw.Write((short)Math.Round(position.Z * 100.0f)); raw.Write((ushort)Math.Round(position.Height * 100.0f)); raw.Write((short)Math.Round(position.YOffset * 100.0f)); } private void Write(Damage damage) { raw.WriteUInt16(damage.Points); raw.WriteUInt16(damage.Frame); } private void Write(Shortcut shortcut) { raw.WriteUInt16((ushort)shortcut.FromState); raw.WriteUInt16(shortcut.Length); raw.Write(shortcut.ReplaceAtomic ? 1 : 0); } private void Write(Footstep footstep) { raw.WriteUInt16(footstep.Frame); raw.WriteUInt16((ushort)footstep.Type); } private void Write(Sound sound) { raw.Write(sound.Name, 32); raw.WriteUInt16(sound.Start); } private void Write(Particle particle) { raw.WriteUInt16(particle.Start); raw.WriteUInt16(particle.End); raw.Write((int)particle.Bone); raw.Write(particle.Name, 16); } private void Write(MotionBlur m) { raw.Write((int)m.Bones); raw.WriteUInt16(m.Start); raw.WriteUInt16(m.End); raw.WriteByte(m.Lifetime); raw.WriteByte(m.Alpha); raw.WriteByte(m.Interval); raw.WriteByte(0); } private void Write(DatExtent extent) { raw.WriteInt16(extent.Frame); raw.Write((short)Math.Round(extent.Extent.Angle * 65535.0f / 360.0f)); raw.Write((ushort)Math.Round(extent.Extent.Length * 100.0f)); raw.WriteInt16(0); raw.Write((short)Math.Round(extent.Extent.MinY * 100.0f)); raw.Write((short)Math.Round(extent.Extent.MaxY * 100.0f)); } private void Write(Attack attack) { raw.Write((int)attack.Bones); raw.Write(attack.Knockback); raw.Write((int)attack.Flags); raw.WriteInt16(attack.HitPoints); raw.WriteInt16(attack.Start); raw.WriteInt16(attack.End); raw.WriteInt16((short)attack.HitType); raw.WriteInt16(attack.HitLength); raw.WriteInt16(attack.StunLength); raw.WriteInt16(attack.StaggerLength); raw.WriteInt16(0); raw.Write(0); } private void WriteRawArray(List list, Action writeElement) { if (list.Count == 0) { dat.Write(0); return; } dat.Write(raw.Align32()); foreach (T t in list) writeElement(t); } private void GenerateExtentInfo() { float[] attackRing = animation.AttackRing; Array.Clear(attackRing, 0, attackRing.Length); foreach (var attack in animation.Attacks) { attack.Extents.Clear(); for (int frame = attack.Start; frame <= attack.End; frame++) { var position = animation.Positions[frame].XZ; var framePoints = animation.AllPoints[frame]; for (int j = 0; j < framePoints.Count / 8; j++) { if ((attack.Bones & (BoneMask)(1 << j)) == 0) continue; for (int k = j * 8; k < (j + 1) * 8; k++) { var point = framePoints[k]; var delta = point.XZ - animation.Positions[0].XZ; float distance = delta.Length(); float angle = FMath.Atan2(delta.X, delta.Y); if (angle < 0.0f) angle += MathHelper.TwoPi; for (int r = 0; r < attackRing.Length; r++) { float ringAngle = r * MathHelper.TwoPi / attackRing.Length; if (Math.Abs(ringAngle - angle) < MathHelper.ToRadians(30.0f)) attackRing[r] = Math.Max(attackRing[r], distance); } } } float minHeight = +1e09f; float maxHeight = -1e09f; float maxDistance = -1e09f; float maxAngle = 0.0f; for (int j = 0; j < framePoints.Count / 8; j++) { if ((attack.Bones & (BoneMask)(1 << j)) == 0) continue; for (int k = j * 8; k < (j + 1) * 8; k++) { var point = framePoints[k]; var delta = point.XZ - position; float distance; switch (animation.Direction) { case Direction.Forward: distance = delta.Y; break; case Direction.Left: distance = delta.X; break; case Direction.Right: distance = -delta.X; break; case Direction.Backward: distance = -delta.Y; break; default: distance = delta.Length(); break; } if (distance > maxDistance) { maxDistance = distance; maxAngle = FMath.Atan2(delta.X, delta.Y); } minHeight = Math.Min(minHeight, point.Y); maxHeight = Math.Max(maxHeight, point.Y); } } maxDistance = Math.Max(maxDistance, 0.0f); if (maxAngle < 0) maxAngle += MathHelper.TwoPi; attack.Extents.Add(new AttackExtent { Angle = MathHelper.ToDegrees(maxAngle), Length = maxDistance, MinY = minHeight, MaxY = maxHeight }); } } } private void GenerateExtentSummary() { if (extents.Count == 0) return; var positions = animation.Positions; var attacks = animation.Attacks; var heights = animation.Heights; float minY = float.MaxValue, maxY = float.MinValue; foreach (var datExtent in extents) { minY = Math.Min(minY, datExtent.Extent.MinY); maxY = Math.Max(maxY, datExtent.Extent.MaxY); } var firstExtent = extents[0]; var maxExtent = firstExtent; foreach (var datExtent in extents) { if (datExtent.Extent.Length + positions[datExtent.Frame].Z > maxExtent.Extent.Length + positions[maxExtent.Frame].Z) maxExtent = datExtent; } int maxAttackIndex = 0, maxAttackOffset = 0; for (int i = 0; i < attacks.Count; i++) { var attack = attacks[i]; if (attack.Start <= maxExtent.Frame && maxExtent.Frame <= attack.End) { maxAttackIndex = i; maxAttackOffset = maxExtent.Frame - attack.Start; break; } } extentInfo.MaxDistance = animation.AttackRing.Max(); extentInfo.MinY = minY; extentInfo.MaxY = maxY; extentInfo.FirstExtent.Frame = firstExtent.Frame; extentInfo.FirstExtent.Attack = 0; extentInfo.FirstExtent.AttackOffset = 0; extentInfo.FirstExtent.Location.X = positions[firstExtent.Frame].X; extentInfo.FirstExtent.Location.Y = -positions[firstExtent.Frame].Z; extentInfo.FirstExtent.Height = heights[firstExtent.Frame]; extentInfo.FirstExtent.Angle = MathHelper.ToRadians(firstExtent.Extent.Angle); extentInfo.FirstExtent.Length = firstExtent.Extent.Length; extentInfo.FirstExtent.MinY = FMath.Round(firstExtent.Extent.MinY, 2); extentInfo.FirstExtent.MaxY = firstExtent.Extent.MaxY; if ((animation.Flags & AnimationFlags.ThrowTarget) == 0) { extentInfo.MaxExtent.Frame = maxExtent.Frame; extentInfo.MaxExtent.Attack = maxAttackIndex; extentInfo.MaxExtent.AttackOffset = maxAttackOffset; extentInfo.MaxExtent.Location.X = positions[maxExtent.Frame].X; extentInfo.MaxExtent.Location.Y = -positions[maxExtent.Frame].Z; extentInfo.MaxExtent.Height = heights[maxExtent.Frame]; extentInfo.MaxExtent.Angle = MathHelper.ToRadians(maxExtent.Extent.Angle); extentInfo.MaxExtent.Length = maxExtent.Extent.Length; extentInfo.MaxExtent.MinY = maxExtent.Extent.MinY; extentInfo.MaxExtent.MaxY = FMath.Round(maxExtent.Extent.MaxY, 2); } } private List> CompressFrames(List> tracks) { float tolerance = 0.5f; float cosTolerance = FMath.Cos(MathHelper.ToRadians(tolerance) * 0.5f); var newTracks = new List>(); foreach (var keys in tracks) { var newFrames = new List(keys.Count); for (int i = 0; i < keys.Count;) { var key = keys[i]; int duration = key.Duration; var q0 = new Quaternion(key.Rotation); if (duration == 1) { for (int j = i + 2; j < keys.Count; j++) { if (!IsLinearRange(keys, i, j, cosTolerance)) break; duration = j - i; } } var eulerXYZ = q0.ToEulerXYZ(); newFrames.Add(new KeyFrame { Duration = duration, Rotation = { X = eulerXYZ.X, Y = eulerXYZ.Y, Z = eulerXYZ.Z } }); i += duration; } newTracks.Add(newFrames); } return newTracks; } private static bool IsLinearRange(List frames, int first, int last, float tolerance) { var q0 = new Quaternion(frames[first].Rotation); var q1 = new Quaternion(frames[last].Rotation); float length = last - first; for (int i = first + 1; i < last; ++i) { float t = (i - first) / length; var linear = Quaternion.Lerp(q0, q1, t); var real = new Quaternion(frames[i].Rotation); var error = Quaternion.Conjugate(linear) * real; if (Math.Abs(error.W) < tolerance) return false; } return true; } } }