diff options
Diffstat (limited to 'src/resources')
55 files changed, 2199 insertions, 1293 deletions
diff --git a/src/resources/abilitydb.cpp b/src/resources/abilitydb.cpp new file mode 100644 index 00000000..cb596ea8 --- /dev/null +++ b/src/resources/abilitydb.cpp @@ -0,0 +1,106 @@ +/* + * The Mana Client + * Copyright (C) 2010-2013 The Mana Developers + * + * This file is part of The Mana Client. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "resources/abilitydb.h" + +#include "log.h" + +#include "utils/dtor.h" + +#include <map> + +namespace +{ + std::map<int, AbilityInfo *> mAbilityInfos; + bool mLoaded = false; +} + +static AbilityInfo::TargetMode targetModeFromString(const std::string& str) +{ + if (str == "being") + return AbilityInfo::TARGET_BEING; + if (str == "point") + return AbilityInfo::TARGET_POINT; + if (str == "direction") + return AbilityInfo::TARGET_DIRECTION; + + Log::info("AbilityDB: Warning, unknown target mode \"%s\"", str.c_str() ); + return AbilityInfo::TARGET_BEING; +} + + +void AbilityDB::init() +{ + if (mLoaded) + unload(); +} + +void AbilityDB::readAbilityNode(XML::Node node, const std::string &filename) +{ + auto *info = new AbilityInfo(); + int id = node.getProperty("id", 0); + info->id = id; + info->name = node.getProperty("name", std::string()); + info->icon = node.getProperty("icon", std::string()); + info->useAction = node.getProperty("useaction", std::string()); + + info->targetMode = targetModeFromString(node.getProperty("target", "being")); + + info->rechargeable = node.getBoolProperty("rechargeable", true); + info->rechargeNeeded = 0; + info->rechargeCurrent = 0; + + if (mAbilityInfos.find(id) != mAbilityInfos.end()) + Log::info("AbilityDB: Duplicate ability ID %d in %s, ignoring", id, filename.c_str()); + else + mAbilityInfos[id] = info; +} + +void AbilityDB::checkStatus() +{ + mLoaded = true; +} + +void AbilityDB::unload() +{ + delete_all(mAbilityInfos); + mAbilityInfos.clear(); + + mLoaded = false; +} + +AbilityInfo *AbilityDB::get(int id) +{ + auto i = mAbilityInfos.find(id); + if (i != mAbilityInfos.end()) + return i->second; + + return nullptr; +} + +AbilityInfo *AbilityDB::find(std::string_view name) +{ + for (auto &[_, abilityInfo] : mAbilityInfos) + { + if (abilityInfo->name == name) + return abilityInfo; + } + return nullptr; +} diff --git a/src/resources/specialdb.h b/src/resources/abilitydb.h index 20ba0075..6b3de5a6 100644 --- a/src/resources/specialdb.h +++ b/src/resources/abilitydb.h @@ -18,50 +18,52 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SPECIAL_DB_H -#define SPECIAL_DB_H +#pragma once #include <string> #include "utils/xml.h" -struct SpecialInfo +struct AbilityInfo { enum TargetMode { - TARGET_BEING, // target any being - TARGET_POINT // target map location + TARGET_BEING, // target any being + TARGET_POINT, // target map location + TARGET_DIRECTION // target a direction }; int id; - std::string set; // tab on which the special is shown - std::string name; // displayed name of special + std::string name; // displayed name of ability std::string icon; // filename of graphical icon + std::string useAction; // action when using the ability - TargetMode targetMode; // target mode + TargetMode targetMode; - bool rechargeable; // true when the special has a recharge bar + bool rechargeable; // true when the ability has a recharge bar int rechargeNeeded; // maximum recharge when applicable int rechargeCurrent; // current recharge when applicable }; /** - * Special information database. + * Ability information database. */ -namespace SpecialDB +namespace AbilityDB { void init(); - void readSpecialSetNode(XML::Node node, const std::string &filename); + void readAbilityNode(XML::Node node, const std::string &filename); void checkStatus(); void unload(); - /** gets the special info for ID. Will return 0 when it is - * a server-specific special. + /** Gets the ability info for ID. Will return nullptr when it is + * a server-specific ability. */ - SpecialInfo *get(int id); + AbilityInfo *get(int id); - SpecialInfo::TargetMode targetModeFromString(const std::string& str); + /** + * Finds an ability by name. Returns nullptr when the ability could not be + * found. + */ + AbilityInfo *find(std::string_view name); } - -#endif diff --git a/src/resources/action.h b/src/resources/action.h index 37f29810..c3e47dbb 100644 --- a/src/resources/action.h +++ b/src/resources/action.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef ACTION_H -#define ACTION_H +#pragma once #include <map> @@ -43,5 +42,3 @@ class Action protected: std::map<int, Animation *> mAnimations; }; - -#endif diff --git a/src/resources/ambientlayer.h b/src/resources/ambientlayer.h index e62af33f..8e0137b1 100644 --- a/src/resources/ambientlayer.h +++ b/src/resources/ambientlayer.h @@ -18,8 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef RESOURCES_AMBIENTOVERLAY_H -#define RESOURCES_AMBIENTOVERLAY_H +#pragma once #include "resource.h" @@ -52,5 +51,3 @@ class AmbientLayer float mPosX = 0; /**< Current layer X position. */ float mPosY = 0; /**< Current layer Y position. */ }; - -#endif diff --git a/src/resources/animation.cpp b/src/resources/animation.cpp index b48e8cff..c529400f 100644 --- a/src/resources/animation.cpp +++ b/src/resources/animation.cpp @@ -21,6 +21,11 @@ #include "resources/animation.h" +#include "dye.h" +#include "game.h" +#include "log.h" +#include "resourcemanager.h" + void Animation::addFrame(Image *image, int delay, int offsetX, int offsetY) { auto &frame = mFrames.emplace_back(); @@ -41,3 +46,90 @@ bool Animation::isTerminator(const Frame &candidate) { return candidate.image == nullptr; } + +Animation Animation::fromXML(XML::Node node, const std::string &dyePalettes) +{ + Animation animation; + + std::string imagePath = node.getProperty("imageset", std::string()); + + // Instanciate the dye coloration. + Dye::instantiate(imagePath, dyePalettes); + + auto imageSet = ResourceManager::getInstance()->getImageSet( + imagePath, + node.getProperty("width", 0), + node.getProperty("height", 0) + ); + + if (!imageSet) + return animation; + + // Get animation frames + for (auto frameNode : node.children()) + { + int delay = frameNode.getProperty("delay", 0); + int offsetX = frameNode.getProperty("offsetX", 0); + int offsetY = frameNode.getProperty("offsetY", 0); + Game *game = Game::instance(); + if (game) + { + offsetX -= imageSet->getWidth() / 2 - game->getCurrentTileWidth() / 2; + offsetY -= imageSet->getHeight() - game->getCurrentTileHeight(); + } + + if (frameNode.name() == "frame") + { + int index = frameNode.getProperty("index", -1); + + if (index < 0) + { + Log::info("No valid value for 'index'"); + continue; + } + + Image *img = imageSet->get(index); + + if (!img) + { + Log::info("No image at index %d", index); + continue; + } + + animation.addFrame(img, delay, offsetX, offsetY); + } + else if (frameNode.name() == "sequence") + { + int start = frameNode.getProperty("start", -1); + int end = frameNode.getProperty("end", -1); + + if (start < 0 || end < 0) + { + Log::info("No valid value for 'start' or 'end'"); + continue; + } + + while (end >= start) + { + Image *img = imageSet->get(start); + + if (!img) + { + Log::info("No image at index %d", start); + continue; + } + + animation.addFrame(img, delay, offsetX, offsetY); + start++; + } + } + else if (frameNode.name() == "end") + { + animation.addTerminator(); + } + } + + animation.mImageSet = imageSet; + + return animation; +} diff --git a/src/resources/animation.h b/src/resources/animation.h index 812e0547..cc3abd58 100644 --- a/src/resources/animation.h +++ b/src/resources/animation.h @@ -19,8 +19,10 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef ANIMATION_H -#define ANIMATION_H +#pragma once + +#include "resources/imageset.h" +#include "utils/xml.h" #include <vector> @@ -77,9 +79,14 @@ class Animation final */ static bool isTerminator(const Frame &phase); + /** + * Loads an animation from XML. + */ + static Animation fromXML(XML::Node node, + const std::string &dyePalettes = {}); + protected: + ResourceRef<ImageSet> mImageSet; std::vector<Frame> mFrames; int mDuration = 0; }; - -#endif diff --git a/src/resources/attributes.cpp b/src/resources/attributes.cpp index ab270b65..5b1018ee 100644 --- a/src/resources/attributes.cpp +++ b/src/resources/attributes.cpp @@ -40,7 +40,7 @@ namespace Attributes { - using Attribute = struct + struct Attribute { unsigned int id; std::string name; @@ -94,11 +94,11 @@ namespace Attributes { { // Fill up the modifiable attribute label list. attributeLabels.clear(); - for (auto it = attributes.cbegin(), it_end = attributes.cend(); it != it_end; it++) + for (const auto &[_, attribute] : attributes) { - if (it->second.modifiable && - (it->second.scope == "character" || it->second.scope == "being")) - attributeLabels.push_back(it->second.name + ":"); + if (attribute.modifiable && + (attribute.scope == "character" || attribute.scope == "being")) + attributeLabels.push_back(attribute.name + ":"); } } @@ -228,7 +228,7 @@ namespace Attributes { void init() { - if (attributes.size()) + if (!attributes.empty()) unload(); } @@ -238,24 +238,24 @@ namespace Attributes { void readAttributeNode(XML::Node node, const std::string &filename) { int id = node.getProperty("id", 0); - if (!id) { - logger->log("Attributes: Invalid or missing stat ID in " - DEFAULT_ATTRIBUTESDB_FILE "!"); + Log::info("Attributes: Invalid or missing stat ID in " + DEFAULT_ATTRIBUTESDB_FILE "!"); return; } - else if (attributes.find(id) != attributes.end()) + + if (attributes.find(id) != attributes.end()) { - logger->log("Attributes: Redefinition of stat ID %d", id); + Log::info("Attributes: Redefinition of stat ID %d", id); } std::string name = node.getProperty("name", ""); if (name.empty()) { - logger->log("Attributes: Invalid or missing stat name in " - DEFAULT_ATTRIBUTESDB_FILE "!"); + Log::info("Attributes: Invalid or missing stat name in " + DEFAULT_ATTRIBUTESDB_FILE "!"); return; } @@ -280,10 +280,10 @@ namespace Attributes { { if (name.empty()) { - logger->log("Attribute modifier in attribute %u:%s: " - "Empty name definition " - "on empty tag definition, skipping.", - a.id, a.name.c_str()); + Log::info("Attribute modifier in attribute %u:%s: " + "Empty name definition " + "on empty tag definition, skipping.", + a.id, a.name.c_str()); --count; continue; } @@ -296,10 +296,10 @@ namespace Attributes { { if (name.empty()) { - logger->log("Attribute modifier in attribute %u:%s: " - "Empty name definition " - "on empty effect definition, skipping.", - a.id, a.name.c_str()); + Log::info("Attribute modifier in attribute %u:%s: " + "Empty name definition " + "on empty effect definition, skipping.", + a.id, a.name.c_str()); --count; continue; } @@ -308,7 +308,7 @@ namespace Attributes { } tags.insert(std::make_pair(tag, effect)); } - logger->log("Found %d tags for attribute %d.", count, id); + Log::info("Found %d tags for attribute %d.", count, id); } /** @@ -321,8 +321,8 @@ namespace Attributes { DEFAULT_MIN_PTS); attributeMaximum = node.getProperty("maximum", DEFAULT_MAX_PTS); - logger->log("Loaded points: start: %i, min: %i, max: %i.", - creationPoints, attributeMinimum, attributeMaximum); + Log::info("Loaded points: start: %i, min: %i, max: %i.", + creationPoints, attributeMinimum, attributeMaximum); } /** @@ -330,8 +330,8 @@ namespace Attributes { */ void checkStatus() { - logger->log("Found %d tags for %d attributes.", int(tags.size()), - int(attributes.size())); + Log::info("Found %d tags for %d attributes.", int(tags.size()), + int(attributes.size())); if (attributes.size() == 0) { @@ -346,9 +346,9 @@ namespace Attributes { if (averageValue > attributeMaximum || averageValue < attributeMinimum || creationPoints < 1) { - logger->log("Attributes: Character's point values make " - "the character's creation impossible. " - "Switch back to defaults."); + Log::info("Attributes: Character's point values make " + "the character's creation impossible. " + "Switch back to defaults."); creationPoints = DEFAULT_POINTS; attributeMinimum = DEFAULT_MIN_PTS; attributeMaximum = DEFAULT_MAX_PTS; @@ -364,23 +364,23 @@ namespace Attributes { { std::list<ItemStat> dbStats; - for (auto it = tags.cbegin(), it_end = tags.cend(); it != it_end; ++it) - dbStats.emplace_back(it->first, it->second); + for (const auto &[tag, format] : tags) + dbStats.emplace_back(tag, format); setStatsList(std::move(dbStats)); } void informStatusWindow() { - for (auto it = attributes.cbegin(), it_end = attributes.cend(); it != it_end; it++) + for (const auto &[_, attribute] : attributes) { - if (it->second.playerInfoId == -1 && - (it->second.scope == "character" || it->second.scope == "being")) + if (attribute.playerInfoId == -1 && + (attribute.scope == "character" || attribute.scope == "being")) { - statusWindow->addAttribute(it->second.id, - it->second.name, - it->second.modifiable, - it->second.description); + statusWindow->addAttribute(attribute.id, + attribute.name, + attribute.modifiable, + attribute.description); } } } diff --git a/src/resources/attributes.h b/src/resources/attributes.h index 9071d6b8..e70a5435 100644 --- a/src/resources/attributes.h +++ b/src/resources/attributes.h @@ -18,8 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef RESOURCES_ATTRIBUTES_H -#define RESOURCES_ATTRIBUTES_H +#pragma once #include <string> #include <vector> @@ -73,5 +72,3 @@ namespace Attributes unsigned int getAttributeMaximum(); } // namespace Attributes - -#endif // RESOURCES_ATTRIBUTES_H diff --git a/src/resources/beinginfo.cpp b/src/resources/beinginfo.cpp index 17e270dc..f2edf1d8 100644 --- a/src/resources/beinginfo.cpp +++ b/src/resources/beinginfo.cpp @@ -52,15 +52,15 @@ static std::optional<ActorSprite::TargetCursorSize> targetCursorSizeFromString(c static std::optional<Cursor> cursorFromString(const std::string &cursor) { - if (cursor == "pointer") return Cursor::POINTER; - if (cursor == "attack") return Cursor::FIGHT; - if (cursor == "pickup") return Cursor::PICKUP; - if (cursor == "talk") return Cursor::TALK; - if (cursor == "action") return Cursor::ACTION; - if (cursor == "left") return Cursor::LEFT; - if (cursor == "up") return Cursor::UP; - if (cursor == "right") return Cursor::RIGHT; - if (cursor == "down") return Cursor::DOWN; + if (cursor == "pointer") return Cursor::Pointer; + if (cursor == "attack") return Cursor::Fight; + if (cursor == "pickup") return Cursor::PickUp; + if (cursor == "talk") return Cursor::Talk; + if (cursor == "action") return Cursor::Action; + if (cursor == "left") return Cursor::Left; + if (cursor == "up") return Cursor::Up; + if (cursor == "right") return Cursor::Right; + if (cursor == "down") return Cursor::Down; return {}; } @@ -73,8 +73,8 @@ void BeingInfo::setTargetCursorSize(const std::string &size) const auto cursorSize = targetCursorSizeFromString(size); if (!cursorSize) { - logger->log("Unknown targetCursor value \"%s\" for %s", - size.c_str(), name.c_str()); + Log::info("Unknown targetCursor value \"%s\" for %s", + size.c_str(), name.c_str()); } targetCursorSize = cursorSize.value_or(ActorSprite::TC_MEDIUM); } @@ -84,10 +84,10 @@ void BeingInfo::setHoverCursor(const std::string &cursorName) const auto cursor = cursorFromString(cursorName); if (!cursor) { - logger->log("Unknown hoverCursor value \"%s\" for %s", - cursorName.c_str(), name.c_str()); + Log::info("Unknown hoverCursor value \"%s\" for %s", + cursorName.c_str(), name.c_str()); } - hoverCursor = cursor.value_or(Cursor::POINTER); + hoverCursor = cursor.value_or(Cursor::Pointer); } void BeingInfo::addSound(SoundEvent event, const std::string &filename) diff --git a/src/resources/beinginfo.h b/src/resources/beinginfo.h index 2eac4237..91343d4f 100644 --- a/src/resources/beinginfo.h +++ b/src/resources/beinginfo.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef BEINGINFO_H -#define BEINGINFO_H +#pragma once #include "actorsprite.h" #include "map.h" @@ -43,10 +42,10 @@ struct Attack enum class SoundEvent { - HIT, - MISS, - HURT, - DIE + Hit, + Miss, + Hurt, + Die }; /** @@ -67,7 +66,7 @@ public: std::string name; SpriteDisplay display; ActorSprite::TargetCursorSize targetCursorSize = ActorSprite::TC_MEDIUM; - Cursor hoverCursor = Cursor::POINTER; + Cursor hoverCursor = Cursor::Pointer; unsigned char walkMask = Map::BLOCKMASK_ALL; Map::BlockType blockType = Map::BLOCKTYPE_CHARACTER; bool targetSelection = true; @@ -85,5 +84,3 @@ private: std::map<SoundEvent, std::vector<std::string>> mSounds; std::map<int, Attack> mAttacks; }; - -#endif // BEINGINFO_H diff --git a/src/resources/chardb.cpp b/src/resources/chardb.cpp index 9001b6c2..97f86d3a 100644 --- a/src/resources/chardb.cpp +++ b/src/resources/chardb.cpp @@ -54,7 +54,7 @@ void CharDB::load() if (!root || root.name() != "chars") { - logger->log("CharDB: Failed to parse charcreation.xml."); + Log::info("CharDB: Failed to parse charcreation.xml."); return; } @@ -86,7 +86,7 @@ void CharDB::load() void CharDB::unload() { - logger->log("Unloading chars database..."); + Log::info("Unloading chars database..."); mLoaded = false; } diff --git a/src/resources/chardb.h b/src/resources/chardb.h index 10530b26..de49dad6 100644 --- a/src/resources/chardb.h +++ b/src/resources/chardb.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef RESOURCES_CHARDB_H -#define RESOURCES_CHARDB_H +#pragma once #include <vector> @@ -44,5 +43,3 @@ namespace CharDB const std::vector<int> &getDefaultItems(); } - -#endif // RESOURCES_CHARDB_H diff --git a/src/resources/dye.cpp b/src/resources/dye.cpp index d594299c..136c9334 100644 --- a/src/resources/dye.cpp +++ b/src/resources/dye.cpp @@ -63,14 +63,14 @@ DyePalette::DyePalette(const std::string &description) } else { - logger->log("Error, invalid embedded palette: %s", - description.c_str()); + Log::info("Error, invalid embedded palette: %s", + description.c_str()); return; } v = (v << 4) | n; } - Color c = { { (unsigned char) (v >> 16), (unsigned char) (v >> 8), (unsigned char) v } }; + Color c = { (unsigned char) (v >> 16), (unsigned char) (v >> 8), (unsigned char) v }; mColors.push_back(c); pos += 6; @@ -82,7 +82,7 @@ DyePalette::DyePalette(const std::string &description) ++pos; } - logger->log("Error, invalid embedded palette: %s", description.c_str()); + Log::info("Error, invalid embedded palette: %s", description.c_str()); } void DyePalette::getColor(int intensity, int color[3]) const @@ -103,9 +103,9 @@ void DyePalette::getColor(int intensity, int color[3]) const int j = t != 0 ? i : i - 1; // Get the exact color if any, the next color otherwise. - int r2 = mColors[j].value[0], - g2 = mColors[j].value[1], - b2 = mColors[j].value[2]; + int r2 = mColors[j].r, + g2 = mColors[j].g, + b2 = mColors[j].b; if (t == 0) { @@ -120,9 +120,9 @@ void DyePalette::getColor(int intensity, int color[3]) const int r1 = 0, g1 = 0, b1 = 0; if (i > 0) { - r1 = mColors[i - 1].value[0]; - g1 = mColors[i - 1].value[1]; - b1 = mColors[i - 1].value[2]; + r1 = mColors[i - 1].r; + g1 = mColors[i - 1].g; + b1 = mColors[i - 1].b; } // Perform a linear interpolation. @@ -153,9 +153,9 @@ void DyePalette::getColor(double intensity, int color[3]) const if (i == j) { // Exact color. - color[0] = mColors[i].value[0]; - color[1] = mColors[i].value[1]; - color[2] = mColors[i].value[2]; + color[0] = mColors[i].r; + color[1] = mColors[i].g; + color[2] = mColors[i].b; return; } @@ -163,12 +163,12 @@ void DyePalette::getColor(double intensity, int color[3]) const double rest = 1 - intensity; // Get the colors - int r1 = mColors[i].value[0], - g1 = mColors[i].value[1], - b1 = mColors[i].value[2], - r2 = mColors[j].value[0], - g2 = mColors[j].value[1], - b2 = mColors[j].value[2]; + int r1 = mColors[i].r, + g1 = mColors[i].g, + b1 = mColors[i].b, + r2 = mColors[j].r, + g2 = mColors[j].g, + b2 = mColors[j].b; // Perform the interpolation. color[0] = (rest * r1 + intensity * r2); @@ -195,7 +195,7 @@ Dye::Dye(const std::string &description) if (next_pos <= pos + 3 || description[pos + 1] != ':') { - logger->log("Error, invalid dye: %s", description.c_str()); + Log::info("Error, invalid dye: %s", description.c_str()); return; } @@ -211,7 +211,7 @@ Dye::Dye(const std::string &description) case 'C': i = 5; break; case 'W': i = 6; break; default: - logger->log("Error, invalid dye: %s", description.c_str()); + Log::info("Error, invalid dye: %s", description.c_str()); return; } mDyePalettes[i] = new DyePalette(description.substr(pos + 2, @@ -289,7 +289,7 @@ void Dye::instantiate(std::string &target, const std::string &palettes) } else { - logger->log("Error, invalid dye placeholder: %s", target.c_str()); + Log::info("Error, invalid dye placeholder: %s", target.c_str()); return; } s << target[next_pos]; diff --git a/src/resources/dye.h b/src/resources/dye.h index ce5565f8..0fe68f07 100644 --- a/src/resources/dye.h +++ b/src/resources/dye.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef DYE_H -#define DYE_H +#pragma once #include <string> #include <vector> @@ -31,7 +30,6 @@ class DyePalette { public: - /** * Creates a palette based on the given string. * The string is either a file name or a sequence of hexadecimal RGB @@ -51,8 +49,12 @@ class DyePalette void getColor(double intensity, int color[3]) const; private: - - struct Color { unsigned char value[3]; }; + struct Color + { + unsigned char r; + unsigned char g; + unsigned char b; + }; std::vector<Color> mColors; }; @@ -63,7 +65,6 @@ class DyePalette class Dye { public: - /** * Creates a set of palettes based on the given string. * @@ -97,5 +98,3 @@ class Dye */ DyePalette *mDyePalettes[7]; }; - -#endif diff --git a/src/resources/emotedb.cpp b/src/resources/emotedb.cpp index 6f1ec6e4..c0f5f777 100644 --- a/src/resources/emotedb.cpp +++ b/src/resources/emotedb.cpp @@ -22,10 +22,9 @@ #include "resources/emotedb.h" #include "log.h" -#include "imagesprite.h" -#include "resources/resourcemanager.h" #include "resources/imageset.h" +#include "resources/resourcemanager.h" #include <algorithm> #include <vector> @@ -44,8 +43,7 @@ void EmoteDB::init() mUnknown.name = "unknown"; mUnknown.effectId = -1; - mUnknown.sprite = std::make_unique<ImageSprite>( - ResourceManager::getInstance()->getImageRef("graphics/sprites/error.png")); + mUnknown.image = ResourceManager::getInstance()->getImage("graphics/sprites/error.png"); } void EmoteDB::readEmoteNode(XML::Node node, const std::string &filename) @@ -53,7 +51,7 @@ void EmoteDB::readEmoteNode(XML::Node node, const std::string &filename) const int id = node.getProperty("id", -1); if (id == -1) { - logger->log("Emote Database: Emote with missing ID in %s!", filename.c_str()); + Log::info("Emote Database: Emote with missing ID in %s!", filename.c_str()); return; } @@ -65,8 +63,8 @@ void EmoteDB::readEmoteNode(XML::Node node, const std::string &filename) if (emote.effectId == -1) { - logger->log("Emote Database: Warning: Emote %s has no attached effect in %s!", - emote.name.c_str(), filename.c_str()); + Log::info("Emote Database: Warning: Emote %s has no attached effect in %s!", + emote.name.c_str(), filename.c_str()); return; } @@ -76,25 +74,24 @@ void EmoteDB::readEmoteNode(XML::Node node, const std::string &filename) if (imageName.empty() || width <= 0 || height <= 0) { - logger->log("Emote Database: Warning: Emote %s has bad imageset values in %s", - emote.name.c_str(), filename.c_str()); + Log::info("Emote Database: Warning: Emote %s has bad imageset values in %s", + emote.name.c_str(), filename.c_str()); return; } emote.is = ResourceManager::getInstance()->getImageSet(imageName, width, height); - emote.is->decRef(); // clear automatic reference if (!emote.is || emote.is->size() == 0) { - logger->log("Emote Database: Error loading imageset for emote %s in %s", - emote.name.c_str(), filename.c_str()); + Log::info("Emote Database: Error loading imageset for emote %s in %s", + emote.name.c_str(), filename.c_str()); return; } // For now we just use the first image in the animation - emote.sprite = std::make_unique<ImageSprite>(emote.is->get(0)); + emote.image = emote.is->get(0); mEmotes.push_back(std::move(emote)); } @@ -106,14 +103,12 @@ void EmoteDB::checkStatus() void EmoteDB::unload() { + // These images are owned by the ImageSet for (auto &emote : mEmotes) - emote.sprite->releaseImageRef(); + emote.image.release(); mEmotes.clear(); - - if (mUnknown.sprite) - mUnknown.sprite->releaseImageRef(); - + mUnknown.image = nullptr; mLoaded = false; } @@ -124,7 +119,7 @@ const Emote &EmoteDB::get(int id) if (i == mEmotes.end()) { - logger->log("EmoteDB: Warning, unknown emote ID %d requested", id); + Log::info("EmoteDB: Warning, unknown emote ID %d requested", id); return mUnknown; } diff --git a/src/resources/emotedb.h b/src/resources/emotedb.h index 7c6f4644..b722c88b 100644 --- a/src/resources/emotedb.h +++ b/src/resources/emotedb.h @@ -19,26 +19,22 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef EMOTE_DB_H -#define EMOTE_DB_H +#pragma once -#include <memory> #include <string> -#include "resources/resource.h" +#include "resources/image.h" +#include "resources/imageset.h" #include "utils/xml.h" -class ImageSet; -class ImageSprite; - struct Emote { int id; int effectId; std::string name; ResourceRef<ImageSet> is; - std::unique_ptr<ImageSprite> sprite; + ResourceRef<Image> image; }; /** @@ -59,5 +55,3 @@ namespace EmoteDB int getEmoteCount(); } - -#endif // EMOTE_DB_H diff --git a/src/resources/hairdb.cpp b/src/resources/hairdb.cpp index 6b88a4df..312188d6 100644 --- a/src/resources/hairdb.cpp +++ b/src/resources/hairdb.cpp @@ -40,7 +40,7 @@ void HairDB::readHairColorNode(XML::Node node, const std::string &filename) int id = node.getProperty("id", 0); if (mHairColors.find(id) != mHairColors.end()) - logger->log("HairDb: Redefinition of color Id %d in %s", id, filename.c_str()); + Log::info("HairDb: Redefinition of color Id %d in %s", id, filename.c_str()); mHairColors[id] = node.getProperty("value", COLOR_WHITE); } @@ -55,7 +55,7 @@ void HairDB::unload() if (!mLoaded) return; - logger->log("Unloading hair style and color database..."); + Log::info("Unloading hair style and color database..."); mHairColors.clear(); mHairStyles.clear(); @@ -71,7 +71,7 @@ void HairDB::addHairStyle(int id) id = -id; if (mHairStyles.find(id) != mHairStyles.end()) - logger->log("Warning: Redefinition of hairstyle id %i:", id); + Log::warn("Redefinition of hairstyle id %i:", id); mHairStyles.insert(id); } @@ -81,13 +81,13 @@ const std::string &HairDB::getHairColor(int id) const if (!mLoaded) { // no idea if this can happen, but that check existed before - logger->log("WARNING: HairDB::getHairColor() called before settings were loaded!"); + Log::warn("HairDB::getHairColor() called before settings were loaded!"); } auto it = mHairColors.find(id); if (it != mHairColors.end()) return it->second; - logger->log("HairDb: Error, unknown color Id# %d", id); + Log::info("HairDb: Error, unknown color Id# %d", id); return mHairColors.at(0); } diff --git a/src/resources/hairdb.h b/src/resources/hairdb.h index 374f2e03..a228c72e 100644 --- a/src/resources/hairdb.h +++ b/src/resources/hairdb.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef HAIR_MANAGER_H -#define HAIR_MANAGER_H +#pragma once #include <map> #include <set> @@ -93,5 +92,3 @@ private: }; extern HairDB hairDB; - -#endif diff --git a/src/resources/image.cpp b/src/resources/image.cpp index 328ea9b8..11d5c275 100644 --- a/src/resources/image.cpp +++ b/src/resources/image.cpp @@ -54,8 +54,7 @@ Image::Image(SDL_Texture *texture, int width, int height): if (!texture) { - logger->log( - "Image::Image(SDL_Texture*, ...): Couldn't load invalid Surface!"); + Log::info("Image::Image(SDL_Texture*, ...): Couldn't load invalid Surface!"); } } @@ -72,8 +71,7 @@ Image::Image(GLuint glimage, int width, int height, int texWidth, int texHeight) if (glimage == 0) { - logger->log( - "Image::Image(GLuint, ...): Couldn't load invalid Surface!"); + Log::info("Image::Image(GLuint, ...): Couldn't load invalid Surface!"); } } #endif @@ -101,7 +99,7 @@ Resource *Image::load(SDL_RWops *rw) if (!tmpImage) { - logger->log("Error, image load failed: %s", IMG_GetError()); + Log::info("Error, image load failed: %s", IMG_GetError()); return nullptr; } @@ -113,37 +111,41 @@ Resource *Image::load(SDL_RWops *rw) Resource *Image::load(SDL_RWops *rw, const Dye &dye) { - SDL_Surface *tmpImage = IMG_Load_RW(rw, 1); + SDL_Surface *surf = IMG_Load_RW(rw, 1); - if (!tmpImage) + if (!surf) { - logger->log("Error, image load failed: %s", IMG_GetError()); + Log::info("Error, image load failed: %s", IMG_GetError()); return nullptr; } - SDL_PixelFormat rgba; - rgba.palette = nullptr; - rgba.BitsPerPixel = 32; - rgba.BytesPerPixel = 4; - rgba.Rmask = 0xFF000000; rgba.Rloss = 0; rgba.Rshift = 24; - rgba.Gmask = 0x00FF0000; rgba.Gloss = 0; rgba.Gshift = 16; - rgba.Bmask = 0x0000FF00; rgba.Bloss = 0; rgba.Bshift = 8; - rgba.Amask = 0x000000FF; rgba.Aloss = 0; rgba.Ashift = 0; + if (surf->format->format != SDL_PIXELFORMAT_RGBA32) + { + Log::warn("Image format is %s, not SDL_PIXELFORMAT_RGBA32. Converting...", + SDL_GetPixelFormatName(surf->format->format)); - SDL_Surface *surf = SDL_ConvertSurface(tmpImage, &rgba, 0); - SDL_FreeSurface(tmpImage); + SDL_Surface *convertedSurf = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_RGBA32, 0); + SDL_FreeSurface(surf); + if (!convertedSurf) + { + Log::info("Error, image convert failed: %s", SDL_GetError()); + return nullptr; + } + surf = convertedSurf; + } - auto *pixels = static_cast< Uint32 * >(surf->pixels); - for (Uint32 *p_end = pixels + surf->w * surf->h; pixels != p_end; ++pixels) + auto *pixels = static_cast<SDL_Color *>(surf->pixels); + for (SDL_Color *p_end = pixels + surf->w * surf->h; pixels != p_end; ++pixels) { - int alpha = *pixels & 255; - if (!alpha) continue; - int v[3]; - v[0] = (*pixels >> 24) & 255; - v[1] = (*pixels >> 16) & 255; - v[2] = (*pixels >> 8 ) & 255; + if (!pixels->a) + continue; + + int v[3] = { pixels->r, pixels->g, pixels->b }; dye.update(v); - *pixels = (v[0] << 24) | (v[1] << 16) | (v[2] << 8) | alpha; + + pixels->r = v[0]; + pixels->g = v[1]; + pixels->b = v[2]; } Image *image = load(surf); @@ -215,8 +217,8 @@ Image *Image::_GLload(SDL_Surface *image) if (realWidth < width || realHeight < height) { - logger->log("Warning: image too large, cropping to %dx%d texture!", - realWidth, realHeight); + Log::warn("Image too large, cropping to %dx%d texture!", + realWidth, realHeight); } // Determine 32-bit masks based on byte order @@ -249,7 +251,7 @@ Image *Image::_GLload(SDL_Surface *image) if (!image) { - logger->log("Error, image convert failed: out of memory"); + Log::info("Error, image convert failed: out of memory"); return nullptr; } @@ -304,7 +306,7 @@ Image *Image::_GLload(SDL_Surface *image) errmsg = "GL_OUT_OF_MEMORY"; break; } - logger->log("Error: Image GL import failed: %s", errmsg); + Log::error("Image GL import failed: %s", errmsg); return nullptr; } diff --git a/src/resources/image.h b/src/resources/image.h index e2e240c3..37dd5e1d 100644 --- a/src/resources/image.h +++ b/src/resources/image.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef IMAGE_H -#define IMAGE_H +#pragma once #include "resources/resource.h" @@ -216,5 +215,3 @@ class SubImage : public Image private: ResourceRef<Image> mParent; }; - -#endif diff --git a/src/resources/imageset.cpp b/src/resources/imageset.cpp index 34cf1fd8..1f194b4f 100644 --- a/src/resources/imageset.cpp +++ b/src/resources/imageset.cpp @@ -50,7 +50,7 @@ Image *ImageSet::get(size_t i) const { if (i >= mImages.size()) { - logger->log("Warning: No sprite %d in this image set", (int) i); + Log::warn("No sprite %d in this image set", (int) i); return nullptr; } diff --git a/src/resources/imageset.h b/src/resources/imageset.h index a6501cc9..97dbec90 100644 --- a/src/resources/imageset.h +++ b/src/resources/imageset.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef IMAGESET_H -#define IMAGESET_H +#pragma once #include "resources/resource.h" @@ -78,5 +77,3 @@ class ImageSet : public Resource int mOffsetX = 0; int mOffsetY = 0; }; - -#endif diff --git a/src/resources/imagewriter.cpp b/src/resources/imagewriter.cpp index ddf1fbee..cf4c6803 100644 --- a/src/resources/imagewriter.cpp +++ b/src/resources/imagewriter.cpp @@ -42,7 +42,7 @@ bool ImageWriter::writePNG(SDL_Surface *surface, const std::string &filename) png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) { - logger->log("Had trouble creating png_structp"); + Log::info("Had trouble creating png_structp"); return false; } @@ -50,21 +50,21 @@ bool ImageWriter::writePNG(SDL_Surface *surface, const std::string &filename) if (!info_ptr) { png_destroy_write_struct(&png_ptr, (png_infopp)nullptr); - logger->log("Could not create png_info"); + Log::info("Could not create png_info"); return false; } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct(&png_ptr, (png_infopp)nullptr); - logger->log("problem writing to %s", filename.c_str()); + Log::info("problem writing to %s", filename.c_str()); return false; } FILE *fp = fopen(filename.c_str(), "wb"); if (!fp) { - logger->log("could not open file %s for writing", filename.c_str()); + Log::info("could not open file %s for writing", filename.c_str()); return false; } @@ -83,7 +83,7 @@ bool ImageWriter::writePNG(SDL_Surface *surface, const std::string &filename) row_pointers = new png_bytep[surface->h]; if (!row_pointers) { - logger->log("Had trouble converting surface to row pointers"); + Log::info("Had trouble converting surface to row pointers"); fclose(fp); return false; } diff --git a/src/resources/imagewriter.h b/src/resources/imagewriter.h index 23e85bd8..41ca267a 100644 --- a/src/resources/imagewriter.h +++ b/src/resources/imagewriter.h @@ -19,6 +19,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#pragma once + #include <iosfwd> struct SDL_Surface; diff --git a/src/resources/itemdb.cpp b/src/resources/itemdb.cpp index 68ddcd75..1d217fc2 100644 --- a/src/resources/itemdb.cpp +++ b/src/resources/itemdb.cpp @@ -21,6 +21,7 @@ #include "resources/itemdb.h" +#include "configuration.h" #include "log.h" #include "resources/hairdb.h" @@ -29,10 +30,11 @@ #include "utils/dtor.h" #include "utils/gettext.h" #include "utils/stringutils.h" -#include "configuration.h" +#include "net/tmwa/protocol.h" #include <cassert> +#include <string_view> void setStatsList(std::list<ItemStat> stats) { @@ -60,14 +62,70 @@ static ItemType itemTypeFromString(const std::string &name, int id = 0) return ITEM_UNUSABLE; } +static uint8_t spriteFromString(std::string_view name) +{ + if (name.empty()) + return SPRITE_ALL; + if (name == "race" || name == "type") + return TmwAthena::SPRITE_BASE; + if (name == "shoes" || name == "boot" || name == "boots") + return TmwAthena::SPRITE_SHOE; + if (name == "bottomclothes" || name == "bottom" || name == "pants") + return TmwAthena::SPRITE_BOTTOMCLOTHES; + if (name == "topclothes" || name == "top" || name == "torso" || name == "body") + return TmwAthena::SPRITE_TOPCLOTHES; + if (name == "misc1") + return TmwAthena::SPRITE_MISC1; + if (name == "misc2" || name == "scarf" || name == "scarfs") + return TmwAthena::SPRITE_MISC2; + if (name == "hair") + return TmwAthena::SPRITE_HAIR; + if (name == "hat" || name == "hats") + return TmwAthena::SPRITE_HAT; + if (name == "wings") + return TmwAthena::SPRITE_CAPE; + if (name == "glove" || name == "gloves") + return TmwAthena::SPRITE_GLOVES; + if (name == "weapon" || name == "weapons") + return TmwAthena::SPRITE_WEAPON; + if (name == "shield" || name == "shields") + return TmwAthena::SPRITE_SHIELD; + if (name == "amulet" || name == "amulets") + return 12; + if (name == "ring" || name == "rings") + return 13; + + return SPRITE_UNKNOWN; +} + +static uint8_t directionFromString(std::string_view name) +{ + if (name.empty()) + return DIRECTION_ALL; + if (name == "down" || name == "downall") + return DIRECTION_DOWN; + if (name == "left") + return DIRECTION_LEFT; + if (name == "up" || name == "upall") + return DIRECTION_UP; + if (name == "right") + return DIRECTION_RIGHT; + + // hack for died action. + if (name == "died") + return DIRECTION_DEAD; + + return DIRECTION_UNKNOWN; +} + void ItemDB::loadEmptyItemDefinition() { mUnknown->name = _("Unknown item"); mUnknown->display = SpriteDisplay(); std::string errFile = paths.getStringValue("spriteErrorFile"); - mUnknown->setSprite(errFile, Gender::MALE, 0); - mUnknown->setSprite(errFile, Gender::FEMALE, 0); - mUnknown->setSprite(errFile, Gender::HIDDEN, 0); + mUnknown->setSprite(errFile, Gender::Male, 0); + mUnknown->setSprite(errFile, Gender::Female, 0); + mUnknown->setSprite(errFile, Gender::Hidden, 0); mUnknown->hitEffectId = paths.getIntValue("hitEffectId"); mUnknown->criticalHitEffectId = paths.getIntValue("criticalHitEffectId"); } @@ -90,7 +148,7 @@ const ItemInfo &ItemDB::get(int id) const auto i = mItemInfos.find(id); if (i == mItemInfos.end()) { - logger->log("ItemDB: Warning, unknown item ID# %d", id); + Log::info("ItemDB: Warning, unknown item ID# %d", id); return *mUnknown; } @@ -106,8 +164,8 @@ const ItemInfo &ItemDB::get(const std::string &name) const { if (!name.empty()) { - logger->log("ItemDB: Warning, unknown item name \"%s\"", - name.c_str()); + Log::info("ItemDB: Warning, unknown item name \"%s\"", + name.c_str()); } return *mUnknown; } @@ -122,11 +180,11 @@ void ItemDB::loadSpriteRef(ItemInfo &itemInfo, XML::Node node) const int race = node.getProperty("race", 0); if (gender == "male" || gender == "unisex") - itemInfo.setSprite(filename, Gender::MALE, race); + itemInfo.setSprite(filename, Gender::Male, race); if (gender == "female" || gender == "unisex") - itemInfo.setSprite(filename, Gender::FEMALE, race); + itemInfo.setSprite(filename, Gender::Female, race); if (gender == "hidden" || gender == "other" || gender == "unisex") - itemInfo.setSprite(filename, Gender::HIDDEN, race); + itemInfo.setSprite(filename, Gender::Hidden, race); } void ItemDB::loadSoundRef(ItemInfo &itemInfo, XML::Node node) @@ -136,16 +194,16 @@ void ItemDB::loadSoundRef(ItemInfo &itemInfo, XML::Node node) if (event == "hit") { - itemInfo.addSound(EquipmentSoundEvent::HIT, filename); + itemInfo.addSound(EquipmentSoundEvent::Hit, filename); } else if (event == "strike" || event == "miss") { - itemInfo.addSound(EquipmentSoundEvent::STRIKE, filename); + itemInfo.addSound(EquipmentSoundEvent::Strike, filename); } else { - logger->log("ItemDB: Ignoring unknown sound event '%s'", - event.c_str()); + Log::info("ItemDB: Ignoring unknown sound event '%s'", + event.c_str()); } } @@ -166,9 +224,49 @@ void ItemDB::loadFloorSprite(SpriteDisplay &display, XML::Node floorNode) } } +void ItemDB::loadReplacement(ItemInfo &info, XML::Node replaceNode) +{ + std::string_view spriteString; + std::string_view directionString; + + replaceNode.attribute("sprite", spriteString); + replaceNode.attribute("direction", directionString); + + const uint8_t sprite = spriteFromString(spriteString); + const uint8_t direction = directionFromString(directionString); + + if (sprite == SPRITE_UNKNOWN) + { + Log::info("ItemDB: Invalid sprite name '%s' in replace tag", + spriteString.data()); + return; + } + + if (direction == DIRECTION_UNKNOWN) + { + Log::info("ItemDB: Invalid direction name '%s' in replace tag", + directionString.data()); + return; + } + + Replacement &replace = info.replacements.emplace_back(); + replace.sprite = sprite; + replace.direction = direction; + + for (auto child : replaceNode.children()) + { + if (child.name() == "item") + { + Replacement::Item &item = replace.items.emplace_back(); + child.attribute("from", item.from); + child.attribute("to", item.to); + } + } +} + void ItemDB::unload() { - logger->log("Unloading item database..."); + Log::info("Unloading item database..."); delete mUnknown; mUnknown = nullptr; @@ -185,12 +283,12 @@ void ItemDB::loadCommonRef(ItemInfo &itemInfo, XML::Node node, const std::string if (!itemInfo.id) { - logger->log("ItemDB: Invalid or missing item Id in %s!", filename.c_str()); + Log::info("ItemDB: Invalid or missing item Id in %s!", filename.c_str()); return; } else if (mItemInfos.find(itemInfo.id) != mItemInfos.end()) { - logger->log("ItemDB: Redefinition of item Id %d in %s", itemInfo.id, filename.c_str()); + Log::info("ItemDB: Redefinition of item Id %d in %s", itemInfo.id, filename.c_str()); } itemInfo.mView = node.getProperty("view", 0); @@ -228,6 +326,10 @@ void ItemDB::loadCommonRef(ItemInfo &itemInfo, XML::Node node, const std::string { loadFloorSprite(itemInfo.display, itemChild); } + else if (itemChild.name() == "replace") + { + loadReplacement(itemInfo, itemChild); + } } } @@ -244,8 +346,8 @@ void ItemDB::addItem(ItemInfo *itemInfo) if (itr == mNamedItemInfos.end()) mNamedItemInfos[temp] = itemInfo; else - logger->log("ItemDB: Duplicate name (%s) for item id %d found.", - temp.c_str(), itemInfo->id); + Log::info("ItemDB: Duplicate name (%s) for item id %d found.", + temp.c_str(), itemInfo->id); } } @@ -257,7 +359,7 @@ static void checkParameter(int id, const T param, const T errorValue) std::stringstream errMsg; errMsg << "ItemDB: Missing " << param << " attribute for item id " << id << "!"; - logger->log("%s", errMsg.str().c_str()); + Log::info("%s", errMsg.str().c_str()); } } @@ -266,7 +368,7 @@ void ItemDB::checkItemInfo(ItemInfo &itemInfo) int id = itemInfo.id; if (!itemInfo.attackAction.empty()) if (itemInfo.attackRange == 0) - logger->log("ItemDB: Missing attack range from weapon %i!", id); + Log::info("ItemDB: Missing attack range from weapon %i!", id); if (id >= 0) { @@ -402,7 +504,7 @@ void ManaServItemDB::readItemNode(XML::Node node, const std::string &filename) std::string trigger = itemChild.getProperty("trigger", std::string()); if (trigger.empty()) { - logger->log("Found empty trigger effect label in %s, skipping.", filename.c_str()); + Log::info("Found empty trigger effect label in %s, skipping.", filename.c_str()); continue; } @@ -412,8 +514,8 @@ void ManaServItemDB::readItemNode(XML::Node node, const std::string &filename) auto triggerLabel = triggerTable.find(trigger); if (triggerLabel == triggerTable.end()) { - logger->log("Warning: unknown trigger %s in item %d!", - trigger.c_str(), itemInfo->id); + Log::warn("Unknown trigger %s in item %d!", + trigger.c_str(), itemInfo->id); continue; } @@ -426,7 +528,7 @@ void ManaServItemDB::readItemNode(XML::Node node, const std::string &filename) int duration = effectChild.getProperty("duration", 0); if (attribute.empty() || !value) { - logger->log("Warning: incomplete modifier definition in %s, skipping.", filename.c_str()); + Log::warn("Incomplete modifier definition in %s, skipping.", filename.c_str()); continue; } auto it = extraStats.cbegin(); @@ -435,7 +537,7 @@ void ManaServItemDB::readItemNode(XML::Node node, const std::string &filename) ++it; if (it == extraStats.end()) { - logger->log("Warning: unknown modifier tag %s in %s, skipping.", attribute.c_str(), filename.c_str()); + Log::warn("Unknown modifier tag %s in %s, skipping.", attribute.c_str(), filename.c_str()); continue; } effect.push_back( diff --git a/src/resources/itemdb.h b/src/resources/itemdb.h index ef0985a3..69620122 100644 --- a/src/resources/itemdb.h +++ b/src/resources/itemdb.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef ITEM_MANAGER_H -#define ITEM_MANAGER_H +#pragma once #include <list> #include <map> @@ -134,13 +133,18 @@ class ItemDB /** * Loads the sound references contained in a <sound> tag. */ - void loadSoundRef(ItemInfo &itemInfo, XML::Node node); + void loadSoundRef(ItemInfo &itemInfo, XML::Node node); /** * Loads the floor item references contained in a <floor> tag. */ void loadFloorSprite(SpriteDisplay &display, XML::Node node); + /** + * Loads the <replace> tag. + */ + void loadReplacement(ItemInfo &info, XML::Node replaceNode); + // Items database std::map<int, ItemInfo *> mItemInfos; std::map<std::string, ItemInfo *> mNamedItemInfos; @@ -204,5 +208,3 @@ class ManaServItemDB : public ItemDB } // namespace ManaServ extern ItemDB *itemDb; - -#endif diff --git a/src/resources/iteminfo.h b/src/resources/iteminfo.h index 78c808da..62e4796d 100644 --- a/src/resources/iteminfo.h +++ b/src/resources/iteminfo.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef ITEMINFO_H -#define ITEMINFO_H +#pragma once #include "being.h" @@ -32,8 +31,8 @@ enum class EquipmentSoundEvent { - STRIKE, - HIT + Strike, + Hit }; /** @@ -68,6 +67,32 @@ namespace ManaServ { class ManaServItemDB; } +enum ReplacementDirection : uint8_t +{ + DIRECTION_ALL = DIRECTION_DEFAULT, + DIRECTION_DEAD = DIRECTION_INVALID, + DIRECTION_UNKNOWN, +}; + +enum ReplacementSprite : uint8_t +{ + SPRITE_UNKNOWN = 254, + SPRITE_ALL = 255, +}; + +struct Replacement +{ + struct Item + { + int from = 0; // ID to replace (0: any) + int to = 0; // Replace with this ID (0: remove) + }; + + uint8_t sprite = SPRITE_ALL; // sprite slot to replace + uint8_t direction = DIRECTION_ALL; // direction in which to replace + std::vector<Item> items; // specific items to replace (empty: remove) +}; + /** * Defines a class for storing generic item infos. */ @@ -110,6 +135,8 @@ public: ItemType type = ITEM_UNUSABLE; /**< Item type. */ + std::vector<Replacement> replacements; + const std::string &getSprite(Gender gender, int race) const; const std::string &getSound(EquipmentSoundEvent event) const; @@ -162,5 +189,3 @@ enum EquipmentSlot }; } // namespace TmwAthena - -#endif // ITEMINFO_H diff --git a/src/resources/mapreader.cpp b/src/resources/mapreader.cpp index b5a5e258..3ca5763c 100644 --- a/src/resources/mapreader.cpp +++ b/src/resources/mapreader.cpp @@ -77,7 +77,7 @@ static std::string resolveRelativePath(std::string base, std::string relative) Map *MapReader::readMap(const std::string &filename) { - logger->log("Attempting to read map %s", filename.c_str()); + Log::info("Attempting to read map %s", filename.c_str()); Map *map = nullptr; XML::Document doc(filename); @@ -89,7 +89,7 @@ Map *MapReader::readMap(const std::string &filename) { if (node.name() != "map") { - logger->log("Error: Not a map file (%s)!", filename.c_str()); + Log::error("Not a map file (%s)!", filename.c_str()); } else { @@ -98,7 +98,7 @@ Map *MapReader::readMap(const std::string &filename) } else { - logger->log("Error while parsing map file (%s)!", filename.c_str()); + Log::info("Error while parsing map file (%s)!", filename.c_str()); } if (map) @@ -119,9 +119,9 @@ Map *MapReader::readMap(XML::Node node, const std::string &path) if (tilew < 0 || tileh < 0) { - logger->log("MapReader: Warning: " - "Unitialized tile width or height value for map: %s", - path.c_str()); + Log::info("MapReader: Warning: " + "Unitialized tile width or height value for map: %s", + path.c_str()); return nullptr; } @@ -174,15 +174,15 @@ Map *MapReader::readMap(XML::Node node, const std::string &path) const int objW = objectNode.getProperty("width", 0); const int objH = objectNode.getProperty("height", 0); - logger->log("- Loading object name: %s type: %s at %d:%d", - objName.c_str(), objType.c_str(), - objX, objY); + Log::info("- Loading object name: %s type: %s at %d:%d", + objName.c_str(), objType.c_str(), + objX, objY); if (objType == "PARTICLE_EFFECT") { if (objName.empty()) { - logger->log(" Warning: No particle file given"); + Log::info(" Warning: No particle file given"); continue; } @@ -203,7 +203,7 @@ Map *MapReader::readMap(XML::Node node, const std::string &path) } else { - logger->log(" Warning: Unknown object type"); + Log::info(" Warning: Unknown object type"); } } } @@ -293,7 +293,7 @@ static void readLayer(XML::Node node, Map *map) map->addLayer(layer); } - logger->log("- Loading layer \"%s\"", name.c_str()); + Log::info("- Loading layer \"%s\"", name.c_str()); int x = 0; int y = 0; @@ -333,8 +333,8 @@ static void readLayer(XML::Node node, Map *map) if (!compression.empty() && compression != "gzip" && compression != "zlib") { - logger->log("Warning: only gzip or zlib layer " - "compression supported!"); + Log::warn("Only gzip or zlib layer " + "compression supported!"); return; } @@ -383,7 +383,7 @@ static void readLayer(XML::Node node, Map *map) if (!inflated) { - logger->log("Error: Could not decompress layer!"); + Log::error("Could not decompress layer!"); return; } } @@ -415,7 +415,7 @@ static void readLayer(XML::Node node, Map *map) const auto data = childNode.textContent(); if (data.empty()) { - logger->log("Error: CSV layer data is empty!"); + Log::error("CSV layer data is empty!"); continue; } @@ -432,7 +432,7 @@ static void readLayer(XML::Node node, Map *map) if (errno == ERANGE) { - logger->log("Error: Range error in tile layer data!"); + Log::error("Range error in tile layer data!"); break; } @@ -452,7 +452,7 @@ static void readLayer(XML::Node node, Map *map) pos = strchr(end, ','); if (!pos) { - logger->log("Error: CSV layer data too short!"); + Log::error("CSV layer data too short!"); break; } ++pos; @@ -527,7 +527,7 @@ static Tileset *readTileset(XML::Node node, const std::string &path, std::string sourceStr = resolveRelativePath(pathDir, source); ResourceManager *resman = ResourceManager::getInstance(); - auto tilebmp = resman->getImageRef(sourceStr); + auto tilebmp = resman->getImage(sourceStr); if (tilebmp) { @@ -536,8 +536,7 @@ static Tileset *readTileset(XML::Node node, const std::string &path, } else { - logger->log("Warning: Failed to load tileset (%s)", - source.c_str()); + Log::warn("Failed to load tileset (%s)", source.c_str()); } } } diff --git a/src/resources/mapreader.h b/src/resources/mapreader.h index 105c5d1d..e646fb04 100644 --- a/src/resources/mapreader.h +++ b/src/resources/mapreader.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef MAPREADER_H -#define MAPREADER_H +#pragma once #include "utils/xml.h" @@ -45,5 +44,3 @@ public: */ static Map *readMap(XML::Node node, const std::string &path); }; - -#endif // MAPREADER_H diff --git a/src/resources/monsterdb.cpp b/src/resources/monsterdb.cpp index 215ca2f8..7f092a0e 100644 --- a/src/resources/monsterdb.cpp +++ b/src/resources/monsterdb.cpp @@ -54,7 +54,7 @@ void MonsterDB::init() unload(); // This can be overridden by an 'offset' attribute on a 'monsters' root tag. - mMonsterIdOffset = Net::getNetworkType() == ServerType::TMWATHENA ? OLD_TMWATHENA_OFFSET : 0; + mMonsterIdOffset = Net::getNetworkType() == ServerType::TmwAthena ? OLD_TMWATHENA_OFFSET : 0; } void MonsterDB::setMonsterIdOffset(int offset) @@ -95,27 +95,27 @@ void MonsterDB::readMonsterNode(XML::Node node, const std::string &filename) if (event == "hit") { - currentInfo->addSound(SoundEvent::HIT, soundFile); + currentInfo->addSound(SoundEvent::Hit, soundFile); } else if (event == "miss") { - currentInfo->addSound(SoundEvent::MISS, soundFile); + currentInfo->addSound(SoundEvent::Miss, soundFile); } else if (event == "hurt") { - currentInfo->addSound(SoundEvent::HURT, soundFile); + currentInfo->addSound(SoundEvent::Hurt, soundFile); } else if (event == "die") { - currentInfo->addSound(SoundEvent::DIE, soundFile); + currentInfo->addSound(SoundEvent::Die, soundFile); } else { - logger->log("MonsterDB: Warning, sound effect %s for " - "unknown event %s of monster %s in %s", - soundFile.c_str(), event.c_str(), - currentInfo->name.c_str(), - filename.c_str()); + Log::info("MonsterDB: Warning, sound effect %s for " + "unknown event %s of monster %s in %s", + soundFile.c_str(), event.c_str(), + currentInfo->name.c_str(), + filename.c_str()); } } else if (spriteNode.name() == "attack") @@ -168,7 +168,7 @@ BeingInfo *MonsterDB::get(int id) if (i == mMonsterInfos.end()) { - logger->log("MonsterDB: Warning, unknown monster ID %d requested", id); + Log::info("MonsterDB: Warning, unknown monster ID %d requested", id); return BeingInfo::Unknown; } diff --git a/src/resources/monsterdb.h b/src/resources/monsterdb.h index ff709486..ec71952f 100644 --- a/src/resources/monsterdb.h +++ b/src/resources/monsterdb.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef MONSTER_DB_H -#define MONSTER_DB_H +#pragma once #include "utils/xml.h" @@ -43,5 +42,3 @@ namespace MonsterDB BeingInfo *get(int id); } - -#endif diff --git a/src/resources/music.cpp b/src/resources/music.cpp index 12c723bd..b73d89ce 100644 --- a/src/resources/music.cpp +++ b/src/resources/music.cpp @@ -33,14 +33,14 @@ Music::~Music() Mix_FreeMusic(mMusic); } -Resource *Music::load(SDL_RWops *rw) +Music *Music::load(SDL_RWops *rw) { if (Mix_Music *music = Mix_LoadMUS_RW(rw, 1)) { return new Music(music); } - logger->log("Error, failed to load music: %s", Mix_GetError()); + Log::info("Error, failed to load music: %s", Mix_GetError()); return nullptr; } diff --git a/src/resources/music.h b/src/resources/music.h index 0c445b2b..d22257da 100644 --- a/src/resources/music.h +++ b/src/resources/music.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef MUSIC_H -#define MUSIC_H +#pragma once #include "resources/resource.h" @@ -39,10 +38,10 @@ class Music : public Resource * * @param rw The SDL_RWops to load the music data from. * - * @return <code>NULL</code> if the an error occurred, a valid pointer - * otherwise. + * @return <code>nullptr</code> if the an error occurred, a valid + * pointer otherwise. */ - static Resource *load(SDL_RWops *rw); + static Music *load(SDL_RWops *rw); /** * Plays the music. @@ -61,5 +60,3 @@ class Music : public Resource Mix_Music *mMusic; }; - -#endif diff --git a/src/resources/npcdb.cpp b/src/resources/npcdb.cpp index 6b1c3150..44292525 100644 --- a/src/resources/npcdb.cpp +++ b/src/resources/npcdb.cpp @@ -46,7 +46,7 @@ void NPCDB::readNPCNode(XML::Node node, const std::string &filename) int id = node.getProperty("id", 0); if (id == 0) { - logger->log("NPC Database: NPC with missing ID in %s", filename.c_str()); + Log::info("NPC Database: NPC with missing ID in %s", filename.c_str()); return; } @@ -94,7 +94,7 @@ BeingInfo *NPCDB::get(int id) if (i == mNPCInfos.end()) { - logger->log("NPCDB: Warning, unknown NPC ID %d requested", id); + Log::info("NPCDB: Warning, unknown NPC ID %d requested", id); return BeingInfo::Unknown; } diff --git a/src/resources/npcdb.h b/src/resources/npcdb.h index 306167de..779f4919 100644 --- a/src/resources/npcdb.h +++ b/src/resources/npcdb.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef NPC_DB_H -#define NPC_DB_H +#pragma once #include <string> #include "utils/xml.h" @@ -42,5 +41,3 @@ namespace NPCDB BeingInfo *get(int id); } - -#endif diff --git a/src/resources/questdb.cpp b/src/resources/questdb.cpp new file mode 100644 index 00000000..a3dda637 --- /dev/null +++ b/src/resources/questdb.cpp @@ -0,0 +1,227 @@ +/* + * The Mana Client + * Copyright (C) 2025 The Mana Developers + * + * This file is part of The Mana Client. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "resources/questdb.h" +#include "log.h" + +#include <algorithm> +#include <unordered_map> +#include <utility> + +namespace QuestDB { + +// The quests are stored in a map using their variable ID as the key +static std::unordered_map<int, Quest> quests; + +// Helper function to check if a container contains a value +template<typename Container, typename Value> +static bool contains(const Container &container, const Value &value) +{ + return std::find(container.begin(), container.end(), value) != container.end(); +} + +void readQuestVarNode(XML::Node node, const std::string &filename) +{ + int varId = 0; + if (!node.attribute("id", varId)) + return; + + Quest &quest = quests[varId]; + + for (auto child : node.children()) + { + if (child.name() == "effect") + { + QuestEffect &effect = quest.effects.emplace_back(); + child.attribute("map", effect.map); + child.attribute("npc", effect.npcId); + child.attribute("effect", effect.statusEffectId); + child.attribute("value", effect.values); + + if (effect.map.empty() || effect.npcId == 0 || effect.statusEffectId == 0 || effect.values.empty()) + { + Log::warn("effect node for var %d is missing required attributes", varId); + } + } + else if (child.name() == "quest") + { + QuestState &state = quest.states.emplace_back(); + child.attribute("name", state.name); + child.attribute("group", state.group); + child.attribute("incomplete", state.incomplete); + child.attribute("complete", state.complete); + + if (state.incomplete.empty() && state.complete.empty()) + { + Log::warn("quest node for var %d ('%s') has neither 'complete' nor 'incomplete' values", + varId, state.name.c_str()); + continue; + } + + for (auto questChild : child.children()) + { + QuestRowType rowType; + std::string_view tag = questChild.name(); + if (tag == "text") + rowType = QuestRowType::Text; + else if (tag == "name") + rowType = QuestRowType::Name; + else if (tag == "reward") + rowType = QuestRowType::Reward; + else if (tag == "questgiver" || tag == "giver") + rowType = QuestRowType::Giver; + else if (tag == "coordinates") + rowType = QuestRowType::Coordinates; + else if (tag == "npc") + rowType = QuestRowType::NPC; + else + { + Log::warn("unknown quest row type '%s' for var %d ('%s')", + tag.data(), varId, state.name.c_str()); + continue; + } + + QuestRow &row = state.rows.emplace_back(rowType); + row.text = questChild.textContent(); + + if (rowType == QuestRowType::Coordinates) + { + questChild.attribute("x", row.x); + questChild.attribute("y", row.y); + } + } + } + } +} + +void unload() +{ + quests.clear(); +} + +bool hasQuests() +{ + return !quests.empty(); +} + +// In quests, the map name may include the file extension. This is discouraged +// but supported for compatibility. +static std::string_view baseName(const std::string &fileName) +{ + auto pos = fileName.find_last_of('.'); + return pos == std::string::npos ? fileName : std::string_view(fileName.data(), pos); +} + +QuestEffectMap getActiveEffects(const QuestVars &questVars, + const std::string &mapName) +{ + QuestEffectMap activeEffects; + + for (auto &[var, quest] : std::as_const(quests)) + { + auto value = questVars.get(var); + + for (auto &effect : quest.effects) + { + if (baseName(effect.map) != mapName) + continue; + if (!contains(effect.values, value)) + continue; + + activeEffects.set(effect.npcId, effect.statusEffectId); + } + } + + return activeEffects; +} + +std::vector<QuestEntry> getQuestsEntries(const QuestVars &questVars, + bool skipCompleted) +{ + std::vector<QuestEntry> activeQuests; + + for (auto &[varId, quest] : std::as_const(quests)) + { + auto value = questVars.get(varId); + + for (auto &state : quest.states) + { + bool matchesIncomplete = contains(state.incomplete, value); + bool matchesComplete = contains(state.complete, value); + + if (skipCompleted && matchesComplete) + continue; + + if (matchesIncomplete || matchesComplete) + { + QuestEntry &entry = activeQuests.emplace_back(); + entry.varId = varId; + entry.completed = matchesComplete; + entry.state = &state; + } + } + } + + return activeQuests; +} + +static std::pair<int, int> countQuestEntries(const Quest &quest, int value) +{ + int totalEntries = 0; + int completedEntries = 0; + + for (const auto &state : quest.states) + { + bool matchesIncomplete = contains(state.incomplete, value); + bool matchesComplete = contains(state.complete, value); + + if (matchesIncomplete || matchesComplete) + { + totalEntries++; + if (matchesComplete) + completedEntries++; + } + } + + return { totalEntries, completedEntries }; +} + +QuestChange questChange(int varId, int oldValue, int newValue) +{ + if (newValue == oldValue) + return QuestChange::None; + + auto questIt = quests.find(varId); + if (questIt == quests.end()) + return QuestChange::None; + + const Quest &quest = questIt->second; + + auto [oldQuestEntries, oldCompletedEntries] = countQuestEntries(quest, oldValue); + auto [newQuestEntries, newCompletedEntries] = countQuestEntries(quest, newValue); + + if (newCompletedEntries > oldCompletedEntries) + return QuestChange::Completed; + if (newQuestEntries > oldQuestEntries) + return QuestChange::New; + return QuestChange::None; +} + +} // namespace QuestDB diff --git a/src/resources/questdb.h b/src/resources/questdb.h new file mode 100644 index 00000000..5e943e76 --- /dev/null +++ b/src/resources/questdb.h @@ -0,0 +1,138 @@ +/* + * The Mana Client + * Copyright (C) 2025 The Mana Developers + * + * This file is part of The Mana Client. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/xml.h" + +#include <map> +#include <string> +#include <vector> + +/** + * A map that returns a default value for non-existent keys. + */ +template<typename Key, typename Value, Value def = Value()> +class MapWithDefault +{ +public: + void set(Key key, Value value) + { + mVars[key] = value; + } + + Value get(Key key) const + { + auto it = mVars.find(key); + return it != mVars.end() ? it->second : def; + } + + void clear() + { + mVars.clear(); + } + +private: + std::map<Key, Value> mVars; +}; + +struct QuestEffect +{ + std::vector<int> values; // Quest variable values to which the effect applies + std::string map; // Map name the NPC is located on + int npcId = 0; + int statusEffectId = 0; +}; + +// Map of quest variables, from variable ID to value +using QuestVars = MapWithDefault<int, int>; + +// Map of quest effects, from NPC ID to status effect ID +using QuestEffectMap = MapWithDefault<int, int>; + +enum class QuestRowType +{ + Text, + Name, + Reward, + Giver, + Coordinates, + NPC +}; + +struct QuestRow +{ + QuestRow(QuestRowType type) + : type(type) + {} + + QuestRowType type; + std::string text; + int x = 0; + int y = 0; +}; + +struct QuestState +{ + std::string name; // Name of the quest in this state + std::string group; // Group name of the quest in this state + std::vector<int> incomplete; // Quest variable values for this state (quest incomplete) + std::vector<int> complete; // Quest variable values for this state (quest complete) + std::vector<QuestRow> rows; // Rows of text in the Quests window for this state +}; + +struct Quest +{ + std::vector<QuestEffect> effects; + std::vector<QuestState> states; +}; + +struct QuestEntry +{ + int varId; + bool completed; + const QuestState *state; + + const std::string &name() const { return state->name; } + const std::vector<QuestRow> &rows() const { return state->rows; } +}; + +enum class QuestChange +{ + None, + New, + Completed +}; + +namespace QuestDB +{ + void readQuestVarNode(XML::Node node, const std::string &filename); + void unload(); + + bool hasQuests(); + + QuestEffectMap getActiveEffects(const QuestVars &questVars, + const std::string &mapName); + + std::vector<QuestEntry> getQuestsEntries(const QuestVars &questVars, + bool skipCompleted = false); + + QuestChange questChange(int varId, int oldValue, int newValue); +}; diff --git a/src/resources/resource.cpp b/src/resources/resource.cpp index cdff8060..17864cf5 100644 --- a/src/resources/resource.cpp +++ b/src/resources/resource.cpp @@ -31,7 +31,7 @@ void Resource::decRef(OrphanPolicy orphanPolicy) { // Reference may not already have reached zero if (mRefCount == 0) { - logger->log("Warning: mRefCount already zero for %s", mIdPath.c_str()); + Log::warn("mRefCount already zero for %s", mIdPath.c_str()); assert(false); } diff --git a/src/resources/resource.h b/src/resources/resource.h index e1f37d73..ba8d17cc 100644 --- a/src/resources/resource.h +++ b/src/resources/resource.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef RESOURCE_H -#define RESOURCE_H +#pragma once #include <ctime> #include <string> @@ -151,11 +150,13 @@ public: * This is currently necessary to avoid calls to decRef on instances of * SubImage, which are not reference counted resources. */ - void release() - { mResource = nullptr; } + RESOURCE *release() + { + RESOURCE *resource = mResource; + mResource = nullptr; + return resource; + } private: RESOURCE *mResource; }; - -#endif diff --git a/src/resources/resourcemanager.cpp b/src/resources/resourcemanager.cpp index ff83f422..2857c0df 100644 --- a/src/resources/resourcemanager.cpp +++ b/src/resources/resourcemanager.cpp @@ -31,10 +31,7 @@ #include "resources/soundeffect.h" #include "resources/spritedef.h" -#include "utils/zlib.h" -#include "utils/physfsrwops.h" - -#include <physfs.h> +#include "utils/filesystem.h" #include <SDL_image.h> @@ -47,67 +44,53 @@ ResourceManager *ResourceManager::instance = nullptr; ResourceManager::ResourceManager() - : mOldestOrphan(0) { - logger->log("Initializing resource manager..."); + Log::info("Initializing resource manager..."); } ResourceManager::~ResourceManager() { - mResources.insert(mOrphanedResources.begin(), mOrphanedResources.end()); - - // Release any remaining spritedefs first because they depend on image sets - auto iter = mResources.begin(); - while (iter != mResources.end()) + auto cleanupResources = [&](auto match) { - if (dynamic_cast<SpriteDef*>(iter->second) != nullptr) - { - cleanUp(iter->second); - auto toErase = iter; - ++iter; - mResources.erase(toErase); - } - else - { - ++iter; - } - } + // Include any orphaned resources into the main list for cleanup + mResources.insert(mOrphanedResources.begin(), mOrphanedResources.end()); + mOrphanedResources.clear(); - // Release any remaining image sets first because they depend on images - iter = mResources.begin(); - while (iter != mResources.end()) - { - if (dynamic_cast<ImageSet*>(iter->second) != nullptr) - { - cleanUp(iter->second); - auto toErase = iter; - ++iter; - mResources.erase(toErase); - } - else + for (auto iter = mResources.begin(); iter != mResources.end(); ) { - ++iter; + if (match(iter->second)) + { + cleanUp(iter->second); + iter = mResources.erase(iter); + } + else + { + ++iter; + } } - } + }; - // Release remaining resources, logging the number of dangling references. - iter = mResources.begin(); - while (iter != mResources.end()) - { - cleanUp(iter->second); - ++iter; - } + // SpriteDef references ImageSet + cleanupResources([](Resource *res) { return dynamic_cast<SpriteDef *>(res); }); + + // ImageSet references Image + cleanupResources([](Resource *res) { return dynamic_cast<ImageSet *>(res); }); + + // Release remaining resources + cleanupResources([](Resource *res) { return true; }); + + assert(mOrphanedResources.empty()); } void ResourceManager::cleanUp(Resource *res) { if (res->mRefCount > 0) { - logger->log("ResourceManager::~ResourceManager() cleaning up %d " - "reference%s to %s", - res->mRefCount, - (res->mRefCount == 1) ? "" : "s", - res->mIdPath.c_str()); + Log::info("ResourceManager::~ResourceManager() cleaning up %d " + "reference%s to %s", + res->mRefCount, + (res->mRefCount == 1) ? "" : "s", + res->mIdPath.c_str()); } delete res; @@ -117,7 +100,7 @@ void ResourceManager::cleanOrphans() { // Delete orphaned resources after 30 seconds. time_t oldest = time(nullptr); - time_t threshold = oldest - 30; + const time_t threshold = oldest - 30; if (mOrphanedResources.empty() || mOldestOrphan >= threshold) return; @@ -135,7 +118,7 @@ void ResourceManager::cleanOrphans() } else { - logger->log("ResourceManager::release(%s)", res->mIdPath.c_str()); + Log::info("ResourceManager::release(%s)", res->mIdPath.c_str()); iter = mOrphanedResources.erase(iter); delete res; // delete only after removal from list, to avoid issues in recursion } @@ -144,19 +127,14 @@ void ResourceManager::cleanOrphans() mOldestOrphan = oldest; } -bool ResourceManager::setWriteDir(const std::string &path) -{ - return (bool) PHYSFS_setWriteDir(path.c_str()); -} - bool ResourceManager::addToSearchPath(const std::string &path, bool append) { - logger->log("Adding to PhysicsFS: %s", path.c_str()); - if (!PHYSFS_mount(path.c_str(), nullptr, append ? 1 : 0)) + if (!FS::addToSearchPath(path, append)) { - logger->log("Error: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + Log::error("Couldn't add search path: %s (%s)", path.c_str(), FS::getLastError()); return false; } + Log::info("Added search path: %s", path.c_str()); return true; } @@ -164,58 +142,33 @@ void ResourceManager::searchAndAddArchives(const std::string &path, const std::string &ext, bool append) { - const char *dirSep = PHYSFS_getDirSeparator(); - char **list = PHYSFS_enumerateFiles(path.c_str()); + const char *dirSep = FS::getDirSeparator(); - for (char **i = list; *i; i++) + for (auto fileName : FS::enumerateFiles(path)) { - size_t len = strlen(*i); + const size_t len = strlen(fileName); - if (len > ext.length() && !ext.compare((*i)+(len - ext.length()))) + if (len > ext.length() && ext != (fileName + (len - ext.length()))) { - std::string file, realPath, archive; - - file = path + (*i); - realPath = std::string(PHYSFS_getRealDir(file.c_str())); - archive = realPath + dirSep + file; - - addToSearchPath(archive, append); + std::string file = path + fileName; + if (auto realDir = FS::getRealDir(file)) + { + std::string archive = std::string(*realDir) + dirSep + file; + addToSearchPath(archive, append); + } } } - - PHYSFS_freeList(list); -} - -bool ResourceManager::mkdir(const std::string &path) -{ - return (bool) PHYSFS_mkdir(path.c_str()); -} - -bool ResourceManager::exists(const std::string &path) -{ - return PHYSFS_exists(path.c_str()); -} - -bool ResourceManager::isDirectory(const std::string &path) -{ - PHYSFS_Stat stat; - if (PHYSFS_stat(path.c_str(), &stat) != 0) - { - return stat.filetype == PHYSFS_FILETYPE_DIRECTORY; - } - return false; } std::string ResourceManager::getPath(const std::string &file) { - // get the real path to the file - const char* tmp = PHYSFS_getRealDir(file.c_str()); + // Get the real directory of the file + auto realDir = FS::getRealDir(file); std::string path; - // if the file is not in the search path, then its NULL - if (tmp) + if (realDir) { - path = std::string(tmp) + "/" + file; + path = std::string(*realDir) + "/" + file; } else { @@ -226,11 +179,6 @@ std::string ResourceManager::getPath(const std::string &file) return path; } -SDL_RWops *ResourceManager::open(const std::string &path) -{ - return PHYSFSRWOPS_openRead(path.c_str()); -} - Resource *ResourceManager::get(const std::string &idPath, const std::function<Resource *()> &generator) { @@ -238,7 +186,6 @@ Resource *ResourceManager::get(const std::string &idPath, auto resIter = mResources.find(idPath); if (resIter != mResources.end()) { - resIter->second->incRef(); return resIter->second; } @@ -248,44 +195,41 @@ Resource *ResourceManager::get(const std::string &idPath, Resource *res = resIter->second; mResources.insert(*resIter); mOrphanedResources.erase(resIter); - res->incRef(); return res; } Resource *resource = generator(); - if (resource) { - resource->incRef(); resource->mIdPath = idPath; mResources[idPath] = resource; cleanOrphans(); } - // Returns NULL if the object could not be created. return resource; } -Resource *ResourceManager::get(const std::string &path, loader fun) +ResourceRef<Music> ResourceManager::getMusic(const std::string &path) { - return get(path, [&] () -> Resource * { - if (SDL_RWops *rw = open(path)) - return fun(rw); + return static_cast<Music*>(get(path, [&] () -> Resource * { + if (SDL_RWops *rw = FS::openBufferedRWops(path)) + return Music::load(rw); + return nullptr; - }); + })); } -Music *ResourceManager::getMusic(const std::string &idPath) +ResourceRef<SoundEffect> ResourceManager::getSoundEffect(const std::string &path) { - return static_cast<Music*>(get(idPath, Music::load)); -} + return static_cast<SoundEffect*>(get(path, [&] () -> Resource * { + if (SDL_RWops *rw = FS::openBufferedRWops(path)) + return SoundEffect::load(rw); -SoundEffect *ResourceManager::getSoundEffect(const std::string &idPath) -{ - return static_cast<SoundEffect*>(get(idPath, SoundEffect::load)); + return nullptr; + })); } -Image *ResourceManager::getImage(const std::string &idPath) +ResourceRef<Image> ResourceManager::getImage(const std::string &idPath) { return static_cast<Image*>(get(idPath, [&] () -> Resource * { std::string path = idPath; @@ -296,7 +240,7 @@ Image *ResourceManager::getImage(const std::string &idPath) d = std::make_unique<Dye>(path.substr(p + 1)); path = path.substr(0, p); } - SDL_RWops *rw = open(path); + SDL_RWops *rw = FS::openRWops(path); if (!rw) return nullptr; @@ -306,21 +250,14 @@ Image *ResourceManager::getImage(const std::string &idPath) })); } -ResourceRef<Image> ResourceManager::getImageRef(const std::string &idPath) -{ - ResourceRef<Image> img = getImage(idPath); - img->decRef(); // remove ref added by ResourceManager::get - return img; -} - -ImageSet *ResourceManager::getImageSet(const std::string &imagePath, - int w, int h) +ResourceRef<ImageSet> ResourceManager::getImageSet(const std::string &imagePath, + int w, int h) { std::stringstream ss; ss << imagePath << "[" << w << "x" << h << "]"; return static_cast<ImageSet*>(get(ss.str(), [&] () -> Resource * { - auto img = getImageRef(imagePath); + auto img = getImage(imagePath); if (!img) return nullptr; @@ -328,7 +265,7 @@ ImageSet *ResourceManager::getImageSet(const std::string &imagePath, })); } -SpriteDef *ResourceManager::getSprite(const std::string &path, int variant) +ResourceRef<SpriteDef> ResourceManager::getSprite(const std::string &path, int variant) { std::stringstream ss; ss << path << "[" << variant << "]"; @@ -373,101 +310,3 @@ void ResourceManager::deleteInstance() delete instance; instance = nullptr; } - -void *ResourceManager::loadFile(const std::string &filename, int &filesize, - bool inflate) -{ - // Attempt to open the specified file using PhysicsFS - PHYSFS_file *file = PHYSFS_openRead(filename.c_str()); - - // If the handler is an invalid pointer indicate failure - if (file == nullptr) - { - logger->log("Warning: Failed to load %s: %s", - filename.c_str(), PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); - return nullptr; - } - - // Log the real dir of the file - logger->log("Loaded %s/%s", PHYSFS_getRealDir(filename.c_str()), - filename.c_str()); - - // Get the size of the file - filesize = PHYSFS_fileLength(file); - - // Allocate memory and load the file - void *buffer = malloc(filesize); - PHYSFS_readBytes(file, buffer, filesize); - - // Close the file and let the user deallocate the memory - PHYSFS_close(file); - - if (inflate && filename.find(".gz", filename.length() - 3) - != std::string::npos) - { - unsigned char *inflated; - - // Inflate the gzipped map data - filesize = inflateMemory((unsigned char*) buffer, filesize, inflated); - free(buffer); - - buffer = inflated; - - if (!buffer) - { - logger->log("Could not decompress file: %s", filename.c_str()); - } - } - - return buffer; -} - -bool ResourceManager::copyFile(const std::string &src, const std::string &dst) -{ - PHYSFS_file *srcFile = PHYSFS_openRead(src.c_str()); - if (!srcFile) - { - logger->log("Read error: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); - return false; - } - PHYSFS_file *dstFile = PHYSFS_openWrite(dst.c_str()); - if (!dstFile) - { - logger->log("Write error: %s", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); - PHYSFS_close(srcFile); - return false; - } - - int fileSize = PHYSFS_fileLength(srcFile); - void *buf = malloc(fileSize); - PHYSFS_readBytes(srcFile, buf, fileSize); - PHYSFS_writeBytes(dstFile, buf, fileSize); - - PHYSFS_close(srcFile); - PHYSFS_close(dstFile); - free(buf); - return true; -} - -std::vector<std::string> ResourceManager::loadTextFile( - const std::string &fileName) -{ - int contentsLength; - char *fileContents = (char*)loadFile(fileName, contentsLength); - std::vector<std::string> lines; - - if (!fileContents) - { - logger->log("Couldn't load text file: %s", fileName.c_str()); - return lines; - } - - std::istringstream iss(std::string(fileContents, contentsLength)); - std::string line; - - while (getline(iss, line)) - lines.push_back(line); - - free(fileContents); - return lines; -} diff --git a/src/resources/resourcemanager.h b/src/resources/resourcemanager.h index d1c32d8c..728a9b74 100644 --- a/src/resources/resourcemanager.h +++ b/src/resources/resourcemanager.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef RESOURCE_MANAGER_H -#define RESOURCE_MANAGER_H +#pragma once #include "resources/resource.h" @@ -28,7 +27,6 @@ #include <functional> #include <map> #include <string> -#include <vector> class Image; class ImageSet; @@ -36,8 +34,6 @@ class Music; class SoundEffect; class SpriteDef; -struct SDL_RWops; - /** * A class for loading and managing resources. */ @@ -46,9 +42,6 @@ class ResourceManager friend class Resource; public: - - using loader = Resource *(*)(SDL_RWops *); - ResourceManager(); /** @@ -58,43 +51,20 @@ class ResourceManager ~ResourceManager(); /** - * Sets the write directory. - * - * @param path The path of the directory to be added. - * @return <code>true</code> on success, <code>false</code> otherwise. - */ - bool setWriteDir(const std::string &path); - - /** * Adds a directory or archive to the search path. If append is true * then the directory is added to the end of the search path, otherwise * it is added at the front. * * @return <code>true</code> on success, <code>false</code> otherwise. */ - bool addToSearchPath(const std::string &path, bool append); + static bool addToSearchPath(const std::string &path, bool append); /** * Searches for zip files and adds them to the search path. */ - void searchAndAddArchives(const std::string &path, - const std::string &ext, - bool append); - - /** - * Creates a directory in the write path - */ - bool mkdir(const std::string &path); - - /** - * Checks whether the given file or directory exists in the search path - */ - bool exists(const std::string &path); - - /** - * Checks whether the given path is a directory. - */ - bool isDirectory(const std::string &path); + static void searchAndAddArchives(const std::string &path, + const std::string &ext, + bool append); /** * Returns the real path to a file. Note that this method will always @@ -103,106 +73,34 @@ class ResourceManager * @param file The file to get the real path to. * @return The real path. */ - std::string getPath(const std::string &file); - - /** - * Opens a file for reading. The caller is responsible for closing the - * file. - * - * @param path The file name. - * @return A valid SDL_RWops pointer or <code>NULL</code> if the file - * could not be opened. - */ - SDL_RWops *open(const std::string &path); - - /** - * Creates a resource and adds it to the resource map. - * - * @param idPath The resource identifier path. - * @param fun A function for generating the resource. - * @param data Extra parameters for the generator. - * @return A valid resource or <code>NULL</code> if the resource could - * not be generated. - */ - Resource *get(const std::string &idPath, - const std::function<Resource *()> &generator); - - /** - * Loads a resource from a file and adds it to the resource map. - * - * @param path The file name. - * @param fun A function for parsing the file. - * @return A valid resource or <code>NULL</code> if the resource could - * not be loaded. - */ - Resource *get(const std::string &path, loader fun); - - /** - * Convenience wrapper around ResourceManager::get for loading - * images. - */ - Image *getImage(const std::string &idPath); + static std::string getPath(const std::string &file); /** - * Convenience wrapper around ResourceManager::get for loading - * images. Returns an automatically reference-counted resource. + * Loads the Image resource found at the given identifier path. The + * path can include a dye specification after a '|' character. */ - ResourceRef<Image> getImageRef(const std::string &idPath); + ResourceRef<Image> getImage(const std::string &idPath); /** - * Convenience wrapper around ResourceManager::get for loading - * songs. + * Loads the Music resource found at the given path. */ - Music *getMusic(const std::string &idPath); + ResourceRef<Music> getMusic(const std::string &path); /** - * Convenience wrapper around ResourceManager::get for loading - * samples. + * Loads the SoundEffect resource found at the given path. */ - SoundEffect *getSoundEffect(const std::string &idPath); + ResourceRef<SoundEffect> getSoundEffect(const std::string &path); /** - * Creates a image set based on the image referenced by the given - * path and the supplied sprite sizes + * Loads a image set based on the image referenced by the given path + * and the supplied sprite sizes. */ - ImageSet *getImageSet(const std::string &imagePath, int w, int h); + ResourceRef<ImageSet> getImageSet(const std::string &imagePath, int w, int h); /** - * Creates a sprite definition based on a given path and the supplied - * variant. + * Loads a SpriteDef based on a given path and the supplied variant. */ - SpriteDef *getSprite(const std::string &path, int variant = 0); - - /** - * Allocates data into a buffer pointer for raw data loading. The - * returned data is expected to be freed using <code>free()</code>. - * - * @param filename The name of the file to be loaded. - * @param filesize The size of the file that was loaded. - * @param inflate True to uncompress the file if the filename ends in - * ".gz", false to ignore that. - * - * @return An allocated byte array containing the data that was loaded, - * or <code>NULL</code> on fail. - */ - void *loadFile(const std::string &filename, int &filesize, - bool inflate = true); - - /** - * Copies a file from one place to another (useful for extracting - * raw files from a zip archive, for example) - * - * @param src Source file name - * @param dst Destination file name - * @return true on success, false on failure. An error message should be - * in the log file. - */ - bool copyFile(const std::string &src, const std::string &dst); - - /** - * Retrieves the contents of a text file. - */ - std::vector<std::string> loadTextFile(const std::string &fileName); + ResourceRef<SpriteDef> getSprite(const std::string &path, int variant = 0); /** * Returns an instance of the class, creating one if it does not @@ -217,6 +115,19 @@ class ResourceManager private: /** + * Looks up a resource, creating it with the generator function if it + * does not exist. Does not increment the reference count of the + * resource. + * + * @param idPath The resource identifier path. + * @param generator A function for generating the resource. + * @return A valid resource or <code>nullptr</code> if the resource could + * not be generated. + */ + Resource *get(const std::string &idPath, + const std::function<Resource *()> &generator); + + /** * Releases a resource, placing it in the set of orphaned resources. * Only called from Resource::decRef, */ @@ -238,7 +149,5 @@ class ResourceManager static ResourceManager *instance; std::map<std::string, Resource *> mResources; std::map<std::string, Resource *> mOrphanedResources; - time_t mOldestOrphan; + time_t mOldestOrphan = 0; }; - -#endif diff --git a/src/resources/settingsmanager.cpp b/src/resources/settingsmanager.cpp index 8966f976..eabd63ae 100644 --- a/src/resources/settingsmanager.cpp +++ b/src/resources/settingsmanager.cpp @@ -20,22 +20,24 @@ #include "resources/settingsmanager.h" -#include "configuration.h" #include "resources/attributes.h" +#include "resources/emotedb.h" #include "resources/hairdb.h" #include "resources/itemdb.h" #include "resources/monsterdb.h" -#include "resources/specialdb.h" #include "resources/npcdb.h" -#include "resources/emotedb.h" -#include "statuseffect.h" -#include "units.h" +#include "resources/abilitydb.h" +#include "resources/questdb.h" +#include "resources/statuseffectdb.h" #include "net/net.h" #include "utils/xml.h" #include "utils/path.h" + +#include "configuration.h" #include "log.h" +#include "units.h" namespace SettingsManager { @@ -52,10 +54,10 @@ namespace SettingsManager hairDB.init(); itemDb->init(); MonsterDB::init(); - SpecialDB::init(); + AbilityDB::init(); NPCDB::init(); EmoteDB::init(); - StatusEffect::init(); + StatusEffectDB::init(); Units::init(); // load stuff from settings @@ -76,13 +78,13 @@ namespace SettingsManager hairDB.checkStatus(); itemDb->checkStatus(); MonsterDB::checkStatus(); - SpecialDB::checkStatus(); + AbilityDB::checkStatus(); NPCDB::checkStatus(); EmoteDB::checkStatus(); - StatusEffect::checkStatus(); + StatusEffectDB::checkStatus(); Units::checkStatus(); - if (Net::getNetworkType() == ServerType::MANASERV) + if (Net::getNetworkType() == ServerType::ManaServ) { Attributes::informItemDB(); } @@ -90,11 +92,12 @@ namespace SettingsManager void unload() { - StatusEffect::unload(); + StatusEffectDB::unload(); EmoteDB::unload(); NPCDB::unload(); - SpecialDB::unload(); + AbilityDB::unload(); MonsterDB::unload(); + QuestDB::unload(); if (itemDb) itemDb->unload(); hairDB.unload(); @@ -106,7 +109,7 @@ namespace SettingsManager */ static bool loadFile(const std::string &filename) { - logger->log("Loading game settings from %s", filename.c_str()); + Log::info("Loading game settings from %s", filename.c_str()); XML::Document doc(filename); XML::Node node = doc.rootNode(); @@ -117,7 +120,7 @@ namespace SettingsManager // FIXME: check root node's name when bjorn decides it's time if (!node /*|| node.name() != "settings" */) { - logger->log("Settings Manager: %s is not a valid settings file!", filename.c_str()); + Log::info("Settings Manager: %s is not a valid settings file!", filename.c_str()); return false; } @@ -155,7 +158,7 @@ namespace SettingsManager // check if we're not entering a loop if (mIncludedFiles.find(includeFile) != mIncludedFiles.end()) { - logger->log("Warning: Circular include loop detecting while including %s from %s", includeFile.c_str(), filename.c_str()); + Log::warn("Circular include loop detecting while including %s from %s", includeFile.c_str(), filename.c_str()); } else { @@ -164,7 +167,7 @@ namespace SettingsManager } else { - logger->log("Warning: <include> element without 'file' or 'name' attribute in %s", filename.c_str()); + Log::warn("<include> element without 'file' or 'name' attribute in %s", filename.c_str()); } } else if (childNode.name() == "option") @@ -176,11 +179,10 @@ namespace SettingsManager if (!name.empty()) paths.setValue(name, value); else - logger->log("Warning: option without a name found in %s", filename.c_str()); + Log::warn("option without a name found in %s", filename.c_str()); } else if (childNode.name() == "attribute") { - // map config Attributes::readAttributeNode(childNode, filename); } else if (childNode.name() == "points") @@ -213,35 +215,30 @@ namespace SettingsManager { MonsterDB::readMonsterNode(childNode, filename); } - else if (childNode.name() == "special-set") + else if (childNode.name() == "ability") { - SpecialDB::readSpecialSetNode(childNode, filename); + AbilityDB::readAbilityNode(childNode, filename); } else if (childNode.name() == "npc") { NPCDB::readNPCNode(childNode, filename); } + else if (childNode.name() == "var") + { + QuestDB::readQuestVarNode(childNode, filename); + } else if (childNode.name() == "emote") { EmoteDB::readEmoteNode(childNode, filename); } - else if (childNode.name() == "status-effect" || childNode.name() == "stun-effect") + else if (childNode.name() == "status-effect") { - StatusEffect::readStatusEffectNode(childNode, filename); + StatusEffectDB::readStatusEffectNode(childNode, filename); } else if (childNode.name() == "unit") { Units::readUnitNode(childNode, filename); } - else - { - // compatibility stuff with older configs/games - if (node.name() == "specials" && childNode.name() == "set") - { - // specials.xml:/specials/set - SpecialDB::readSpecialSetNode(childNode, filename); - } - } } mIncludedFiles.erase(filename); diff --git a/src/resources/settingsmanager.h b/src/resources/settingsmanager.h index 25feb86b..5b70f865 100644 --- a/src/resources/settingsmanager.h +++ b/src/resources/settingsmanager.h @@ -18,8 +18,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SETTINGSMANAGER_HPP -#define SETTINGSMANAGER_HPP +#pragma once #include <string> #include <list> @@ -30,6 +29,3 @@ namespace SettingsManager void load(); void unload(); } - - -#endif // SETTINGSMANAGER_HPP diff --git a/src/resources/soundeffect.cpp b/src/resources/soundeffect.cpp index 8f8cdfc5..1b0492d7 100644 --- a/src/resources/soundeffect.cpp +++ b/src/resources/soundeffect.cpp @@ -28,23 +28,20 @@ SoundEffect::~SoundEffect() Mix_FreeChunk(mChunk); } -Resource *SoundEffect::load(SDL_RWops *rw) +SoundEffect *SoundEffect::load(SDL_RWops *rw) { // Load the music data and free the RWops structure - Mix_Chunk *tmpSoundEffect = Mix_LoadWAV_RW(rw, 1); - - if (tmpSoundEffect) + if (Mix_Chunk *soundEffect = Mix_LoadWAV_RW(rw, 1)) { - return new SoundEffect(tmpSoundEffect); + return new SoundEffect(soundEffect); } - logger->log("Error, failed to load sound effect: %s", Mix_GetError()); + Log::info("Error, failed to load sound effect: %s", Mix_GetError()); return nullptr; } -bool SoundEffect::play(int loops, int volume, int channel) +int SoundEffect::play(int loops, int volume, int channel) { Mix_VolumeChunk(mChunk, volume); - - return Mix_PlayChannel(channel, mChunk, loops) != -1; + return Mix_PlayChannel(channel, mChunk, loops); } diff --git a/src/resources/soundeffect.h b/src/resources/soundeffect.h index eada80b5..9ca8490d 100644 --- a/src/resources/soundeffect.h +++ b/src/resources/soundeffect.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SOUND_EFFECT_H -#define SOUND_EFFECT_H +#pragma once #include "resources/resource.h" @@ -37,12 +36,12 @@ class SoundEffect : public Resource /** * Loads a sample from a buffer in memory. * - * @param rw The SDL_RWops to load the sample data from. + * @param rw The SDL_RWops to load the sample data from. * - * @return <code>NULL</code> if the an error occurred, a valid pointer + * @return <code>nullptr</code> if the an error occurred, a valid pointer * otherwise. */ - static Resource *load(SDL_RWops *rw); + static SoundEffect *load(SDL_RWops *rw); /** * Plays the sample. @@ -51,15 +50,13 @@ class SoundEffect : public Resource * @param volume Sample playback volume. * @param channel Sample playback channel. * - * @return <code>true</code> if the playback started properly - * <code>false</code> otherwise. + * @return which channel was used to play the sound, or -1 if sound could not + * be played. */ - bool play(int loops, int volume, int channel = -1); + int play(int loops, int volume, int channel = -1); protected: SoundEffect(Mix_Chunk *soundEffect): mChunk(soundEffect) {} Mix_Chunk *mChunk; }; - -#endif // SOUND_EFFECT_H diff --git a/src/resources/specialdb.cpp b/src/resources/specialdb.cpp deleted file mode 100644 index ec0b3f2f..00000000 --- a/src/resources/specialdb.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* - * The Mana Client - * Copyright (C) 2010-2013 The Mana Developers - * - * This file is part of The Mana Client. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -#include "resources/specialdb.h" - -#include "log.h" - -#include "utils/dtor.h" - -#include <map> - -namespace -{ - std::map<int, SpecialInfo *> mSpecialInfos; - bool mLoaded = false; -} - -SpecialInfo::TargetMode SpecialDB::targetModeFromString(const std::string& str) -{ - if (str == "being") - return SpecialInfo::TARGET_BEING; - if (str == "point") - return SpecialInfo::TARGET_POINT; - - logger->log("SpecialDB: Warning, unknown target mode \"%s\"", str.c_str() ); - return SpecialInfo::TARGET_BEING; -} - - -void SpecialDB::init() -{ - if (mLoaded) - unload(); -} - -void SpecialDB::readSpecialSetNode(XML::Node node, const std::string &filename) -{ - std::string setName = node.getProperty("name", "Actions"); - - for (auto special : node.children()) - { - if (special.name() == "special") - { - auto *info = new SpecialInfo(); - int id = special.getProperty("id", 0); - info->id = id; - info->set = setName; - info->name = special.getProperty("name", ""); - info->icon = special.getProperty("icon", ""); - - info->targetMode = targetModeFromString(special.getProperty("target", "being")); - - info->rechargeable = special.getBoolProperty("rechargeable", true); - info->rechargeNeeded = 0; - info->rechargeCurrent = 0; - - if (mSpecialInfos.find(id) != mSpecialInfos.end()) - { - logger->log("SpecialDB: Duplicate special ID %d in %s, ignoring", id, filename.c_str()); - } else { - mSpecialInfos[id] = info; - } - } - } - -} - -void SpecialDB::checkStatus() -{ - mLoaded = true; -} - - -void SpecialDB::unload() -{ - - delete_all(mSpecialInfos); - mSpecialInfos.clear(); - - mLoaded = false; -} - - -SpecialInfo *SpecialDB::get(int id) -{ - - auto i = mSpecialInfos.find(id); - - if (i == mSpecialInfos.end()) - { - return nullptr; - } - else - { - return i->second; - } - return nullptr; -} diff --git a/src/resources/spritedef.cpp b/src/resources/spritedef.cpp index f42e623e..0e7f12dd 100644 --- a/src/resources/spritedef.cpp +++ b/src/resources/spritedef.cpp @@ -28,7 +28,6 @@ #include "resources/animation.h" #include "resources/dye.h" #include "resources/image.h" -#include "resources/imageset.h" #include "resources/resourcemanager.h" #include "configuration.h" @@ -45,7 +44,7 @@ Action *SpriteDef::getAction(const std::string &action) const if (i == mActions.end()) { - logger->log("Warning: no action \"%s\" defined!", action.c_str()); + Log::warn("No action \"%s\" defined!", action.c_str()); return nullptr; } @@ -67,7 +66,7 @@ SpriteDef *SpriteDef::load(const std::string &animationFile, int variant) if (!rootNode || rootNode.name() != "sprite") { - logger->log("Error, failed to parse %s", animationFile.c_str()); + Log::info("Error, failed to parse %s", animationFile.c_str()); std::string errorFile = paths.getStringValue("sprites") + paths.getStringValue("spriteErrorFile"); @@ -155,11 +154,10 @@ void SpriteDef::loadImageSet(XML::Node node, const std::string &palettes) Dye::instantiate(imageSrc, palettes); ResourceManager *resman = ResourceManager::getInstance(); - ImageSet *imageSet = resman->getImageSet(imageSrc, width, height); - + auto imageSet = resman->getImageSet(imageSrc, width, height); if (!imageSet) { - logger->error(strprintf("Couldn't load imageset (%s)!", + Log::critical(strprintf("Couldn't load imageset (%s)!", imageSrc.c_str())); } @@ -176,16 +174,16 @@ void SpriteDef::loadAction(XML::Node node, int variant_offset) auto si = mImageSets.find(imageSetName); if (si == mImageSets.end()) { - logger->log("Warning: imageset \"%s\" not defined in %s", - imageSetName.c_str(), getIdPath().c_str()); + Log::warn("imageset \"%s\" not defined in %s", + imageSetName.c_str(), getIdPath().c_str()); return; } ImageSet *imageSet = si->second; if (actionName == SpriteAction::INVALID) { - logger->log("Warning: Unknown action \"%s\" defined in %s", - actionName.c_str(), getIdPath().c_str()); + Log::warn("Unknown action \"%s\" defined in %s", + actionName.c_str(), getIdPath().c_str()); return; } auto *action = new Action; @@ -217,8 +215,8 @@ void SpriteDef::loadAnimation(XML::Node animationNode, if (directionType == DIRECTION_INVALID) { - logger->log("Warning: Unknown direction \"%s\" used in %s", - directionName.c_str(), getIdPath().c_str()); + Log::warn("Unknown direction \"%s\" used in %s", + directionName.c_str(), getIdPath().c_str()); return; } @@ -241,7 +239,7 @@ void SpriteDef::loadAnimation(XML::Node animationNode, if (index < 0) { - logger->log("No valid value for 'index'"); + Log::info("No valid value for 'index'"); continue; } @@ -249,7 +247,7 @@ void SpriteDef::loadAnimation(XML::Node animationNode, if (!img) { - logger->log("No image at index %d", index + variant_offset); + Log::info("No image at index %d", index + variant_offset); continue; } @@ -262,7 +260,7 @@ void SpriteDef::loadAnimation(XML::Node animationNode, if (start < 0 || end < 0) { - logger->log("No valid value for 'start' or 'end'"); + Log::info("No valid value for 'start' or 'end'"); continue; } @@ -272,7 +270,7 @@ void SpriteDef::loadAnimation(XML::Node animationNode, if (!img) { - logger->log("No image at index %d", start + variant_offset); + Log::info("No image at index %d", start + variant_offset); break; } @@ -297,8 +295,8 @@ void SpriteDef::includeSprite(XML::Node includeNode) if (processedFiles.find(filename) != processedFiles.end()) { - logger->log("Error, Tried to include %s which already is included.", - filename.c_str()); + Log::info("Error, Tried to include %s which already is included.", + filename.c_str()); return; } processedFiles.insert(filename); @@ -308,7 +306,7 @@ void SpriteDef::includeSprite(XML::Node includeNode) if (!rootNode || rootNode.name() != "sprite") { - logger->log("Error, no sprite root node in %s", filename.c_str()); + Log::info("Error, no sprite root node in %s", filename.c_str()); return; } @@ -328,11 +326,6 @@ SpriteDef::~SpriteDef() { delete action; } - - for (auto &imageSet : mImageSets) - { - imageSet.second->decRef(); - } } SpriteDirection SpriteDef::makeSpriteDirection(const std::string &direction) diff --git a/src/resources/spritedef.h b/src/resources/spritedef.h index fa44deea..0d48d145 100644 --- a/src/resources/spritedef.h +++ b/src/resources/spritedef.h @@ -19,10 +19,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SPRITEDEF_H -#define SPRITEDEF_H +#pragma once -#include "resources/resource.h" +#include "resources/imageset.h" #include "utils/xml.h" @@ -50,7 +49,7 @@ struct SpriteDisplay * Remember those are the main action. * Action subtypes, e.g.: "attack_bow" are to be passed by items.xml after * an ACTION_ATTACK call. - * Which special to be use to to be passed with the USE_SPECIAL call. + * Which ability to be use to to be passed with the USE_ABILITY call. * Running, walking, ... is a sub-type of moving. * ... * Please don't add hard-coded subtypes here! @@ -65,7 +64,7 @@ namespace SpriteAction static const std::string MOVE = "walk"; static const std::string ATTACK = "attack"; static const std::string HURT = "hurt"; - static const std::string USE_SPECIAL = "special"; + static const std::string USE_ABILITY = "ability"; static const std::string CAST_MAGIC = "magic"; static const std::string USE_ITEM = "item"; static const std::string INVALID; @@ -147,8 +146,6 @@ class SpriteDef : public Resource */ void substituteAction(std::string complete, std::string with); - std::map<std::string, ImageSet *> mImageSets; + std::map<std::string, ResourceRef<ImageSet>> mImageSets; std::map<std::string, Action *> mActions; }; - -#endif // SPRITEDEF_H diff --git a/src/resources/statuseffectdb.cpp b/src/resources/statuseffectdb.cpp new file mode 100644 index 00000000..fe179191 --- /dev/null +++ b/src/resources/statuseffectdb.cpp @@ -0,0 +1,96 @@ +/* + * The Mana Client + * Copyright (C) 2008-2009 The Mana World Development Team + * Copyright (C) 2009-2025 The Mana Developers + * + * This file is part of The Mana Client. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "statuseffectdb.h" + +bool StatusEffectDB::mLoaded = false; +std::map<int, StatusEffect> StatusEffectDB::mStatusEffects; +StatusEffectDB::OptionsMap StatusEffectDB::mOpt0ToIdMap; +StatusEffectDB::OptionsMap StatusEffectDB::mOpt1ToIdMap; +StatusEffectDB::OptionsMap StatusEffectDB::mOpt2ToIdMap; +StatusEffectDB::OptionsMap StatusEffectDB::mOpt3ToIdMap; + + +const StatusEffect *StatusEffectDB::getStatusEffect(int id) +{ + auto it = mStatusEffects.find(id); + if (it == mStatusEffects.end()) + return nullptr; + return &it->second; +} + +void StatusEffectDB::init() +{ + if (mLoaded) + unload(); +} + +void StatusEffectDB::readStatusEffectNode(XML::Node node, const std::string &/* filename */) +{ + const int id = node.getProperty("id", -1); + + const int opt0 = node.getProperty("option", 0); + const int opt1 = node.getProperty("opt1", 0); + const int opt2 = node.getProperty("opt2", 0); + const int opt3 = node.getProperty("opt3", 0); + if (opt0 != 0 && opt0 <= UINT16_MAX) + mOpt0ToIdMap[opt0] = id; + if (opt1 != 0 && opt1 <= UINT16_MAX) + mOpt1ToIdMap[opt1] = id; + if (opt2 != 0 && opt2 <= UINT16_MAX) + mOpt2ToIdMap[opt2] = id; + if (opt3 != 0 && opt3 <= UINT16_MAX) + mOpt3ToIdMap[opt3] = id; + + auto &effect = mStatusEffects[id]; + + node.attribute("name", effect.name); + + node.attribute("start-message", effect.start.message); + node.attribute("start-audio", effect.start.sfx); + node.attribute("start-particle", effect.start.particleEffect); + + // For now we don't support separate particle effect for "already applied" + // status effects. + if (effect.start.particleEffect.empty()) + node.attribute("particle", effect.start.particleEffect); + + node.attribute("end-message", effect.end.message); + node.attribute("end-audio", effect.end.sfx); + node.attribute("end-particle", effect.end.particleEffect); + + node.attribute("icon", effect.icon); + node.attribute("persistent-particle-effect", effect.persistentParticleEffect); +} + +void StatusEffectDB::checkStatus() +{ + mLoaded = true; +} + +void StatusEffectDB::unload() +{ + if (!mLoaded) + return; + + mStatusEffects.clear(); + mLoaded = false; +} diff --git a/src/resources/statuseffectdb.h b/src/resources/statuseffectdb.h new file mode 100644 index 00000000..d1f1a6bf --- /dev/null +++ b/src/resources/statuseffectdb.h @@ -0,0 +1,67 @@ +/* + * The Mana Client + * Copyright (C) 2008-2009 The Mana World Development Team + * Copyright (C) 2009-2025 The Mana Developers + * + * This file is part of The Mana Client. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef STATUSEFFECTDB_H +#define STATUSEFFECTDB_H + +#include "statuseffect.h" +#include "utils/xml.h" + +#include <cstdint> +#include <map> + +class StatusEffectDB +{ +public: + /** + * Retrieves a status effect. + * + * \param id ID of the status effect. + */ + static const StatusEffect *getStatusEffect(int id); + + using OptionsMap = std::map<uint16_t, int>; + + /** + * These map flags or indexes to their corresponding status effect ID. + * This is tmwAthena-specific. + */ + static const OptionsMap &opt0ToIdMap() { return mOpt0ToIdMap; } + static const OptionsMap &opt1ToIdMap() { return mOpt1ToIdMap; } + static const OptionsMap &opt2ToIdMap() { return mOpt2ToIdMap; } + static const OptionsMap &opt3ToIdMap() { return mOpt3ToIdMap; } + + static void init(); + static void readStatusEffectNode(XML::Node node, const std::string &filename); + static void checkStatus(); + static void unload(); + +private: + static bool mLoaded; + + static std::map<int, StatusEffect> mStatusEffects; + static OptionsMap mOpt0ToIdMap; + static OptionsMap mOpt1ToIdMap; + static OptionsMap mOpt2ToIdMap; + static OptionsMap mOpt3ToIdMap; +}; + +#endif // STATUSEFFECTDB_H diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp index ad686e19..fa5f1a7d 100644 --- a/src/resources/theme.cpp +++ b/src/resources/theme.cpp @@ -25,165 +25,428 @@ #include "configuration.h" #include "log.h" +#include "textrenderer.h" #include "resources/dye.h" #include "resources/image.h" #include "resources/imageset.h" #include "resources/resourcemanager.h" -#include "utils/dtor.h" -#include "utils/stringutils.h" -#include "utils/xml.h" +#include "utils/filesystem.h" -#include <physfs.h> +#include <guichan/font.hpp> +#include <guichan/widget.hpp> #include <algorithm> +/** + * Initializes the directory in which the client looks for GUI themes, which at + * the same time functions as a fallback directory when looking up files + * relevant for the GUI theme. + */ static std::string defaultThemePath; -std::string Theme::mThemePath; -Theme *Theme::mInstance = nullptr; -// Set the theme path... static void initDefaultThemePath() { - ResourceManager *resman = ResourceManager::getInstance(); defaultThemePath = branding.getStringValue("guiThemePath"); - if (defaultThemePath.empty() || !resman->isDirectory(defaultThemePath)) + if (defaultThemePath.empty() || !FS::isDirectory(defaultThemePath)) defaultThemePath = "graphics/gui/"; } -Skin::Skin(ImageRect skin, Image *close, Image *stickyUp, Image *stickyDown): - mBorder(skin), - mCloseImage(close), - mStickyImageUp(stickyUp), - mStickyImageDown(stickyDown) +static bool isThemePath(const std::string &theme) +{ + return FS::exists(defaultThemePath + theme + "/theme.xml"); +} + + +ThemeInfo::ThemeInfo(const std::string &path) + : path(path) +{ + auto themeFile = getFullPath() + "/theme.xml"; + if (!FS::exists(themeFile)) + return; + + auto doc = std::make_unique<XML::Document>(themeFile); + XML::Node rootNode = doc->rootNode(); + if (!rootNode || rootNode.name() != "theme") + return; + + if (rootNode.attribute("name", name) && !name.empty()) + this->doc = std::move(doc); + else + Log::error("Theme '%s' has no name!", path.c_str()); +} + +std::string ThemeInfo::getFullPath() const +{ + return defaultThemePath + path; +} + + +WidgetState::WidgetState(const gcn::Widget *widget) + : width(widget->getWidth()) + , height(widget->getHeight()) +{ + // x and y are not set based on the widget because the rendering usually + // happens in local coordinates. + + if (!widget->isEnabled()) + flags |= STATE_DISABLED; + if (widget->isFocused()) + flags |= STATE_FOCUSED; +} + +WidgetState::WidgetState(const gcn::Rectangle &dim, uint8_t flags) + : x(dim.x) + , y(dim.y) + , width(dim.width) + , height(dim.height) + , flags(flags) {} + Skin::~Skin() { - // Clean up static resources - for (auto img : mBorder.grid) - delete img; + // Raw Image* need explicit deletion + for (auto &state : mStates) + for (auto &part : state.parts) + if (auto image = std::get_if<Image *>(&part.data)) + delete *image; +} - mCloseImage->decRef(); - delete mStickyImageUp; - delete mStickyImageDown; +void Skin::addState(SkinState state) +{ + mStates.emplace_back(std::move(state)); } -void Skin::updateAlpha(float minimumOpacityAllowed) +void Skin::draw(Graphics *graphics, const WidgetState &state) const { - const float alpha = std::max(minimumOpacityAllowed, - config.guiAlpha); + // Only draw the first matching state + auto skinState = getState(state.flags); + if (!skinState) + return; + + for (const auto &part : skinState->parts) + { + std::visit([&](const auto &data) { + using T = std::decay_t<decltype(data)>; - mBorder.setAlpha(alpha); + if constexpr (std::is_same_v<T, ImageRect>) + { + graphics->drawImageRect(data, + state.x + part.offsetX, + state.y + part.offsetY, + state.width, + state.height); + } + else if constexpr (std::is_same_v<T, Image*>) + { + graphics->drawImage(data, state.x + part.offsetX, state.y + part.offsetY); + } + else if constexpr (std::is_same_v<T, ColoredRectangle>) + { + const auto color = graphics->getColor(); + // TODO: Take GUI alpha into account + graphics->setColor(data.color); - mCloseImage->setAlpha(alpha); - mStickyImageUp->setAlpha(alpha); - mStickyImageDown->setAlpha(alpha); + const gcn::Rectangle rect(state.x + part.offsetX, + state.y + part.offsetY, + state.width, + state.height); + + if (data.filled) + graphics->fillRectangle(rect); + else + graphics->drawRectangle(rect); + + graphics->setColor(color); + } + }, part.data); + } +} + +const SkinState *Skin::getState(uint8_t flags) const +{ + for (const auto &skinState : mStates) + if (skinState.stateFlags == (skinState.setFlags & flags)) + return &skinState; + + return nullptr; } int Skin::getMinWidth() const { - return mBorder.grid[ImageRect::UPPER_LEFT]->getWidth() + - mBorder.grid[ImageRect::UPPER_RIGHT]->getWidth(); + int minWidth = 0; + + for (const auto &state : mStates) + { + for (const auto &part : state.parts) + { + if (auto imageRect = std::get_if<ImageRect>(&part.data)) + minWidth = std::max(minWidth, imageRect->minWidth()); + else if (auto img = std::get_if<Image *>(&part.data)) + minWidth = std::max(minWidth, (*img)->getWidth()); + } + } + + return minWidth; } int Skin::getMinHeight() const { - return mBorder.grid[ImageRect::UPPER_LEFT]->getHeight() + - mBorder.grid[ImageRect::LOWER_LEFT]->getHeight(); + int minHeight = 0; + + for (const auto &state : mStates) + { + for (const auto &part : state.parts) + { + if (auto imageRect = std::get_if<ImageRect>(&part.data)) + minHeight = std::max(minHeight, imageRect->minHeight()); + else if (auto img = std::get_if<Image *>(&part.data)) + minHeight = std::max(minHeight, (*img)->getHeight()); + } + } + + return minHeight; } -Theme::Theme(): - Palette(THEME_COLORS_END), - mMinimumOpacity(-1.0f), - mProgressColors(THEME_PROG_END) +void Skin::updateAlpha(float alpha) { - initDefaultThemePath(); + for (auto &state : mStates) + { + for (auto &part : state.parts) + { + if (auto rect = std::get_if<ImageRect>(&part.data)) + rect->image->setAlpha(alpha); + else if (auto img = std::get_if<Image *>(&part.data)) + (*img)->setAlpha(alpha); + } + } +} + +Theme::Theme(const ThemeInfo &themeInfo) + : mThemePath(themeInfo.getFullPath()) +{ listen(Event::ConfigChannel); - loadColors(); + readTheme(themeInfo); + + if (mPalettes.empty()) + { + Log::info("Error, theme did not define any palettes: %s", + themeInfo.getPath().c_str()); - mColors[HIGHLIGHT].ch = 'H'; - mColors[CHAT].ch = 'C'; - mColors[GM].ch = 'G'; - mColors[PLAYER].ch = 'Y'; - mColors[WHISPER].ch = 'W'; - mColors[IS].ch = 'I'; - mColors[PARTY].ch = 'P'; - mColors[GUILD].ch = 'U'; - mColors[SERVER].ch = 'S'; - mColors[LOGGER].ch = 'L'; - mColors[HYPERLINK].ch = '<'; + // Avoid crashing + mPalettes.emplace_back(THEME_COLORS_END); + } } Theme::~Theme() { - delete_all(mSkins); - delete_all(mProgressColors); + for (auto &[_, image] : mIcons) + delete image; +} + +std::string Theme::prepareThemePath() +{ + initDefaultThemePath(); + + // Try theme from settings + if (isThemePath(config.theme)) + return config.theme; + + // Try theme from branding + if (isThemePath(branding.getStringValue("theme"))) + return branding.getStringValue("theme"); + + return std::string(); +} + +std::vector<ThemeInfo> Theme::getAvailableThemes() +{ + std::vector<ThemeInfo> themes; + themes.emplace_back(std::string()); + + for (const auto &entry : FS::enumerateFiles(defaultThemePath)) + { + ThemeInfo theme{entry}; + if (theme.isValid()) + themes.push_back(std::move(theme)); + } + + std::sort(themes.begin(), themes.end(), [](const ThemeInfo &a, const ThemeInfo &b) { + return a.getName() < b.getName(); + }); + + return themes; +} + +std::string Theme::resolvePath(const std::string &path) const +{ + // Need to strip off any dye info for the existence tests + int pos = path.find('|'); + std::string file; + if (pos > 0) + file = path.substr(0, pos); + else + file = path; + + // Try the theme + file = mThemePath + "/" + file; + if (FS::exists(file)) + return mThemePath + "/" + path; + + // Backup + return defaultThemePath + "/" + path; } -Theme *Theme::instance() +ResourceRef<Image> Theme::getImage(const std::string &path) const { - if (!mInstance) - mInstance = new Theme; + return ResourceManager::getInstance()->getImage(resolvePath(path)); +} - return mInstance; +ResourceRef<Image> Theme::getImageFromTheme(const std::string &path) +{ + return gui->getTheme()->getImage(path); } -void Theme::deleteInstance() +const gcn::Color &Theme::getThemeColor(int type) { - delete mInstance; - mInstance = nullptr; + return gui->getTheme()->getColor(type); } gcn::Color Theme::getProgressColor(int type, float progress) { - DyePalette *dye = mInstance->mProgressColors[type]; - int color[3] = {0, 0, 0}; - dye->getColor(progress, color); + + if (const auto &dye = gui->getTheme()->mProgressColors[type]) + dye->getColor(progress, color); return gcn::Color(color[0], color[1], color[2]); } -Skin *Theme::load(const std::string &filename, const std::string &defaultPath) +const Palette &Theme::getPalette(size_t index) const { - // Check if this skin was already loaded - auto skinIterator = mSkins.find(filename); - if (skinIterator != mSkins.end()) - { - Skin *skin = skinIterator->second; - skin->instances++; - return skin; + return mPalettes.at(index < mPalettes.size() ? index : 0); +} + +const gcn::Color &Theme::getColor(int type) const +{ + return getPalette(0).getColor(type); +} + +std::optional<int> Theme::getColorIdForChar(char c) +{ + switch (c) { + case '0': return BLACK; + case '1': return RED; + case '2': return GREEN; + case '3': return BLUE; + case '4': return ORANGE; + case '5': return YELLOW; + case '6': return PINK; + case '7': return PURPLE; + case '8': return GRAY; + case '9': return BROWN; + + case 'H': return HIGHLIGHT; + case 'C': return CHAT; + case 'G': return GM; + case 'g': return GLOBAL; + case 'Y': return PLAYER; + case 'W': return WHISPER; + // case 'w': return WHISPER_TAB_OFFLINE; + case 'I': return IS; + case 'P': return PARTY; + case 'U': return GUILD; + case 'S': return SERVER; + case 'L': return LOGGER; + case '<': return HYPERLINK; + // case 's': return SELFNICK; + case 'o': return OLDCHAT; + case 'a': return AWAYCHAT; } - Skin *skin = readSkin(filename); + return {}; +} - if (!skin) - { - // Try falling back on the defaultPath if this makes sense - if (filename != defaultPath) - { - logger->log("Error loading skin '%s', falling back on default.", - filename.c_str()); +void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const +{ + getSkin(type).draw(graphics, state); +} - skin = readSkin(defaultPath); - } +void Theme::drawProgressBar(Graphics *graphics, + const gcn::Rectangle &area, + const gcn::Color &color, + float progress, + const std::string &text, + ProgressPalette progressType) const +{ + gcn::Font *oldFont = graphics->getFont(); + gcn::Color oldColor = graphics->getColor(); + + WidgetState widgetState; + widgetState.x = area.x; + widgetState.y = area.y; + widgetState.width = area.width; + widgetState.height = area.height; + + auto &skin = getSkin(SkinType::ProgressBar); + skin.draw(graphics, widgetState); + + // The bar + if (progress > 0) + { + graphics->setColor(color); + graphics->fillRectangle(gcn::Rectangle(area.x + 4, + area.y + 4, + (int) (progress * (area.width - 8)), + area.height - 8)); + } - if (!skin) + // The label + if (!text.empty()) + { + if (auto skinState = skin.getState(widgetState.flags)) { - logger->error(strprintf("Error: Loading default skin '%s' failed. " - "Make sure the skin file is valid.", - defaultPath.c_str())); + const TextFormat *textFormat = &skinState->textFormat; + + if (progressType < THEME_PROG_END && mProgressTextFormats[progressType]) + textFormat = &(*mProgressTextFormats[progressType]); + + auto font = textFormat->bold ? boldFont : gui->getFont(); + const int textX = area.x + area.width / 2; + const int textY = area.y + (area.height - font->getHeight()) / 2; + + TextRenderer::renderText(graphics, + text, + textX, + textY, + gcn::Graphics::CENTER, + font, + *textFormat); } } - // Add the skin to the loaded skins - mSkins[filename] = skin; + graphics->setFont(oldFont); + graphics->setColor(oldColor); +} + +const Skin &Theme::getSkin(SkinType skinType) const +{ + static Skin emptySkin; + const auto it = mSkins.find(skinType); + return it != mSkins.end() ? it->second : emptySkin; +} + +const Image *Theme::getIcon(const std::string &name) const +{ + auto it = mIcons.find(name); + if (it == mIcons.end()) + return nullptr; - return skin; + return it->second; } void Theme::setMinimumOpacity(float minimumOpacity) @@ -197,8 +460,14 @@ void Theme::setMinimumOpacity(float minimumOpacity) void Theme::updateAlpha() { - for (auto &skin : mSkins) - skin.second->updateAlpha(mMinimumOpacity); + const float alpha = std::max(config.guiAlpha, mMinimumOpacity); + if (mAlpha == alpha) + return; + + mAlpha = alpha; + + for (auto &[_, skin] : mSkins) + skin.updateAlpha(mAlpha); } void Theme::event(Event::Channel channel, const Event &event) @@ -211,198 +480,329 @@ void Theme::event(Event::Channel channel, const Event &event) } } -Skin *Theme::readSkin(const std::string &filename) +static bool check(bool value, const char *msg, ...) { - if (filename.empty()) - return nullptr; + if (!value) + { + va_list ap; + va_start(ap, msg); + Log::vinfo(msg, ap); + va_end(ap); + } + return !value; +} - logger->log("Loading skin '%s'.", filename.c_str()); +bool Theme::readTheme(const ThemeInfo &themeInfo) +{ + Log::info("Loading %s theme from '%s'...", + themeInfo.getName().c_str(), + themeInfo.getPath().c_str()); - XML::Document doc(resolveThemePath(filename)); - XML::Node rootNode = doc.rootNode(); + XML::Node rootNode = themeInfo.getDocument().rootNode(); - if (!rootNode || rootNode.name() != "skinset") - return nullptr; + if (!rootNode || rootNode.name() != "theme") + return false; - const std::string skinSetImage = rootNode.getProperty("image", ""); - - if (skinSetImage.empty()) + for (auto childNode : rootNode.children()) { - logger->log("Theme::readSkin(): Skinset does not define an image!"); - return nullptr; + if (childNode.name() == "skin") + readSkinNode(childNode); + else if (childNode.name() == "palette") + readPaletteNode(childNode); + else if (childNode.name() == "progressbar") + readProgressBarNode(childNode); + else if (childNode.name() == "icon") + readIconNode(childNode); + else + Log::info("Theme: Unknown node '%s'!", childNode.name().data()); } - logger->log("Theme::load(): <skinset> defines '%s' as a skin image.", - skinSetImage.c_str()); + Log::info("Finished loading theme."); - Image *dBorders = Theme::getImageFromTheme(skinSetImage); - ImageRect border; - memset(&border, 0, sizeof(ImageRect)); + for (auto &[_, skin] : mSkins) + skin.updateAlpha(mAlpha); - // iterate <widget>'s - for (auto widgetNode : rootNode.children()) - { - if (widgetNode.name() != "widget") - continue; + return true; +} - const std::string widgetType = - widgetNode.getProperty("type", "unknown"); - if (widgetType == "Window") - { - // Iterate through <part>'s - // LEEOR / TODO: - // We need to make provisions to load in a CloseButton image. For - // now it can just be hard-coded. - for (auto partNode : widgetNode.children()) - { - if (partNode.name() != "part") - continue; - - const std::string partType = - partNode.getProperty("type", "unknown"); - // TOP ROW - const int xPos = partNode.getProperty("xpos", 0); - const int yPos = partNode.getProperty("ypos", 0); - const int width = partNode.getProperty("width", 1); - const int height = partNode.getProperty("height", 1); - - if (partType == "top-left-corner") - border.grid[0] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "top-edge") - border.grid[1] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "top-right-corner") - border.grid[2] = dBorders->getSubImage(xPos, yPos, width, height); - - // MIDDLE ROW - else if (partType == "left-edge") - border.grid[3] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "bg-quad") - border.grid[4] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "right-edge") - border.grid[5] = dBorders->getSubImage(xPos, yPos, width, height); - - // BOTTOM ROW - else if (partType == "bottom-left-corner") - border.grid[6] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "bottom-edge") - border.grid[7] = dBorders->getSubImage(xPos, yPos, width, height); - else if (partType == "bottom-right-corner") - border.grid[8] = dBorders->getSubImage(xPos, yPos, width, height); +static std::optional<SkinType> readSkinType(std::string_view type) +{ + if (type == "Window") return SkinType::Window; + if (type == "ToolWindow") return SkinType::ToolWindow; + if (type == "Popup") return SkinType::Popup; + if (type == "SpeechBubble") return SkinType::SpeechBubble; + if (type == "Button") return SkinType::Button; + if (type == "ButtonUp") return SkinType::ButtonUp; + if (type == "ButtonDown") return SkinType::ButtonDown; + if (type == "ButtonLeft") return SkinType::ButtonLeft; + if (type == "ButtonRight") return SkinType::ButtonRight; + if (type == "ButtonClose") return SkinType::ButtonClose; + if (type == "ButtonSticky") return SkinType::ButtonSticky; + if (type == "CheckBox") return SkinType::CheckBox; + if (type == "RadioButton") return SkinType::RadioButton; + if (type == "TextField") return SkinType::TextField; + if (type == "Tab") return SkinType::Tab; + if (type == "ScrollArea") return SkinType::ScrollArea; + if (type == "ScrollAreaHBar") return SkinType::ScrollAreaHBar; + if (type == "ScrollAreaHMarker") return SkinType::ScrollAreaHMarker; + if (type == "ScrollAreaVBar") return SkinType::ScrollAreaVBar; + if (type == "ScrollAreaVMarker") return SkinType::ScrollAreaVMarker; + if (type == "DropDownFrame") return SkinType::DropDownFrame; + if (type == "DropDownButton") return SkinType::DropDownButton; + if (type == "ProgressBar") return SkinType::ProgressBar; + if (type == "Slider") return SkinType::Slider; + if (type == "SliderHandle") return SkinType::SliderHandle; + if (type == "ResizeGrip") return SkinType::ResizeGrip; + if (type == "ShortcutBox") return SkinType::ShortcutBox; + if (type == "EquipmentBox") return SkinType::EquipmentBox; + if (type == "ItemSlot") return SkinType::ItemSlot; + if (type == "EmoteSlot") return SkinType::EmoteSlot; + return {}; +} - else - logger->log("Theme::readSkin(): Unknown part type '%s'", - partType.c_str()); - } - } - else - { - logger->log("Theme::readSkin(): Unknown widget type '%s'", - widgetType.c_str()); - } - } +void Theme::readSkinNode(XML::Node node) +{ + const auto skinTypeStr = node.getProperty("type", std::string()); + const auto skinType = readSkinType(skinTypeStr); + if (check(skinType.has_value(), "Theme: Unknown skin type '%s'", skinTypeStr.c_str())) + return; - dBorders->decRef(); + auto &skin = mSkins[*skinType]; + + node.attribute("width", skin.width); + node.attribute("height", skin.height); + node.attribute("frameSize", skin.frameSize); + node.attribute("padding", skin.padding); + node.attribute("spacing", skin.spacing); + node.attribute("titleBarHeight", skin.titleBarHeight); + node.attribute("titleOffsetX", skin.titleOffsetX); + node.attribute("titleOffsetY", skin.titleOffsetY); + node.attribute("palette", skin.palette); + node.attribute("showButtons", skin.showButtons); + + for (auto childNode : node.children()) + if (childNode.name() == "state") + readSkinStateNode(childNode, skin); +} - logger->log("Finished loading skin."); +static void readSkinStateRectNode(XML::Node node, SkinState &state) +{ + auto &part = state.parts.emplace_back(); + auto &rect = part.data.emplace<ColoredRectangle>(); - // Hard-coded for now until we update the above code to look for window buttons - Image *closeImage = Theme::getImageFromTheme("close_button.png"); - Image *sticky = Theme::getImageFromTheme("sticky_button.png"); - Image *stickyImageUp = sticky->getSubImage(0, 0, 15, 15); - Image *stickyImageDown = sticky->getSubImage(15, 0, 15, 15); - sticky->decRef(); + node.attribute("color", rect.color); + node.attribute("alpha", rect.color.a); + node.attribute("fill", rect.filled); +} - Skin *skin = new Skin(border, closeImage, stickyImageUp, stickyImageDown); - skin->updateAlpha(mMinimumOpacity); - return skin; +static void readTextNode(XML::Node node, TextFormat &textFormat) +{ + node.attribute("bold", textFormat.bold); + node.attribute("color", textFormat.color); + node.attribute("outlineColor", textFormat.outlineColor); + node.attribute("shadowColor", textFormat.shadowColor); } -bool Theme::tryThemePath(std::string themePath) +void Theme::readSkinStateNode(XML::Node node, Skin &skin) const { - if (!themePath.empty()) + SkinState state; + + auto readFlag = [&] (const char *name, int flag) { - themePath = defaultThemePath + themePath; + std::optional<bool> value; + node.attribute(name, value); - if (PHYSFS_exists(themePath.c_str())) + if (value.has_value()) { - mThemePath = themePath; - return true; + state.setFlags |= flag; + state.stateFlags |= *value ? flag : 0; } + }; + + readFlag("selected", STATE_SELECTED); + readFlag("disabled", STATE_DISABLED); + readFlag("hovered", STATE_HOVERED); + readFlag("focused", STATE_FOCUSED); + + for (auto childNode : node.children()) + { + if (childNode.name() == "img") + readSkinStateImgNode(childNode, state); + else if (childNode.name() == "rect") + readSkinStateRectNode(childNode, state); + else if (childNode.name() == "text") + readTextNode(childNode, state.textFormat); } - return false; + skin.addState(std::move(state)); } -void Theme::prepareThemePath() +template<> +inline void fromString(const char *str, FillMode &value) { - // Ensure the Theme object has been created - instance(); - - // Try theme from settings - if (!tryThemePath(config.theme)) - // Try theme from branding - if (!tryThemePath(branding.getStringValue("theme"))) - // Use default - mThemePath = defaultThemePath; - - instance()->loadColors(mThemePath); + if (strcmp(str, "repeat") == 0) + value = FillMode::Repeat; + else if (strcmp(str, "stretch") == 0) + value = FillMode::Stretch; } -std::string Theme::resolveThemePath(const std::string &path) +void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const { - // Need to strip off any dye info for the existence tests - int pos = path.find('|'); - std::string file; - if (pos > 0) - file = path.substr(0, pos); - else - file = path; + const std::string src = node.getProperty("src", std::string()); + if (check(!src.empty(), "Theme: 'img' element has empty 'src' attribute!")) + return; - // Might be a valid path already - if (PHYSFS_exists(file.c_str())) - return path; + auto image = getImage(src); + if (check(image, "Theme: Failed to load image '%s'!", src.c_str())) + return; - // Try the theme - file = getThemePath() + "/" + file; - if (PHYSFS_exists(file.c_str())) - return getThemePath() + "/" + path; + int left = 0; + int right = 0; + int top = 0; + int bottom = 0; + int x = 0; + int y = 0; + int width = image->getWidth(); + int height = image->getHeight(); + + node.attribute("left", left); + node.attribute("right", right); + node.attribute("top", top); + node.attribute("bottom", bottom); + node.attribute("x", x); + node.attribute("y", y); + node.attribute("width", width); + node.attribute("height", height); + + if (check(left >= 0 || right >= 0 || top >= 0 || bottom >= 0, "Theme: Invalid border value!")) + return; + if (check(x >= 0 || y >= 0, "Theme: Invalid position value!")) + return; + if (check(width >= 0 || height >= 0, "Theme: Invalid size value!")) + return; + if (check(x + width <= image->getWidth() || y + height <= image->getHeight(), "Theme: Image size out of bounds!")) + return; - // Backup - return std::string(defaultThemePath) + "/" + path; + auto &part = state.parts.emplace_back(); + + node.attribute("offsetX", part.offsetX); + node.attribute("offsetY", part.offsetY); + + if (left + right + top + bottom > 0) + { + auto &border = part.data.emplace<ImageRect>(); + border.left = left; + border.right = right; + border.top = top; + border.bottom = bottom; + border.image.reset(image->getSubImage(x, y, width, height)); + + node.attribute("fill", border.fillMode); + } + else + { + part.data = image->getSubImage(x, y, width, height); + } } -Image *Theme::getImageFromTheme(const std::string &path) +template<> +inline void fromString(const char *str, gcn::Color &value) { - ResourceManager *resman = ResourceManager::getInstance(); - return resman->getImage(resolveThemePath(path)); + if (strlen(str) < 7 || str[0] != '#') + { + error: + Log::info("Error, invalid theme color palette: %s", str); + value = gcn::Color(0, 0, 0); + return; + } + + int v = 0; + for (int i = 1; i < 7; ++i) + { + char c = str[i]; + int n; + + if ('0' <= c && c <= '9') + n = c - '0'; + else if ('A' <= c && c <= 'F') + n = c - 'A' + 10; + else if ('a' <= c && c <= 'f') + n = c - 'a' + 10; + else + goto error; + + v = (v << 4) | n; + } + + value = gcn::Color(v); } -ImageSet *Theme::getImageSetFromTheme(const std::string &path, - int w, int h) +void Theme::readIconNode(XML::Node node) { - ResourceManager *resman = ResourceManager::getInstance(); - return resman->getImageSet(resolveThemePath(path), w, h); + std::string name; + std::string src; + node.attribute("name", name); + node.attribute("src", src); + + if (check(!name.empty(), "Theme: 'icon' element has empty 'name' attribute!")) + return; + if (check(!src.empty(), "Theme: 'icon' element has empty 'src' attribute!")) + return; + + auto image = getImage(src); + if (check(image, "Theme: Failed to load image '%s'!", src.c_str())) + return; + + int x = 0; + int y = 0; + int width = image->getWidth(); + int height = image->getHeight(); + + node.attribute("x", x); + node.attribute("y", y); + node.attribute("width", width); + node.attribute("height", height); + + if (check(x >= 0 || y >= 0, "Theme: Invalid position value!")) + return; + if (check(width >= 0 || height >= 0, "Theme: Invalid size value!")) + return; + if (check(x + width <= image->getWidth() || y + height <= image->getHeight(), "Theme: Image size out of bounds!")) + return; + + mIcons[name] = image->getSubImage(x, y, width, height); } -static int readColorType(const std::string &type) +static int readColorId(const std::string &id) { - static std::string colors[] = { + static constexpr const char *colors[Theme::THEME_COLORS_END] = { "TEXT", + "BLACK", + "RED", + "GREEN", + "BLUE", + "ORANGE", + "YELLOW", + "PINK", + "PURPLE", + "GRAY", + "BROWN", + "CARET", "SHADOW", "OUTLINE", - "PROGRESS_BAR", - "BUTTON", - "BUTTON_DISABLED", - "TAB", - "PARTY_CHAT_TAB", - "PARTY_SOCIAL_TAB", + "PARTY_TAB", + "WHISPER_TAB", "BACKGROUND", "HIGHLIGHT", "TAB_FLASH", "SHOP_WARNING", "ITEM_EQUIPPED", "CHAT", + "OLDCHAT", + "AWAYCHAT", + "BUBBLE_TEXT", "GM", + "GLOBAL", "PLAYER", "WHISPER", "IS", @@ -428,55 +828,19 @@ static int readColorType(const std::string &type) "SERVER_VERSION_NOT_SUPPORTED" }; - if (type.empty()) + if (id.empty()) return -1; for (int i = 0; i < Theme::THEME_COLORS_END; i++) - { - if (compareStrI(type, colors[i]) == 0) - { + if (id == colors[i]) return i; - } - } return -1; } -static gcn::Color readColor(const std::string &description) +static Palette::GradientType readGradientType(const std::string &grad) { - int size = description.length(); - if (size < 7 || description[0] != '#') - { - error: - logger->log("Error, invalid theme color palette: %s", - description.c_str()); - return Palette::BLACK; - } - - int v = 0; - for (int i = 1; i < 7; ++i) - { - char c = description[i]; - int n; - - if ('0' <= c && c <= '9') - n = c - '0'; - else if ('A' <= c && c <= 'F') - n = c - 'A' + 10; - else if ('a' <= c && c <= 'f') - n = c - 'a' + 10; - else - goto error; - - v = (v << 4) | n; - } - - return gcn::Color(v); -} - -static Palette::GradientType readColorGradient(const std::string &grad) -{ - static std::string grads[] = { + static constexpr const char *grads[] = { "STATIC", "PULSE", "SPECTRUM", @@ -487,17 +851,50 @@ static Palette::GradientType readColorGradient(const std::string &grad) return Palette::STATIC; for (int i = 0; i < 4; i++) - { - if (compareStrI(grad, grads[i])) - return (Palette::GradientType) i; - } + if (grad == grads[i]) + return static_cast<Palette::GradientType>(i); return Palette::STATIC; } -static int readProgressType(const std::string &type) +static void readColorNode(XML::Node node, Palette &palette) { - static std::string colors[] = { + const auto idStr = node.getProperty("id", std::string()); + const int id = readColorId(idStr); + if (check(id >= 0, "Theme: 'color' element has unknown 'id' attribute: '%s'!", idStr.c_str())) + return; + + gcn::Color color; + if (check(node.attribute("color", color), "Theme: 'color' element missing 'color' attribute!")) + return; + + std::optional<gcn::Color> outlineColor; + node.attribute("outlineColor", outlineColor); + + const auto grad = readGradientType(node.getProperty("effect", std::string())); + palette.setColor(id, color, outlineColor, grad, 10); +} + +void Theme::readPaletteNode(XML::Node node) +{ + int paletteId; + if (node.attribute("id", paletteId) && static_cast<size_t>(paletteId) != mPalettes.size()) + Log::info("Theme: Non-consecutive palette 'id' attribute with value %d!", paletteId); + + Palette &palette = mPalettes.emplace_back(THEME_COLORS_END); + + for (auto childNode : node.children()) + { + if (childNode.name() == "color") + readColorNode(childNode, palette); + else + Log::info("Theme: Unknown node '%s'!", childNode.name().data()); + } +} + +static int readProgressId(const std::string &id) +{ + static constexpr const char *colors[Theme::THEME_PROG_END] = { "DEFAULT", "HP", "MP", @@ -508,66 +905,32 @@ static int readProgressType(const std::string &type) "JOB" }; - if (type.empty()) + if (id.empty()) return -1; for (int i = 0; i < Theme::THEME_PROG_END; i++) - { - if (compareStrI(type, colors[i]) == 0) + if (id == colors[i]) return i; - } return -1; } -void Theme::loadColors(std::string file) +void Theme::readProgressBarNode(XML::Node node) { - if (file == defaultThemePath) - return; // No need to reload - - if (file.empty()) - file = defaultThemePath; - - file += "/colors.xml"; - - XML::Document doc(file); - XML::Node root = doc.rootNode(); - - if (!root || root.name() != "colors") - { - logger->log("Error loading colors file: %s", file.c_str()); + const auto idStr = node.getProperty("id", std::string()); + const int id = readProgressId(idStr); + if (check(id >= 0, "Theme: 'progress' element has unknown 'id' attribute: '%s'!", idStr.c_str())) return; - } - int type; - std::string temp; - gcn::Color color; - GradientType grad; + std::string color; + if (node.attribute("color", color)) + mProgressColors[id] = std::make_unique<DyePalette>(color); - for (auto node : root.children()) + for (auto childNode : node.children()) { - if (node.name() == "color") - { - type = readColorType(node.getProperty("id", "")); - if (type < 0) // invalid or no type given - continue; - - temp = node.getProperty("color", ""); - if (temp.empty()) // no color set, so move on - continue; - - color = readColor(temp); - grad = readColorGradient(node.getProperty("effect", "")); - - mColors[type].set(type, color, grad, 10); - } - else if (node.name() == "progressbar") - { - type = readProgressType(node.getProperty("id", "")); - if (type < 0) // invalid or no type given - continue; - - mProgressColors[type] = new DyePalette(node.getProperty( "color", "")); - } + if (childNode.name() == "text") + readTextNode(childNode, mProgressTextFormats[id].emplace()); + else + Log::info("Theme: Unknown node '%s' in progressbar!", childNode.name().data()); } } diff --git a/src/resources/theme.h b/src/resources/theme.h index 7edae416..85a720a2 100644 --- a/src/resources/theme.h +++ b/src/resources/theme.h @@ -21,44 +21,145 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SKIN_H -#define SKIN_H +#pragma once #include "graphics.h" #include "eventlistener.h" #include "gui/palette.h" +#include "resources/image.h" +#include "utils/xml.h" +#include <array> #include <map> +#include <memory> +#include <optional> #include <string> +#include <variant> + +namespace gcn { +class Widget; +} class DyePalette; class Image; class ImageSet; class ProgressBar; +class ThemeInfo +{ +public: + ThemeInfo() = default; + explicit ThemeInfo(const std::string &path); + + bool isValid() const { return !name.empty(); } + + const std::string &getName() const { return name; } + const std::string &getPath() const { return path; } + std::string getFullPath() const; + const XML::Document &getDocument() const { return *doc; } + +private: + std::string name; + std::string path; + std::unique_ptr<XML::Document> doc; +}; + +enum class SkinType +{ + Window, + ToolWindow, + Popup, + SpeechBubble, + Button, + ButtonUp, + ButtonDown, + ButtonLeft, + ButtonRight, + ButtonClose, + ButtonSticky, + CheckBox, + RadioButton, + TextField, + Tab, + ScrollArea, + ScrollAreaHBar, + ScrollAreaHMarker, + ScrollAreaVBar, + ScrollAreaVMarker, + DropDownFrame, + DropDownButton, + ProgressBar, + Slider, + SliderHandle, + ResizeGrip, + ShortcutBox, + EquipmentBox, + ItemSlot, + EmoteSlot, +}; + +enum StateFlags : uint8_t +{ + STATE_HOVERED = 0x01, + STATE_SELECTED = 0x02, + STATE_DISABLED = 0x04, + STATE_FOCUSED = 0x08, +}; + +struct ColoredRectangle +{ + gcn::Color color; + bool filled = true; +}; + +struct SkinPart +{ + int offsetX = 0; + int offsetY = 0; + std::variant<ImageRect, Image *, ColoredRectangle> data; +}; + +struct TextFormat +{ + bool bold = false; + gcn::Color color; + std::optional<gcn::Color> outlineColor; + std::optional<gcn::Color> shadowColor; +}; + +struct SkinState +{ + uint8_t stateFlags = 0; + uint8_t setFlags = 0; + TextFormat textFormat; + std::vector<SkinPart> parts; +}; + +struct WidgetState +{ + WidgetState() = default; + explicit WidgetState(const gcn::Widget *widget); + explicit WidgetState(const gcn::Rectangle &dim, uint8_t flags = 0); + + int x = 0; + int y = 0; + int width = 0; + int height = 0; + uint8_t flags = 0; +}; + class Skin { public: - Skin(ImageRect skin, Image *close, Image *stickyUp, Image *stickyDown); - + Skin() = default; ~Skin(); - /** - * Returns the background skin. - */ - const ImageRect &getBorder() const { return mBorder; } + void addState(SkinState state); - /** - * Returns the image used by a close button for this skin. - */ - Image *getCloseImage() const { return mCloseImage; } + void draw(Graphics *graphics, const WidgetState &state) const; - /** - * Returns the image used by a sticky button for this skin. - */ - Image *getStickyImage(bool state) const - { return state ? mStickyImageDown : mStickyImageUp; } + const SkinState *getState(uint8_t flags) const; /** * Returns the minimum width which can be used with this skin. @@ -73,53 +174,69 @@ class Skin /** * Updates the alpha value of the skin */ - void updateAlpha(float minimumOpacityAllowed = 0.0f); - - int instances = 0; + void updateAlpha(float alpha); + + int width = 0; + int height = 0; + int frameSize = 0; + int padding = 0; + int spacing = 0; + int titleBarHeight = 0; + int titleOffsetX = 0; + int titleOffsetY = 0; + int palette = 0; + bool showButtons = true; private: - ImageRect mBorder; /**< The window border and background */ - Image *mCloseImage; /**< Close Button Image */ - Image *mStickyImageUp; /**< Sticky Button Image */ - Image *mStickyImageDown; /**< Sticky Button Image */ + std::vector<SkinState> mStates; }; -class Theme : public Palette, public EventListener +class Theme : public EventListener { public: - static Theme *instance(); - static void deleteInstance(); + static std::string prepareThemePath(); + static std::vector<ThemeInfo> getAvailableThemes(); - static void prepareThemePath(); - static const std::string &getThemePath() { return mThemePath; } + Theme(const ThemeInfo &themeInfo); + ~Theme() override; + + const std::string &getThemePath() const { return mThemePath; } /** - * Returns the patch to the given gui resource relative to the theme + * Returns the patch to the given GUI resource relative to the theme * or, if it isn't in the theme, relative to 'graphics/gui'. */ - static std::string resolveThemePath(const std::string &path); - - static Image *getImageFromTheme(const std::string &path); - static ImageSet *getImageSetFromTheme(const std::string &path, - int w, int h); + std::string resolvePath(const std::string &path) const; + static ResourceRef<Image> getImageFromTheme(const std::string &path); enum ThemePalette { TEXT, + BLACK, // Color 0 + RED, // Color 1 + GREEN, // Color 2 + BLUE, // Color 3 + ORANGE, // Color 4 + YELLOW, // Color 5 + PINK, // Color 6 + PURPLE, // Color 7 + GRAY, // Color 8 + BROWN, // Color 9 + CARET, SHADOW, OUTLINE, - PROGRESS_BAR, - BUTTON, - BUTTON_DISABLED, - TAB, - PARTY_CHAT_TAB, - PARTY_SOCIAL_TAB, + PARTY_TAB, + WHISPER_TAB, BACKGROUND, HIGHLIGHT, TAB_FLASH, SHOP_WARNING, ITEM_EQUIPPED, CHAT, + OLDCHAT, + AWAYCHAT, + BUBBLE_TEXT, GM, + GLOBAL, PLAYER, WHISPER, IS, @@ -159,42 +276,52 @@ class Theme : public Palette, public EventListener }; /** - * Gets the color associated with the type. Sets the alpha channel - * before returning. + * Gets the color associated with the type in the default palette (0). * * @param type the color type requested - * @param alpha alpha channel to use - * * @return the requested color */ - static const gcn::Color &getThemeColor(int type, int alpha = 255) - { - return mInstance->getColor(type, alpha); - } - - static const gcn::Color &getThemeColor(char c, bool &valid) - { - return mInstance->getColor(c, valid); - } + static const gcn::Color &getThemeColor(int type); static gcn::Color getProgressColor(int type, float progress); + const Palette &getPalette(size_t index) const; + + /** + * Returns a color from the default palette (0). + */ + const gcn::Color &getColor(int type) const; + /** - * Loads a skin. + * Returns the color ID associated with a character, if it exists. + * Returns no value if the character is not found. + * + * @param c character requested + * @return the requested color or none */ - Skin *load(const std::string &filename, - const std::string &defaultPath = getThemePath()); + static std::optional<int> getColorIdForChar(char c); + + void drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const; + void drawProgressBar(Graphics *graphics, + const gcn::Rectangle &area, + const gcn::Color &color, + float progress, + const std::string &text = std::string(), + ProgressPalette progressType = ProgressPalette::THEME_PROG_END) const; + + const Skin &getSkin(SkinType skinType) const; + + const Image *getIcon(const std::string &name) const; /** - * Updates the alpha values of all of the skins. + * Get the current GUI alpha value. */ - void updateAlpha(); + int getGuiAlpha() const { return static_cast<int>(mAlpha * 255.0f); } /** * Get the minimum opacity allowed to skins. */ - float getMinimumOpacity() const - { return mMinimumOpacity; } + float getMinimumOpacity() const { return mMinimumOpacity; } /** * Set the minimum opacity allowed to skins. @@ -205,28 +332,33 @@ class Theme : public Palette, public EventListener void event(Event::Channel channel, const Event &event) override; private: - Theme(); - ~Theme() override; - - Skin *readSkin(const std::string &filename); - - // Map containing all window skins - std::map<std::string, Skin *> mSkins; + /** + * Updates the alpha values of all of the skins and images. + */ + void updateAlpha(); - static std::string mThemePath; - static Theme *mInstance; + ResourceRef<Image> getImage(const std::string &path) const; - static bool tryThemePath(std::string themePath); + bool readTheme(const ThemeInfo &themeInfo); + void readSkinNode(XML::Node node); + void readSkinStateNode(XML::Node node, Skin &skin) const; + void readSkinStateImgNode(XML::Node node, SkinState &state) const; + void readIconNode(XML::Node node); + void readPaletteNode(XML::Node node); + void readProgressBarNode(XML::Node node); - void loadColors(std::string file = std::string()); + std::string mThemePath; + std::map<SkinType, Skin> mSkins; + std::map<std::string, Image *> mIcons; /** * Tells if the current skins opacity * should not get less than the given value */ - float mMinimumOpacity; + float mMinimumOpacity = 0.0f; + float mAlpha = 1.0; - std::vector<DyePalette *> mProgressColors; + std::vector<Palette> mPalettes; + std::array<std::unique_ptr<DyePalette>, THEME_PROG_END> mProgressColors; + std::array<std::optional<TextFormat>, THEME_PROG_END> mProgressTextFormats; }; - -#endif diff --git a/src/resources/userpalette.h b/src/resources/userpalette.h index 946a4725..347491f1 100644 --- a/src/resources/userpalette.h +++ b/src/resources/userpalette.h @@ -20,8 +20,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef USER_PALETTE_H -#define USER_PALETTE_H +#pragma once #include "gui/palette.h" @@ -185,5 +184,3 @@ class UserPalette : public Palette, public gcn::ListModel }; extern UserPalette *userPalette; - -#endif // USER_PALETTE_H diff --git a/src/resources/wallpaper.cpp b/src/resources/wallpaper.cpp index e8167b6b..2bdcd656 100644 --- a/src/resources/wallpaper.cpp +++ b/src/resources/wallpaper.cpp @@ -23,7 +23,7 @@ #include "configuration.h" -#include <physfs.h> +#include "utils/filesystem.h" #include <algorithm> #include <cstring> @@ -90,35 +90,24 @@ void Wallpaper::loadWallpapers() initWallpaperPaths(); - char **fileNames = PHYSFS_enumerateFiles(wallpaperPath.c_str()); - - for (char **fileName = fileNames; *fileName; fileName++) + for (auto fileName : FS::enumerateFiles(wallpaperPath)) { - int width; - int height; - // If the backup file is found, we tell it. - if (strncmp(*fileName, wallpaperFile.c_str(), strlen(*fileName)) == 0) + if (wallpaperFile == fileName) haveBackup = true; // If the image format is terminated by: "_<width>x<height>.png" // It is taken as a potential wallpaper. - - // First, get the base filename of the image: - std::string filename = *fileName; - filename = filename.substr(0, filename.rfind("_")); - - // Check that the base filename doesn't have any '%' markers. - if (filename.find("%") == std::string::npos) + if (auto sizeSuffix = strrchr(fileName, '_')) { - // Then, append the width and height search mask. - filename.append("_%dx%d.png"); + int width; + int height; - if (sscanf(*fileName, filename.c_str(), &width, &height) == 2) + if (sscanf(sizeSuffix, "_%dx%d.png", &width, &height) == 2) { WallpaperData wp; wp.filename = wallpaperPath; - wp.filename.append(*fileName); + wp.filename.append(fileName); wp.width = width; wp.height = height; wallpaperData.push_back(wp); @@ -126,8 +115,6 @@ void Wallpaper::loadWallpapers() } } - PHYSFS_freeList(fileNames); - std::sort(wallpaperData.begin(), wallpaperData.end(), wallpaperCompare); } diff --git a/src/resources/wallpaper.h b/src/resources/wallpaper.h index 532dfd38..7e72e2f0 100644 --- a/src/resources/wallpaper.h +++ b/src/resources/wallpaper.h @@ -19,8 +19,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef WALLPAPER_H -#define WALLPAPER_H +#pragma once #include <string> @@ -46,5 +45,3 @@ class Wallpaper */ static std::string getWallpaper(int width, int height); }; - -#endif // WALLPAPER_H |