Refactoring, FFmpeg updates

This commit is contained in:
2025-11-03 00:16:05 -05:00
parent 62cd94ca78
commit 1e35910b0a
65 changed files with 2322 additions and 2236 deletions

View File

@@ -8,7 +8,6 @@
using namespace anm2ed::util;
using namespace glm;
using namespace tinyxml2;
namespace anm2ed::anm2
@@ -79,7 +78,7 @@ namespace anm2ed::anm2
}
}
void Animation::serialize(XMLDocument& document, XMLElement* parent)
XMLElement* Animation::to_element(XMLDocument& document)
{
auto element = document.NewElement("Animation");
element->SetAttribute("Name", name.c_str());
@@ -103,7 +102,19 @@ namespace anm2ed::anm2
triggers.serialize(document, element, TRIGGER);
parent->InsertEndChild(element);
return element;
}
void Animation::serialize(XMLDocument& document, XMLElement* parent)
{
parent->InsertEndChild(to_element(document));
}
std::string Animation::to_string()
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
return xml::document_to_string(document);
}
int Animation::length()
@@ -126,49 +137,16 @@ namespace anm2ed::anm2
return length;
}
std::string Animation::to_string()
{
XMLDocument document{};
auto* element = document.NewElement("Animation");
document.InsertFirstChild(element);
element->SetAttribute("Name", name.c_str());
element->SetAttribute("FrameNum", frameNum);
element->SetAttribute("Loop", isLoop);
rootAnimation.serialize(document, element, ROOT);
auto layerAnimationsElement = document.NewElement("LayerAnimations");
for (auto& i : layerOrder)
{
Item& layerAnimation = layerAnimations.at(i);
layerAnimation.serialize(document, layerAnimationsElement, LAYER, i);
}
element->InsertEndChild(layerAnimationsElement);
auto nullAnimationsElement = document.NewElement("NullAnimations");
for (auto& [id, nullAnimation] : nullAnimations)
nullAnimation.serialize(document, nullAnimationsElement, NULL_, id);
element->InsertEndChild(nullAnimationsElement);
triggers.serialize(document, element, TRIGGER);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
vec4 Animation::rect(bool isRootTransform)
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
bool any = false;
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
for (float t = 0.0f; t < (float)frameNum; t += 1.0f)
{
mat4 transform(1.0f);
@@ -202,5 +180,4 @@ namespace anm2ed::anm2
if (!any) return vec4(-1.0f);
return {minX, minY, maxX - minX, maxY - minY};
}
}

View File

@@ -26,10 +26,11 @@ namespace anm2ed::anm2
Animation(tinyxml2::XMLElement*);
Item* item_get(Type, int = -1);
void item_remove(Type, int = -1);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
std::string to_string();
int length();
glm::vec4 rect(bool);
std::string to_string();
};
}

View File

@@ -1,7 +1,5 @@
#include "animations.h"
#include <ranges>
#include "xml_.h"
using namespace tinyxml2;
@@ -42,114 +40,4 @@ namespace anm2ed::anm2
return length;
}
int Animations::merge(int target, std::set<int>& sources, merge::Type type, bool isDeleteAfter)
{
Animation& animation = items.at(target);
if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING;
auto merge_item = [&](Item& destination, Item& source)
{
switch (type)
{
case merge::APPEND:
destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end());
break;
case merge::PREPEND:
destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end());
break;
case merge::REPLACE:
if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size());
for (int i = 0; i < (int)source.frames.size(); i++)
destination.frames[i] = source.frames[i];
break;
case merge::IGNORE:
default:
break;
}
};
for (auto& i : sources)
{
if (i == target) continue;
if (i < 0 || i >= (int)items.size()) continue;
auto& source = items.at(i);
merge_item(animation.rootAnimation, source.rootAnimation);
for (auto& [id, layerAnimation] : source.layerAnimations)
{
if (!animation.layerAnimations.contains(id))
{
animation.layerAnimations[id] = layerAnimation;
animation.layerOrder.emplace_back(id);
}
merge_item(animation.layerAnimations[id], layerAnimation);
}
for (auto& [id, nullAnimation] : source.nullAnimations)
{
if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation;
merge_item(animation.nullAnimations[id], nullAnimation);
}
merge_item(animation.triggers, source.triggers);
}
if (isDeleteAfter)
{
for (auto& source : std::ranges::reverse_view(sources))
{
if (source == target) continue;
items.erase(items.begin() + source);
}
}
int finalIndex = target;
if (isDeleteAfter)
{
int numDeletedBefore = 0;
for (auto& idx : sources)
{
if (idx == target) continue;
if (idx >= 0 && idx < target) ++numDeletedBefore;
}
finalIndex -= numDeletedBefore;
}
return finalIndex;
}
bool Animations::animations_deserialize(const std::string& string, int start, std::set<int>& indices,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
if (!document.FirstChildElement("Animation"))
{
if (errorString) *errorString = "No valid animation(s).";
return false;
}
int count{};
for (auto element = document.FirstChildElement("Animation"); element;
element = element->NextSiblingElement("Animation"))
{
auto index = start + count;
items.insert(items.begin() + start + count, Animation(element));
indices.insert(index);
count++;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

View File

@@ -14,9 +14,7 @@ namespace anm2ed::anm2
Animations() = default;
Animations(tinyxml2::XMLElement*);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
int length();
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
int merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
int length();
};
}

View File

@@ -1,12 +1,9 @@
#include "anm2.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
#include "time_.h"
#include "unordered_map_.h"
#include "vector_.h"
#include "xml_.h"
using namespace tinyxml2;
using namespace anm2ed::types;
@@ -20,41 +17,6 @@ namespace anm2ed::anm2
info.createdOn = time::get("%d-%B-%Y %I:%M:%S");
}
bool Anm2::serialize(const std::string& path, std::string* errorString)
{
XMLDocument document;
auto* element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
std::string Anm2::to_string()
{
XMLDocument document;
auto* element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
XMLPrinter printer;
document.Print(&printer);
return std::string(printer.CStr());
}
Anm2::Anm2(const std::string& path, std::string* errorString)
{
XMLDocument document;
@@ -75,302 +37,53 @@ namespace anm2ed::anm2
animations = Animations((XMLElement*)animationsElement);
}
bool Anm2::serialize(const std::string& path, std::string* errorString)
{
XMLDocument document;
auto* element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
if (errorString) *errorString = document.ErrorStr();
return false;
}
return true;
}
XMLElement* Anm2::to_element(XMLDocument& document)
{
auto element = document.NewElement("AnimatedActor");
document.InsertFirstChild(element);
info.serialize(document, element);
content.serialize(document, element);
animations.serialize(document, element);
return element;
}
std::string Anm2::to_string()
{
XMLDocument document{};
document.InsertEndChild(to_element(document));
return xml::document_to_string(document);
}
uint64_t Anm2::hash()
{
return std::hash<std::string>{}(to_string());
}
Animation* Anm2::animation_get(Reference reference)
{
return vector::find(animations.items, reference.animationIndex);
}
std::vector<std::string> Anm2::animation_names_get()
{
std::vector<std::string> names{};
for (auto& animation : animations.items)
names.push_back(animation.name);
return names;
}
Item* Anm2::item_get(Reference reference)
{
if (Animation* animation = animation_get(reference))
{
switch (reference.itemType)
{
case ROOT:
return &animation->rootAnimation;
case LAYER:
return unordered_map::find(animation->layerAnimations, reference.itemID);
case NULL_:
return map::find(animation->nullAnimations, reference.itemID);
case TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
Frame* Anm2::frame_get(Reference reference)
{
Item* item = item_get(reference);
if (!item) return nullptr;
return vector::find(item->frames, reference.frameIndex);
if (auto item = item_get(reference); item)
if (vector::in_bounds(item->frames, reference.frameIndex)) return &item->frames[reference.frameIndex];
return nullptr;
}
bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id)
{
Spritesheet spritesheet(directory, path);
if (!spritesheet.is_valid()) return false;
id = map::next_id_get(content.spritesheets);
content.spritesheets[id] = std::move(spritesheet);
return true;
}
void Anm2::spritesheet_remove(int id)
{
content.spritesheets.erase(id);
}
Spritesheet* Anm2::spritesheet_get(int id)
{
return map::find(content.spritesheets, id);
}
std::set<int> Anm2::spritesheets_unused()
{
return content.spritesheets_unused();
}
std::vector<std::string> Anm2::spritesheet_names_get()
{
std::vector<std::string> names{};
for (auto& [id, spritesheet] : content.spritesheets)
names.push_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str()));
return names;
}
Reference Anm2::layer_add(Reference reference, std::string name, int spritesheetID, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID;
auto& layer = content.layers[id];
layer.name = !name.empty() ? name : layer.name;
layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0;
auto add = [&](Animation* animation, int id)
{
animation->layerAnimations[id] = Item();
animation->layerOrder.push_back(id);
};
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.layerAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->layerAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
Reference Anm2::null_add(Reference reference, std::string name, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID;
auto& null = content.nulls[id];
null.name = !name.empty() ? name : null.name;
auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); };
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.nullAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->nullAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
void Anm2::event_add(int& id)
{
content.event_add(id);
}
std::set<int> Anm2::events_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& frame : animation->triggers.frames)
used.insert(frame.eventID);
else
for (auto& animation : animations.items)
for (auto& frame : animation.triggers.frames)
used.insert(frame.eventID);
for (auto& id : content.events | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::set<int> Anm2::layers_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->layerAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.layerAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.layers | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::set<int> Anm2::nulls_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->nullAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.nullAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.nulls | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::vector<std::string> Anm2::event_names_get()
{
std::vector<std::string> names{};
for (auto& event : content.events | std::views::values)
names.push_back(event.name);
return names;
}
bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id)
{
id = map::next_id_get(content.sounds);
content.sounds[id] = Sound(directory, path);
return true;
}
std::set<int> Anm2::sounds_unused()
{
std::set<int> used;
for (auto& event : content.events | std::views::values)
used.insert(event.soundID);
std::set<int> unused;
for (auto& id : content.sounds | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::vector<std::string> Anm2::sound_names_get()
{
std::vector<std::string> names{};
for (auto& [id, sound] : content.sounds)
names.push_back(std::format(SOUND_FORMAT, id, sound.path.c_str()));
return names;
}
void Anm2::bake(Reference reference, int interval, bool isRoundScale, bool isRoundRotation)
{
Item* item = item_get(reference);
if (!item) return;
Frame* frame = frame_get(reference);
if (!frame) return;
if (frame->delay == FRAME_DELAY_MIN) return;
Reference referenceNext = reference;
referenceNext.frameIndex = reference.frameIndex + 1;
Frame* frameNext = frame_get(referenceNext);
if (!frameNext) frameNext = frame;
Frame baseFrame = *frame;
Frame baseFrameNext = *frameNext;
int delay{};
int index = reference.frameIndex;
while (delay < baseFrame.delay)
{
float interpolation = (float)delay / baseFrame.delay;
Frame baked = baseFrame;
baked.delay = std::min(interval, baseFrame.delay - delay);
baked.isInterpolated = (index == reference.frameIndex) ? baseFrame.isInterpolated : false;
baked.rotation = glm::mix(baseFrame.rotation, baseFrameNext.rotation, interpolation);
baked.position = glm::mix(baseFrame.position, baseFrameNext.position, interpolation);
baked.scale = glm::mix(baseFrame.scale, baseFrameNext.scale, interpolation);
baked.colorOffset = glm::mix(baseFrame.colorOffset, baseFrameNext.colorOffset, interpolation);
baked.tint = glm::mix(baseFrame.tint, baseFrameNext.tint, interpolation);
if (isRoundScale) baked.scale = vec2(ivec2(baked.scale));
if (isRoundRotation) baked.rotation = (int)baked.rotation;
if (index == reference.frameIndex)
item->frames[index] = baked;
else
item->frames.insert(item->frames.begin() + index, baked);
index++;
delay += baked.delay;
}
}
void Anm2::generate_from_grid(Reference reference, ivec2 startPosition, ivec2 size, ivec2 pivot, int columns,
int count, int delay)
{
auto item = item_get(reference);
if (!item) return;
for (int i = 0; i < count; i++)
{
auto row = i / columns;
auto column = i % columns;
Frame frame{};
frame.delay = delay;
frame.pivot = pivot;
frame.size = size;
frame.crop = startPosition + ivec2(size.x * column, size.y * row);
item->frames.emplace_back(frame);
}
}
}
}

View File

@@ -24,8 +24,6 @@ namespace anm2ed::anm2
auto operator<=>(const Reference&) const = default;
};
constexpr anm2::Reference REFERENCE_DEFAULT = {-1, anm2::NONE, -1, -1, -1};
class Anm2
{
public:
@@ -34,39 +32,47 @@ namespace anm2ed::anm2
Animations animations{};
Anm2();
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&);
bool serialize(const std::string&, std::string* = nullptr);
std::string to_string();
Anm2(const std::string&, std::string* = nullptr);
uint64_t hash();
Animation* animation_get(Reference);
std::vector<std::string> animation_names_get();
Item* item_get(Reference);
Frame* frame_get(Reference);
bool spritesheet_add(const std::string&, const std::string&, int&);
Spritesheet* spritesheet_get(int);
bool spritesheet_add(const std::string&, const std::string&, int&);
void spritesheet_remove(int);
std::vector<std::string> spritesheet_labels_get();
std::set<int> spritesheets_unused();
std::vector<std::string> spritesheet_names_get();
bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type type, std::string*);
int layer_add();
Reference layer_add(Reference = REFERENCE_DEFAULT, std::string = {}, int = 0,
types::locale::Type = types::locale::GLOBAL);
std::set<int> layers_unused(Reference = REFERENCE_DEFAULT);
void layer_add(int&);
std::set<int> layers_unused(Reference = {});
bool layers_deserialize(const std::string&, types::merge::Type, std::string*);
Reference null_add(Reference = REFERENCE_DEFAULT, std::string = {}, types::locale::Type = types::locale::GLOBAL);
std::set<int> nulls_unused(Reference = REFERENCE_DEFAULT);
bool sound_add(const std::string& directory, const std::string& path, int& id);
std::vector<std::string> sound_names_get();
std::set<int> sounds_unused();
void null_add(int&);
std::set<int> nulls_unused(Reference = {});
bool nulls_deserialize(const std::string&, types::merge::Type, std::string*);
void event_add(int&);
std::set<int> events_unused(Reference = REFERENCE_DEFAULT);
std::vector<std::string> event_names_get();
void bake(Reference, int = 1, bool = true, bool = true);
void generate_from_grid(Reference, glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
std::vector<std::string> event_labels_get();
std::set<int> events_unused(Reference = {});
bool events_deserialize(const std::string&, types::merge::Type, std::string*);
bool sound_add(const std::string& directory, const std::string& path, int& id);
std::vector<std::string> sound_labels_get();
std::set<int> sounds_unused();
bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string*);
Animation* animation_get(Reference);
std::vector<std::string> animation_labels_get();
int animations_merge(int, std::set<int>&, types::merge::Type = types::merge::APPEND, bool = true);
bool animations_deserialize(const std::string&, int, std::set<int>&, std::string* = nullptr);
Item* item_get(Reference);
Reference layer_animation_add(Reference = {}, std::string = {}, int = 0,
types::locale::Type = types::locale::GLOBAL);
Reference null_animation_add(Reference = {}, std::string = {}, types::locale::Type = types::locale::GLOBAL);
Frame* frame_get(Reference);
};
}

View File

@@ -0,0 +1,137 @@
#include "anm2.h"
#include "vector_.h"
using namespace anm2ed::util;
using namespace anm2ed::types;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Animation* Anm2::animation_get(Reference reference)
{
return vector::find(animations.items, reference.animationIndex);
}
std::vector<std::string> Anm2::animation_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& animation : animations.items)
labels.emplace_back(animation.name);
return labels;
}
bool Anm2::animations_deserialize(const std::string& string, int start, std::set<int>& indices,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
if (!document.FirstChildElement("Animation"))
{
if (errorString) *errorString = "No valid animation(s).";
return false;
}
int count{};
for (auto element = document.FirstChildElement("Animation"); element;
element = element->NextSiblingElement("Animation"))
{
auto index = start + count;
animations.items.insert(animations.items.begin() + start + count, Animation(element));
indices.insert(index);
count++;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
int Anm2::animations_merge(int target, std::set<int>& sources, merge::Type type, bool isDeleteAfter)
{
auto& items = animations.items;
auto& animation = animations.items.at(target);
if (!animation.name.ends_with(MERGED_STRING)) animation.name = animation.name + " " + MERGED_STRING;
auto merge_item = [&](Item& destination, Item& source)
{
switch (type)
{
case merge::APPEND:
destination.frames.insert(destination.frames.end(), source.frames.begin(), source.frames.end());
break;
case merge::PREPEND:
destination.frames.insert(destination.frames.begin(), source.frames.begin(), source.frames.end());
break;
case merge::REPLACE:
if (destination.frames.size() < source.frames.size()) destination.frames.resize(source.frames.size());
for (int i = 0; i < (int)source.frames.size(); i++)
destination.frames[i] = source.frames[i];
break;
case merge::IGNORE:
default:
break;
}
};
for (auto& i : sources)
{
if (i == target) continue;
if (i < 0 || i >= (int)items.size()) continue;
auto& source = items.at(i);
merge_item(animation.rootAnimation, source.rootAnimation);
for (auto& [id, layerAnimation] : source.layerAnimations)
{
if (!animation.layerAnimations.contains(id))
{
animation.layerAnimations[id] = layerAnimation;
animation.layerOrder.emplace_back(id);
}
merge_item(animation.layerAnimations[id], layerAnimation);
}
for (auto& [id, nullAnimation] : source.nullAnimations)
{
if (!animation.nullAnimations.contains(id)) animation.nullAnimations[id] = nullAnimation;
merge_item(animation.nullAnimations[id], nullAnimation);
}
merge_item(animation.triggers, source.triggers);
}
if (isDeleteAfter)
{
for (auto& source : std::ranges::reverse_view(sources))
{
if (source == target) continue;
items.erase(items.begin() + source);
}
}
int finalIndex = target;
if (isDeleteAfter)
{
int numDeletedBefore = 0;
for (auto& idx : sources)
{
if (idx == target) continue;
if (idx >= 0 && idx < target) ++numDeletedBefore;
}
finalIndex -= numDeletedBefore;
}
return finalIndex;
}
}

75
src/anm2/anm2_events.cpp Normal file
View File

@@ -0,0 +1,75 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::event_add(int& id)
{
id = map::next_id_get(content.events);
content.events[id] = Event();
}
std::vector<std::string> Anm2::event_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& event : content.events | std::views::values)
labels.emplace_back(event.name);
return labels;
}
std::set<int> Anm2::events_unused(Reference reference)
{
std::set<int> used{};
if (auto animation = animation_get(reference); animation)
for (auto& frame : animation->triggers.frames)
used.insert(frame.eventID);
else
for (auto& animation : animations.items)
for (auto& frame : animation.triggers.frames)
used.insert(frame.eventID);
std::set<int> unused{};
for (auto& id : content.events | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::events_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Event"))
{
if (errorString) *errorString = "No valid event(s).";
return false;
}
for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event"))
{
auto event = Event(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.events);
content.events[id] = event;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

83
src/anm2/anm2_items.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "anm2.h"
#include "map_.h"
#include "types.h"
#include "unordered_map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
namespace anm2ed::anm2
{
Item* Anm2::item_get(Reference reference)
{
if (Animation* animation = animation_get(reference))
{
switch (reference.itemType)
{
case ROOT:
return &animation->rootAnimation;
case LAYER:
return unordered_map::find(animation->layerAnimations, reference.itemID);
case NULL_:
return map::find(animation->nullAnimations, reference.itemID);
case TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
Reference Anm2::layer_animation_add(Reference reference, std::string name, int spritesheetID, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.layers) : reference.itemID;
auto& layer = content.layers[id];
layer.name = !name.empty() ? name : layer.name;
layer.spritesheetID = content.spritesheets.contains(spritesheetID) ? spritesheetID : 0;
auto add = [&](Animation* animation, int id)
{
animation->layerAnimations[id] = Item();
animation->layerOrder.push_back(id);
};
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.layerAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->layerAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
Reference Anm2::null_animation_add(Reference reference, std::string name, locale::Type locale)
{
auto id = reference.itemID == -1 ? map::next_id_get(content.nulls) : reference.itemID;
auto& null = content.nulls[id];
null.name = !name.empty() ? name : null.name;
auto add = [&](Animation* animation, int id) { animation->nullAnimations[id] = Item(); };
if (locale == locale::GLOBAL)
{
for (auto& animation : animations.items)
if (!animation.nullAnimations.contains(id)) add(&animation, id);
}
else if (locale == locale::LOCAL)
{
if (auto animation = animation_get(reference))
if (!animation->nullAnimations.contains(id)) add(animation, id);
}
return {reference.animationIndex, LAYER, id};
}
}

66
src/anm2/anm2_layers.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::layer_add(int& id)
{
id = map::next_id_get(content.layers);
content.layers[id] = Layer();
}
std::set<int> Anm2::layers_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->layerAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.layerAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.layers | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Layer"))
{
if (errorString) *errorString = "No valid layer(s).";
return false;
}
for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer"))
{
auto layer = Layer(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.layers);
content.layers[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

66
src/anm2/anm2_nulls.cpp Normal file
View File

@@ -0,0 +1,66 @@
#include "anm2.h"
#include <ranges>
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
void Anm2::null_add(int& id)
{
id = map::next_id_get(content.nulls);
content.nulls[id] = Null();
}
std::set<int> Anm2::nulls_unused(Reference reference)
{
std::set<int> used{};
std::set<int> unused{};
if (auto animation = animation_get(reference); animation)
for (auto& id : animation->nullAnimations | std::views::keys)
used.insert(id);
else
for (auto& animation : animations.items)
for (auto& id : animation.nullAnimations | std::views::keys)
used.insert(id);
for (auto& id : content.nulls | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Null"))
{
if (errorString) *errorString = "No valid null(s).";
return false;
}
for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null"))
{
auto null = Null(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.nulls);
content.nulls[id] = null;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

75
src/anm2/anm2_sounds.cpp Normal file
View File

@@ -0,0 +1,75 @@
#include "anm2.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
bool Anm2::sound_add(const std::string& directory, const std::string& path, int& id)
{
id = map::next_id_get(content.sounds);
content.sounds[id] = Sound(directory, path);
return true;
}
std::vector<std::string> Anm2::sound_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& [id, sound] : content.sounds)
labels.emplace_back(sound.path.string());
return labels;
}
std::set<int> Anm2::sounds_unused()
{
std::set<int> used;
for (auto& animation : animations.items)
for (auto& trigger : animation.triggers.frames)
if (content.sounds.contains(trigger.soundID)) used.insert(trigger.soundID);
std::set<int> unused;
for (auto& id : content.sounds | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
bool Anm2::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Sound"))
{
if (errorString) *errorString = "No valid sound(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound"))
{
auto sound = Sound(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.sounds);
content.sounds[id] = std::move(sound);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

View File

@@ -0,0 +1,87 @@
#include "anm2.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
{
Spritesheet* Anm2::spritesheet_get(int id)
{
return map::find(content.spritesheets, id);
}
void Anm2::spritesheet_remove(int id)
{
content.spritesheets.erase(id);
}
bool Anm2::spritesheet_add(const std::string& directory, const std::string& path, int& id)
{
Spritesheet spritesheet(directory, path);
if (!spritesheet.is_valid()) return false;
id = map::next_id_get(content.spritesheets);
content.spritesheets[id] = std::move(spritesheet);
return true;
}
std::set<int> Anm2::spritesheets_unused()
{
std::set<int> used{};
for (auto& layer : content.layers | std::views::values)
if (layer.is_spritesheet_valid()) used.insert(layer.spritesheetID);
std::set<int> unused{};
for (auto& id : content.spritesheets | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
std::vector<std::string> Anm2::spritesheet_labels_get()
{
std::vector<std::string> labels{};
labels.emplace_back("None");
for (auto& [id, spritesheet] : content.spritesheets)
labels.emplace_back(std::format(SPRITESHEET_FORMAT, id, spritesheet.path.c_str()));
return labels;
}
bool Anm2::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Spritesheet"))
{
if (errorString) *errorString = "No valid spritesheet(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Spritesheet"); element;
element = element->NextSiblingElement("Spritesheet"))
{
auto spritesheet = Spritesheet(element, id);
if (type == merge::APPEND) id = map::next_id_get(content.spritesheets);
content.spritesheets[id] = std::move(spritesheet);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}

View File

@@ -1,12 +1,5 @@
#include "content.h"
#include <ranges>
#include "filesystem_.h"
#include "map_.h"
using namespace anm2ed::types;
using namespace anm2ed::util;
using namespace tinyxml2;
namespace anm2ed::anm2
@@ -60,197 +53,4 @@ namespace anm2ed::anm2
parent->InsertEndChild(element);
}
std::set<int> Content::spritesheets_unused()
{
std::set<int> used;
for (auto& layer : layers | std::views::values)
if (layer.spritesheetID != -1) used.insert(layer.spritesheetID);
std::set<int> unused;
for (auto& id : spritesheets | std::views::keys)
if (!used.contains(id)) unused.insert(id);
return unused;
}
void Content::layer_add(int& id)
{
id = map::next_id_get(layers);
layers[id] = Layer();
}
void Content::null_add(int& id)
{
id = map::next_id_get(nulls);
nulls[id] = Null();
}
void Content::event_add(int& id)
{
id = map::next_id_get(events);
events[id] = Event();
}
bool Content::spritesheets_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Spritesheet"))
{
if (errorString) *errorString = "No valid spritesheet(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Spritesheet"); element;
element = element->NextSiblingElement("Spritesheet"))
{
auto spritesheet = Spritesheet(element, id);
if (type == merge::APPEND) id = map::next_id_get(spritesheets);
spritesheets[id] = std::move(spritesheet);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::layers_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Layer"))
{
if (errorString) *errorString = "No valid layer(s).";
return false;
}
for (auto element = document.FirstChildElement("Layer"); element; element = element->NextSiblingElement("Layer"))
{
auto layer = Layer(element, id);
if (type == merge::APPEND) id = map::next_id_get(layers);
layers[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::nulls_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Null"))
{
if (errorString) *errorString = "No valid null(s).";
return false;
}
for (auto element = document.FirstChildElement("Null"); element; element = element->NextSiblingElement("Null"))
{
auto layer = Null(element, id);
if (type == merge::APPEND) id = map::next_id_get(nulls);
nulls[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::events_deserialize(const std::string& string, merge::Type type, std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Event"))
{
if (errorString) *errorString = "No valid event(s).";
return false;
}
for (auto element = document.FirstChildElement("Event"); element; element = element->NextSiblingElement("Event"))
{
auto layer = Event(element, id);
if (type == merge::APPEND) id = map::next_id_get(events);
events[id] = layer;
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
bool Content::sounds_deserialize(const std::string& string, const std::string& directory, merge::Type type,
std::string* errorString)
{
XMLDocument document{};
if (document.Parse(string.c_str()) == XML_SUCCESS)
{
int id{};
if (!document.FirstChildElement("Sound"))
{
if (errorString) *errorString = "No valid sound(s).";
return false;
}
filesystem::WorkingDirectory workingDirectory(directory);
for (auto element = document.FirstChildElement("Sound"); element; element = element->NextSiblingElement("Sound"))
{
auto sound = Sound(element, id);
if (type == merge::APPEND) id = map::next_id_get(sounds);
sounds[id] = std::move(sound);
}
return true;
}
else if (errorString)
*errorString = document.ErrorStr();
return false;
}
}
}

View File

@@ -1,7 +1,6 @@
#pragma once
#include <map>
#include <set>
#include "event.h"
#include "layer.h"
@@ -9,8 +8,6 @@
#include "sound.h"
#include "spritesheet.h"
#include "types.h"
namespace anm2ed::anm2
{
struct Content
@@ -25,22 +22,5 @@ namespace anm2ed::anm2
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*);
Content(tinyxml2::XMLElement*);
bool spritesheet_add(const std::string&, const std::string&, int&);
std::set<int> spritesheets_unused();
void spritesheet_remove(int&);
bool spritesheets_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr);
void layer_add(int&);
bool layers_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
void null_add(int&);
bool nulls_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
void event_add(int&);
bool events_deserialize(const std::string&, types::merge::Type, std::string* = nullptr);
void sound_add(int&);
bool sounds_deserialize(const std::string&, const std::string&, types::merge::Type, std::string* = nullptr);
};
}

View File

@@ -9,7 +9,7 @@ namespace anm2ed::anm2
{
public:
std::string name{"New Event"};
int soundID{};
int soundID{-1};
Event() = default;
Event(tinyxml2::XMLElement*, int&);

View File

@@ -123,4 +123,12 @@ namespace anm2ed::anm2
{
delay = glm::clamp(++delay, FRAME_DELAY_MIN, FRAME_DELAY_MAX);
}
bool Frame::is_visible(Type type)
{
if (type == anm2::TRIGGER)
return isVisible && eventID > -1;
else
return isVisible;
}
}

View File

@@ -55,6 +55,7 @@ namespace anm2ed::anm2
X(delay, int, FRAME_DELAY_MIN) \
X(atFrame, int, -1) \
X(eventID, int, -1) \
X(soundID, int, -1) \
X(pivot, glm::vec2, {}) \
X(crop, glm::vec2, {}) \
X(position, glm::vec2, {}) \
@@ -77,6 +78,7 @@ namespace anm2ed::anm2
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, Type);
void shorten();
void extend();
bool is_visible(Type = NONE);
};
struct FrameChange

View File

@@ -1,10 +1,12 @@
#include "item.h"
#include <ranges>
#include "vector_.h"
#include "xml_.h"
using namespace anm2ed::util;
using namespace tinyxml2;
using namespace glm;
namespace anm2ed::anm2
{
@@ -114,7 +116,7 @@ namespace anm2ed::anm2
return frame;
}
void Item::frames_change(anm2::FrameChange& change, ChangeType type, int start, int numberFrames)
void Item::frames_change(FrameChange& change, ChangeType type, int start, int numberFrames)
{
auto useStart = numberFrames > -1 ? start : 0;
auto end = numberFrames > -1 ? start + numberFrames : (int)frames.size();
@@ -197,4 +199,54 @@ namespace anm2ed::anm2
return false;
}
void Item::frames_bake(int index, int interval, bool isRoundScale, bool isRoundRotation)
{
if (!vector::in_bounds(frames, index)) return;
Frame& frame = frames[index];
if (frame.delay == FRAME_DELAY_MIN) return;
Frame frameNext = vector::in_bounds(frames, index + 1) ? frames[index + 1] : frame;
int delay{};
int i = index;
while (delay < frame.delay)
{
Frame baked = frame;
float interpolation = (float)delay / frame.delay;
baked.delay = std::min(interval, frame.delay - delay);
baked.isInterpolated = (i == index) ? frame.isInterpolated : false;
baked.rotation = glm::mix(frame.rotation, frameNext.rotation, interpolation);
baked.position = glm::mix(frame.position, frameNext.position, interpolation);
baked.scale = glm::mix(frame.scale, frameNext.scale, interpolation);
baked.colorOffset = glm::mix(frame.colorOffset, frameNext.colorOffset, interpolation);
baked.tint = glm::mix(frame.tint, frameNext.tint, interpolation);
if (isRoundScale) baked.scale = vec2(ivec2(baked.scale));
if (isRoundRotation) baked.rotation = (int)baked.rotation;
if (i == index)
frames[i] = baked;
else
frames.insert(frames.begin() + i, baked);
i++;
delay += baked.delay;
}
}
void Item::frames_generate_from_grid(ivec2 startPosition, ivec2 size, ivec2 pivot, int columns, int count, int delay)
{
for (int i = 0; i < count; i++)
{
Frame frame{};
frame.delay = delay;
frame.pivot = pivot;
frame.size = size;
frame.crop = startPosition + ivec2(size.x * (i % columns), size.y * (i / columns));
frames.emplace_back(frame);
}
}
}

View File

@@ -20,7 +20,9 @@ namespace anm2ed::anm2
std::string to_string(Type, int = -1);
int length(Type);
Frame frame_generate(float, Type);
void frames_change(anm2::FrameChange&, ChangeType, int, int = 0);
void frames_change(FrameChange&, ChangeType, int, int = 0);
bool frames_deserialize(const std::string&, Type, int, std::set<int>&, std::string*);
void frames_bake(int, int, bool, bool);
void frames_generate_from_grid(glm::ivec2, glm::ivec2, glm::ivec2, int, int, int);
};
}

View File

@@ -36,4 +36,8 @@ namespace anm2ed::anm2
return xml::document_to_string(document);
}
bool Layer::is_spritesheet_valid()
{
return spritesheetID > -1;
}
}

View File

@@ -6,6 +6,7 @@
namespace anm2ed::anm2
{
constexpr auto LAYER_FORMAT = "#{} {} (Spritesheet: #{})";
constexpr auto LAYER_NO_SPRITESHEET_FORMAT = "#{} {} (No Spritesheet)";
class Layer
{
@@ -18,5 +19,6 @@ namespace anm2ed::anm2
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
void serialize(tinyxml2::XMLDocument&, tinyxml2::XMLElement*, int);
std::string to_string(int);
bool is_spritesheet_valid();
};
}

View File

@@ -1,7 +1,6 @@
#include "sound.h"
#include "filesystem_.h"
#include "string_.h"
#include "xml_.h"
using namespace anm2ed::resource;
@@ -28,8 +27,8 @@ namespace anm2ed::anm2
Sound::Sound(const std::string& directory, const std::string& path)
{
filesystem::WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string();
this->path = string::backslash_replace_to_lower(this->path);
this->path = !path.empty() ? std::filesystem::relative(path) : this->path;
this->path = filesystem::path_lower_case_backslash_handle(this->path);
audio = Audio(this->path.c_str());
}
@@ -38,8 +37,8 @@ namespace anm2ed::anm2
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
string::backslash_replace_to_lower(this->path);
audio = Audio(this->path.c_str());
path = filesystem::path_lower_case_backslash_handle(path);
audio = Audio(path.c_str());
}
XMLElement* Sound::to_element(XMLDocument& document, int id)
@@ -56,4 +55,19 @@ namespace anm2ed::anm2
document.InsertEndChild(to_element(document, id));
return xml::document_to_string(document);
}
void Sound::reload(const std::string& directory)
{
*this = Sound(directory, this->path);
}
bool Sound::is_valid()
{
return audio.is_valid();
}
void Sound::play()
{
audio.play();
}
}

View File

@@ -7,8 +7,7 @@
namespace anm2ed::anm2
{
constexpr auto SOUND_FORMAT = "#{} {}";
constexpr auto SOUND_FORMAT_C = "#%d %s";
constexpr auto SOUND_FORMAT = "{}";
class Sound
{
@@ -26,5 +25,8 @@ namespace anm2ed::anm2
Sound(const std::string&, const std::string&);
tinyxml2::XMLElement* to_element(tinyxml2::XMLDocument&, int);
std::string to_string(int);
void reload(const std::string&);
bool is_valid();
void play();
};
}

View File

@@ -1,7 +1,6 @@
#include "spritesheet.h"
#include "filesystem_.h"
#include "string_.h"
#include "xml_.h"
using namespace anm2ed::resource;
@@ -18,15 +17,15 @@ namespace anm2ed::anm2
// Spritesheet paths from Isaac Rebirth are made with the assumption that paths are case-insensitive
// However when using the resource dumper, the spritesheet paths are all lowercase (on Linux anyway)
// This will handle this case and make the paths OS-agnostic
this->path = string::backslash_replace_to_lower(this->path);
path = filesystem::path_lower_case_backslash_handle(path);
texture = Texture(path);
}
Spritesheet::Spritesheet(const std::string& directory, const std::string& path)
{
filesystem::WorkingDirectory workingDirectory(directory);
this->path = !path.empty() ? std::filesystem::relative(path).string() : this->path.string();
this->path = string::backslash_replace_to_lower(this->path);
this->path = !path.empty() ? std::filesystem::relative(path) : this->path;
this->path = filesystem::path_lower_case_backslash_handle(this->path);
texture = Texture(this->path);
}