summaryrefslogtreecommitdiff
path: root/src/resources
diff options
context:
space:
mode:
Diffstat (limited to 'src/resources')
-rw-r--r--src/resources/abilitydb.cpp106
-rw-r--r--src/resources/abilitydb.h (renamed from src/resources/specialdb.h)38
-rw-r--r--src/resources/action.h5
-rw-r--r--src/resources/ambientlayer.h5
-rw-r--r--src/resources/animation.cpp92
-rw-r--r--src/resources/animation.h15
-rw-r--r--src/resources/attributes.cpp76
-rw-r--r--src/resources/attributes.h5
-rw-r--r--src/resources/beinginfo.cpp28
-rw-r--r--src/resources/beinginfo.h15
-rw-r--r--src/resources/chardb.cpp4
-rw-r--r--src/resources/chardb.h5
-rw-r--r--src/resources/dye.cpp44
-rw-r--r--src/resources/dye.h15
-rw-r--r--src/resources/emotedb.cpp33
-rw-r--r--src/resources/emotedb.h14
-rw-r--r--src/resources/hairdb.cpp10
-rw-r--r--src/resources/hairdb.h5
-rw-r--r--src/resources/image.cpp64
-rw-r--r--src/resources/image.h5
-rw-r--r--src/resources/imageset.cpp2
-rw-r--r--src/resources/imageset.h5
-rw-r--r--src/resources/imagewriter.cpp10
-rw-r--r--src/resources/imagewriter.h2
-rw-r--r--src/resources/itemdb.cpp154
-rw-r--r--src/resources/itemdb.h12
-rw-r--r--src/resources/iteminfo.h37
-rw-r--r--src/resources/mapreader.cpp41
-rw-r--r--src/resources/mapreader.h5
-rw-r--r--src/resources/monsterdb.cpp22
-rw-r--r--src/resources/monsterdb.h5
-rw-r--r--src/resources/music.cpp4
-rw-r--r--src/resources/music.h11
-rw-r--r--src/resources/npcdb.cpp4
-rw-r--r--src/resources/npcdb.h5
-rw-r--r--src/resources/questdb.cpp227
-rw-r--r--src/resources/questdb.h138
-rw-r--r--src/resources/resource.cpp2
-rw-r--r--src/resources/resource.h13
-rw-r--r--src/resources/resourcemanager.cpp299
-rw-r--r--src/resources/resourcemanager.h155
-rw-r--r--src/resources/settingsmanager.cpp59
-rw-r--r--src/resources/settingsmanager.h6
-rw-r--r--src/resources/soundeffect.cpp15
-rw-r--r--src/resources/soundeffect.h17
-rw-r--r--src/resources/specialdb.cpp115
-rw-r--r--src/resources/spritedef.cpp41
-rw-r--r--src/resources/spritedef.h13
-rw-r--r--src/resources/statuseffectdb.cpp96
-rw-r--r--src/resources/statuseffectdb.h67
-rw-r--r--src/resources/theme.cpp1003
-rw-r--r--src/resources/theme.h284
-rw-r--r--src/resources/userpalette.h5
-rw-r--r--src/resources/wallpaper.cpp29
-rw-r--r--src/resources/wallpaper.h5
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