diff options
Diffstat (limited to 'src/resources')
28 files changed, 962 insertions, 355 deletions
diff --git a/src/resources/abilitydb.cpp b/src/resources/abilitydb.cpp index 311ee9eb..cb596ea8 100644 --- a/src/resources/abilitydb.cpp +++ b/src/resources/abilitydb.cpp @@ -41,7 +41,7 @@ static AbilityInfo::TargetMode targetModeFromString(const std::string& str) if (str == "direction") return AbilityInfo::TARGET_DIRECTION; - logger->log("AbilityDB: Warning, unknown target mode \"%s\"", str.c_str() ); + Log::info("AbilityDB: Warning, unknown target mode \"%s\"", str.c_str() ); return AbilityInfo::TARGET_BEING; } @@ -68,7 +68,7 @@ void AbilityDB::readAbilityNode(XML::Node node, const std::string &filename) info->rechargeCurrent = 0; if (mAbilityInfos.find(id) != mAbilityInfos.end()) - logger->log("AbilityDB: Duplicate ability ID %d in %s, ignoring", id, filename.c_str()); + Log::info("AbilityDB: Duplicate ability ID %d in %s, ignoring", id, filename.c_str()); else mAbilityInfos[id] = info; } diff --git a/src/resources/animation.cpp b/src/resources/animation.cpp index 91c22236..c529400f 100644 --- a/src/resources/animation.cpp +++ b/src/resources/animation.cpp @@ -84,7 +84,7 @@ Animation Animation::fromXML(XML::Node node, const std::string &dyePalettes) if (index < 0) { - logger->log("No valid value for 'index'"); + Log::info("No valid value for 'index'"); continue; } @@ -92,7 +92,7 @@ Animation Animation::fromXML(XML::Node node, const std::string &dyePalettes) if (!img) { - logger->log("No image at index %d", index); + Log::info("No image at index %d", index); continue; } @@ -105,7 +105,7 @@ Animation Animation::fromXML(XML::Node node, const std::string &dyePalettes) if (start < 0 || end < 0) { - logger->log("No valid value for 'start' or 'end'"); + Log::info("No valid value for 'start' or 'end'"); continue; } @@ -115,7 +115,7 @@ Animation Animation::fromXML(XML::Node node, const std::string &dyePalettes) if (!img) { - logger->log("No image at index %d", start); + Log::info("No image at index %d", start); continue; } diff --git a/src/resources/attributes.cpp b/src/resources/attributes.cpp index 7ec6b516..5b1018ee 100644 --- a/src/resources/attributes.cpp +++ b/src/resources/attributes.cpp @@ -240,22 +240,22 @@ namespace Attributes { 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; } 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; diff --git a/src/resources/beinginfo.cpp b/src/resources/beinginfo.cpp index 20c24d4f..f2edf1d8 100644 --- a/src/resources/beinginfo.cpp +++ b/src/resources/beinginfo.cpp @@ -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,8 +84,8 @@ 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); } 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/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 61ad4df2..1730d2fd 100644 --- a/src/resources/dye.h +++ b/src/resources/dye.h @@ -30,13 +30,12 @@ class DyePalette { public: - /** * Creates a palette based on the given string. * The string is either a file name or a sequence of hexadecimal RGB * values separated by ',' and starting with '#'. */ - DyePalette(const std::string &pallete); + DyePalette(const std::string &description); /** * Gets a pixel color depending on its intensity. First color is @@ -50,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; }; @@ -62,7 +65,6 @@ class DyePalette class Dye { public: - /** * Creates a set of palettes based on the given string. * diff --git a/src/resources/emotedb.cpp b/src/resources/emotedb.cpp index d29483d1..c0f5f777 100644 --- a/src/resources/emotedb.cpp +++ b/src/resources/emotedb.cpp @@ -51,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; } @@ -63,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; } @@ -74,8 +74,8 @@ 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; } @@ -85,8 +85,8 @@ void EmoteDB::readEmoteNode(XML::Node node, const std::string &filename) 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; } @@ -119,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/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/image.cpp b/src/resources/image.cpp index 328ea9b8..6e6f1187 100644 --- a/src/resources/image.cpp +++ b/src/resources/image.cpp @@ -32,6 +32,8 @@ #include <SDL_image.h> +#include <algorithm> + #ifdef USE_OPENGL bool Image::mUseOpenGL = false; bool Image::mPowerOfTwoTextures = true; @@ -54,8 +56,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 +73,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 +101,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 +113,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); @@ -174,18 +178,7 @@ void Image::setAlpha(float alpha) if (!useOpenGL() && mDisableTransparency) return; - if (mAlpha == alpha) - return; - - if (alpha < 0.0f || alpha > 1.0f) - return; - - mAlpha = alpha; - - if (mTexture) - { - SDL_SetTextureAlphaMod(mTexture, (Uint8) (255 * mAlpha)); - } + mAlpha = std::clamp(alpha, 0.0f, 1.0f); } Image *Image::_SDLload(SDL_Surface *image) @@ -215,8 +208,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 +242,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 +297,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/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/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/itemdb.cpp b/src/resources/itemdb.cpp index 05f6ad0b..7f4cbdec 100644 --- a/src/resources/itemdb.cpp +++ b/src/resources/itemdb.cpp @@ -125,7 +125,7 @@ void ItemDB::loadEmptyItemDefinition() 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::Neutral, 0); mUnknown->hitEffectId = paths.getIntValue("hitEffectId"); mUnknown->criticalHitEffectId = paths.getIntValue("criticalHitEffectId"); } @@ -148,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; } @@ -164,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; } @@ -184,7 +184,7 @@ void ItemDB::loadSpriteRef(ItemInfo &itemInfo, XML::Node node) if (gender == "female" || gender == "unisex") itemInfo.setSprite(filename, Gender::Female, race); if (gender == "hidden" || gender == "other" || gender == "unisex") - itemInfo.setSprite(filename, Gender::Hidden, race); + itemInfo.setSprite(filename, Gender::Neutral, race); } void ItemDB::loadSoundRef(ItemInfo &itemInfo, XML::Node node) @@ -202,8 +202,8 @@ void ItemDB::loadSoundRef(ItemInfo &itemInfo, XML::Node node) } else { - logger->log("ItemDB: Ignoring unknown sound event '%s'", - event.c_str()); + Log::info("ItemDB: Ignoring unknown sound event '%s'", + event.c_str()); } } @@ -237,15 +237,15 @@ void ItemDB::loadReplacement(ItemInfo &info, XML::Node replaceNode) if (sprite == SPRITE_UNKNOWN) { - logger->log("ItemDB: Invalid sprite name '%s' in replace tag", - spriteString.data()); + Log::info("ItemDB: Invalid sprite name '%s' in replace tag", + spriteString.data()); return; } if (direction == DIRECTION_UNKNOWN) { - logger->log("ItemDB: Invalid direction name '%s' in replace tag", - directionString.data()); + Log::info("ItemDB: Invalid direction name '%s' in replace tag", + directionString.data()); return; } @@ -266,7 +266,7 @@ void ItemDB::loadReplacement(ItemInfo &info, XML::Node replaceNode) void ItemDB::unload() { - logger->log("Unloading item database..."); + Log::info("Unloading item database..."); delete mUnknown; mUnknown = nullptr; @@ -283,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); @@ -346,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); } } @@ -359,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()); } } @@ -368,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) { @@ -504,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; } @@ -514,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; } @@ -528,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(); @@ -537,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/mapreader.cpp b/src/resources/mapreader.cpp index b952cdcc..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; @@ -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/monsterdb.cpp b/src/resources/monsterdb.cpp index 4963f93f..7f092a0e 100644 --- a/src/resources/monsterdb.cpp +++ b/src/resources/monsterdb.cpp @@ -111,11 +111,11 @@ void MonsterDB::readMonsterNode(XML::Node node, const std::string &filename) } 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/music.cpp b/src/resources/music.cpp index 069af588..b73d89ce 100644 --- a/src/resources/music.cpp +++ b/src/resources/music.cpp @@ -40,7 +40,7 @@ Music *Music::load(SDL_RWops *rw) 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/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/questdb.cpp b/src/resources/questdb.cpp new file mode 100644 index 00000000..1424c20e --- /dev/null +++ b/src/resources/questdb.cpp @@ -0,0 +1,232 @@ +/* + * 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 init() +{ + unload(); +} + +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..43996b0b --- /dev/null +++ b/src/resources/questdb.h @@ -0,0 +1,139 @@ +/* + * 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 init(); + 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/resourcemanager.cpp b/src/resources/resourcemanager.cpp index e62407e3..2857c0df 100644 --- a/src/resources/resourcemanager.cpp +++ b/src/resources/resourcemanager.cpp @@ -45,7 +45,7 @@ ResourceManager *ResourceManager::instance = nullptr; ResourceManager::ResourceManager() { - logger->log("Initializing resource manager..."); + Log::info("Initializing resource manager..."); } ResourceManager::~ResourceManager() @@ -86,11 +86,11 @@ 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; @@ -118,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 } @@ -129,12 +129,12 @@ void ResourceManager::cleanOrphans() bool ResourceManager::addToSearchPath(const std::string &path, bool append) { - logger->log("Adding to PhysicsFS: %s", path.c_str()); if (!FS::addToSearchPath(path, append)) { - logger->log("Error: %s", FS::getLastError()); + 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; } diff --git a/src/resources/settingsmanager.cpp b/src/resources/settingsmanager.cpp index 9323d4d1..3dfb5eb4 100644 --- a/src/resources/settingsmanager.cpp +++ b/src/resources/settingsmanager.cpp @@ -27,6 +27,7 @@ #include "resources/monsterdb.h" #include "resources/npcdb.h" #include "resources/abilitydb.h" +#include "resources/questdb.h" #include "resources/statuseffectdb.h" #include "net/net.h" @@ -38,6 +39,9 @@ #include "log.h" #include "units.h" +#include <string> +#include <set> + namespace SettingsManager { static std::string mSettingsFile; @@ -53,6 +57,7 @@ namespace SettingsManager hairDB.init(); itemDb->init(); MonsterDB::init(); + QuestDB::init(); AbilityDB::init(); NPCDB::init(); EmoteDB::init(); @@ -96,6 +101,7 @@ namespace SettingsManager NPCDB::unload(); AbilityDB::unload(); MonsterDB::unload(); + QuestDB::unload(); if (itemDb) itemDb->unload(); hairDB.unload(); @@ -107,7 +113,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(); @@ -118,7 +124,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; } @@ -156,7 +162,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 { @@ -165,7 +171,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") @@ -177,7 +183,7 @@ 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") { @@ -221,6 +227,10 @@ namespace SettingsManager { NPCDB::readNPCNode(childNode, filename); } + else if (childNode.name() == "var") + { + QuestDB::readQuestVarNode(childNode, filename); + } else if (childNode.name() == "emote") { EmoteDB::readEmoteNode(childNode, filename); diff --git a/src/resources/settingsmanager.h b/src/resources/settingsmanager.h index 5b70f865..d9a6994c 100644 --- a/src/resources/settingsmanager.h +++ b/src/resources/settingsmanager.h @@ -20,10 +20,6 @@ #pragma once -#include <string> -#include <list> -#include <set> - namespace SettingsManager { void load(); diff --git a/src/resources/soundeffect.cpp b/src/resources/soundeffect.cpp index 19d7a820..1b0492d7 100644 --- a/src/resources/soundeffect.cpp +++ b/src/resources/soundeffect.cpp @@ -36,7 +36,7 @@ SoundEffect *SoundEffect::load(SDL_RWops *rw) 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; } diff --git a/src/resources/spritedef.cpp b/src/resources/spritedef.cpp index 85e5e566..0e7f12dd 100644 --- a/src/resources/spritedef.cpp +++ b/src/resources/spritedef.cpp @@ -44,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; } @@ -66,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"); @@ -157,7 +157,7 @@ void SpriteDef::loadImageSet(XML::Node node, const std::string &palettes) 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())); } @@ -174,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; @@ -215,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; } @@ -239,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; } @@ -247,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; } @@ -260,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; } @@ -270,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; } @@ -295,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); @@ -306,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; } diff --git a/src/resources/theme.cpp b/src/resources/theme.cpp index 694f2210..ea2cef45 100644 --- a/src/resources/theme.cpp +++ b/src/resources/theme.cpp @@ -25,7 +25,6 @@ #include "configuration.h" #include "log.h" -#include "textrenderer.h" #include "resources/dye.h" #include "resources/image.h" @@ -38,7 +37,6 @@ #include <guichan/widget.hpp> #include <algorithm> -#include <optional> /** * Initializes the directory in which the client looks for GUI themes, which at @@ -55,18 +53,33 @@ static void initDefaultThemePath() defaultThemePath = "graphics/gui/"; } -static std::optional<std::string> findThemePath(const std::string &theme) +static bool isThemePath(const std::string &theme) { - if (theme.empty()) - return {}; + return FS::exists(defaultThemePath + theme + "/theme.xml"); +} - std::string themePath = defaultThemePath; - themePath += theme; - if (FS::isDirectory(themePath)) - return themePath; +ThemeInfo::ThemeInfo(const std::string &path) + : path(path) +{ + auto themeFile = getFullPath() + "/theme.xml"; + if (!FS::exists(themeFile)) + return; - 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; } @@ -120,11 +133,11 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const if constexpr (std::is_same_v<T, ImageRect>) { - graphics->drawImageRect(state.x + part.offsetX, + graphics->drawImageRect(data, + state.x + part.offsetX, state.y + part.offsetY, state.width, - state.height, - data); + state.height); } else if constexpr (std::is_same_v<T, Image*>) { @@ -132,12 +145,21 @@ void Skin::draw(Graphics *graphics, const WidgetState &state) const } else if constexpr (std::is_same_v<T, ColoredRectangle>) { + const auto color = graphics->getColor(); + // TODO: Take GUI alpha into account graphics->setColor(data.color); - graphics->fillRectangle(gcn::Rectangle(state.x + part.offsetX, - state.y + part.offsetY, - state.width, - state.height)); - graphics->setColor(gcn::Color(255, 255, 255)); + + 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); } @@ -195,7 +217,7 @@ void Skin::updateAlpha(float alpha) for (auto &part : state.parts) { if (auto rect = std::get_if<ImageRect>(&part.data)) - rect->setAlpha(alpha); + rect->image->setAlpha(alpha); else if (auto img = std::get_if<Image *>(&part.data)) (*img)->setAlpha(alpha); } @@ -203,41 +225,60 @@ void Skin::updateAlpha(float alpha) } -Theme::Theme(const std::string &path) - : Palette(THEME_COLORS_END) - , mThemePath(path) - , mProgressColors(THEME_PROG_END) +Theme::Theme(const ThemeInfo &themeInfo) + : mThemePath(themeInfo.getFullPath()) { listen(Event::ConfigChannel); - readTheme("theme.xml"); + 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() = default; +Theme::~Theme() +{ + for (auto &[_, image] : mIcons) + delete image; +} std::string Theme::prepareThemePath() { initDefaultThemePath(); // Try theme from settings - auto themePath = findThemePath(config.theme); + if (isThemePath(config.theme)) + return config.theme; // Try theme from branding - if (!themePath) - themePath = findThemePath(branding.getStringValue("theme")); + if (isThemePath(branding.getStringValue("theme"))) + return branding.getStringValue("theme"); - return themePath.value_or(defaultThemePath); + 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 @@ -269,14 +310,9 @@ ResourceRef<Image> Theme::getImageFromTheme(const std::string &path) return gui->getTheme()->getImage(path); } -const gcn::Color &Theme::getThemeColor(int type, int alpha) -{ - return gui->getTheme()->getColor(type, alpha); -} - -const gcn::Color &Theme::getThemeColor(char c, bool &valid) +const gcn::Color &Theme::getThemeColor(int type) { - return gui->getTheme()->getColor(c, valid); + return gui->getTheme()->getColor(type); } gcn::Color Theme::getProgressColor(int type, float progress) @@ -289,14 +325,62 @@ gcn::Color Theme::getProgressColor(int type, float progress) return gcn::Color(color[0], color[1], color[2]); } +const Palette &Theme::getPalette(size_t index) const +{ + 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; + } + + return {}; +} + void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state) const { getSkin(type).draw(graphics, state); } -void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, - const gcn::Color &color, float progress, - const std::string &text) const +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(); @@ -325,17 +409,21 @@ void Theme::drawProgressBar(Graphics *graphics, const gcn::Rectangle &area, { if (auto skinState = skin.getState(widgetState.flags)) { - auto font = skinState->textFormat.bold ? boldFont : gui->getFont(); + 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, - skinState->textFormat); + graphics->drawText(text, + textX, + textY, + gcn::Graphics::CENTER, + font, + *textFormat); } } @@ -350,14 +438,13 @@ const Skin &Theme::getSkin(SkinType skinType) const return it != mSkins.end() ? it->second : emptySkin; } -int Theme::getMinWidth(SkinType skinType) const +const Image *Theme::getIcon(const std::string &name) const { - return getSkin(skinType).getMinWidth(); -} + auto it = mIcons.find(name); + if (it == mIcons.end()) + return nullptr; -int Theme::getMinHeight(SkinType skinType) const -{ - return getSkin(skinType).getMinHeight(); + return it->second; } void Theme::setMinimumOpacity(float minimumOpacity) @@ -377,8 +464,8 @@ void Theme::updateAlpha() mAlpha = alpha; - for (auto &skin : mSkins) - skin.second.updateAlpha(mAlpha); + for (auto &[_, skin] : mSkins) + skin.updateAlpha(mAlpha); } void Theme::event(Event::Channel channel, const Event &event) @@ -395,20 +482,21 @@ static bool check(bool value, const char *msg, ...) { if (!value) { - va_list args; - va_start(args, msg); - logger->log(msg, args); - va_end(args); + va_list ap; + va_start(ap, msg); + Log::vinfo(msg, ap); + va_end(ap); } return !value; } -bool Theme::readTheme(const std::string &filename) +bool Theme::readTheme(const ThemeInfo &themeInfo) { - logger->log("Loading theme '%s'.", filename.c_str()); + Log::info("Loading %s theme from '%s'...", + themeInfo.getName().c_str(), + themeInfo.getPath().c_str()); - XML::Document doc(resolvePath(filename)); - XML::Node rootNode = doc.rootNode(); + XML::Node rootNode = themeInfo.getDocument().rootNode(); if (!rootNode || rootNode.name() != "theme") return false; @@ -417,13 +505,17 @@ bool Theme::readTheme(const std::string &filename) { if (childNode.name() == "skin") readSkinNode(childNode); - else if (childNode.name() == "color") - readColorNode(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("Finished loading theme."); + Log::info("Finished loading theme."); for (auto &[_, skin] : mSkins) skin.updateAlpha(mAlpha); @@ -434,8 +526,10 @@ bool Theme::readTheme(const std::string &filename) 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 == "Desktop") return SkinType::Desktop; if (type == "Button") return SkinType::Button; if (type == "ButtonUp") return SkinType::ButtonUp; if (type == "ButtonDown") return SkinType::ButtonDown; @@ -459,6 +553,9 @@ static std::optional<SkinType> readSkinType(std::string_view type) 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 {}; } @@ -471,18 +568,40 @@ void Theme::readSkinNode(XML::Node node) 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); } +static void readSkinStateRectNode(XML::Node node, SkinState &state) +{ + auto &part = state.parts.emplace_back(); + auto &rect = part.data.emplace<ColoredRectangle>(); + + node.attribute("color", rect.color); + node.attribute("alpha", rect.color.a); + node.attribute("fill", rect.filled); +} + +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); +} + void Theme::readSkinStateNode(XML::Node node, Skin &skin) const { SkinState state; @@ -511,21 +630,12 @@ void Theme::readSkinStateNode(XML::Node node, Skin &skin) const else if (childNode.name() == "rect") readSkinStateRectNode(childNode, state); else if (childNode.name() == "text") - readSkinStateTextNode(childNode, state); + readTextNode(childNode, state.textFormat); } skin.addState(std::move(state)); } -void Theme::readSkinStateTextNode(XML::Node node, SkinState &state) const -{ - auto &textFormat = state.textFormat; - node.attribute("bold", textFormat.bold); - node.attribute("color", textFormat.color); - node.attribute("outlineColor", textFormat.outlineColor); - node.attribute("shadowColor", textFormat.shadowColor); -} - template<> inline void fromString(const char *str, FillMode &value) { @@ -580,24 +690,13 @@ void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const 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); - - const int gridx[4] = {x, x + left, x + width - right, x + width}; - const int gridy[4] = {y, y + top, y + height - bottom, y + height}; - unsigned a = 0; - - for (unsigned y = 0; y < 3; y++) - { - for (unsigned x = 0; x < 3; x++) - { - border.grid[a] = image->getSubImage(gridx[x], - gridy[y], - gridx[x + 1] - gridx[x], - gridy[y + 1] - gridy[y]); - a++; - } - } } else { @@ -611,8 +710,8 @@ inline void fromString(const char *str, gcn::Color &value) if (strlen(str) < 7 || str[0] != '#') { error: - logger->log("Error, invalid theme color palette: %s", str); - value = Palette::BLACK; + Log::info("Error, invalid theme color palette: %s", str); + value = gcn::Color(0, 0, 0); return; } @@ -637,31 +736,73 @@ inline void fromString(const char *str, gcn::Color &value) value = gcn::Color(v); } -void Theme::readSkinStateRectNode(XML::Node node, SkinState &state) const +void Theme::readIconNode(XML::Node node) { - auto &part = state.parts.emplace_back(); - auto &rect = part.data.emplace<ColoredRectangle>(); + std::string name; + std::string src; + node.attribute("name", name); + node.attribute("src", src); - node.attribute("color", rect.color); - node.attribute("alpha", rect.color.a); + 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 constexpr const char *colors[Theme::THEME_COLORS_END] = { "TEXT", + "BLACK", + "RED", + "GREEN", + "BLUE", + "ORANGE", + "YELLOW", + "PINK", + "PURPLE", + "GRAY", + "BROWN", + "CARET", "SHADOW", "OUTLINE", - "PARTY_CHAT_TAB", - "PARTY_SOCIAL_TAB", + "PARTY_TAB", + "WHISPER_TAB", "BACKGROUND", "HIGHLIGHT", + "HIGHLIGHT_TEXT", "TAB_FLASH", "SHOP_WARNING", "ITEM_EQUIPPED", "CHAT", + "OLDCHAT", + "AWAYCHAT", "BUBBLE_TEXT", "GM", + "GLOBAL", "PLAYER", "WHISPER", "IS", @@ -687,17 +828,17 @@ 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 (type == colors[i]) + if (id == colors[i]) return i; return -1; } -static Palette::GradientType readColorGradient(const std::string &grad) +static Palette::GradientType readGradientType(const std::string &grad) { static constexpr const char *grads[] = { "STATIC", @@ -716,22 +857,42 @@ static Palette::GradientType readColorGradient(const std::string &grad) return Palette::STATIC; } -void Theme::readColorNode(XML::Node node) +static void readColorNode(XML::Node node, Palette &palette) { - const int type = readColorType(node.getProperty("id", std::string())); - if (check(type > 0, "Theme: 'color' element has invalid or no 'type' attribute!")) + 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; - const GradientType grad = readColorGradient(node.getProperty("effect", std::string())); + std::optional<gcn::Color> outlineColor; + node.attribute("outlineColor", outlineColor); - mColors[type].set(type, color, grad, 10); + const auto grad = readGradientType(node.getProperty("effect", std::string())); + palette.setColor(id, color, outlineColor, grad, 10); } -static int readProgressType(const std::string &type) +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", @@ -744,11 +905,11 @@ 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 (type == colors[i]) + if (id == colors[i]) return i; return -1; @@ -756,9 +917,20 @@ static int readProgressType(const std::string &type) void Theme::readProgressBarNode(XML::Node node) { - const int type = readProgressType(node.getProperty("id", std::string())); - if (type < 0) // invalid or no type given + 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; - mProgressColors[type] = std::make_unique<DyePalette>(node.getProperty("color", std::string())); + std::string color; + if (node.attribute("color", color)) + mProgressColors[id] = std::make_unique<DyePalette>(color); + + for (auto childNode : node.children()) + { + 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 6d71b067..80b78fdd 100644 --- a/src/resources/theme.h +++ b/src/resources/theme.h @@ -30,8 +30,10 @@ #include "resources/image.h" #include "utils/xml.h" +#include <array> #include <map> #include <memory> +#include <optional> #include <string> #include <variant> @@ -44,11 +46,32 @@ 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, + Desktop, Button, ButtonUp, ButtonDown, @@ -72,6 +95,9 @@ enum class SkinType SliderHandle, ResizeGrip, ShortcutBox, + EquipmentBox, + ItemSlot, + EmoteSlot, }; enum StateFlags : uint8_t @@ -85,6 +111,7 @@ enum StateFlags : uint8_t struct ColoredRectangle { gcn::Color color; + bool filled = true; }; struct SkinPart @@ -150,25 +177,32 @@ class Skin */ 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: std::vector<SkinState> mStates; }; -class Theme : public Palette, public EventListener +class Theme : public EventListener { public: static std::string prepareThemePath(); + static std::vector<ThemeInfo> getAvailableThemes(); - Theme(const std::string &path); + 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 * or, if it isn't in the theme, relative to 'graphics/gui'. @@ -178,18 +212,33 @@ class Theme : public Palette, public EventListener 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, - PARTY_CHAT_TAB, - PARTY_SOCIAL_TAB, + PARTY_TAB, + WHISPER_TAB, BACKGROUND, HIGHLIGHT, + HIGHLIGHT_TEXT, TAB_FLASH, SHOP_WARNING, ITEM_EQUIPPED, CHAT, + OLDCHAT, + AWAYCHAT, BUBBLE_TEXT, GM, + GLOBAL, PLAYER, WHISPER, IS, @@ -229,30 +278,42 @@ 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); - static const gcn::Color &getThemeColor(char c, bool &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; + + /** + * 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 + */ + 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()) const; + const std::string &text = std::string(), + ProgressPalette progressType = ProgressPalette::THEME_PROG_END) const; const Skin &getSkin(SkinType skinType) const; - int getMinWidth(SkinType skinType) const; - int getMinHeight(SkinType skinType) const; + const Image *getIcon(const std::string &name) const; /** * Get the current GUI alpha value. @@ -280,17 +341,17 @@ class Theme : public Palette, public EventListener ResourceRef<Image> getImage(const std::string &path) const; - bool readTheme(const std::string &filename); + bool readTheme(const ThemeInfo &themeInfo); void readSkinNode(XML::Node node); void readSkinStateNode(XML::Node node, Skin &skin) const; - void readSkinStateTextNode(XML::Node node, SkinState &state) const; void readSkinStateImgNode(XML::Node node, SkinState &state) const; - void readSkinStateRectNode(XML::Node node, SkinState &state) const; - void readColorNode(XML::Node node); + void readIconNode(XML::Node node); + void readPaletteNode(XML::Node node); void readProgressBarNode(XML::Node node); std::string mThemePath; std::map<SkinType, Skin> mSkins; + std::map<std::string, Image *> mIcons; /** * Tells if the current skins opacity @@ -299,5 +360,7 @@ class Theme : public Palette, public EventListener float mMinimumOpacity = 0.0f; float mAlpha = 1.0; - std::vector<std::unique_ptr<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; }; diff --git a/src/resources/wallpaper.cpp b/src/resources/wallpaper.cpp index 2bdcd656..ece3e950 100644 --- a/src/resources/wallpaper.cpp +++ b/src/resources/wallpaper.cpp @@ -87,6 +87,7 @@ bool wallpaperCompare(const WallpaperData &a, const WallpaperData &b) void Wallpaper::loadWallpapers() { wallpaperData.clear(); + haveBackup = false; initWallpaperPaths(); |