From 7183b9f7551f8336f09afd85ca70b1e94eda28db Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 12:20:14 -0500 Subject: [PATCH 1/6] Make keyframe interpolate opt-in and make pivots a warning in non-strict mode --- kanimal-cli/Options.cs | 7 +++++++ kanimal-cli/Program.cs | 5 ++++- kanimal/Reader/ScmlReader.cs | 20 ++++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/kanimal-cli/Options.cs b/kanimal-cli/Options.cs index d31b4e8..703f47e 100644 --- a/kanimal-cli/Options.cs +++ b/kanimal-cli/Options.cs @@ -19,6 +19,13 @@ public abstract class ConversionOptions : ProgramOptions { [Option('S', "strict", Required = false, HelpText = "When writing to scml, enabling this flag ")] public bool Strict { get; set; } + + [Option('i', "interpolate", Required = false, HelpText = "Enable interpolating scml files on load.")] + public bool Interp { get; set; } + + [Option('b', "debone", Required = false, HelpText = "Enable deboning scml files on load.")] + public bool Debone { get; set; } + } [Verb("dump", HelpText = "Output a dump of the specified kanim.")] diff --git a/kanimal-cli/Program.cs b/kanimal-cli/Program.cs index aec1125..da04877 100644 --- a/kanimal-cli/Program.cs +++ b/kanimal-cli/Program.cs @@ -58,7 +58,10 @@ private static void Convert(string inputFormat, string outputFormat, List path.EndsWith(".scml")); var scmlreader = new ScmlReader(scml) { - AllowMissingSprites = !opt.Strict + AllowMissingSprites = !opt.Strict, + AllowInFramePivots = !opt.Strict, + InterpolateMissingFrames = opt.Interp, + Debone = opt.Debone }; scmlreader.Read(); reader = scmlreader; diff --git a/kanimal/Reader/ScmlReader.cs b/kanimal/Reader/ScmlReader.cs index 20e4356..4773ae4 100644 --- a/kanimal/Reader/ScmlReader.cs +++ b/kanimal/Reader/ScmlReader.cs @@ -30,6 +30,7 @@ public class ScmlReader : Reader private Dictionary inputSprites; // The keys in this dictionary are filenames, *with* the file extension, if it exists. public bool AllowMissingSprites = true; + public bool AllowInFramePivots = true; public bool InterpolateMissingFrames = true; public bool Debone = true; @@ -587,15 +588,26 @@ float Interpolate(string attrName, float defaultValue) if (hasInconsistentIntervals) { var anims = inconsistentAnims.ToList().Join(); - throw new ProjectParseException( - $"SCML format exception: The intervals in the anims {anims} were inconsistent. Aborting read."); + string error = $"SCML format exception: The intervals in the anims {anims} were inconsistent. Aborting read."; + if (!InterpolateMissingFrames) + { + error += " Try enabling keyframe interpolation with the \"-i\" flag and try again."; + } + throw new ProjectParseException(error); } if (hasPivotsSpecifiedInTimeline) { var anims = pivotAnims.ToList().Join(); - throw new ProjectParseException( - $"SCML format exception: There were pivot points specified in timelines rather than only on the sprites in anims {anims}. Aborting read."); + if (AllowInFramePivots) + { + Logger.Warn($"Encountered pivot points specified in timelines in anims {anims}. These pivot point changes will not be respected. Strict-mode is off. Converting anyway."); + } + else + { + throw new ProjectParseException($"SCML format exception: There were pivot points specified in timelines rather than only on the sprites in anims {anims}. Aborting read."); + } + } AnimData.AnimCount = animCount; From 85ac2d22006fbc6c44065c7adaff45f6c8951e2a Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 13:00:30 -0500 Subject: [PATCH 2/6] Handle looping --- .../Processor/KeyFrameInterpolateProcessor.cs | 179 ++++++++++-------- kanimal/Reader/ScmlReader.cs | 39 ++-- 2 files changed, 107 insertions(+), 111 deletions(-) diff --git a/kanimal/Processor/KeyFrameInterpolateProcessor.cs b/kanimal/Processor/KeyFrameInterpolateProcessor.cs index a4f1ea5..e02d529 100644 --- a/kanimal/Processor/KeyFrameInterpolateProcessor.cs +++ b/kanimal/Processor/KeyFrameInterpolateProcessor.cs @@ -75,6 +75,11 @@ class ProcessingAnimation /* the length of the animation in milliseconds - integer */ public int Length { get; set; } + /* if the animation is looped - i.e. does the interpolator + * consider the animation as wrapping around from the end-to-the-start + * or does it consider the end as not having to match up with the start */ + public bool Looping { get; set; } + /* 2d array of the frames, 1st axis is the ids of the timelines for all * of the sprites and bones in the animation, 2nd axis is the timesteps */ public List> FrameArray { get; set; } @@ -354,6 +359,12 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio } } + var looping = true; + if (animation.HasAttribute("looping")) + { + looping = bool.Parse(animation.Attributes["looping"].Value); + } + /* create an additional array that indicates presence of each timeline on a per-frame basis */ List> presenceArray = new List>(); for (int i = 0; i < infoProvider.Size(); i++) @@ -380,21 +391,21 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio } presenceArray[i][j] = currentPresence; } - for (int j = 0; j < numberOfFrames; j++) + /* executing the loop twice is the most straightforward way to ensure that a keyframe at the end + * of the timeline wraps around to the front of the timeline */ + if (looping) { - /* if this frame is a key frame then update the current presence based on if there - * is a frame populated at this location */ - if (keyFrames[j]) + for (int j = 0; j < numberOfFrames; j++) { - currentPresence = (frameArray[i][j] != null); + /* if this frame is a key frame then update the current presence based on if there + * is a frame populated at this location */ + if (keyFrames[j]) + { + currentPresence = (frameArray[i][j] != null); + } + presenceArray[i][j] = currentPresence; } - presenceArray[i][j] = currentPresence; } - /* executing the loop twice is the most straightforward way to ensure that a keyframe at the end - * of the timeline wraps around to the front of the timeline - * this does mess with animations that aren't looped that don't have keyframes at time = 0 but that just doesn't make - * much sense (who wouldn't keyframe at time = 0 for a non-looping animation!) - * so I'll just document that and ignore that problem for now */ } /* for every frame with presence in the array set to true that still has a null frame @@ -419,26 +430,29 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio beforeFrame = frameArray[i][j]; beforeFrameIndex = j; /* probe forward to find the after array when a before array is found - * will use a endless loop because eventually at least we know we will - * terminate when it hits the exact same before array */ + * if not looping it only probes until the end of the timeline */ int jPrime = j + 1; - if (jPrime >= numberOfFrames) + if (jPrime < numberOfFrames || looping) { - jPrime = 0; - } - while (presenceArray[i][jPrime]) - { - if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) - { - afterFrame = frameArray[i][jPrime]; - afterFrameIndex = jPrime; - break; - } - jPrime++; if (jPrime >= numberOfFrames) { jPrime = 0; } + while (presenceArray[i][jPrime] && + (looping || jPrime < numberOfFrames)) + { + if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) + { + afterFrame = frameArray[i][jPrime]; + afterFrameIndex = jPrime; + break; + } + jPrime++; + if (jPrime >= numberOfFrames) + { + jPrime = 0; + } + } } /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined * but there are more frames that need to be interpolated from this frame only @@ -470,80 +484,77 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); } } - } - for (int i = 0; i < infoProvider.Size(); i++) - { - ProcessingFrame beforeFrame = null; - ProcessingFrame afterFrame = null; - int beforeFrameIndex = -1; - int afterFrameIndex = -1; - for (int j = 0; j < numberOfFrames; j++) + /* interpolation is run twice to fix issue where time = 0 is not key frame + * since otherwise every frame before the first keyframe will not be processed */ + if (looping) { - /* skip this frame if it isn't supposed to be present */ - if (!presenceArray[i][j]) + for (int j = 0; j < numberOfFrames; j++) { - continue; - } - /* if this frame exists and is populated then it will be used - * as the before frame */ - if (frameArray[i][j] != null && frameArray[i][j].IsPopulated()) - { - beforeFrame = frameArray[i][j]; - beforeFrameIndex = j; - /* probe forward to find the after array when a before array is found - * will use a endless loop because eventually at least we know we will - * terminate when it hits the exact same before array */ - int jPrime = j + 1; - if (jPrime >= numberOfFrames) + /* skip this frame if it isn't supposed to be present */ + if (!presenceArray[i][j]) { - jPrime = 0; + continue; } - while (presenceArray[i][jPrime]) + /* if this frame exists and is populated then it will be used + * as the before frame */ + if (frameArray[i][j] != null && frameArray[i][j].IsPopulated()) { - if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) - { - afterFrame = frameArray[i][jPrime]; - afterFrameIndex = jPrime; - break; - } - jPrime++; + beforeFrame = frameArray[i][j]; + beforeFrameIndex = j; + /* probe forward to find the after array when a before array is found + * will use a endless loop because eventually at least we know we will + * terminate when it hits the exact same before array */ + int jPrime = j + 1; if (jPrime >= numberOfFrames) { jPrime = 0; } + while (presenceArray[i][jPrime]) + { + if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) + { + afterFrame = frameArray[i][jPrime]; + afterFrameIndex = jPrime; + break; + } + jPrime++; + if (jPrime >= numberOfFrames) + { + jPrime = 0; + } + } + /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined + * but there are more frames that need to be interpolated from this frame only + * since this is the only frame, spriter interprets this frame as being the frame used for all of the interpolated positions + * in which this sprite exists */ + if (afterFrame == null) + { + Logger.Debug("Could not find after frame to interpolate between. Interpreting this to mean that this frame is expected to take the entire duration of the timeline."); + afterFrame = beforeFrame; + afterFrameIndex = beforeFrameIndex; + } } - /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined - * but there are more frames that need to be interpolated from this frame only - * since this is the only frame, spriter interprets this frame as being the frame used for all of the interpolated positions - * in which this sprite exists */ - if (afterFrame == null) + else if (beforeFrame != null && afterFrame != null) { - Logger.Debug("Could not find after frame to interpolate between. Interpreting this to mean that this frame is expected to take the entire duration of the timeline."); - afterFrame = beforeFrame; - afterFrameIndex = beforeFrameIndex; - } - } - else if (beforeFrame != null && afterFrame != null) - { - float x = LinearInterpolate(beforeFrame.X, afterFrame.X, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float y = LinearInterpolate(beforeFrame.Y, afterFrame.Y, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float angle = LinearInterpolateAngle(beforeFrame.Angle, afterFrame.Angle, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float xScale = LinearInterpolate(beforeFrame.ScaleX, afterFrame.ScaleX, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float yScale = LinearInterpolate(beforeFrame.ScaleY, afterFrame.ScaleY, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - if (frameArray[i][j] == null) - { - frameArray[i][j] = new ProcessingFrame(beforeFrame.ParentId, beforeFrame.ZIndex); + float x = LinearInterpolate(beforeFrame.X, afterFrame.X, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float y = LinearInterpolate(beforeFrame.Y, afterFrame.Y, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float angle = LinearInterpolateAngle(beforeFrame.Angle, afterFrame.Angle, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float xScale = LinearInterpolate(beforeFrame.ScaleX, afterFrame.ScaleX, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float yScale = LinearInterpolate(beforeFrame.ScaleY, afterFrame.ScaleY, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + if (frameArray[i][j] == null) + { + frameArray[i][j] = new ProcessingFrame(beforeFrame.ParentId, beforeFrame.ZIndex); + } + frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); } - frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); } } } - /* interpolation is run twice to fix issue where time = 0 is not key frame */ return new ProcessingAnimation(name, interval, length, frameArray, infoProvider); } diff --git a/kanimal/Reader/ScmlReader.cs b/kanimal/Reader/ScmlReader.cs index 4773ae4..c28cdd8 100644 --- a/kanimal/Reader/ScmlReader.cs +++ b/kanimal/Reader/ScmlReader.cs @@ -34,10 +34,8 @@ public class ScmlReader : Reader public bool InterpolateMissingFrames = true; public bool Debone = true; - public ScmlReader(Stream scmlStream, Dictionary sprites) + private XmlDocument HandleProcessors(XmlDocument scml) { - scml = new XmlDocument(); - scml.Load(scmlStream); if (InterpolateMissingFrames) { try @@ -62,7 +60,14 @@ public ScmlReader(Stream scmlStream, Dictionary sprites) ExceptionDispatchInfo.Capture(e).Throw(); } } + return scml; + } + public ScmlReader(Stream scmlStream, Dictionary sprites) + { + scml = new XmlDocument(); + scml.Load(scmlStream); + scml = HandleProcessors(scml); inputSprites = sprites; } @@ -78,30 +83,7 @@ public ScmlReader(string scmlpath) Logger.Fatal($"You must specify a path to load the SCML from. Original exception is as follows:"); ExceptionDispatchInfo.Capture(e).Throw(); } - if (InterpolateMissingFrames) - { - try - { - scml = new KeyFrameInterpolateProcessor().Process(scml); // replace the scml with fully keyframed scml - } - catch (Exception e) - { - Logger.Fatal($"Failed to interpolate in-between frames. Original exception is as follows:"); - ExceptionDispatchInfo.Capture(e).Throw(); - } - } - if (Debone) - { - try - { - scml = new DebonerProcessor().Process(scml); // replace the scml with deboned scml - } - catch (Exception e) - { - Logger.Fatal($"Failed to debone the scml document. Original exception is as follows:"); - ExceptionDispatchInfo.Capture(e).Throw(); - } - } + scml = HandleProcessors(scml); // Due to scml conventions, our input directory is the same as the scml file's var inputDir = Path.Join(scmlpath, "../"); inputSprites = new Dictionary(); @@ -307,6 +289,9 @@ private void PackAnim() Logger.Debug($"bank.name={bank.Name}\nhashTable={bank.Hash}"); bank.Frames = new List(); + // the consistent interval between frames + // -1 indicates it starts off as being unknown + // since all valid values for the interval will be >= 0 var interval = -1; var timelines = anim.ChildNodes; From 94bfd5f327c9521c2af20061e37912fca54130f2 Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 13:17:10 -0500 Subject: [PATCH 3/6] Revert "Handle looping" This reverts commit 85ac2d22006fbc6c44065c7adaff45f6c8951e2a. --- .../Processor/KeyFrameInterpolateProcessor.cs | 179 ++++++++---------- kanimal/Reader/ScmlReader.cs | 39 ++-- 2 files changed, 111 insertions(+), 107 deletions(-) diff --git a/kanimal/Processor/KeyFrameInterpolateProcessor.cs b/kanimal/Processor/KeyFrameInterpolateProcessor.cs index e02d529..a4f1ea5 100644 --- a/kanimal/Processor/KeyFrameInterpolateProcessor.cs +++ b/kanimal/Processor/KeyFrameInterpolateProcessor.cs @@ -75,11 +75,6 @@ class ProcessingAnimation /* the length of the animation in milliseconds - integer */ public int Length { get; set; } - /* if the animation is looped - i.e. does the interpolator - * consider the animation as wrapping around from the end-to-the-start - * or does it consider the end as not having to match up with the start */ - public bool Looping { get; set; } - /* 2d array of the frames, 1st axis is the ids of the timelines for all * of the sprites and bones in the animation, 2nd axis is the timesteps */ public List> FrameArray { get; set; } @@ -359,12 +354,6 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio } } - var looping = true; - if (animation.HasAttribute("looping")) - { - looping = bool.Parse(animation.Attributes["looping"].Value); - } - /* create an additional array that indicates presence of each timeline on a per-frame basis */ List> presenceArray = new List>(); for (int i = 0; i < infoProvider.Size(); i++) @@ -391,21 +380,21 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio } presenceArray[i][j] = currentPresence; } - /* executing the loop twice is the most straightforward way to ensure that a keyframe at the end - * of the timeline wraps around to the front of the timeline */ - if (looping) + for (int j = 0; j < numberOfFrames; j++) { - for (int j = 0; j < numberOfFrames; j++) + /* if this frame is a key frame then update the current presence based on if there + * is a frame populated at this location */ + if (keyFrames[j]) { - /* if this frame is a key frame then update the current presence based on if there - * is a frame populated at this location */ - if (keyFrames[j]) - { - currentPresence = (frameArray[i][j] != null); - } - presenceArray[i][j] = currentPresence; + currentPresence = (frameArray[i][j] != null); } + presenceArray[i][j] = currentPresence; } + /* executing the loop twice is the most straightforward way to ensure that a keyframe at the end + * of the timeline wraps around to the front of the timeline + * this does mess with animations that aren't looped that don't have keyframes at time = 0 but that just doesn't make + * much sense (who wouldn't keyframe at time = 0 for a non-looping animation!) + * so I'll just document that and ignore that problem for now */ } /* for every frame with presence in the array set to true that still has a null frame @@ -430,28 +419,25 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio beforeFrame = frameArray[i][j]; beforeFrameIndex = j; /* probe forward to find the after array when a before array is found - * if not looping it only probes until the end of the timeline */ + * will use a endless loop because eventually at least we know we will + * terminate when it hits the exact same before array */ int jPrime = j + 1; - if (jPrime < numberOfFrames || looping) + if (jPrime >= numberOfFrames) { - if (jPrime >= numberOfFrames) + jPrime = 0; + } + while (presenceArray[i][jPrime]) + { + if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) { - jPrime = 0; + afterFrame = frameArray[i][jPrime]; + afterFrameIndex = jPrime; + break; } - while (presenceArray[i][jPrime] && - (looping || jPrime < numberOfFrames)) + jPrime++; + if (jPrime >= numberOfFrames) { - if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) - { - afterFrame = frameArray[i][jPrime]; - afterFrameIndex = jPrime; - break; - } - jPrime++; - if (jPrime >= numberOfFrames) - { - jPrime = 0; - } + jPrime = 0; } } /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined @@ -484,77 +470,80 @@ private ProcessingAnimation ParseAnimation(XmlDocument scml, XmlElement animatio frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); } } - /* interpolation is run twice to fix issue where time = 0 is not key frame - * since otherwise every frame before the first keyframe will not be processed */ - if (looping) + } + for (int i = 0; i < infoProvider.Size(); i++) + { + ProcessingFrame beforeFrame = null; + ProcessingFrame afterFrame = null; + int beforeFrameIndex = -1; + int afterFrameIndex = -1; + for (int j = 0; j < numberOfFrames; j++) { - for (int j = 0; j < numberOfFrames; j++) + /* skip this frame if it isn't supposed to be present */ + if (!presenceArray[i][j]) { - /* skip this frame if it isn't supposed to be present */ - if (!presenceArray[i][j]) + continue; + } + /* if this frame exists and is populated then it will be used + * as the before frame */ + if (frameArray[i][j] != null && frameArray[i][j].IsPopulated()) + { + beforeFrame = frameArray[i][j]; + beforeFrameIndex = j; + /* probe forward to find the after array when a before array is found + * will use a endless loop because eventually at least we know we will + * terminate when it hits the exact same before array */ + int jPrime = j + 1; + if (jPrime >= numberOfFrames) { - continue; + jPrime = 0; } - /* if this frame exists and is populated then it will be used - * as the before frame */ - if (frameArray[i][j] != null && frameArray[i][j].IsPopulated()) + while (presenceArray[i][jPrime]) { - beforeFrame = frameArray[i][j]; - beforeFrameIndex = j; - /* probe forward to find the after array when a before array is found - * will use a endless loop because eventually at least we know we will - * terminate when it hits the exact same before array */ - int jPrime = j + 1; - if (jPrime >= numberOfFrames) - { - jPrime = 0; - } - while (presenceArray[i][jPrime]) + if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) { - if (frameArray[i][jPrime] != null && frameArray[i][jPrime].IsPopulated()) - { - afterFrame = frameArray[i][jPrime]; - afterFrameIndex = jPrime; - break; - } - jPrime++; - if (jPrime >= numberOfFrames) - { - jPrime = 0; - } + afterFrame = frameArray[i][jPrime]; + afterFrameIndex = jPrime; + break; } - /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined - * but there are more frames that need to be interpolated from this frame only - * since this is the only frame, spriter interprets this frame as being the frame used for all of the interpolated positions - * in which this sprite exists */ - if (afterFrame == null) + jPrime++; + if (jPrime >= numberOfFrames) { - Logger.Debug("Could not find after frame to interpolate between. Interpreting this to mean that this frame is expected to take the entire duration of the timeline."); - afterFrame = beforeFrame; - afterFrameIndex = beforeFrameIndex; + jPrime = 0; } } - else if (beforeFrame != null && afterFrame != null) + /* if we found a before frame but couldn't find an after frame this means that there was a frame that is completely defined + * but there are more frames that need to be interpolated from this frame only + * since this is the only frame, spriter interprets this frame as being the frame used for all of the interpolated positions + * in which this sprite exists */ + if (afterFrame == null) { - float x = LinearInterpolate(beforeFrame.X, afterFrame.X, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float y = LinearInterpolate(beforeFrame.Y, afterFrame.Y, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float angle = LinearInterpolateAngle(beforeFrame.Angle, afterFrame.Angle, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float xScale = LinearInterpolate(beforeFrame.ScaleX, afterFrame.ScaleX, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - float yScale = LinearInterpolate(beforeFrame.ScaleY, afterFrame.ScaleY, beforeFrameIndex, - afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); - if (frameArray[i][j] == null) - { - frameArray[i][j] = new ProcessingFrame(beforeFrame.ParentId, beforeFrame.ZIndex); - } - frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); + Logger.Debug("Could not find after frame to interpolate between. Interpreting this to mean that this frame is expected to take the entire duration of the timeline."); + afterFrame = beforeFrame; + afterFrameIndex = beforeFrameIndex; } } + else if (beforeFrame != null && afterFrame != null) + { + float x = LinearInterpolate(beforeFrame.X, afterFrame.X, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float y = LinearInterpolate(beforeFrame.Y, afterFrame.Y, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float angle = LinearInterpolateAngle(beforeFrame.Angle, afterFrame.Angle, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float xScale = LinearInterpolate(beforeFrame.ScaleX, afterFrame.ScaleX, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + float yScale = LinearInterpolate(beforeFrame.ScaleY, afterFrame.ScaleY, beforeFrameIndex, + afterFrameIndex + ((afterFrameIndex < beforeFrameIndex) ? numberOfFrames : 0), j); + if (frameArray[i][j] == null) + { + frameArray[i][j] = new ProcessingFrame(beforeFrame.ParentId, beforeFrame.ZIndex); + } + frameArray[i][j].Populate(beforeFrame.Folder, beforeFrame.File, x, y, angle, xScale, yScale); + } } } + /* interpolation is run twice to fix issue where time = 0 is not key frame */ return new ProcessingAnimation(name, interval, length, frameArray, infoProvider); } diff --git a/kanimal/Reader/ScmlReader.cs b/kanimal/Reader/ScmlReader.cs index c28cdd8..4773ae4 100644 --- a/kanimal/Reader/ScmlReader.cs +++ b/kanimal/Reader/ScmlReader.cs @@ -34,8 +34,10 @@ public class ScmlReader : Reader public bool InterpolateMissingFrames = true; public bool Debone = true; - private XmlDocument HandleProcessors(XmlDocument scml) + public ScmlReader(Stream scmlStream, Dictionary sprites) { + scml = new XmlDocument(); + scml.Load(scmlStream); if (InterpolateMissingFrames) { try @@ -60,14 +62,7 @@ private XmlDocument HandleProcessors(XmlDocument scml) ExceptionDispatchInfo.Capture(e).Throw(); } } - return scml; - } - public ScmlReader(Stream scmlStream, Dictionary sprites) - { - scml = new XmlDocument(); - scml.Load(scmlStream); - scml = HandleProcessors(scml); inputSprites = sprites; } @@ -83,7 +78,30 @@ public ScmlReader(string scmlpath) Logger.Fatal($"You must specify a path to load the SCML from. Original exception is as follows:"); ExceptionDispatchInfo.Capture(e).Throw(); } - scml = HandleProcessors(scml); + if (InterpolateMissingFrames) + { + try + { + scml = new KeyFrameInterpolateProcessor().Process(scml); // replace the scml with fully keyframed scml + } + catch (Exception e) + { + Logger.Fatal($"Failed to interpolate in-between frames. Original exception is as follows:"); + ExceptionDispatchInfo.Capture(e).Throw(); + } + } + if (Debone) + { + try + { + scml = new DebonerProcessor().Process(scml); // replace the scml with deboned scml + } + catch (Exception e) + { + Logger.Fatal($"Failed to debone the scml document. Original exception is as follows:"); + ExceptionDispatchInfo.Capture(e).Throw(); + } + } // Due to scml conventions, our input directory is the same as the scml file's var inputDir = Path.Join(scmlpath, "../"); inputSprites = new Dictionary(); @@ -289,9 +307,6 @@ private void PackAnim() Logger.Debug($"bank.name={bank.Name}\nhashTable={bank.Hash}"); bank.Frames = new List(); - // the consistent interval between frames - // -1 indicates it starts off as being unknown - // since all valid values for the interval will be >= 0 var interval = -1; var timelines = anim.ChildNodes; From 2f1de8f54d05ac010ce84485d31379c76964a066 Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 13:49:50 -0500 Subject: [PATCH 4/6] In SCMLRead only read textures that are actually used, in SCMLWrite write out file extension since spriter breaks pivot copy/paste if this is not done --- kanimal/Reader/ScmlReader.cs | 3 +++ kanimal/Writer/ScmlWriter.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/kanimal/Reader/ScmlReader.cs b/kanimal/Reader/ScmlReader.cs index 4773ae4..b12e45a 100644 --- a/kanimal/Reader/ScmlReader.cs +++ b/kanimal/Reader/ScmlReader.cs @@ -129,6 +129,9 @@ public override void Read() Logger.Info("Reading image files."); ReadProjectSprites(); + // Only use the sprites that are included in the project + inputSprites = inputSprites.Where(sprite => projectSprites.ContainsKey(sprite.Key.ToSpriteName())).ToDictionary(x => x.Key, x => x.Value); + // Also set the output list of sprites Sprites = new List(); Sprites = inputSprites.Select(sprite => new Sprite {Bitmap = sprite.Value, Name = sprite.Key.ToSpriteName()}).ToList(); diff --git a/kanimal/Writer/ScmlWriter.cs b/kanimal/Writer/ScmlWriter.cs index edb4543..5b0c859 100644 --- a/kanimal/Writer/ScmlWriter.cs +++ b/kanimal/Writer/ScmlWriter.cs @@ -112,7 +112,7 @@ protected virtual void AddFolderInfo() var fileNode = Scml.CreateElement("file"); fileNode.SetAttribute("id", fileIndex.ToString()); - fileNode.SetAttribute("name", $"{row.Name}_{row.Index}"); + fileNode.SetAttribute("name", $"{row.Name}_{row.Index}.png"); fileNode.SetAttribute("width", ((int) row.Width).ToString()); fileNode.SetAttribute("height", ((int) row.Height).ToString()); fileNode.SetAttribute("pivot_x", pivotX.ToStringInvariant()); From aac24cbf2fbed4eaf831419445179a21a1dabe77 Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 14:00:58 -0500 Subject: [PATCH 5/6] Warn for unused sprites in SCML --- kanimal/Reader/ScmlReader.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/kanimal/Reader/ScmlReader.cs b/kanimal/Reader/ScmlReader.cs index b12e45a..b7efcf9 100644 --- a/kanimal/Reader/ScmlReader.cs +++ b/kanimal/Reader/ScmlReader.cs @@ -130,7 +130,15 @@ public override void Read() ReadProjectSprites(); // Only use the sprites that are included in the project + List allSprites = inputSprites.Select(sprite => sprite.Key.ToSpriteName()).ToList(); inputSprites = inputSprites.Where(sprite => projectSprites.ContainsKey(sprite.Key.ToSpriteName())).ToDictionary(x => x.Key, x => x.Value); + List usedSprites = inputSprites.Select(sprite => sprite.Key.ToSpriteName()).ToList(); + + List unusedSprites = allSprites.FindAll(sprite => !usedSprites.Contains(sprite)).Select(sprite => sprite.ToFilename().ToString()).ToList(); + if (unusedSprites.Count > 0) + { + Logger.Warn($"There were unused sprites in the SCML project folder: {unusedSprites.Join()}. Did you forget to included these in the SCML file? You must manually add in files that are part of a symbol_override if they aren't explicitly placed into any animations in the SCML. "); + } // Also set the output list of sprites Sprites = new List(); From 8c61137a377422a885d3ba1733718f5139e4d2ad Mon Sep 17 00:00:00 2001 From: Davis Cook Date: Mon, 13 Jan 2020 14:31:29 -0500 Subject: [PATCH 6/6] Downgrade duplicate sprite to a warning so we can parse some base game files with duplicated sprites --- kanimal-cli/Program.cs | 3 ++- kanimal/Writer/ScmlWriter.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/kanimal-cli/Program.cs b/kanimal-cli/Program.cs index da04877..5da1998 100644 --- a/kanimal-cli/Program.cs +++ b/kanimal-cli/Program.cs @@ -115,7 +115,8 @@ private static void Convert(string inputFormat, string outputFormat, List