The Mega Snivy Update
Some checks failed
Build / Build Game (push) Has been cancelled

This commit is contained in:
2026-02-28 21:48:00 -05:00
parent 8b2edd1359
commit 17f3348e94
163 changed files with 8725 additions and 13281 deletions

View File

@@ -1,421 +0,0 @@
#include "actor.h"
#include "../util/map_.h"
#include "../util/math_.h"
#include "../util/unordered_map_.h"
#include "../util/vector_.h"
#include "../resource/audio.h"
#include "../resource/texture.h"
#include <glm/glm.hpp>
#include <iostream>
using namespace glm;
using namespace game::util;
using namespace game::anm2;
namespace game::resource
{
Actor::Actor(Anm2* _anm2, vec2 _position, Mode mode, float time) : anm2(_anm2), position(_position)
{
if (anm2)
{
this->mode = mode;
this->startTime = time;
play(anm2->animations.defaultAnimation, mode, time);
}
}
anm2::Animation* Actor::animation_get(int index)
{
if (!anm2) return nullptr;
if (index == -1) index = animationIndex;
if (anm2->animations.mapReverse.contains(index)) return &anm2->animations.items[index];
return nullptr;
}
anm2::Animation* Actor::animation_get(const std::string& name)
{
if (!anm2) return nullptr;
if (anm2->animations.map.contains(name)) return &anm2->animations.items[anm2->animations.map[name]];
return nullptr;
}
bool Actor::is_playing(const std::string& name)
{
if (!anm2) return false;
if (name.empty())
return isPlaying;
else
return isPlaying && anm2->animations.map[name] == animationIndex;
}
int Actor::animation_index_get(const std::string& name)
{
if (!anm2) return -1;
if (anm2->animations.map.contains(name)) return anm2->animations.map[name];
return -1;
}
int Actor::item_id_get(const std::string& name, anm2::Type type)
{
if (!anm2 || (type != anm2::LAYER && type != anm2::NULL_)) return -1;
if (type == anm2::LAYER)
{
for (int i = 0; i < anm2->content.layers.size(); i++)
if (anm2->content.layers.at(i).name == name) return i;
}
else if (type == anm2::NULL_)
{
for (int i = 0; i < anm2->content.nulls.size(); i++)
if (anm2->content.nulls.at(i).name == name) return i;
}
return -1;
}
anm2::Item* Actor::item_get(anm2::Type type, int id, int animationIndex)
{
if (!anm2) return nullptr;
if (animationIndex == -1) animationIndex = this->animationIndex;
if (auto animation = animation_get(animationIndex))
{
switch (type)
{
case anm2::ROOT:
return &animation->rootAnimation;
break;
case anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
int Actor::item_length(anm2::Item* item)
{
if (!item) return -1;
int duration{};
for (auto& frame : item->frames)
duration += frame.duration;
return duration;
}
anm2::Frame* Actor::trigger_get(int atFrame)
{
if (auto item = item_get(anm2::TRIGGER))
for (auto& trigger : item->frames)
if (trigger.atFrame == atFrame) return &trigger;
return nullptr;
}
anm2::Frame* Actor::frame_get(int index, anm2::Type type, int id)
{
if (auto item = item_get(type, id)) return vector::find(item->frames, index);
return nullptr;
}
bool Actor::is_event(const std::string& event)
{
if (!anm2) return false;
if (playedEventID == -1) return false;
return event == anm2->content.events.at(playedEventID).name;
}
anm2::Frame Actor::frame_generate(anm2::Item& item, float time, anm2::Type type, int id)
{
anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
time = time < 0.0f ? 0.0f : time;
anm2::Frame* frameNext = nullptr;
anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < item.frames.size(); i++)
{
anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
for (auto& override : overrides)
{
if (!override || !override->isEnabled) continue;
if (id == override->destinationID)
{
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value())
{
frame.scale += *override->frame.scale;
if (frameNext) frameNextCopy.scale += *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation += *override->frame.rotation;
if (frameNext) frameNextCopy.rotation += *override->frame.rotation;
}
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value())
{
frame.scale = *override->frame.scale;
if (frameNext) frameNextCopy.scale = *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation = *override->frame.rotation;
if (frameNext) frameNextCopy.rotation = *override->frame.rotation;
}
break;
case Override::ITEM_SET:
default:
if (override->animationIndex == -1) break;
auto& animation = anm2->animations.items[override->animationIndex];
auto overrideFrame = frame_generate(animation.layerAnimations[override->sourceID], override->time,
anm2::LAYER, override->sourceID);
frame.crop = overrideFrame.crop;
break;
}
}
}
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
}
return frame;
}
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
{
this->playedEventID = -1;
this->playedTriggers.clear();
if (!anm2) return;
if (mode != FORCE_PLAY && this->animationIndex == index) return;
if (!vector::in_bounds(anm2->animations.items, index)) return;
this->speedMultiplier = speedMultiplier;
this->previousAnimationIndex = animationIndex;
this->animationIndex = index;
this->time = time;
if (mode == PLAY || mode == FORCE_PLAY) isPlaying = true;
}
void Actor::play(const std::string& name, Mode mode, float time, float speedMultiplier)
{
if (!anm2) return;
if (anm2->animations.map.contains(name))
play(anm2->animations.map.at(name), mode, time, speedMultiplier);
else
std::cout << "Animation \"" << name << "\" does not exist! Unable to play!\n";
}
void Actor::tick()
{
if (!anm2) return;
if (!isPlaying) return;
auto animation = animation_get();
if (!animation) return;
playedEventID = -1;
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
if (auto sound = map::find(anm2->content.sounds, trigger.soundID)) sound->audio.play();
playedTriggers.insert((int)trigger.atFrame);
playedEventID = trigger.eventID;
}
}
auto increment = (anm2->info.fps / 30.0f) * speedMultiplier;
time += increment;
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
isPlaying = false;
playedTriggers.clear();
}
for (auto& override : overrides)
{
if (!override->isEnabled || override->length < 0) continue;
override->time += increment;
if (override->time > override->length) override->isLoop ? override->time = 0.0f : override->isEnabled = false;
}
}
glm::vec4 Actor::null_frame_rect(int nullID)
{
auto invalidRect = glm::vec4(0.0f / 0.0f);
if (!anm2 || nullID == -1) return invalidRect;
auto item = item_get(anm2::NULL_, nullID);
if (!item) return invalidRect;
auto animation = animation_get();
if (!animation) return invalidRect;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto frame = frame_generate(*item, time, anm2::NULL_, nullID);
if (!frame.isVisible) return invalidRect;
auto rootScale = math::to_unit(root.scale);
auto frameScale = math::to_unit(frame.scale);
auto combinedScale = rootScale * frameScale;
auto scaledSize = NULL_SIZE * glm::abs(combinedScale);
auto worldPosition = position + root.position + frame.position * rootScale;
auto halfSize = scaledSize * 0.5f;
return glm::vec4(worldPosition - halfSize, scaledSize);
}
void Actor::render(Shader& textureShader, Shader& rectShader, Canvas& canvas)
{
if (!anm2) return;
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto rootModel =
math::quad_model_parent_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(anm2->content.layers, i);
if (!layer) continue;
auto spritesheet = map::find(anm2->content.spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time, anm2::LAYER, i);
if (!frame.isVisible) continue;
auto model =
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto tint = frame.tint * root.tint;
auto colorOffset = frame.colorOffset + root.colorOffset;
auto uvMin = frame.crop / vec2(texture.size);
auto uvMax = (frame.crop + frame.size) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
}
if (isShowNulls)
{
for (int i = 0; i < animation->nullAnimations.size(); i++)
{
auto& nullAnimation = animation->nullAnimations[i];
if (!nullAnimation.isVisible) continue;
auto frame = frame_generate(nullAnimation, time, anm2::NULL_, i);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
model = rootModel * model;
canvas.rect_render(rectShader, model);
}
}
}
void Actor::consume_played_event() { playedEventID = -1; }
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include <unordered_set>
#include "../canvas.h"
#include "anm2.h"
namespace game::resource
{
class Actor
{
public:
static constexpr auto NULL_SIZE = glm::vec2(100, 100);
enum Mode
{
PLAY,
SET,
FORCE_PLAY
};
struct Override
{
enum Mode
{
ITEM_SET,
FRAME_ADD,
FRAME_SET
};
anm2::FrameOptional frame{};
int animationIndex{-1};
int sourceID{-1};
int destinationID{-1};
float length{-1.0f};
bool isLoop{false};
Mode mode{ITEM_SET};
anm2::Type type{anm2::LAYER};
bool isEnabled{true};
float time{};
};
anm2::Anm2* anm2{};
glm::vec2 position{};
float time{};
bool isPlaying{};
bool isShowNulls{};
int animationIndex{-1};
int previousAnimationIndex{-1};
int lastPlayedAnimationIndex{-1};
int playedEventID{-1};
Mode mode{PLAY};
float startTime{};
float speedMultiplier{};
std::unordered_set<int> playedTriggers{};
std::vector<Override*> overrides{};
Actor(anm2::Anm2*, glm::vec2, Mode = PLAY, float = 0.0f);
anm2::Animation* animation_get(int = -1);
anm2::Animation* animation_get(const std::string&);
int animation_index_get(const std::string&);
anm2::Item* item_get(anm2::Type, int = -1, int = -1);
int item_length(anm2::Item*);
anm2::Frame* trigger_get(int);
anm2::Frame* frame_get(int, anm2::Type, int = -1);
int item_id_get(const std::string&, anm2::Type = anm2::LAYER);
anm2::Frame frame_generate(anm2::Item&, float, anm2::Type, int = -1);
void play(const std::string&, Mode = PLAY, float = 0.0f, float = 1.0f);
void play(int, Mode = PLAY, float = 0.0f, float = 1.0f);
bool is_event(const std::string& event);
void tick();
bool is_playing(const std::string& name = {});
void render(Shader& textureShader, Shader& rectShader, Canvas&);
glm::vec4 null_frame_rect(int = -1);
void consume_played_event();
};
}

View File

@@ -1,234 +0,0 @@
#include "anm2.h"
#include <iostream>
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::resource;
using namespace game::util;
namespace game::anm2
{
Info::Info(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Fps", &fps);
}
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
texture = Texture(path);
}
Layer::Layer(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
Null::Null(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
Event::Event(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
}
Sound::Sound(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
audio = Audio(path);
}
Content::Content(XMLElement* element)
{
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet(child, spritesheetId);
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = element->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer(child, layerId);
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = element->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null(child, nullId);
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = element->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event(child, eventId);
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = element->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound(child, soundId);
sounds.emplace(soundId, std::move(sound));
}
}
}
Frame::Frame(XMLElement* element, Type type)
{
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &position.x);
element->QueryFloatAttribute("YPosition", &position.y);
if (type == LAYER)
{
element->QueryFloatAttribute("XPivot", &pivot.x);
element->QueryFloatAttribute("YPivot", &pivot.y);
element->QueryFloatAttribute("XCrop", &crop.x);
element->QueryFloatAttribute("YCrop", &crop.y);
element->QueryFloatAttribute("Width", &size.x);
element->QueryFloatAttribute("Height", &size.y);
}
element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible);
xml::query_color_attribute(element, "RedTint", &tint.r);
xml::query_color_attribute(element, "GreenTint", &tint.g);
xml::query_color_attribute(element, "BlueTint", &tint.b);
xml::query_color_attribute(element, "AlphaTint", &tint.a);
xml::query_color_attribute(element, "RedOffset", &colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
}
else
{
element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("SoundId", &soundID);
element->QueryIntAttribute("AtFrame", &atFrame);
}
}
Item::Item(XMLElement* element, Type type, int& id)
{
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
frames.emplace_back(Frame(child, type));
}
Animation::Animation(XMLElement* element)
{
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
rootAnimation = Item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
Item layerAnimation(child, LAYER, id);
layerOrder.push_back(id);
layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
Item nullAnimation(child, NULL_, id);
nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER, id);
}
Animations::Animations(XMLElement* element)
{
xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
items.emplace_back(Animation(child));
for (int i = 0; i < items.size(); i++)
{
auto& item = items.at(i);
map[item.name] = i;
mapReverse[i] = item.name;
}
}
Anm2::Anm2(const std::filesystem::path& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize anm2: " << document.ErrorStr() << "\n";
return;
}
auto previousPath = std::filesystem::current_path();
std::filesystem::current_path(path.parent_path());
auto element = document.RootElement();
if (auto infoElement = element->FirstChildElement("Info")) info = Info(infoElement);
if (auto contentElement = element->FirstChildElement("Content")) content = Content(contentElement);
if (auto animationsElement = element->FirstChildElement("Animations")) animations = Animations(animationsElement);
std::filesystem::current_path(previousPath);
std::cout << "Initialzed anm2: " << path.string() << "\n";
}
}

View File

@@ -1,176 +0,0 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "audio.h"
#include "texture.h"
namespace game::anm2
{
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
class Info
{
public:
int fps = 30;
Info() = default;
Info(tinyxml2::XMLElement*);
};
class Spritesheet
{
public:
std::filesystem::path path{};
resource::Texture texture{};
Spritesheet(tinyxml2::XMLElement*, int&);
};
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{-1};
Layer(tinyxml2::XMLElement*, int&);
};
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
Null(tinyxml2::XMLElement*, int&);
};
class Event
{
public:
std::string name{"New Event"};
Event(tinyxml2::XMLElement*, int&);
};
class Sound
{
public:
std::filesystem::path path{};
resource::Audio audio{};
Sound(tinyxml2::XMLElement*, int&);
};
class Content
{
public:
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
Content() = default;
Content(tinyxml2::XMLElement*);
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int soundID{-1};
int atFrame{-1};
bool isVisible{true};
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{};
Item() = default;
Item(tinyxml2::XMLElement*, Type, int&);
};
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
Animation() = default;
Animation(tinyxml2::XMLElement*);
};
class Animations
{
public:
std::string defaultAnimation{};
std::vector<Animation> items{};
std::unordered_map<std::string, int> map{};
std::unordered_map<int, std::string> mapReverse{};
Animations() = default;
Animations(tinyxml2::XMLElement*);
};
class Anm2
{
public:
Info info;
Content content{};
Animations animations{};
Anm2() = default;
Anm2(const std::filesystem::path&);
};
}

View File

@@ -1,73 +1,119 @@
#include "audio.h"
#include "audio.hpp"
#include <SDL3/SDL_properties.h>
#include <iostream>
#include "../log.hpp"
#include <string>
#include <unordered_map>
using namespace game::util;
namespace game::resource
{
static std::shared_ptr<MIX_Audio> audio_make(MIX_Audio* audio)
{
return std::shared_ptr<MIX_Audio>(audio,
[](MIX_Audio* a)
{
if (a) MIX_DestroyAudio(a);
});
}
static std::unordered_map<std::string, std::weak_ptr<MIX_Audio>> audioCache{};
static std::shared_ptr<MIX_Audio> cache_get(const std::string& key)
{
auto it = audioCache.find(key);
if (it == audioCache.end()) return {};
auto cached = it->second.lock();
if (!cached) audioCache.erase(it);
return cached;
}
static void cache_set(const std::string& key, const std::shared_ptr<MIX_Audio>& audio)
{
if (!audio) return;
audioCache[key] = audio;
}
MIX_Mixer* Audio::mixer_get()
{
static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
return mixer;
}
void Audio::set_gain(float gain)
void Audio::volume_set(float volume)
{
auto mixer = mixer_get();
MIX_SetMasterGain(mixer, gain);
}
void Audio::retain()
{
if (refCount) ++(*refCount);
}
void Audio::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (internal) MIX_DestroyAudio(internal);
delete refCount;
}
refCount = nullptr;
}
internal = nullptr;
MIX_SetMasterGain(mixer, volume);
}
Audio::Audio(const std::filesystem::path& path)
{
internal = MIX_LoadAudio(mixer_get(), path.c_str(), true);
auto key = std::string("fs:") + path.string();
internal = cache_get(key);
if (internal)
{
refCount = new int(1);
std::cout << "Initialized audio: '" << path.string() << "'\n";
logger.info(std::format("Using cached audio: {}", path.string()));
return;
}
else
internal = audio_make(MIX_LoadAudio(mixer_get(), path.c_str(), true));
cache_set(key, internal);
if (internal) logger.info(std::format("Initialized audio: {}", path.string()));
if (!internal) logger.info(std::format("Failed to intialize audio: {} ({})", path.string(), SDL_GetError()));
}
Audio::Audio(const physfs::Path& path)
{
if (!path.is_valid())
{
std::cout << "Failed to initialize audio: '" << path.string() << "'\n";
logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
return;
}
auto key = std::string("physfs:") + path.c_str();
internal = cache_get(key);
if (internal)
{
logger.info(std::format("Using cached audio: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
internal = audio_make(MIX_LoadAudio_IO(mixer_get(), ioStream, false, true));
cache_set(key, internal);
if (internal)
logger.info(std::format("Initialized audio: {}", path.c_str()));
else
logger.info(std::format("Failed to intialize audio: {} ({})", path.c_str(), SDL_GetError()));
}
Audio::Audio(const Audio& other)
{
internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr;
}
Audio::Audio(Audio&& other) noexcept
{
internal = other.internal;
internal = std::move(other.internal);
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
Audio& Audio::operator=(const Audio& other)
@@ -76,8 +122,7 @@ namespace game::resource
{
unload();
internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr;
}
return *this;
}
@@ -87,13 +132,10 @@ namespace game::resource
if (this != &other)
{
unload();
internal = other.internal;
internal = std::move(other.internal);
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
return *this;
}
@@ -105,7 +147,7 @@ namespace game::resource
MIX_DestroyTrack(track);
track = nullptr;
}
release();
internal.reset();
}
void Audio::play(bool isLoop)
@@ -126,7 +168,7 @@ namespace game::resource
if (!track) return;
}
MIX_SetTrackAudio(track, internal);
MIX_SetTrackAudio(track, internal.get());
SDL_PropertiesID options = 0;
@@ -149,5 +191,5 @@ namespace game::resource
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
Audio::~Audio() { unload(); }
bool Audio::is_valid() const { return internal != nullptr; }
bool Audio::is_valid() const { return (bool)internal; }
}

View File

@@ -2,22 +2,24 @@
#include <SDL3_mixer/SDL_mixer.h>
#include <filesystem>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource
{
class Audio
{
MIX_Audio* internal{nullptr};
MIX_Track* track{nullptr};
int* refCount{nullptr};
static MIX_Mixer* mixer_get();
void unload();
void retain();
void release();
std::shared_ptr<MIX_Audio> internal{};
MIX_Track* track{nullptr};
public:
Audio() = default;
Audio(const std::filesystem::path&);
Audio(const util::physfs::Path&);
Audio(const Audio&);
Audio(Audio&&) noexcept;
Audio& operator=(const Audio&);
@@ -27,6 +29,6 @@ namespace game::resource
void play(bool isLoop = false);
void stop();
bool is_playing() const;
static void set_gain(float vol);
static void volume_set(float volume);
};
}

View File

@@ -1,137 +0,0 @@
#include "dialogue.h"
#include <iostream>
#include "../util/map_.h"
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource
{
void label_map_query(XMLElement* element, std::map<std::string, int>& labelMap, const char* attribute, int& id)
{
std::string label{};
xml::query_string_attribute(element, attribute, &label);
if (auto foundID = map::find(labelMap, label))
id = *foundID;
else
id = -1;
}
Dialogue::Color::Color(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Start", &start);
element->QueryIntAttribute("End", &end);
xml::query_color_attribute(element, "R", &value.r);
xml::query_color_attribute(element, "G", &value.g);
xml::query_color_attribute(element, "B", &value.b);
xml::query_color_attribute(element, "A", &value.a);
}
Dialogue::Animation::Animation(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("At", &at);
xml::query_string_attribute(element, "Name", &name);
}
Dialogue::Branch::Branch(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
label_map_query(element, labelMap, "Label", nextID);
xml::query_string_attribute(element, "Content", &content);
}
Dialogue::Entry::Entry(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
xml::query_string_attribute(element, "Content", &content);
label_map_query(element, labelMap, "Next", nextID);
std::string flagString{};
xml::query_string_attribute(element, "Flag", &flagString);
for (int i = 0; i < std::size(FLAG_STRINGS); i++)
{
if (flagString == FLAG_STRINGS[i])
{
flag = (Flag)i;
break;
}
}
for (auto child = element->FirstChildElement("Color"); child; child = child->NextSiblingElement("Color"))
colors.emplace_back(child);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
animations.emplace_back(child);
for (auto child = element->FirstChildElement("Branch"); child; child = child->NextSiblingElement("Branch"))
branches.emplace_back(child, labelMap);
}
Dialogue::Dialogue(const std::string& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize dialogue: " << document.ErrorStr() << "\n";
return;
}
auto element = document.RootElement();
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string label{};
xml::query_string_attribute(child, "Label", &label);
labelMap.emplace(label, id++);
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
entryMap.emplace(id++, Entry(child, labelMap));
for (auto& [label, id] : labelMap)
{
if (label.starts_with(BURP_SMALL_LABEL)) burpSmallIDs.emplace_back(id);
if (label.starts_with(BURP_BIG_LABEL)) burpBigIDs.emplace_back(id);
if (label.starts_with(EAT_HUNGRY_LABEL)) eatHungryIDs.emplace_back(id);
if (label.starts_with(EAT_FULL_LABEL)) eatFullIDs.emplace_back(id);
if (label.starts_with(FULL_LABEL)) fullIDs.emplace_back(id);
if (label.starts_with(CAPACITY_LOW_LABEL)) capacityLowIDs.emplace_back(id);
if (label.starts_with(FEED_HUNGRY_LABEL)) feedHungryIDs.emplace_back(id);
if (label.starts_with(FEED_FULL_LABEL)) feedFullIDs.emplace_back(id);
if (label.starts_with(FOOD_STOLEN_LABEL)) foodStolenIDs.emplace_back(id);
if (label.starts_with(FOOD_EASED_LABEL)) foodEasedIDs.emplace_back(id);
if (label.starts_with(PERFECT_LABEL)) perfectIDs.emplace_back(id);
if (label.starts_with(MISS_LABEL)) missIDs.emplace_back(id);
if (label.starts_with(POST_DIGEST_LABEL)) postDigestIDs.emplace_back(id);
if (label.starts_with(RANDOM_LABEL)) randomIDs.emplace_back(id);
}
std::cout << "Initialzed dialogue: " << path << "\n";
}
Dialogue::Entry* Dialogue::get(int id)
{
if (id == -1) return nullptr;
return map::find(entryMap, id);
}
Dialogue::Entry* Dialogue::get(const std::string& label)
{
auto id = map::find(labelMap, label);
if (!id) return nullptr;
return get(*id);
}
Dialogue::Entry* Dialogue::next(Dialogue::Entry* entry) { return get(entry->nextID); }
}

View File

@@ -1,118 +0,0 @@
#pragma once
#include "glm/ext/vector_float4.hpp"
#include <tinyxml2.h>
#include <string>
#include <map>
namespace game::resource
{
class Dialogue
{
public:
static constexpr auto FULL_LABEL = "Full";
static constexpr auto POST_DIGEST_LABEL = "PostDigest";
static constexpr auto BURP_SMALL_LABEL = "BurpSmall";
static constexpr auto BURP_BIG_LABEL = "BurpBig";
static constexpr auto FEED_HUNGRY_LABEL = "FeedHungry";
static constexpr auto FEED_FULL_LABEL = "FeedFull";
static constexpr auto EAT_HUNGRY_LABEL = "EatHungry";
static constexpr auto EAT_FULL_LABEL = "EatFull";
static constexpr auto FOOD_STOLEN_LABEL = "FoodStolen";
static constexpr auto FOOD_EASED_LABEL = "FoodEased";
static constexpr auto CAPACITY_LOW_LABEL = "CapacityLow";
static constexpr auto PERFECT_LABEL = "Perfect";
static constexpr auto MISS_LABEL = "Miss";
static constexpr auto RANDOM_LABEL = "StartRandom";
;
class Color
{
public:
int start{};
int end{};
glm::vec4 value{};
Color(tinyxml2::XMLElement*);
};
class Animation
{
public:
int at{-1};
std::string name{};
Animation(tinyxml2::XMLElement*);
};
class Branch
{
public:
std::string content{};
int nextID{-1};
Branch(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
class Entry
{
public:
#define FLAGS \
X(NONE, "None") \
X(ACTIVATE_WINDOWS, "ActivateWindows") \
X(DEACTIVATE_WINDOWS, "DeactivateWindows") \
X(ONLY_INFO, "OnlyInfo") \
X(ACTIVATE_CHEATS, "ActivateCheats")
enum Flag
{
#define X(symbol, name) symbol,
FLAGS
#undef X
};
static constexpr const char* FLAG_STRINGS[] = {
#define X(symbol, name) name,
FLAGS
#undef X
};
#undef FLAGS
std::string content{};
std::vector<Color> colors{};
std::vector<Animation> animations{};
std::vector<Branch> branches{};
int nextID{-1};
Flag flag{Flag::NONE};
Entry(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
std::map<std::string, int> labelMap;
std::map<int, Entry> entryMap{};
std::vector<int> eatHungryIDs{};
std::vector<int> eatFullIDs{};
std::vector<int> feedHungryIDs{};
std::vector<int> feedFullIDs{};
std::vector<int> burpSmallIDs{};
std::vector<int> burpBigIDs{};
std::vector<int> fullIDs{};
std::vector<int> foodStolenIDs{};
std::vector<int> foodEasedIDs{};
std::vector<int> perfectIDs{};
std::vector<int> postDigestIDs{};
std::vector<int> missIDs{};
std::vector<int> capacityLowIDs{};
std::vector<int> randomIDs{};
Dialogue(const std::string&);
Entry* get(const std::string&);
Entry* get(int = -1);
Entry* next(Entry*);
};
}

View File

@@ -1,10 +1,52 @@
#include "font.h"
#include "font.hpp"
#include "../log.hpp"
using namespace game::util;
namespace game::resource
{
Font::Font(const std::string& path, float size)
Font::Font(const std::filesystem::path& path, float size)
{
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size);
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", path.c_str()));
else
logger.error(std::format("Failed to initialize font: {}", path.c_str()));
}
Font::Font(const physfs::Path& path, float size)
{
if (!path.is_valid())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromMemoryTTF(buffer.data(), (int)buffer.size(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", path.c_str()));
else
logger.error(std::format("Failed to initialize font: {}", path.c_str()));
}
ImFont* Font::get() { return internal; };
}
}

View File

@@ -1,20 +1,28 @@
#pragma once
#include "imgui.h"
#include <string>
#include <filesystem>
#include <imgui.h>
#include "../util/physfs.hpp"
namespace game::resource
{
class Font
{
ImFont* internal{};
public:
ImFont* internal;
static constexpr auto NORMAL = 20;
static constexpr auto ABOVE_AVERAGE = 24;
static constexpr auto BIG = 30;
static constexpr auto HEADER_3 = 40;
static constexpr auto HEADER_2 = 50;
static constexpr auto HEADER_1 = 60;
static constexpr auto NORMAL = 12;
static constexpr auto BIG = 16;
static constexpr auto LARGE = 24;
Font(const std::string&, float = NORMAL);
Font() = default;
Font(const std::filesystem::path&, float = NORMAL);
Font(const util::physfs::Path&, float = NORMAL);
ImFont* get();
};
}
}

View File

@@ -1,6 +1,6 @@
#include "shader.h"
#include "shader.hpp"
#include <iostream>
#include "../log.hpp"
namespace game::resource
{
@@ -20,7 +20,7 @@ namespace game::resource
glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength);
std::string log(logLength, '\0');
if (logLength > 0) glGetShaderInfoLog(shaderHandle, logLength, nullptr, log.data());
std::cout << "Failed to compile shader: " << log << '\n';
logger.error(std::format("Failed to compile shader: {}", log));
return false;
}
return true;
@@ -49,11 +49,11 @@ namespace game::resource
if (!isLinked)
{
glDeleteProgram(id);
logger.error(std::format("Failed to link shader: {}", id));
id = 0;
std::cout << "Failed to link shader: " << id << "\n";
}
else
std::cout << "Initialized shader: " << id << "\n";
logger.info(std::format("Initialized shader: {}", id));
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);

View File

@@ -1,60 +1,75 @@
#include "texture.h"
#include "texture.hpp"
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#include <SDL3/SDL_surface.h>
#include <string>
#include <unordered_map>
#define STBI_ONLY_PNG
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#include <iostream>
#include "../log.hpp"
using namespace glm;
using namespace game::util;
namespace game::resource
{
struct CachedTexture
{
std::weak_ptr<GLuint> idShared{};
glm::ivec2 size{};
int channels{};
};
static std::unordered_map<std::string, CachedTexture> textureCache{};
static bool cache_get(const std::string& key, std::shared_ptr<GLuint>& idShared, GLuint& id, ivec2& size, int& channels)
{
auto it = textureCache.find(key);
if (it == textureCache.end()) return false;
auto shared = it->second.idShared.lock();
if (!shared)
{
textureCache.erase(it);
return false;
}
idShared = shared;
id = *shared;
size = it->second.size;
channels = it->second.channels;
return true;
}
static void cache_set(const std::string& key, const std::shared_ptr<GLuint>& idShared, ivec2 size, int channels)
{
if (!idShared) return;
textureCache[key] = CachedTexture{.idShared = idShared, .size = size, .channels = channels};
}
static std::shared_ptr<GLuint> texture_id_make(GLuint id)
{
return std::shared_ptr<GLuint>(new GLuint(id),
[](GLuint* p)
{
if (!p) return;
if (*p != 0) glDeleteTextures(1, p);
delete p;
});
}
bool Texture::is_valid() const { return id != 0; }
void Texture::retain()
Texture::~Texture()
{
if (refCount) ++(*refCount);
}
void Texture::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (is_valid()) glDeleteTextures(1, &id);
delete refCount;
}
refCount = nullptr;
}
else if (is_valid())
{
glDeleteTextures(1, &id);
}
idShared.reset();
id = 0;
}
Texture::~Texture() { release(); }
Texture::Texture(const Texture& other)
{
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
idShared = other.idShared;
}
Texture::Texture(Texture&& other) noexcept
@@ -62,24 +77,21 @@ namespace game::resource
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
idShared = std::move(other.idShared);
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
Texture& Texture::operator=(const Texture& other)
{
if (this != &other)
{
release();
idShared = other.idShared;
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
}
return *this;
}
@@ -88,45 +100,103 @@ namespace game::resource
{
if (this != &other)
{
release();
idShared.reset();
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
idShared = std::move(other.idShared);
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
return *this;
}
void Texture::init(const uint8_t* data)
{
idShared.reset();
id = 0;
GLuint newId{};
glGenTextures(1, &newId);
glBindTexture(GL_TEXTURE_2D, newId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
channels = CHANNELS;
idShared = texture_id_make(newId);
id = newId;
}
Texture::Texture(const std::filesystem::path& path)
{
if (auto data = stbi_load(path.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
auto key = std::string("fs:") + path.string();
if (cache_get(key, idShared, id, size, channels))
{
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(data);
channels = CHANNELS;
refCount = new int(1);
std::cout << "Initialized texture: '" << path.string() << "\n";
logger.info(std::format("Using cached texture: {}", path.string()));
return;
}
if (auto surface = SDL_LoadPNG(path.c_str()))
{
auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(surface);
surface = rgbaSurface;
this->size = ivec2(surface->w, surface->h);
init((const uint8_t*)surface->pixels);
SDL_DestroySurface(surface);
cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", path.string()));
}
else
logger.error(std::format("Failed to initialize texture: {} ({})", path.string(), SDL_GetError()));
}
Texture::Texture(const physfs::Path& path)
{
if (!path.is_valid())
{
id = 0;
size = {};
channels = 0;
refCount = nullptr;
std::cout << "Failed to initialize texture: '" << path.string() << "'\n";
logger.error(
std::format("Failed to initialize texture from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
return;
}
auto key = std::string("physfs:") + path.c_str();
if (cache_get(key, idShared, id, size, channels))
{
logger.info(std::format("Using cached texture: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize texture from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
if (auto surface = SDL_LoadPNG_IO(ioStream, true))
{
auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(surface);
surface = rgbaSurface;
this->size = ivec2(surface->w, surface->h);
init((const uint8_t*)surface->pixels);
SDL_DestroySurface(surface);
cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", path.c_str()));
}
else
logger.error(std::format("Failed to initialize texture: {} ({})", path.c_str(), SDL_GetError()));
}
}

View File

@@ -6,8 +6,12 @@
#include <glad/glad.h>
#endif
#include <cstdint>
#include <filesystem>
#include <glm/ext/vector_int2.hpp>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource
{
@@ -29,10 +33,10 @@ namespace game::resource
Texture& operator=(const Texture&);
Texture& operator=(Texture&&) noexcept;
Texture(const std::filesystem::path&);
Texture(const util::physfs::Path&);
void init(const uint8_t*);
private:
int* refCount{nullptr};
void retain();
void release();
std::shared_ptr<GLuint> idShared{};
};
}

View File

@@ -0,0 +1,11 @@
#include "animation_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
const std::string& AnimationEntryCollection::get()
{
return at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).animation;
}
}

View File

@@ -0,0 +1,23 @@
// Handles animation entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include <string>
#include <vector>
namespace game::resource::xml
{
class AnimationEntry
{
public:
std::string animation{};
float weight{1.0f};
};
class AnimationEntryCollection : public std::vector<AnimationEntry>
{
public:
const std::string& get();
};
}

338
src/resource/xml/anm2.cpp Normal file
View File

@@ -0,0 +1,338 @@
#include "anm2.hpp"
#include "util.hpp"
#include "../../util/working_directory.hpp"
#include "../../log.hpp"
#include <ranges>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Anm2::Anm2(const Anm2&) = default;
Anm2::Anm2(Anm2&&) = default;
Anm2& Anm2::operator=(const Anm2&) = default;
Anm2& Anm2::operator=(Anm2&&) = default;
void Anm2::init(XMLDocument& document, Flags flags, const physfs::Path& archive)
{
this->flags = flags;
auto element = document.RootElement();
if (!element) return;
auto parse_frame = [&](XMLElement* element, Type type, int spritesheetID = -1)
{
Frame frame{};
if (!element) return frame;
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &frame.position.x);
element->QueryFloatAttribute("YPosition", &frame.position.y);
if (type == LAYER)
{
if (element->FindAttribute("RegionId") && spritesheets.contains(spritesheetID))
{
auto& spritesheet = spritesheets.at(spritesheetID);
element->QueryIntAttribute("RegionId", &frame.regionID);
auto& region = spritesheet.regions.at(frame.regionID);
frame.crop = region.crop;
frame.size = region.size;
frame.pivot = region.origin == Spritesheet::Region::Origin::CENTER
? glm::vec2(glm::ivec2(frame.size * 0.5f))
: region.origin == Spritesheet::Region::Origin::TOP_LEFT ? glm::vec2{}
: region.pivot;
}
else
{
element->QueryFloatAttribute("XPivot", &frame.pivot.x);
element->QueryFloatAttribute("YPivot", &frame.pivot.y);
element->QueryFloatAttribute("XCrop", &frame.crop.x);
element->QueryFloatAttribute("YCrop", &frame.crop.y);
element->QueryFloatAttribute("Width", &frame.size.x);
element->QueryFloatAttribute("Height", &frame.size.y);
}
}
element->QueryFloatAttribute("XScale", &frame.scale.x);
element->QueryFloatAttribute("YScale", &frame.scale.y);
element->QueryIntAttribute("Delay", &frame.duration);
element->QueryBoolAttribute("Visible", &frame.isVisible);
xml::query_color_attribute(element, "RedTint", &frame.tint.r);
xml::query_color_attribute(element, "GreenTint", &frame.tint.g);
xml::query_color_attribute(element, "BlueTint", &frame.tint.b);
xml::query_color_attribute(element, "AlphaTint", &frame.tint.a);
xml::query_color_attribute(element, "RedOffset", &frame.colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &frame.colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &frame.colorOffset.b);
element->QueryFloatAttribute("Rotation", &frame.rotation);
element->QueryBoolAttribute("Interpolated", &frame.isInterpolated);
}
else
{
for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundID{};
child->QueryIntAttribute("Id", &soundID);
frame.soundIDs.emplace_back(soundID);
}
element->QueryIntAttribute("EventId", &frame.eventID);
element->QueryIntAttribute("AtFrame", &frame.atFrame);
}
return frame;
};
auto parse_item = [&](XMLElement* element, Type type, int& id)
{
Item item{};
if (!element) return item;
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &item.isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
item.frames.emplace_back(parse_frame(child, type, type == LAYER ? layers.at(id).spritesheetID : -1));
return item;
};
auto parse_animation = [&](XMLElement* element)
{
Animation animation{};
if (!element) return animation;
xml::query_string_attribute(element, "Name", &animation.name);
element->QueryIntAttribute("FrameNum", &animation.frameNum);
element->QueryBoolAttribute("Loop", &animation.isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
animation.rootAnimation = parse_item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
auto layerAnimation = parse_item(child, LAYER, id);
animation.layerOrder.push_back(id);
animation.layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
auto nullAnimation = parse_item(child, NULL_, id);
animation.nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers"))
animation.triggers = parse_item(triggersElement, TRIGGER, id);
return animation;
};
if (auto infoElement = element->FirstChildElement("Info")) infoElement->QueryIntAttribute("Fps", &fps);
if (auto contentElement = element->FirstChildElement("Content"))
{
if (auto spritesheetsElement = contentElement->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet{};
child->QueryIntAttribute("Id", &spritesheetId);
xml::query_string_attribute(child, "Path", &spritesheet.path);
if ((this->flags & NO_SPRITESHEETS) != 0)
spritesheet.texture = Texture();
else if (!archive.empty())
spritesheet.texture = Texture(physfs::Path(archive + "/" + spritesheet.path));
else
spritesheet.texture = Texture(std::filesystem::path(spritesheet.path));
for (auto regionChild = child->FirstChildElement("Region"); regionChild;
regionChild = regionChild->NextSiblingElement("Region"))
{
Spritesheet::Region region{};
int regionID{};
regionChild->QueryIntAttribute("Id", &regionID);
xml::query_string_attribute(regionChild, "Name", &region.name);
regionChild->QueryFloatAttribute("XCrop", &region.crop.x);
regionChild->QueryFloatAttribute("YCrop", &region.crop.y);
regionChild->QueryFloatAttribute("Width", &region.size.x);
regionChild->QueryFloatAttribute("Height", &region.size.y);
if (regionChild->FindAttribute("Origin"))
{
std::string origin{};
xml::query_string_attribute(regionChild, "Origin", &origin);
region.origin = origin == "Center" ? Spritesheet::Region::CENTER
: origin == "TopLeft" ? Spritesheet::Region::TOP_LEFT
: Spritesheet::Region::CUSTOM;
}
else
{
regionChild->QueryFloatAttribute("XPivot", &region.pivot.x);
regionChild->QueryFloatAttribute("YPivot", &region.pivot.y);
}
spritesheet.regions.emplace(regionID, std::move(region));
spritesheet.regionOrder.emplace_back(regionID);
}
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = contentElement->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer{};
child->QueryIntAttribute("Id", &layerId);
xml::query_string_attribute(child, "Name", &layer.name);
child->QueryIntAttribute("SpritesheetId", &layer.spritesheetID);
layerMap[layer.name] = layerId;
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = contentElement->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null{};
child->QueryIntAttribute("Id", &nullId);
xml::query_string_attribute(child, "Name", &null.name);
child->QueryBoolAttribute("ShowRect", &null.isShowRect);
nullMap[null.name] = nullId;
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = contentElement->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event{};
child->QueryIntAttribute("Id", &eventId);
xml::query_string_attribute(child, "Name", &event.name);
eventMap[event.name] = eventId;
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = contentElement->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound{};
child->QueryIntAttribute("Id", &soundId);
xml::query_string_attribute(child, "Path", &sound.path);
if ((this->flags & NO_SOUNDS) != 0)
sound.audio = Audio();
else if (!archive.empty())
sound.audio = Audio(physfs::Path(archive + "/" + sound.path));
else
sound.audio = Audio(std::filesystem::path(sound.path));
sounds.emplace(soundId, std::move(sound));
}
}
}
if (auto animationsElement = element->FirstChildElement("Animations"))
{
xml::query_string_attribute(animationsElement, "DefaultAnimation", &defaultAnimation);
for (auto child = animationsElement->FirstChildElement("Animation"); child;
child = child->NextSiblingElement("Animation"))
{
if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0)
{
std::string name{};
xml::query_string_attribute(child, "Name", &name);
if (name == defaultAnimation)
{
animations.emplace_back(parse_animation(child));
break;
}
}
else
animations.emplace_back(parse_animation(child));
}
for (int i = 0; i < (int)animations.size(); i++)
{
auto& animation = animations[i];
animationMap[animation.name] = i;
animationMapReverse[i] = animation.name;
}
if (animationMap.contains(defaultAnimation))
defaultAnimationID = animationMap[defaultAnimation];
else
defaultAnimationID = -1;
}
isValid = true;
}
Anm2::Anm2(const std::filesystem::path& path, Flags flags)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to initialize anm2: {} ({})", path.string(), document.ErrorStr()));
isValid = false;
return;
}
WorkingDirectory workingDirectory(path, WorkingDirectory::FILE);
this->path = path.string();
init(document, flags);
logger.info(std::format("Initialized anm2: {}", path.string()));
}
Anm2::Anm2(const physfs::Path& path, Flags flags)
{
XMLDocument document;
if (xml::document_load(path, document) != XML_SUCCESS)
{
isValid = false;
return;
}
this->path = path;
init(document, flags, path.directory_get());
logger.info(std::format("Initialized anm2: {}", path.c_str()));
}
bool Anm2::is_valid() const { return isValid; }
}

177
src/resource/xml/anm2.hpp Normal file
View File

@@ -0,0 +1,177 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "../audio.hpp"
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Anm2
{
public:
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
enum Flag
{
NO_SPRITESHEETS = (1 << 0),
NO_SOUNDS = (1 << 1),
DEFAULT_ANIMATION_ONLY = (1 << 2)
};
using Flags = int;
struct Spritesheet
{
struct Region
{
enum Origin
{
TOP_LEFT,
CENTER,
CUSTOM
};
std::string name{};
glm::vec2 crop{};
glm::vec2 pivot{};
glm::vec2 size{};
Origin origin{CUSTOM};
};
std::string path{};
resource::Texture texture{};
std::map<int, Region> regions{};
std::vector<int> regionOrder{};
};
struct Sound
{
std::string path{};
resource::Audio audio{};
};
struct Layer
{
std::string name{"New Layer"};
int spritesheetID{-1};
};
struct Null
{
std::string name{"New Null"};
bool isShowRect{};
};
struct Event
{
std::string name{"New Event"};
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int regionID{-1};
std::vector<int> soundIDs{};
int atFrame{-1};
bool isVisible{true};
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
struct Item
{
std::vector<Frame> frames{};
bool isVisible{};
};
struct Animation
{
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
};
int fps{30};
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
std::unordered_map<std::string, int> layerMap{};
std::unordered_map<std::string, int> nullMap{};
std::unordered_map<std::string, int> eventMap{};
std::string defaultAnimation{};
int defaultAnimationID{-1};
std::vector<Animation> animations{};
std::unordered_map<std::string, int> animationMap{};
std::unordered_map<int, std::string> animationMapReverse{};
std::string path{};
bool isValid{};
Flags flags{};
Anm2() = default;
Anm2(const Anm2&);
Anm2(Anm2&&);
Anm2& operator=(const Anm2&);
Anm2& operator=(Anm2&&);
Anm2(const std::filesystem::path&, Flags = 0);
Anm2(const util::physfs::Path&, Flags = 0);
bool is_valid() const;
private:
void init(tinyxml2::XMLDocument& document, Flags flags, const util::physfs::Path& archive = {});
};
}

45
src/resource/xml/area.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "area.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Area::Area(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
for (auto child = root->FirstChildElement("Area"); child; child = child->NextSiblingElement("Area"))
{
Entry area{};
query_texture(child, "Texture", archive, textureRootPath, area.texture);
child->QueryFloatAttribute("Gravity", &area.gravity);
child->QueryFloatAttribute("Friction", &area.friction);
areas.emplace_back(std::move(area));
}
}
if (areas.empty()) areas.emplace_back(Entry());
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Area::is_valid() const { return isValid; };
}

29
src/resource/xml/area.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Area
{
public:
struct Entry
{
Texture texture{};
float gravity{0.95f};
float friction{0.80f};
float airResistance{0.975f};
};
std::vector<Entry> areas{};
bool isValid{};
Area() = default;
Area(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,233 @@
#include "character.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Character::Character(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
query_string_attribute(root, "Name", &name);
root->QueryFloatAttribute("Weight", &weight);
root->QueryFloatAttribute("WeightMin", &weightMin);
root->QueryFloatAttribute("WeightMax", &weightMax);
root->QueryFloatAttribute("Capacity", &capacity);
root->QueryFloatAttribute("CapacityMin", &capacityMin);
root->QueryFloatAttribute("CapacityMax", &capacityMax);
root->QueryFloatAttribute("CapacityMaxMultiplier", &capacityMaxMultiplier);
root->QueryFloatAttribute("CapacityIfOverStuffedOnDigestBonus", &capacityIfOverStuffedOnDigestBonus);
root->QueryFloatAttribute("CaloriesToKilogram", &caloriesToKilogram);
root->QueryFloatAttribute("DigestionRate", &digestionRate);
root->QueryFloatAttribute("DigestionRateMin", &digestionRateMin);
root->QueryFloatAttribute("DigestionRateMax", &digestionRateMax);
root->QueryIntAttribute("DigestionTimerMax", &digestionTimerMax);
root->QueryFloatAttribute("EatSpeed", &eatSpeed);
root->QueryFloatAttribute("EatSpeedMin", &eatSpeedMin);
root->QueryFloatAttribute("EatSpeedMax", &eatSpeedMax);
root->QueryFloatAttribute("BlinkChance", &blinkChance);
root->QueryFloatAttribute("GurgleChance", &gurgleChance);
root->QueryFloatAttribute("GurgleCapacityMultiplier", &gurgleCapacityMultiplier);
auto dialoguePath = physfs::Path(archive + "/" + "dialogue.xml");
if (!dialoguePath.is_valid())
logger.warning(std::format("No character dialogue.xml file found: {}", path.string()));
else
dialogue = Dialogue(dialoguePath);
dialogue.query_pool_id(root, "DialoguePoolID", pool.id);
if (auto element = root->FirstChildElement("AlternateSpritesheet"))
{
query_texture(element, "Texture", archive, textureRootPath, alternateSpritesheet.texture);
query_sound(element, "Sound", archive, soundRootPath, alternateSpritesheet.sound);
element->QueryIntAttribute("ID", &alternateSpritesheet.id);
element->QueryFloatAttribute("ChanceOnNewGame", &alternateSpritesheet.chanceOnNewGame);
}
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "FinishFood", animations.finishFood);
query_animation_entry_collection(element, "PostDigest", animations.postDigest);
if (auto child = element->FirstChildElement("Idle"))
query_string_attribute(child, "Animation", &animations.idle);
if (auto child = element->FirstChildElement("IdleFull"))
query_string_attribute(child, "Animation", &animations.idleFull);
if (auto child = element->FirstChildElement("StageUp"))
query_string_attribute(child, "Animation", &animations.stageUp);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Digest", archive, soundRootPath, sounds.digest);
query_sound_entry_collection(element, "Gurgle", archive, soundRootPath, sounds.gurgle);
}
if (auto element = root->FirstChildElement("Overrides"))
{
if (auto child = element->FirstChildElement("Talk"))
{
query_layer_id(child, "LayerSource", anm2, talkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, talkOverride.layerDestination);
}
if (auto child = element->FirstChildElement("Blink"))
{
query_layer_id(child, "LayerSource", anm2, blinkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, blinkOverride.layerDestination);
}
}
if (auto element = root->FirstChildElement("Stages"))
{
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
{
Stage stage{};
child->QueryFloatAttribute("Threshold", &stage.threshold);
child->QueryIntAttribute("AreaID", &stage.areaID);
dialogue.query_pool_id(child, "DialoguePoolID", stage.pool.id);
stages.emplace_back(std::move(stage));
}
}
if (auto element = root->FirstChildElement("EatAreas"))
{
for (auto child = element->FirstChildElement("EatArea"); child; child = child->NextSiblingElement("EatArea"))
{
EatArea eatArea{};
query_null_id(child, "Null", anm2, eatArea.nullID);
query_event_id(child, "Event", anm2, eatArea.eventID);
query_string_attribute(child, "Animation", &eatArea.animation);
eatAreas.emplace_back(std::move(eatArea));
}
}
if (auto element = root->FirstChildElement("ExpandAreas"))
{
for (auto child = element->FirstChildElement("ExpandArea"); child;
child = child->NextSiblingElement("ExpandArea"))
{
ExpandArea expandArea{};
query_layer_id(child, "Layer", anm2, expandArea.layerID);
query_null_id(child, "Null", anm2, expandArea.nullID);
child->QueryFloatAttribute("ScaleAdd", &expandArea.scaleAdd);
expandAreas.emplace_back(std::move(expandArea));
}
}
if (auto element = root->FirstChildElement("InteractAreas"))
{
for (auto child = element->FirstChildElement("InteractArea"); child;
child = child->NextSiblingElement("InteractArea"))
{
InteractArea interactArea{};
if (child->FindAttribute("Layer")) query_layer_id(child, "Layer", anm2, interactArea.layerID);
query_null_id(child, "Null", anm2, interactArea.nullID);
query_string_attribute(child, "Animation", &interactArea.animation);
query_string_attribute(child, "AnimationFull", &interactArea.animationFull);
query_string_attribute(child, "AnimationCursorHover", &interactArea.animationCursorHover);
query_string_attribute(child, "AnimationCursorActive", &interactArea.animationCursorActive);
query_sound_entry_collection(child, "Sound", archive, soundRootPath, interactArea.sound, "Path");
dialogue.query_pool_id(child, "DialoguePoolID", interactArea.pool.id);
child->QueryFloatAttribute("DigestionBonusRub", &interactArea.digestionBonusRub);
child->QueryFloatAttribute("DigestionBonusClick", &interactArea.digestionBonusClick);
child->QueryFloatAttribute("Time", &interactArea.time);
child->QueryFloatAttribute("ScaleEffectAmplitude", &interactArea.scaleEffectAmplitude);
child->QueryFloatAttribute("ScaleEffectCycles", &interactArea.scaleEffectCycles);
std::string typeString{};
query_string_attribute(child, "Type", &typeString);
for (int i = 0; i < (int)std::size(INTERACT_TYPE_STRINGS); i++)
if (typeString == INTERACT_TYPE_STRINGS[i]) interactArea.type = (InteractType)i;
interactAreas.emplace_back(std::move(interactArea));
}
}
}
auto itemSchemaPath = physfs::Path(archive + "/" + "items.xml");
if (auto itemSchemaPath = physfs::Path(archive + "/" + "items.xml"); itemSchemaPath.is_valid())
itemSchema = Item(itemSchemaPath);
else
logger.warning(std::format("No character items.xml file found: {}", path.string()));
if (auto areaSchemaPath = physfs::Path(archive + "/" + "areas.xml"); areaSchemaPath.is_valid())
areaSchema = Area(areaSchemaPath);
else
logger.warning(std::format("No character areas.xml file found: {}", path.string()));
if (auto menuSchemaPath = physfs::Path(archive + "/" + "menu.xml"); menuSchemaPath.is_valid())
menuSchema = Menu(menuSchemaPath);
else
logger.warning(std::format("No character menu.xml file found: {}", path.string()));
if (auto cursorSchemaPath = physfs::Path(archive + "/" + "cursor.xml"); cursorSchemaPath.is_valid())
cursorSchema = Cursor(cursorSchemaPath);
else
logger.warning(std::format("No character cursor.xml file found: {}", path.string()));
if (auto playSchemaPath = physfs::Path(archive + "/" + "play.xml"); playSchemaPath.is_valid())
playSchema = Play(playSchemaPath, dialogue);
else
logger.warning(std::format("No character play.xml file found: {}", path.string()));
logger.info(std::format("Initialized character: {}", name));
this->path = path;
save = Save(save_path_get());
}
std::filesystem::path Character::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
}

View File

@@ -0,0 +1,144 @@
#pragma once
#include <filesystem>
#include <vector>
#include "../../util/interact_type.hpp"
#include "../audio.hpp"
#include "animation_entry.hpp"
#include "anm2.hpp"
#include "area.hpp"
#include "cursor.hpp"
#include "dialogue.hpp"
#include "item.hpp"
#include "menu.hpp"
#include "play.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class Character
{
public:
struct Stage
{
float threshold{};
int areaID{};
Dialogue::PoolReference pool{-1};
};
struct EatArea
{
int nullID{-1};
int eventID{-1};
std::string animation{};
};
struct ExpandArea
{
int layerID{-1};
int nullID{-1};
float scaleAdd{};
};
struct InteractArea
{
std::string animation{};
std::string animationFull{};
std::string animationCursorActive{};
std::string animationCursorHover{};
SoundEntryCollection sound{};
int nullID{-1};
int layerID{-1};
InteractType type{(InteractType)-1};
Dialogue::PoolReference pool{-1};
float digestionBonusRub{};
float digestionBonusClick{};
float time{};
float scaleEffectAmplitude{};
float scaleEffectCycles{};
};
struct Animations
{
AnimationEntryCollection finishFood{};
AnimationEntryCollection postDigest{};
std::string idle{};
std::string idleFull{};
std::string stageUp{};
};
struct Sounds
{
SoundEntryCollection gurgle{};
SoundEntryCollection digest{};
};
struct Override
{
int layerSource{};
int layerDestination{};
};
struct AlternateSpritesheet
{
Texture texture{};
Audio sound{};
int id{-1};
float chanceOnNewGame{0.001};
};
Anm2 anm2{};
Area areaSchema{};
Dialogue dialogue{};
Item itemSchema{};
Menu menuSchema{};
Cursor cursorSchema{};
Play playSchema{};
Save save{};
Animations animations{};
Override talkOverride{};
Override blinkOverride{};
Sounds sounds{};
std::vector<Stage> stages{};
std::vector<ExpandArea> expandAreas{};
std::vector<EatArea> eatAreas{};
std::vector<InteractArea> interactAreas{};
AlternateSpritesheet alternateSpritesheet{};
std::string name{};
std::filesystem::path path{};
float weight{50};
float weightMin{};
float weightMax{999};
float capacity{2000.0f};
float capacityMin{2000.0f};
float capacityMax{99999.0f};
float capacityMaxMultiplier{1.5f};
float capacityIfOverStuffedOnDigestBonus{0.25f};
float caloriesToKilogram{1000.0f};
float digestionRate{0.05f};
float digestionRateMin{0.0f};
float digestionRateMax{0.25f};
int digestionTimerMax{60};
float eatSpeed{1.0f};
float eatSpeedMin{1.0f};
float eatSpeedMax{3.0f};
float blinkChance{1.0f};
float gurgleChance{1.0f};
float gurgleCapacityMultiplier{1.0f};
Dialogue::PoolReference pool{-1};
Character() = default;
Character(const std::filesystem::path&);
std::filesystem::path save_path_get();
};
}

View File

@@ -0,0 +1,66 @@
#include "character_preview.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
CharacterPreview::CharacterPreview(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character preview from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2, Anm2::NO_SOUNDS | Anm2::DEFAULT_ANIMATION_ONLY);
query_texture(root, "Render", archive, textureRootPath, render);
query_texture(root, "Portrait", archive, textureRootPath, portrait);
query_string_attribute(root, "Name", &name);
query_string_attribute(root, "Description", &description);
query_string_attribute(root, "Author", &author);
root->QueryFloatAttribute("Weight", &weight);
if (auto element = root->FirstChildElement("Stages"))
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
stages++;
}
this->path = path;
save = Save(save_path_get());
isValid = true;
logger.info(std::format("Initialized character preview: {}", name));
}
std::filesystem::path CharacterPreview::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
bool CharacterPreview::is_valid() const { return isValid; }
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
#include "anm2.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class CharacterPreview
{
public:
struct Stage
{
float threshold{};
int dialoguePoolID{-1};
};
Anm2 anm2{};
Texture portrait{};
Texture render{};
Save save{};
int stages{1};
std::string name{};
std::string author{};
std::string description{};
std::filesystem::path path{};
float weight{50};
bool isValid{};
CharacterPreview() = default;
CharacterPreview(const std::filesystem::path&);
std::filesystem::path save_path_get();
bool is_valid() const;
};
}

View File

@@ -0,0 +1,52 @@
#include "cursor.hpp"
#include "../../log.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Cursor::Cursor(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "Idle", animations.idle);
query_animation_entry_collection(element, "Hover", animations.hover);
query_animation_entry_collection(element, "Grab", animations.grab);
query_animation_entry_collection(element, "Pan", animations.pan);
query_animation_entry_collection(element, "Zoom", animations.zoom);
query_animation_entry_collection(element, "Return", animations.return_);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Grab", archive, soundRootPath, sounds.grab);
query_sound_entry_collection(element, "Release", archive, soundRootPath, sounds.release);
query_sound_entry_collection(element, "Throw", archive, soundRootPath, sounds.throw_);
}
}
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Cursor::is_valid() const { return isValid; };
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "util.hpp"
namespace game::resource::xml
{
class Cursor
{
public:
struct Animations
{
AnimationEntryCollection idle{};
AnimationEntryCollection hover{};
AnimationEntryCollection grab{};
AnimationEntryCollection pan{};
AnimationEntryCollection zoom{};
AnimationEntryCollection return_{};
};
struct Sounds
{
SoundEntryCollection grab{};
SoundEntryCollection release{};
SoundEntryCollection throw_{};
};
Animations animations{};
Sounds sounds{};
Anm2 anm2{};
bool isValid{};
Cursor() = default;
Cursor(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,165 @@
#include "dialogue.hpp"
#include "util.hpp"
#include "../../log.hpp"
#include "../../util/math.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
void Dialogue::query_entry_id(XMLElement* element, const char* name, int& id)
{
std::string entryID{};
query_string_attribute(element, name, &entryID);
if (entryIDMap.contains(entryID))
id = entryIDMap.at(entryID);
else if (entryID.empty())
entryID = -1;
else
{
logger.warning("Dialogue entries does not contain: " + entryID);
id = -1;
}
}
void Dialogue::query_pool_id(XMLElement* element, const char* name, int& id)
{
std::string poolID{};
query_string_attribute(element, name, &poolID);
if (poolMap.contains(poolID))
id = poolMap.at(poolID);
else if (poolID.empty())
poolID = -1;
else
{
logger.warning("Dialogue pools does not contain: " + poolID);
id = -1;
}
}
Dialogue::Dialogue(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
if (auto element = root->FirstChildElement("Entries"))
{
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
entryIDMap.emplace(stringID, id);
entryIDMapReverse.emplace(id, stringID);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
Entry entry{};
entry.name = entryIDMapReverse.at(id);
query_string_attribute(child, "Text", &entry.text);
query_string_attribute(child, "Animation", &entry.animation);
if (child->FindAttribute("Next"))
{
std::string nextID{};
query_string_attribute(child, "Next", &nextID);
if (!entryIDMap.contains(nextID))
logger.warning(std::format("Dialogue: next ID does not point to a valid Entry! ({})", nextID));
else
entry.nextID = entryIDMap.at(nextID);
}
for (auto choiceChild = child->FirstChildElement("Choice"); choiceChild;
choiceChild = choiceChild->NextSiblingElement("Choice"))
{
Choice choice{};
query_entry_id(choiceChild, "Next", choice.nextID);
query_string_attribute(choiceChild, "Text", &choice.text);
entry.choices.emplace_back(std::move(choice));
}
entries.emplace_back(std::move(entry));
id++;
}
}
if (auto element = root->FirstChildElement("Pools"))
{
int id{};
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
Pool pool{};
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
poolMap[stringID] = id;
pools.emplace_back(pool);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
auto& pool = pools.at(id);
for (auto entryChild = child->FirstChildElement("PoolEntry"); entryChild;
entryChild = entryChild->NextSiblingElement("PoolEntry"))
{
int entryID{};
query_entry_id(entryChild, "ID", entryID);
pool.emplace_back(entryID);
}
id++;
}
logger.info(std::format("Initialized dialogue: {}", path.c_str()));
isValid = true;
}
if (auto element = root->FirstChildElement("Start"))
{
query_entry_id(element, "ID", start.id);
query_string_attribute(element, "Animation", &start.animation);
}
if (auto element = root->FirstChildElement("End")) query_entry_id(element, "ID", end.id);
if (auto element = root->FirstChildElement("Help")) query_entry_id(element, "ID", help.id);
if (auto element = root->FirstChildElement("Digest")) query_pool_id(element, "PoolID", digest.id);
if (auto element = root->FirstChildElement("Eat")) query_pool_id(element, "PoolID", eat.id);
if (auto element = root->FirstChildElement("EatFull")) query_pool_id(element, "PoolID", eatFull.id);
if (auto element = root->FirstChildElement("Feed")) query_pool_id(element, "PoolID", feed.id);
if (auto element = root->FirstChildElement("FeedFull")) query_pool_id(element, "PoolID", feedFull.id);
if (auto element = root->FirstChildElement("FoodTaken")) query_pool_id(element, "PoolID", foodTaken.id);
if (auto element = root->FirstChildElement("FoodTakenFull")) query_pool_id(element, "PoolID", foodTakenFull.id);
if (auto element = root->FirstChildElement("Full")) query_pool_id(element, "PoolID", full.id);
if (auto element = root->FirstChildElement("LowCapacity")) query_pool_id(element, "PoolID", lowCapacity.id);
if (auto element = root->FirstChildElement("Random")) query_pool_id(element, "PoolID", random.id);
if (auto element = root->FirstChildElement("Throw")) query_pool_id(element, "PoolID", throw_.id);
if (auto element = root->FirstChildElement("StageUp")) query_pool_id(element, "PoolID", stageUp.id);
}
}
int Dialogue::Pool::get() const { return this->at(math::random_max(this->size())); }
Dialogue::Entry* Dialogue::get(int id) { return &entries.at(id); }
Dialogue::Entry* Dialogue::get(Dialogue::EntryReference& entry) { return &entries.at(entry.id); }
Dialogue::Entry* Dialogue::get(const std::string& string) { return &entries.at(entryIDMap.at(string)); }
Dialogue::Entry* Dialogue::get(Dialogue::PoolReference& pool) { return &entries.at(pools.at(pool.id).get()); }
Dialogue::Entry* Dialogue::get(Dialogue::Pool& pool) { return &entries.at(pool.get()); }
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <tinyxml2.h>
#include <string>
#include <map>
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Dialogue
{
public:
struct Choice
{
std::string text{};
int nextID{-1};
};
struct Entry
{
std::string name{};
std::string animation{};
std::string text{};
std::vector<Choice> choices{};
int nextID{-1};
inline bool is_last() const { return choices.empty() && nextID == -1; };
};
struct EntryReference
{
int id{-1};
std::string animation{};
inline bool is_valid() const { return id != -1; };
};
class PoolReference
{
public:
int id{-1};
inline bool is_valid() const { return id != -1; };
};
class Pool : public std::vector<int>
{
public:
int get() const;
};
std::map<std::string, int> entryIDMap;
std::map<int, std::string> entryIDMapReverse;
std::vector<Entry> entries{};
std::vector<Pool> pools{};
std::map<std::string, int> poolMap{};
EntryReference start{-1};
EntryReference end{-1};
EntryReference help{-1};
PoolReference digest{-1};
PoolReference eatFull{-1};
PoolReference eat{-1};
PoolReference feedFull{-1};
PoolReference feed{-1};
PoolReference foodTakenFull{-1};
PoolReference foodTaken{-1};
PoolReference full{-1};
PoolReference random{-1};
PoolReference lowCapacity{-1};
PoolReference throw_{-1};
PoolReference stageUp{-1};
bool isValid{};
Dialogue() = default;
Dialogue(const util::physfs::Path&);
Entry* get(const std::string&);
Entry* get(int id);
Entry* get(Dialogue::EntryReference&);
Entry* get(Dialogue::Pool&);
Entry* get(Dialogue::PoolReference&);
void query_entry_id(tinyxml2::XMLElement* element, const char* name, int& id);
void query_pool_id(tinyxml2::XMLElement* element, const char* name, int& id);
inline bool is_valid() const { return isValid; };
};
}

156
src/resource/xml/item.cpp Normal file
View File

@@ -0,0 +1,156 @@
#include "item.hpp"
#include <ranges>
#include <tinyxml2.h>
#include <algorithm>
#include <utility>
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Item::Item(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "BaseAnm2", archive, textureRootPath, baseAnm2, Anm2::NO_SPRITESHEETS);
if (auto element = root->FirstChildElement("Categories"))
{
for (auto child = element->FirstChildElement("Category"); child; child = child->NextSiblingElement("Category"))
{
Category category{};
query_string_attribute(child, "Name", &category.name);
query_bool_attribute(child, "IsEdible", &category.isEdible);
categoryMap[category.name] = (int)categories.size();
categories.push_back(category);
}
}
if (auto element = root->FirstChildElement("Rarities"))
{
for (auto child = element->FirstChildElement("Rarity"); child; child = child->NextSiblingElement("Rarity"))
{
Rarity rarity{};
query_string_attribute(child, "Name", &rarity.name);
child->QueryFloatAttribute("Chance", &rarity.chance);
query_bool_attribute(child, "IsHidden", &rarity.isHidden);
query_sound(child, "Sound", archive, soundRootPath, rarity.sound);
rarityMap[rarity.name] = (int)rarities.size();
rarities.emplace_back(std::move(rarity));
}
}
if (auto element = root->FirstChildElement("Flavors"))
{
for (auto child = element->FirstChildElement("Flavor"); child; child = child->NextSiblingElement("Flavor"))
{
Flavor flavor{};
query_string_attribute(child, "Name", &flavor.name);
flavorMap[flavor.name] = (int)flavors.size();
flavors.push_back(flavor);
}
}
if (auto element = root->FirstChildElement("Animations"))
{
if (auto child = element->FirstChildElement("Chew"))
query_string_attribute(child, "Animation", &animations.chew);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Bounce", archive, soundRootPath, sounds.bounce);
query_sound_entry_collection(element, "Dispose", archive, soundRootPath, sounds.dispose);
query_sound_entry_collection(element, "Return", archive, soundRootPath, sounds.return_);
query_sound_entry_collection(element, "Summon", archive, soundRootPath, sounds.summon);
}
if (auto element = root->FirstChildElement("Items"))
{
std::string itemTextureRootPath{};
query_string_attribute(element, "TextureRootPath", &itemTextureRootPath);
element->QueryIntAttribute("ChewCount", &chewCount);
element->QueryIntAttribute("QuantityMax", &quantityMax);
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Entry item{};
query_string_attribute(child, "Name", &item.name);
query_string_attribute(child, "Description", &item.description);
query_float_optional_attribute(child, "Calories", item.calories);
query_float_optional_attribute(child, "DigestionBonus", item.digestionBonus);
query_float_optional_attribute(child, "EatSpeedBonus", item.eatSpeedBonus);
query_float_optional_attribute(child, "Gravity", item.gravity);
query_int_optional_attribute(child, "ChewCount", item.chewCount);
query_bool_attribute(child, "IsPlayReward", &item.isPlayReward);
query_bool_attribute(child, "IsToggleSpritesheet", &item.isToggleSpritesheet);
std::string categoryString{};
query_string_attribute(child, "Category", &categoryString);
item.categoryID = categoryMap.contains(categoryString) ? categoryMap[categoryString] : -1;
std::string rarityString{};
query_string_attribute(child, "Rarity", &rarityString);
item.rarityID = rarityMap.contains(rarityString) ? rarityMap[rarityString] : -1;
std::string flavorString{};
query_string_attribute(child, "Flavor", &flavorString);
if (flavorMap.contains(flavorString)) item.flavorID = flavorMap[flavorString];
Texture texture{};
query_texture(child, "Texture", archive, itemTextureRootPath, texture);
Anm2 anm2{baseAnm2};
if (child->FindAttribute("Anm2")) query_anm2(child, "Anm2", archive, textureRootPath, anm2);
anm2.spritesheets.at(0).texture = std::move(texture);
anm2s.emplace_back(std::move(anm2));
items.emplace_back(std::move(item));
}
}
}
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
pools[item.rarityID].emplace_back(i);
if (item.isPlayReward) rewardItemPool.emplace_back(i);
}
for (int i = 0; i < (int)rarities.size(); i++)
{
rarityIDsSortedByChance.emplace_back(i);
}
std::stable_sort(rarityIDsSortedByChance.begin(), rarityIDsSortedByChance.end(),
[&](int a, int b) { return rarities[a].chance > rarities[b].chance; });
isValid = true;
logger.info(std::format("Initialized item schema: {}", path.c_str()));
}
bool Item::is_valid() const { return isValid; };
}

98
src/resource/xml/item.hpp Normal file
View File

@@ -0,0 +1,98 @@
#pragma once
#include <filesystem>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "../audio.hpp"
#include "anm2.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Item
{
public:
static constexpr auto UNDEFINED = "???";
struct Category
{
std::string name{};
bool isEdible{};
};
struct Rarity
{
std::string name{UNDEFINED};
float chance{};
bool isHidden{};
Audio sound{};
};
struct Flavor
{
std::string name{UNDEFINED};
};
struct Entry
{
std::string name{UNDEFINED};
std::string description{UNDEFINED};
int categoryID{};
int rarityID{};
std::optional<int> flavorID;
std::optional<float> calories{};
std::optional<float> eatSpeedBonus{};
std::optional<float> digestionBonus{};
std::optional<float> gravity{};
std::optional<int> chewCount{};
bool isPlayReward{};
bool isToggleSpritesheet{};
};
struct Animations
{
std::string chew{};
};
struct Sounds
{
SoundEntryCollection bounce{};
SoundEntryCollection return_{};
SoundEntryCollection dispose{};
SoundEntryCollection summon{};
};
std::unordered_map<std::string, int> categoryMap{};
std::unordered_map<std::string, int> rarityMap{};
std::unordered_map<std::string, int> flavorMap{};
using Pool = std::vector<int>;
std::vector<Category> categories{};
std::vector<Rarity> rarities{};
std::vector<Flavor> flavors{};
std::vector<Entry> items{};
std::vector<Anm2> anm2s{};
std::vector<int> rarityIDsSortedByChance{};
std::unordered_map<int, Pool> pools{};
Pool rewardItemPool{};
Animations animations{};
Sounds sounds{};
Anm2 baseAnm2{};
int chewCount{2};
int quantityMax{99};
bool isValid{};
Item() = default;
Item(const std::filesystem::path&);
Item(const util::physfs::Path&);
bool is_valid() const;
};
}

46
src/resource/xml/menu.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include "menu.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Menu::Menu(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
std::string fontRootPath{};
query_string_attribute(root, "FontRootPath", &fontRootPath);
query_font(root, "Font", archive, fontRootPath, font);
root->QueryFloatAttribute("Rounding", &rounding);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Open", archive, soundRootPath, sounds.open);
query_sound_entry_collection(element, "Close", archive, soundRootPath, sounds.close);
query_sound_entry_collection(element, "Hover", archive, soundRootPath, sounds.hover);
query_sound_entry_collection(element, "Select", archive, soundRootPath, sounds.select);
}
}
isValid = true;
logger.info(std::format("Initialized menu schema: {}", path.c_str()));
}
bool Menu::is_valid() const { return isValid; };
}

31
src/resource/xml/menu.hpp Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Menu
{
public:
struct Sounds
{
SoundEntryCollection open{};
SoundEntryCollection close{};
SoundEntryCollection hover{};
SoundEntryCollection select{};
};
Sounds sounds{};
Font font{};
float rounding{};
bool isValid{};
Menu() = default;
Menu(const util::physfs::Path&);
bool is_valid() const;
};
}

67
src/resource/xml/play.cpp Normal file
View File

@@ -0,0 +1,67 @@
#include "play.hpp"
#include "../../log.hpp"
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Play::Play(const physfs::Path& path, Dialogue& dialogue)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
root->QueryIntAttribute("RewardScore", &rewardScore);
root->QueryFloatAttribute("RewardScoreBonus", &rewardScoreBonus);
root->QueryFloatAttribute("RewardGradeBonus", &rewardGradeBonus);
root->QueryFloatAttribute("RangeBase", &rangeBase);
root->QueryFloatAttribute("RangeMin", &rangeMin);
root->QueryFloatAttribute("RangeScoreBonus", &rangeScoreBonus);
root->QueryFloatAttribute("SpeedMin", &speedMin);
root->QueryFloatAttribute("SpeedMax", &speedMax);
root->QueryFloatAttribute("SpeedScoreBonus", &speedScoreBonus);
root->QueryIntAttribute("EndTimerMax", &endTimerMax);
root->QueryIntAttribute("EndTimerFailureMax", &endTimerFailureMax);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Fall", archive, soundRootPath, sounds.fall);
query_sound_entry_collection(element, "ScoreLoss", archive, soundRootPath, sounds.scoreLoss);
query_sound_entry_collection(element, "HighScore", archive, soundRootPath, sounds.highScore);
query_sound_entry_collection(element, "HighScoreLoss", archive, soundRootPath, sounds.highScoreLoss);
query_sound_entry_collection(element, "RewardScore", archive, soundRootPath, sounds.rewardScore);
}
if (auto element = root->FirstChildElement("Grades"))
{
for (auto child = element->FirstChildElement("Grade"); child; child = child->NextSiblingElement("Grade"))
{
Grade grade{};
query_string_attribute(child, "Name", &grade.name);
query_string_attribute(child, "NamePlural", &grade.namePlural);
child->QueryIntAttribute("Value", &grade.value);
child->QueryFloatAttribute("Weight", &grade.weight);
query_bool_attribute(child, "IsFailure", &grade.isFailure);
query_sound(child, "Sound", archive, soundRootPath, grade.sound);
dialogue.query_pool_id(child, "DialoguePoolID", grade.pool.id);
grades.emplace_back(std::move(grade));
}
}
}
isValid = true;
logger.info(std::format("Initialized play schema: {}", path.c_str()));
}
bool Play::is_valid() const { return isValid; };
}

56
src/resource/xml/play.hpp Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <string>
#include "util.hpp"
#include "dialogue.hpp"
namespace game::resource::xml
{
class Play
{
public:
struct Grade
{
std::string name{};
std::string namePlural{};
int value{};
float weight{};
bool isFailure{};
Audio sound{};
Dialogue::PoolReference pool{};
};
struct Sounds
{
SoundEntryCollection fall{};
SoundEntryCollection highScore{};
SoundEntryCollection highScoreLoss{};
SoundEntryCollection rewardScore{};
SoundEntryCollection scoreLoss{};
};
Sounds sounds{};
std::vector<Grade> grades{};
float rewardScoreBonus{0.01};
float rewardGradeBonus{0.05};
float speedMin{0.005};
float speedMax{0.075};
float speedScoreBonus{0.000025f};
float rangeBase{0.75};
float rangeMin{0.10};
float rangeScoreBonus{0.0005};
int endTimerMax{20};
int endTimerFailureMax{60};
int rewardScore{999};
bool isValid{};
Play() = default;
Play(const util::physfs::Path&, Dialogue&);
bool is_valid() const;
};
}

183
src/resource/xml/save.cpp Normal file
View File

@@ -0,0 +1,183 @@
#include "save.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace game::util;
using namespace tinyxml2;
namespace game::resource::xml
{
Save::Save(const std::filesystem::path& path)
{
XMLDocument document;
// Fail silently if there's no save.
auto result = document.LoadFile(path.c_str());
if (result == XML_ERROR_FILE_NOT_FOUND || result == XML_ERROR_FILE_COULD_NOT_BE_OPENED) return;
if (result != XML_SUCCESS)
{
logger.error(
std::format("Could not initialize character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
query_bool_attribute(root, "IsPostgame", &isPostgame);
query_bool_attribute(root, "IsAlternateSpritesheet", &isAlternateSpritesheet);
if (auto element = root->FirstChildElement("Character"))
{
element->QueryFloatAttribute("Weight", &weight);
element->QueryFloatAttribute("Calories", &calories);
element->QueryFloatAttribute("Capacity", &capacity);
element->QueryFloatAttribute("DigestionRate", &digestionRate);
element->QueryFloatAttribute("EatSpeed", &eatSpeed);
query_bool_attribute(element, "IsDigesting", &isDigesting);
element->QueryFloatAttribute("DigestionProgress", &digestionProgress);
element->QueryIntAttribute("DigestionTimer", &digestionTimer);
element->QueryFloatAttribute("TotalCaloriesConsumed", &totalCaloriesConsumed);
element->QueryIntAttribute("TotalFoodItemsEaten", &totalFoodItemsEaten);
}
if (auto element = root->FirstChildElement("Play"))
{
element->QueryIntAttribute("TotalPlays", &totalPlays);
element->QueryIntAttribute("HighScore", &highScore);
element->QueryIntAttribute("BestCombo", &bestCombo);
if (auto child = element->FirstChildElement("Grades"))
{
for (auto gradeChild = child->FirstChildElement("Grade"); gradeChild;
gradeChild = gradeChild->NextSiblingElement("Grade"))
{
int id{};
gradeChild->QueryIntAttribute("ID", &id);
gradeChild->QueryIntAttribute("Count", &gradeCounts[id]);
}
}
}
if (auto element = root->FirstChildElement("Inventory"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
int id{};
int quantity{};
child->QueryIntAttribute("ID", &id);
child->QueryIntAttribute("Quantity", &quantity);
inventory[id] = quantity;
}
}
if (auto element = root->FirstChildElement("Items"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Item item{};
child->QueryIntAttribute("ID", &item.id);
child->QueryIntAttribute("ChewCount", &item.chewCount);
child->QueryFloatAttribute("PositionX", &item.position.x);
child->QueryFloatAttribute("PositionY", &item.position.y);
child->QueryFloatAttribute("VelocityX", &item.velocity.x);
child->QueryFloatAttribute("VelocityY", &item.velocity.y);
child->QueryFloatAttribute("Rotation", &item.rotation);
items.emplace_back(std::move(item));
}
}
}
logger.info(std::format("Initialized character save file: {}", path.string()));
isValid = true;
}
bool Save::is_valid() const { return isValid; }
void Save::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto element = document.NewElement("Save");
element->SetAttribute("IsPostgame", isPostgame ? "true" : "false");
element->SetAttribute("IsAlternateSpritesheet", isAlternateSpritesheet ? "true" : "false");
auto characterElement = element->InsertNewChildElement("Character");
characterElement->SetAttribute("Weight", weight);
characterElement->SetAttribute("Calories", calories);
characterElement->SetAttribute("Capacity", capacity);
characterElement->SetAttribute("DigestionRate", digestionRate);
characterElement->SetAttribute("EatSpeed", eatSpeed);
characterElement->SetAttribute("IsDigesting", isDigesting ? "true" : "false");
characterElement->SetAttribute("DigestionProgress", digestionProgress);
characterElement->SetAttribute("DigestionTimer", digestionTimer);
characterElement->SetAttribute("TotalCaloriesConsumed", totalCaloriesConsumed);
characterElement->SetAttribute("TotalFoodItemsEaten", totalFoodItemsEaten);
auto playElement = element->InsertNewChildElement("Play");
playElement->SetAttribute("TotalPlays", totalPlays);
playElement->SetAttribute("HighScore", highScore);
playElement->SetAttribute("BestCombo", bestCombo);
auto gradesElement = playElement->InsertNewChildElement("Grades");
for (auto& [i, count] : gradeCounts)
{
auto gradeElement = gradesElement->InsertNewChildElement("Grade");
gradeElement->SetAttribute("ID", i);
gradeElement->SetAttribute("Count", count);
}
auto inventoryElement = element->InsertNewChildElement("Inventory");
for (auto& [id, quantity] : inventory)
{
auto itemElement = inventoryElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", id);
itemElement->SetAttribute("Quantity", quantity);
}
auto itemsElement = element->InsertNewChildElement("Items");
for (auto& item : items)
{
auto itemElement = itemsElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", item.id);
itemElement->SetAttribute("ChewCount", item.chewCount);
itemElement->SetAttribute("PositionX", item.position.x);
itemElement->SetAttribute("PositionY", item.position.y);
itemElement->SetAttribute("VelocityX", item.velocity.x);
itemElement->SetAttribute("VelocityY", item.velocity.y);
itemElement->SetAttribute("Rotation", item.rotation);
}
document.InsertFirstChild(element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to save character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
logger.info(std::format("Saved character save file: {}", path.string()));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

54
src/resource/xml/save.hpp Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <filesystem>
#include <map>
#include <vector>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Save
{
public:
struct Item
{
int id{};
int chewCount{};
glm::vec2 position{};
glm::vec2 velocity{};
float rotation{};
};
float weight{};
float calories{};
float capacity{};
float eatSpeed{};
float digestionRate{};
float digestionProgress{};
int digestionTimer{};
bool isDigesting{};
bool isAlternateSpritesheet{};
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int totalPlays{};
int highScore{};
int bestCombo{};
std::map<int, int> gradeCounts{};
std::map<int, int> inventory;
std::vector<Item> items;
bool isPostgame{};
bool isValid{};
Save() = default;
Save(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,80 @@
#include "settings.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Settings::Settings(const std::filesystem::path& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
logger.error(
std::format("Could not initialize character save file: {} ({})", path.string(), document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
std::string measurementSystemString{};
query_string_attribute(root, "MeasurementSystem", &measurementSystemString);
measurementSystem = measurementSystemString == "Imperial" ? measurement::IMPERIAL : measurement::METRIC;
root->QueryIntAttribute("Volume", &volume);
root->QueryFloatAttribute("ColorR", &color.r);
root->QueryFloatAttribute("ColorG", &color.g);
root->QueryFloatAttribute("ColorB", &color.b);
root->QueryFloatAttribute("WindowX", &windowPosition.x);
root->QueryFloatAttribute("WindowY", &windowPosition.y);
root->QueryIntAttribute("WindowW", &windowSize.x);
root->QueryIntAttribute("WindowH", &windowSize.y);
}
logger.info(std::format("Initialized settings: {}", path.string()));
isValid = true;
}
bool Settings::is_valid() const { return isValid; }
void Settings::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto element = document.NewElement("Settings");
element->SetAttribute("MeasurementSystem", measurementSystem == measurement::IMPERIAL ? "Imperial" : "Metric");
element->SetAttribute("Volume", volume);
element->SetAttribute("ColorR", color.r);
element->SetAttribute("ColorG", color.g);
element->SetAttribute("ColorB", color.b);
element->SetAttribute("WindowX", windowPosition.x);
element->SetAttribute("WindowY", windowPosition.y);
element->SetAttribute("WindowW", windowSize.x);
element->SetAttribute("WindowH", windowSize.y);
document.InsertFirstChild(element);
if (document.SaveFile(path.c_str()) != XML_SUCCESS)
{
logger.info(std::format("Failed to initialize settings: {} ({})", path.string(), document.ErrorStr()));
return;
}
logger.info(std::format("Saved settings: {}", path.string()));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

View File

@@ -0,0 +1,36 @@
#pragma once
#include "../../util/measurement.hpp"
#include <filesystem>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Settings
{
public:
static constexpr auto VOLUME_MIN = 0;
static constexpr auto VOLUME_MAX = 100;
enum Mode
{
LOADER,
IMGUI
};
util::measurement::System measurementSystem{util::measurement::METRIC};
int volume{VOLUME_MAX};
glm::vec3 color{0.09, 0.2196, 0.37};
glm::ivec2 windowSize{1280, 720};
glm::vec2 windowPosition{};
bool isValid{};
Settings() = default;
Settings(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,16 @@
#include "sound_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
Audio& SoundEntryCollection::get()
{
return at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).sound;
}
void SoundEntryCollection::play()
{
at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).play();
}
}

View File

@@ -0,0 +1,24 @@
// Handles sound entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include "../audio.hpp"
namespace game::resource::xml
{
class SoundEntry
{
public:
Audio sound{};
float weight{1.0f};
inline void play() { sound.play(); };
};
class SoundEntryCollection : public std::vector<SoundEntry>
{
public:
Audio& get();
void play();
};
}

185
src/resource/xml/util.cpp Normal file
View File

@@ -0,0 +1,185 @@
#include "util.hpp"
#include <format>
#include "../../util/physfs.hpp"
#include "../../util/string.hpp"
#include "../../log.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* value)
{
const char* temp = nullptr;
auto result = element->QueryStringAttribute(attribute, &temp);
if (result == XML_SUCCESS && temp && value) *value = temp;
return result;
}
XMLError query_bool_attribute(XMLElement* element, const char* attribute, bool* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
temp = string::to_lower(temp);
if (value) *value = temp == "true" || temp == "1" ? true : false;
return result;
}
XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
if (value) *value = std::filesystem::path(temp);
return result;
}
XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
{
int temp{};
auto result = element->QueryIntAttribute(attribute, &temp);
if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
return result;
}
XMLError query_float_optional_attribute(XMLElement* element, const char* attribute, std::optional<float>& value)
{
value.emplace();
auto result = element->QueryFloatAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError query_int_optional_attribute(XMLElement* element, const char* attribute, std::optional<int>& value)
{
value.emplace();
auto result = element->QueryIntAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError document_load(const physfs::Path& path, XMLDocument& document)
{
if (!path.is_valid())
{
logger.error(std::format("Failed to open XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_NOT_FOUND;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(std::format("Failed to read XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_COULD_NOT_BE_OPENED;
}
auto result = document.Parse((const char*)buffer.data(), buffer.size());
if (result != XML_SUCCESS)
logger.error(std::format("Failed to parse XML document: {} ({})", path.c_str(), document.ErrorStr()));
return result;
}
void query_event_id(XMLElement* element, const char* name, const Anm2& anm2, int& eventID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.eventMap.contains(string))
eventID = anm2.eventMap.at(string);
else
{
logger.error(std::format("Could not query anm2 event ID: {} ({})", string, anm2.path));
eventID = -1;
}
}
void query_layer_id(XMLElement* element, const char* name, const Anm2& anm2, int& layerID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.layerMap.contains(string))
layerID = anm2.layerMap.at(string);
else
{
logger.error(std::format("Could not query anm2 layer ID: {} ({})", string, anm2.path));
layerID = -1;
}
}
void query_null_id(XMLElement* element, const char* name, const Anm2& anm2, int& nullID)
{
std::string string{};
query_string_attribute(element, name, &string);
if (anm2.nullMap.contains(string))
nullID = anm2.nullMap.at(string);
else
{
logger.error(std::format("Could not query anm2 null ID: {} ({})", string, anm2.path));
nullID = -1;
}
}
void query_anm2(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Anm2& anm2, Anm2::Flags flags)
{
std::string string{};
query_string_attribute(element, name, &string);
anm2 = Anm2(physfs::Path(archive + "/" + rootPath + "/" + string), flags);
}
void query_texture(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Texture& texture)
{
std::string string{};
query_string_attribute(element, name, &string);
texture = Texture(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_sound(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Audio& sound)
{
std::string string{};
query_string_attribute(element, name, &string);
sound = Audio(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_font(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Font& font)
{
std::string string{};
query_string_attribute(element, name, &string);
font = Font(physfs::Path(archive + "/" + rootPath + "/" + string));
}
void query_animation_entry(XMLElement* element, AnimationEntry& animationEntry)
{
query_string_attribute(element, "Animation", &animationEntry.animation);
element->QueryFloatAttribute("Weight", &animationEntry.weight);
}
void query_animation_entry_collection(XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection)
{
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
query_animation_entry(child, animationEntryCollection.emplace_back());
}
void query_sound_entry(XMLElement* element, const std::string& archive, const std::string& rootPath,
SoundEntry& soundEntry, const std::string& attributeName)
{
query_sound(element, attributeName.c_str(), archive, rootPath, soundEntry.sound);
element->QueryFloatAttribute("Weight", &soundEntry.weight);
}
void query_sound_entry_collection(XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, SoundEntryCollection& soundEntryCollection,
const std::string& attributeName)
{
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
query_sound_entry(child, archive, rootPath, soundEntryCollection.emplace_back(), attributeName);
}
}

51
src/resource/xml/util.hpp Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <tinyxml2.h>
#include "animation_entry.hpp"
#include "sound_entry.hpp"
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "anm2.hpp"
namespace game::resource::xml
{
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_bool_attribute(tinyxml2::XMLElement*, const char*, bool*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
tinyxml2::XMLError query_color_attribute(tinyxml2::XMLElement*, const char*, float*);
tinyxml2::XMLError query_float_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<float>& value);
tinyxml2::XMLError query_int_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<int>& value);
void query_event_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& eventID);
void query_layer_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& layerID);
void query_null_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& nullID);
void query_anm2(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Anm2& anm2, Anm2::Flags flags = {});
void query_texture(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Texture& texture);
void query_sound(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Audio& sound);
void query_font(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Font& font);
void query_animation_entry(tinyxml2::XMLElement* element, AnimationEntry& animationEntry);
void query_animation_entry_collection(tinyxml2::XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection);
void query_sound_entry(tinyxml2::XMLElement* element, const std::string& archive, const std::string& rootPath,
SoundEntry& soundEntry, const std::string& attributeName = "Sound");
void query_sound_entry_collection(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, SoundEntryCollection& soundEntryCollection,
const std::string& attributeName = "Sound");
tinyxml2::XMLError document_load(const util::physfs::Path&, tinyxml2::XMLDocument&);
}