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

@@ -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&);
}