diff options
Diffstat (limited to 'src')
150 files changed, 3022 insertions, 2044 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a6a93db..8738a41b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ pkg_check_modules(SDL2 REQUIRED SDL2_image SDL2_mixer SDL2_net - SDL2_ttf) + SDL2_ttf>=2.0.12) find_package(PhysFS REQUIRED) find_package(CURL REQUIRED) find_package(LibXml2 REQUIRED) @@ -230,6 +230,8 @@ set(SRCS gui/palette.h gui/popupmenu.cpp gui/popupmenu.h + gui/questswindow.cpp + gui/questswindow.h gui/quitdialog.cpp gui/quitdialog.h gui/recorder.cpp @@ -346,6 +348,8 @@ set(SRCS resources/music.h resources/npcdb.cpp resources/npcdb.h + resources/questdb.cpp + resources/questdb.h resources/resource.cpp resources/resource.h resources/resourcemanager.cpp @@ -495,7 +499,6 @@ set(SRCS textmanager.h textparticle.cpp textparticle.h - textrenderer.h tileset.h units.cpp units.h diff --git a/src/actorsprite.cpp b/src/actorsprite.cpp index 7cb46bd4..f31297e9 100644 --- a/src/actorsprite.cpp +++ b/src/actorsprite.cpp @@ -213,7 +213,7 @@ void ActorSprite::loadTargetCursor(const std::string &filename, auto currentImageSet = resman->getImageSet(filename, width, height); if (!currentImageSet) { - logger->log("Error loading target cursor: %s", filename.c_str()); + Log::info("Error loading target cursor: %s", filename.c_str()); return; } diff --git a/src/being.cpp b/src/being.cpp index 46b67f4b..91fe4bc0 100644 --- a/src/being.cpp +++ b/src/being.cpp @@ -481,15 +481,15 @@ void Being::setShowName(bool doShowName) void Being::setGuildName(const std::string &name) { - logger->log("Got guild name \"%s\" for being %s(%i)", name.c_str(), - mName.c_str(), mId); + Log::info("Got guild name \"%s\" for being %s(%i)", name.c_str(), + mName.c_str(), mId); } void Being::setGuildPos(const std::string &pos) { - logger->log("Got guild position \"%s\" for being %s(%i)", pos.c_str(), - mName.c_str(), mId); + Log::info("Got guild position \"%s\" for being %s(%i)", pos.c_str(), + mName.c_str(), mId); } void Being::addGuild(Guild *guild) diff --git a/src/being.h b/src/being.h index 80620e71..43cc546c 100644 --- a/src/being.h +++ b/src/being.h @@ -166,7 +166,7 @@ class Being : public ActorSprite, public EventListener, public gcn::DeathListene * Puts a damage bubble above this being. * * @param attacker the attacking being - * @param damage the amount of damage recieved (0 means miss) + * @param damage the amount of damage received (0 means miss) * @param type the attack type * @param attackId the attack id (used for monsters) */ diff --git a/src/chatlogger.cpp b/src/chatlogger.cpp index 119d3fc1..854f9780 100644 --- a/src/chatlogger.cpp +++ b/src/chatlogger.cpp @@ -38,6 +38,51 @@ #include "utils/stringutils.h" +static std::string getDateString() +{ + time_t t = time(nullptr); + struct tm *now = localtime(&t); + char buffer[11]; + strftime(buffer, sizeof(buffer), "%Y-%m-%d", now); + return buffer; +} + +static std::string &secureName(std::string &name) +{ + const size_t sz = name.length(); + for (size_t f = 0; f < sz; f ++) + { + const unsigned char ch = name[f]; + if ((ch < '0' || ch > '9') && + (ch < 'a' || ch > 'z') && + (ch < 'A' || ch > 'Z') && + ch != '-' && + ch != '+' && + ch != '=' && + ch != '.' && + ch != ',' && + ch != ')' && + ch != '(' && + ch != '[' && + ch != ']' && + ch != '#') + { + name[f] = '_'; + } + } + return name; +} + +static void makeDir(const std::string &dir) +{ +#ifdef _WIN32 + mkdir(dir.c_str()); +#else + mkdir(dir.c_str(), S_IRWXU | S_IRGRP | S_IXGRP); +#endif +} + + ChatLogger::~ChatLogger() = default; void ChatLogger::setLogFile(const std::string &logFilename) @@ -74,69 +119,32 @@ void ChatLogger::log(std::string str) if (!mLogFile.is_open() || dateStr != mLogDate) { mLogDate = dateStr; - setLogFile(strprintf("%s/%s/#General_%s.log", mLogDir.c_str(), - mServerName.c_str(), dateStr.c_str())); + setLogFile(strprintf("%s/%s/#General_%s.log", + mLogDir.c_str(), + mServerName.c_str(), + dateStr.c_str())); } removeColors(str); - writeTo(mLogFile, str); + mLogFile << str << std::endl; } void ChatLogger::log(std::string name, std::string str) { std::ofstream logFile; - logFile.open(strprintf("%s/%s/%s_%s.log", mLogDir.c_str(), mServerName.c_str(), - secureName(name).c_str(), getDateString().c_str()).c_str(), + logFile.open(strprintf("%s/%s/%s_%s.log", + mLogDir.c_str(), + mServerName.c_str(), + secureName(name).c_str(), + getDateString().c_str()) + .c_str(), std::ios_base::app); if (!logFile.is_open()) return; removeColors(str); - writeTo(logFile, str); - - if (logFile.is_open()) - logFile.close(); -} - -std::string ChatLogger::getDateString() -{ - time_t t = time(nullptr); - struct tm *now = localtime(&t); - char buffer[11]; - strftime(buffer, sizeof(buffer), "%Y-%m-%d", now); - return buffer; -} - -std::string ChatLogger::secureName(std::string &name) -{ - const size_t sz = name.length(); - for (size_t f = 0; f < sz; f ++) - { - const unsigned char ch = name[f]; - if ((ch < '0' || ch > '9') && - (ch < 'a' || ch > 'z') && - (ch < 'A' || ch > 'Z') && - ch != '-' && - ch != '+' && - ch != '=' && - ch != '.' && - ch != ',' && - ch != ')' && - ch != '(' && - ch != '[' && - ch != ']' && - ch != '#') - { - name[f] = '_'; - } - } - return name; -} - -void ChatLogger::writeTo(std::ofstream &file, const std::string &str) -{ - file << str << std::endl; + logFile << str << std::endl; } void ChatLogger::setServerName(const std::string &serverName) @@ -158,12 +166,3 @@ void ChatLogger::setServerName(const std::string &serverName) closedir(dir); } } - -void ChatLogger::makeDir(const std::string &dir) -{ -#ifdef _WIN32 - mkdir(dir.c_str()); -#else - mkdir(dir.c_str(), S_IRWXU | S_IRGRP | S_IXGRP); -#endif -} diff --git a/src/chatlogger.h b/src/chatlogger.h index 59d9f28c..8e4118dc 100644 --- a/src/chatlogger.h +++ b/src/chatlogger.h @@ -35,13 +35,8 @@ class ChatLogger * Enters a message in the log. The message will be timestamped. */ void log(std::string str); - void log(std::string name, std::string str); - static std::string getDateString(); - - static std::string secureName(std::string &str); - void setServerName(const std::string &serverName); private: @@ -50,10 +45,6 @@ class ChatLogger */ void setLogFile(const std::string &logFilename); - static void writeTo(std::ofstream &file, const std::string &str); - - static void makeDir(const std::string &dir); - std::ofstream mLogFile; std::string mLogDir; std::string mServerName; diff --git a/src/client.cpp b/src/client.cpp index f0254d50..7c8763a1 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -29,6 +29,7 @@ #include "game.h" #include "itemshortcut.h" #include "keyboardconfig.h" +#include "log.h" #include "playerrelations.h" #include "sound.h" @@ -87,6 +88,8 @@ #include <sys/stat.h> #include <cassert> +#include <guichan/exception.hpp> + // TODO: Get rid of these globals std::string errorMessage; LoginData loginData; @@ -94,7 +97,6 @@ LoginData loginData; Config config; /**< Global settings (config.xml) */ Configuration branding; /**< XML branding information reader */ Configuration paths; /**< XML default paths information reader */ -Logger *logger; /**< Log object */ ChatLogger *chatLogger; /**< Chat log object */ KeyboardConfig keyboard; @@ -113,7 +115,7 @@ volatile int frame_count = 0; /**< Counts the frames during one second */ * Updates fps. * Called every seconds by SDL_AddTimer() */ -Uint32 nextSecond(Uint32 interval, void *param) +static Uint32 nextSecond(Uint32 interval, void *param) { fps = frame_count; frame_count = 0; @@ -174,8 +176,6 @@ Client::Client(const Options &options): assert(!mInstance); mInstance = this; - logger = new Logger; - // Set default values for configuration files branding.setDefaultValues(getBrandingDefaults()); paths.setDefaultValues(getPathsDefaults()); @@ -190,19 +190,18 @@ Client::Client(const Options &options): initHomeDir(); initConfiguration(); + // Configure logger + Log::init(); + Log::setLogFile(mLocalDataDir + "/mana.log"); + Log::setLogToStandardOut(config.logToStandardOut); + Log::info("%s", FULL_VERSION); + chatLogger = new ChatLogger; if (options.chatLogDir.empty()) chatLogger->setLogDir(mLocalDataDir + "/logs/"); else chatLogger->setLogDir(options.chatLogDir); - // Configure logger - logger->setLogFile(mLocalDataDir + "/mana.log"); - logger->setLogToStandardOut(config.logToStandardOut); - - // Log the mana version - logger->log("%s", FULL_VERSION); - initScreenshotDir(); #if SDL_VERSION_ATLEAST(2, 24, 0) @@ -210,10 +209,10 @@ Client::Client(const Options &options): #endif // Initialize SDL - logger->log("Initializing SDL..."); + Log::info("Initializing SDL..."); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0) { - logger->error(strprintf("Could not initialize SDL: %s", + Log::critical(strprintf("Could not initialize SDL: %s", SDL_GetError())); } atexit(SDL_Quit); @@ -222,7 +221,7 @@ Client::Client(const Options &options): if (!FS::setWriteDir(mLocalDataDir)) { - logger->error(strprintf("%s couldn't be set as write directory! " + Log::critical(strprintf("%s couldn't be set as write directory! " "Exiting.", mLocalDataDir.c_str())); } @@ -285,7 +284,7 @@ Client::Client(const Options &options): iconFile += ".png"; #endif iconFile = ResourceManager::getPath(iconFile); - logger->log("Loading icon from file: %s", iconFile.c_str()); + Log::info("Loading icon from file: %s", iconFile.c_str()); #ifdef _WIN32 static SDL_SysWMinfo pInfo; SDL_GetWindowWMInfo(mVideo.window(), &pInfo); @@ -327,7 +326,7 @@ Client::Client(const Options &options): { mState = STATE_ERROR; errorMessage = err; - logger->log("Warning: %s", err); + Log::warn("%s", err); } // Initialize keyboard @@ -423,15 +422,13 @@ Client::~Client() SDL_FreeSurface(mIcon); - logger->log("Quitting"); + Log::info("Quitting"); delete userPalette; XML::Writer writer(mConfigDir + "/config.xml"); if (writer.isValid()) serialize(writer, config); - delete logger; - mInstance = nullptr; } @@ -441,506 +438,533 @@ int Client::exec() while (mState != STATE_EXIT) { - Time::beginFrame(); - - if (mGame) + // Handle SDL events + SDL_Event event; + while (SDL_PollEvent(&event)) { - // Let the game handle the events while it is active - mGame->handleInput(); - } - else - { - // Handle SDL events - SDL_Event event; - while (SDL_PollEvent(&event)) + switch (event.type) { - switch (event.type) - { - case SDL_QUIT: - mState = STATE_EXIT; - break; - - case SDL_WINDOWEVENT: - switch (event.window.event) { - case SDL_WINDOWEVENT_SIZE_CHANGED: - handleWindowSizeChanged(event.window.data1, - event.window.data2); - break; - } + case SDL_QUIT: + mState = STATE_EXIT; + break; + + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + handleWindowSizeChanged(event.window.data1, + event.window.data2); break; + } + break; - case SDL_KEYDOWN: - if (keyboard.isEnabled()) + case SDL_KEYDOWN: + if (keyboard.isEnabled()) + { + const int tKey = keyboard.getKeyIndex(event.key.keysym.sym); + if (tKey == KeyboardConfig::KEY_WINDOW_SETUP) { - const int tKey = keyboard.getKeyIndex(event.key.keysym.sym); - if (tKey == KeyboardConfig::KEY_WINDOW_SETUP) - { - setupWindow->setVisible(!setupWindow->isVisible()); - if (setupWindow->isVisible()) - setupWindow->requestMoveToTop(); - continue; - } + setupWindow->setVisible(!setupWindow->isVisible()); + if (setupWindow->isVisible()) + setupWindow->requestMoveToTop(); + continue; } - break; } - guiInput->pushInput(event); - } - } - - if (Net::getGeneralHandler()) - Net::getGeneralHandler()->flushNetwork(); - - gui->logic(); - if (mGame) - mGame->logic(); - - sound.logic(); - - // Update the screen when application is active, delay otherwise. - if (isActive()) - { - frame_count++; - gui->draw(); - graphics->updateScreen(); - mFpsManager.limitFps(config.fpsLimit); - } - else - { - mFpsManager.limitFps(10); - } - - // TODO: Add connect timeouts - if (mState == STATE_CONNECT_GAME && - Net::getGameHandler()->isConnected()) - { - Net::getLoginHandler()->disconnect(); - } - else if (mState == STATE_CONNECT_SERVER && - mOldState == STATE_CONNECT_SERVER && - Net::getLoginHandler()->isConnected()) - { - mState = STATE_LOGIN; - } - else if (mState == STATE_WORLD_SELECT && mOldState == STATE_UPDATE) - { - if (Net::getLoginHandler()->getWorlds().size() < 2) - { - mState = STATE_LOGIN; - } - } - else if (mOldState == STATE_START || - (mOldState == STATE_GAME && mState != STATE_GAME)) - { - auto *top = static_cast<gcn::Container*>(gui->getTop()); - - mDesktop = new Desktop; - top->add(mDesktop); - mSetupButton = new Button("", "Setup", this); - mSetupButton->setButtonPopupText(_("Setup")); - mSetupButton->setButtonIcon("button-icon-setup.png"); - mSetupButton->setPosition(top->getWidth() - mSetupButton->getWidth() - - 3, 3); - top->add(mSetupButton); - - mDesktop->setSize(graphics->getWidth(), graphics->getHeight()); - } + if (setupWindow->isVisible() && + keyboard.getNewKeyIndex() > KeyboardConfig::KEY_NO_VALUE) + { + keyboard.setNewKey(event.key.keysym.sym); + keyboard.callbackNewKey(); + keyboard.setNewKeyIndex(KeyboardConfig::KEY_NO_VALUE); + continue; + } - if (mState == STATE_SWITCH_LOGIN && mOldState == STATE_GAME) - { - Net::getGameHandler()->disconnect(); - } + // Check whether the game will handle the event + if (mGame && mGame->keyDownEvent(event.key)) + continue; - if (mState != mOldState) - { - { - Event event(Event::StateChange); - event.setInt("oldState", mOldState); - event.setInt("newState", mState); - event.trigger(Event::ClientChannel); + break; } - if (mOldState == STATE_GAME) + // Push input to GUI when not used + try { - delete mGame; - mGame = nullptr; + guiInput->pushInput(event); } - - mOldState = mState; - - // Get rid of the dialog of the previous state - if (mCurrentDialog) + catch (gcn::Exception e) { - delete mCurrentDialog; - mCurrentDialog = nullptr; + const char *err = e.getMessage().c_str(); + Log::warn("Guichan input exception: %s", err); } - // State has changed, while the quitDialog was active, it might - // not be correct anymore - if (mQuitDialog) - mQuitDialog->scheduleDelete(); + } - switch (mState) - { - case STATE_CHOOSE_SERVER: - logger->log("State: CHOOSE SERVER"); + update(); + } - // Don't allow an alpha opacity - // lower than the default value - gui->getTheme()->setMinimumOpacity(0.8f); + Net::unload(); - mCurrentDialog = new ServerDialog(&mCurrentServer, - mConfigDir); - break; + return 0; +} - case STATE_CONNECT_SERVER: - logger->log("State: CONNECT SERVER"); +void Client::update() +{ + Time::beginFrame(); - Net::connectToServer(mCurrentServer); + mVideo.updateWindowSize(); + checkGraphicsSize(); - mCurrentDialog = new ConnectionDialog( - _("Connecting to server"), STATE_SWITCH_SERVER); - break; + // Let the game handle continuous input while it is active + if (mGame) + mGame->handleInput(); - case STATE_LOGIN: - logger->log("State: LOGIN"); - // Don't allow an alpha opacity - // lower than the default value - gui->getTheme()->setMinimumOpacity(0.8f); + if (Net::getGeneralHandler()) + Net::getGeneralHandler()->flushNetwork(); - if (mOptions.username.empty() || mOptions.password.empty()) - { - mCurrentDialog = new LoginDialog(&loginData); - } - else - { - mState = STATE_LOGIN_ATTEMPT; - // Clear the password so that when login fails, the - // dialog will show up next time. - mOptions.password.clear(); - } - break; + gui->logic(); + if (mGame) + mGame->logic(); - case STATE_LOGIN_ATTEMPT: - logger->log("State: LOGIN ATTEMPT"); - accountLogin(&loginData); - mCurrentDialog = new ConnectionDialog( - _("Logging in"), STATE_SWITCH_SERVER); - break; + sound.logic(); - case STATE_WORLD_SELECT: - logger->log("State: WORLD SELECT"); - { - Worlds worlds = Net::getLoginHandler()->getWorlds(); - - if (worlds.empty()) - { - // Trust that the netcode knows what it's doing - mState = STATE_UPDATE; - } - else if (worlds.size() == 1 || mOptions.chooseDefault) - { - Net::getLoginHandler()->chooseServer(0); - mState = STATE_UPDATE; - } - else - { - mCurrentDialog = new WorldSelectDialog(std::move(worlds)); - } - } - break; + // Update the screen when application is active, delay otherwise. + if (isActive()) + { + frame_count++; + gui->draw(); + graphics->updateScreen(); + mFpsManager.limitFps(config.fpsLimit); + } + else + { + mFpsManager.limitFps(10); + } - case STATE_WORLD_SELECT_ATTEMPT: - logger->log("State: WORLD SELECT ATTEMPT"); - mCurrentDialog = new ConnectionDialog( - _("Entering game world"), STATE_WORLD_SELECT); - break; + // TODO: Add connect timeouts + if (mState == STATE_CONNECT_GAME && + Net::getGameHandler()->isConnected()) + { + Net::getLoginHandler()->disconnect(); + } + else if (mState == STATE_CONNECT_SERVER && + mOldState == STATE_CONNECT_SERVER && + Net::getLoginHandler()->isConnected()) + { + mState = STATE_LOGIN; + } + else if (mState == STATE_WORLD_SELECT && mOldState == STATE_UPDATE) + { + if (Net::getLoginHandler()->getWorlds().size() < 2) + { + mState = STATE_LOGIN; + } + } + else if (mOldState == STATE_START || + (mOldState == STATE_GAME && mState != STATE_GAME)) + { + auto *top = static_cast<gcn::Container*>(gui->getTop()); + + mDesktop = new Desktop; + top->add(mDesktop); + mSetupButton = new Button("", "Setup", this); + mSetupButton->setButtonPopupText(_("Setup")); + mSetupButton->setButtonIcon("button-icon-setup.png"); + mSetupButton->setPosition(top->getWidth() - mSetupButton->getWidth() + - 3, 3); + top->add(mSetupButton); + + mDesktop->setSize(graphics->getWidth(), graphics->getHeight()); + } - case STATE_UPDATE: - logger->log("State: UPDATE"); + if (mState == STATE_SWITCH_LOGIN && mOldState == STATE_GAME) + { + Net::getGameHandler()->disconnect(); + } - if (mOptions.skipUpdate) - { - mState = STATE_LOAD_DATA; - } - else if (initUpdatesDir()) - { - mCurrentDialog = new UpdaterWindow(mUpdateHost, - mLocalDataDir + "/" + mUpdatesDir, - mOptions.dataPath.empty()); - } - break; + if (mState != mOldState) + { + { + Event event(Event::StateChange); + event.setInt("oldState", mOldState); + event.setInt("newState", mState); + event.trigger(Event::ClientChannel); + } - case STATE_LOAD_DATA: - logger->log("State: LOAD DATA"); + if (mOldState == STATE_GAME) + { + delete mGame; + mGame = nullptr; + } - // If another data path has been set, - // we don't load any other files... - if (mOptions.dataPath.empty()) - { - // Add customdata directory - ResourceManager::searchAndAddArchives( - "customdata/", - "zip", - false); - } + mOldState = mState; - // TODO remove this as soon as inventoryhandler stops using this event - Event::trigger(Event::ClientChannel, Event::LoadingDatabases); + // Get rid of the dialog of the previous state + if (mCurrentDialog) + { + delete mCurrentDialog; + mCurrentDialog = nullptr; + } + // State has changed, while the quitDialog was active, it might + // not be correct anymore + if (mQuitDialog) + mQuitDialog->scheduleDelete(); - // Load XML databases - CharDB::load(); + switch (mState) + { + case STATE_CHOOSE_SERVER: + Log::info("State: CHOOSE SERVER"); - delete itemDb; + // Don't allow an alpha opacity + // lower than the default value + gui->getTheme()->setMinimumOpacity(0.8f); - switch (Net::getNetworkType()) - { - case ServerType::TmwAthena: - itemDb = new TmwAthena::TaItemDB; - break; - case ServerType::ManaServ: - itemDb = new ManaServ::ManaServItemDB; - break; - default: - // Nothing - itemDb = nullptr; - break; - } - assert(itemDb); + mCurrentDialog = new ServerDialog(&mCurrentServer, + mConfigDir); + break; - // load settings.xml - SettingsManager::load(); + case STATE_CONNECT_SERVER: + Log::info("State: CONNECT SERVER"); - ActorSprite::load(); + Net::connectToServer(mCurrentServer); - mDesktop->reloadWallpaper(); + mCurrentDialog = new ConnectionDialog( + _("Connecting to server"), STATE_SWITCH_SERVER); + break; - mState = STATE_GET_CHARACTERS; - break; + case STATE_LOGIN: + Log::info("State: LOGIN"); + // Don't allow an alpha opacity + // lower than the default value + gui->getTheme()->setMinimumOpacity(0.8f); - case STATE_GET_CHARACTERS: - logger->log("State: GET CHARACTERS"); - Net::getCharHandler()->requestCharacters(); - mCurrentDialog = new ConnectionDialog( - _("Requesting characters"), - STATE_SWITCH_SERVER); - break; + if (mOptions.username.empty() || mOptions.password.empty()) + { + mCurrentDialog = new LoginDialog(&loginData); + } + else + { + mState = STATE_LOGIN_ATTEMPT; + // Clear the password so that when login fails, the + // dialog will show up next time. + mOptions.password.clear(); + } + break; - case STATE_CHAR_SELECT: - logger->log("State: CHAR SELECT"); - // Don't allow an alpha opacity - // lower than the default value - gui->getTheme()->setMinimumOpacity(0.8f); + case STATE_LOGIN_ATTEMPT: + Log::info("State: LOGIN ATTEMPT"); + accountLogin(&loginData); + mCurrentDialog = new ConnectionDialog( + _("Logging in"), STATE_SWITCH_SERVER); + break; - mCurrentDialog = new CharSelectDialog(&loginData); + case STATE_WORLD_SELECT: + Log::info("State: WORLD SELECT"); + { + Worlds worlds = Net::getLoginHandler()->getWorlds(); - if (!((CharSelectDialog*) mCurrentDialog)->selectByName( - mOptions.character, CharSelectDialog::Choose)) + if (worlds.empty()) { - ((CharSelectDialog*) mCurrentDialog)->selectByName( - config.lastCharacter, - mOptions.chooseDefault ? - CharSelectDialog::Choose : - CharSelectDialog::Focus); + // Trust that the netcode knows what it's doing + mState = STATE_UPDATE; } + else if (worlds.size() == 1 || mOptions.chooseDefault) + { + Net::getLoginHandler()->chooseServer(0); + mState = STATE_UPDATE; + } + else + { + mCurrentDialog = new WorldSelectDialog(std::move(worlds)); + } + } + break; - // Choosing character on the command line should work only - // once, clear it so that 'switch character' works. - mOptions.character.clear(); - mOptions.chooseDefault = false; - - break; - - case STATE_CONNECT_GAME: - logger->log("State: CONNECT GAME"); - - Net::getGameHandler()->connect(); - mCurrentDialog = new ConnectionDialog( - _("Connecting to the game server"), - Net::getNetworkType() == ServerType::TmwAthena ? - STATE_CHOOSE_SERVER : STATE_SWITCH_CHARACTER); - break; - - case STATE_CHANGE_MAP: - logger->log("State: CHANGE_MAP"); - - Net::getGameHandler()->connect(); - mCurrentDialog = new ConnectionDialog( - _("Changing game servers"), - STATE_SWITCH_CHARACTER); - break; - - case STATE_GAME: - logger->log("Memorizing selected character %s", - local_player->getName().c_str()); - config.lastCharacter = local_player->getName(); - - // Fade out logon-music here too to give the desired effect - // of "flowing" into the game. - sound.fadeOutMusic(1000); - - // Allow any alpha opacity - gui->getTheme()->setMinimumOpacity(0.0f); - - delete mSetupButton; - delete mDesktop; - mSetupButton = nullptr; - mDesktop = nullptr; - - mCurrentDialog = nullptr; - - logger->log("State: GAME"); - mGame = new Game; - break; - - case STATE_LOGIN_ERROR: - logger->log("State: LOGIN ERROR"); - showErrorDialog(errorMessage, STATE_LOGIN); - break; - - case STATE_ACCOUNTCHANGE_ERROR: - logger->log("State: ACCOUNT CHANGE ERROR"); - showErrorDialog(errorMessage, STATE_CHAR_SELECT); - break; + case STATE_WORLD_SELECT_ATTEMPT: + Log::info("State: WORLD SELECT ATTEMPT"); + mCurrentDialog = new ConnectionDialog( + _("Entering game world"), STATE_WORLD_SELECT); + break; - case STATE_REGISTER_PREP: - logger->log("State: REGISTER_PREP"); - Net::getLoginHandler()->getRegistrationDetails(); - mCurrentDialog = new ConnectionDialog( - _("Requesting registration details"), STATE_LOGIN); - break; + case STATE_UPDATE: + Log::info("State: UPDATE"); - case STATE_REGISTER: - logger->log("State: REGISTER"); - mCurrentDialog = new RegisterDialog(&loginData); - break; + if (mOptions.skipUpdate) + { + mState = STATE_LOAD_DATA; + } + else if (initUpdatesDir()) + { + mCurrentDialog = new UpdaterWindow(mUpdateHost, + mLocalDataDir + "/" + mUpdatesDir, + mOptions.dataPath.empty()); + } + break; - case STATE_REGISTER_ATTEMPT: - logger->log("Username is %s", loginData.username.c_str()); - Net::getLoginHandler()->registerAccount(&loginData); - loginData.password.clear(); - break; + case STATE_LOAD_DATA: + Log::info("State: LOAD DATA"); - case STATE_CHANGEPASSWORD: - logger->log("State: CHANGE PASSWORD"); - mCurrentDialog = new ChangePasswordDialog(&loginData); - break; + // If another data path has been set, + // we don't load any other files... + if (mOptions.dataPath.empty()) + { + // Add customdata directory + ResourceManager::searchAndAddArchives( + "customdata/", + "zip", + false); + } - case STATE_CHANGEPASSWORD_ATTEMPT: - logger->log("State: CHANGE PASSWORD ATTEMPT"); - Net::getLoginHandler()->changePassword(loginData.username, - loginData.password, - loginData.newPassword); - break; + // TODO remove this as soon as inventoryhandler stops using this event + Event::trigger(Event::ClientChannel, Event::LoadingDatabases); - case STATE_CHANGEPASSWORD_SUCCESS: - logger->log("State: CHANGE PASSWORD SUCCESS"); - showOkDialog(_("Password Change"), - _("Password changed successfully!"), - STATE_CHAR_SELECT); - loginData.password.clear(); - loginData.newPassword.clear(); - break; + // Load XML databases + CharDB::load(); - case STATE_CHANGEEMAIL: - logger->log("State: CHANGE EMAIL"); - mCurrentDialog = new ChangeEmailDialog(&loginData); - break; + delete itemDb; - case STATE_CHANGEEMAIL_ATTEMPT: - logger->log("State: CHANGE EMAIL ATTEMPT"); - Net::getLoginHandler()->changeEmail(loginData.email); - break; + switch (Net::getNetworkType()) + { + case ServerType::TmwAthena: + itemDb = new TmwAthena::TaItemDB; + break; + case ServerType::ManaServ: + itemDb = new ManaServ::ManaServItemDB; + break; + default: + // Nothing + itemDb = nullptr; + break; + } + assert(itemDb); - case STATE_CHANGEEMAIL_SUCCESS: - logger->log("State: CHANGE EMAIL SUCCESS"); - showOkDialog(_("Email Change"), - _("Email changed successfully!"), - STATE_CHAR_SELECT); - break; + // load settings.xml + SettingsManager::load(); - case STATE_UNREGISTER: - logger->log("State: UNREGISTER"); - mCurrentDialog = new UnRegisterDialog(&loginData); - break; + ActorSprite::load(); - case STATE_UNREGISTER_ATTEMPT: - logger->log("State: UNREGISTER ATTEMPT"); - Net::getLoginHandler()->unregisterAccount( - loginData.username, loginData.password); - break; + mDesktop->reloadWallpaper(); - case STATE_UNREGISTER_SUCCESS: - logger->log("State: UNREGISTER SUCCESS"); - Net::getLoginHandler()->disconnect(); + mState = STATE_GET_CHARACTERS; + break; - showOkDialog(_("Unregister Successful"), - _("Farewell, come back any time..."), - STATE_CHOOSE_SERVER); - loginData.clear(); - break; + case STATE_GET_CHARACTERS: + Log::info("State: GET CHARACTERS"); + Net::getCharHandler()->requestCharacters(); + mCurrentDialog = new ConnectionDialog( + _("Requesting characters"), + STATE_SWITCH_SERVER); + break; - case STATE_SWITCH_SERVER: - logger->log("State: SWITCH SERVER"); + case STATE_CHAR_SELECT: + Log::info("State: CHAR SELECT"); + // Don't allow an alpha opacity + // lower than the default value + gui->getTheme()->setMinimumOpacity(0.8f); - Net::getLoginHandler()->disconnect(); - Net::getGameHandler()->disconnect(); + mCurrentDialog = new CharSelectDialog(&loginData); - mCurrentServer.hostname.clear(); - mState = STATE_CHOOSE_SERVER; - break; + if (!((CharSelectDialog*) mCurrentDialog)->selectByName( + mOptions.character, CharSelectDialog::Choose)) + { + ((CharSelectDialog*) mCurrentDialog)->selectByName( + config.lastCharacter, + mOptions.chooseDefault ? + CharSelectDialog::Choose : + CharSelectDialog::Focus); + } - case STATE_SWITCH_LOGIN: - logger->log("State: SWITCH LOGIN"); + // Choosing character on the command line should work only + // once, clear it so that 'switch character' works. + mOptions.character.clear(); + mOptions.chooseDefault = false; - Net::getLoginHandler()->disconnect(); + break; - mState = STATE_CONNECT_SERVER; - break; + case STATE_CONNECT_GAME: + Log::info("State: CONNECT GAME"); - case STATE_SWITCH_CHARACTER: - logger->log("State: SWITCH CHARACTER"); + Net::getGameHandler()->connect(); + mCurrentDialog = new ConnectionDialog( + _("Connecting to the game server"), + Net::getNetworkType() == ServerType::TmwAthena ? + STATE_CHOOSE_SERVER : STATE_SWITCH_CHARACTER); + break; - // Done with game - Net::getGameHandler()->disconnect(); + case STATE_CHANGE_MAP: + Log::info("State: CHANGE_MAP"); - mState = STATE_GET_CHARACTERS; - break; + Net::getGameHandler()->connect(); + mCurrentDialog = new ConnectionDialog( + _("Changing game servers"), + STATE_SWITCH_CHARACTER); + break; - case STATE_LOGOUT_ATTEMPT: - logger->log("State: LOGOUT ATTEMPT"); - // TODO - break; + case STATE_GAME: + Log::info("Memorizing selected character %s", + local_player->getName().c_str()); + config.lastCharacter = local_player->getName(); - case STATE_WAIT: - logger->log("State: WAIT"); - break; + // Fade out logon-music here too to give the desired effect + // of "flowing" into the game. + sound.fadeOutMusic(1000); - case STATE_EXIT: - logger->log("State: EXIT"); - break; + // Allow any alpha opacity + gui->getTheme()->setMinimumOpacity(0.0f); - case STATE_FORCE_QUIT: - logger->log("State: FORCE QUIT"); - mState = STATE_EXIT; - break; + delete mSetupButton; + delete mDesktop; + mSetupButton = nullptr; + mDesktop = nullptr; - case STATE_ERROR: - logger->log("State: ERROR"); - logger->log("Error: %s", errorMessage.c_str()); - showErrorDialog(errorMessage, STATE_CHOOSE_SERVER); - Net::getGameHandler()->disconnect(); - break; + mCurrentDialog = nullptr; - default: - mState = STATE_FORCE_QUIT; - break; - } + Log::info("State: GAME"); + mGame = new Game; + break; + + case STATE_LOGIN_ERROR: + Log::info("State: LOGIN ERROR"); + showErrorDialog(errorMessage, STATE_LOGIN); + break; + + case STATE_ACCOUNTCHANGE_ERROR: + Log::info("State: ACCOUNT CHANGE ERROR"); + showErrorDialog(errorMessage, STATE_CHAR_SELECT); + break; + + case STATE_REGISTER_PREP: + Log::info("State: REGISTER_PREP"); + Net::getLoginHandler()->getRegistrationDetails(); + mCurrentDialog = new ConnectionDialog( + _("Requesting registration details"), STATE_LOGIN); + break; + + case STATE_REGISTER: + Log::info("State: REGISTER"); + mCurrentDialog = new RegisterDialog(&loginData); + break; + + case STATE_REGISTER_ATTEMPT: + Log::info("Username is %s", loginData.username.c_str()); + Net::getLoginHandler()->registerAccount(&loginData); + loginData.password.clear(); + break; + + case STATE_CHANGEPASSWORD: + Log::info("State: CHANGE PASSWORD"); + mCurrentDialog = new ChangePasswordDialog(&loginData); + break; + + case STATE_CHANGEPASSWORD_ATTEMPT: + Log::info("State: CHANGE PASSWORD ATTEMPT"); + Net::getLoginHandler()->changePassword(loginData.username, + loginData.password, + loginData.newPassword); + break; + + case STATE_CHANGEPASSWORD_SUCCESS: + Log::info("State: CHANGE PASSWORD SUCCESS"); + showOkDialog(_("Password Change"), + _("Password changed successfully!"), + STATE_CHAR_SELECT); + loginData.password.clear(); + loginData.newPassword.clear(); + break; + + case STATE_CHANGEEMAIL: + Log::info("State: CHANGE EMAIL"); + mCurrentDialog = new ChangeEmailDialog(&loginData); + break; + + case STATE_CHANGEEMAIL_ATTEMPT: + Log::info("State: CHANGE EMAIL ATTEMPT"); + Net::getLoginHandler()->changeEmail(loginData.email); + break; + + case STATE_CHANGEEMAIL_SUCCESS: + Log::info("State: CHANGE EMAIL SUCCESS"); + showOkDialog(_("Email Change"), + _("Email changed successfully!"), + STATE_CHAR_SELECT); + break; + + case STATE_UNREGISTER: + Log::info("State: UNREGISTER"); + mCurrentDialog = new UnRegisterDialog(&loginData); + break; + + case STATE_UNREGISTER_ATTEMPT: + Log::info("State: UNREGISTER ATTEMPT"); + Net::getLoginHandler()->unregisterAccount( + loginData.username, loginData.password); + break; + + case STATE_UNREGISTER_SUCCESS: + Log::info("State: UNREGISTER SUCCESS"); + Net::getLoginHandler()->disconnect(); + + showOkDialog(_("Unregister Successful"), + _("Farewell, come back any time..."), + STATE_CHOOSE_SERVER); + loginData.clear(); + break; + + case STATE_SWITCH_SERVER: + Log::info("State: SWITCH SERVER"); + + Net::getLoginHandler()->disconnect(); + Net::getGameHandler()->disconnect(); + + mCurrentServer.hostname.clear(); + mState = STATE_CHOOSE_SERVER; + break; + + case STATE_SWITCH_LOGIN: + Log::info("State: SWITCH LOGIN"); + + Net::getLoginHandler()->disconnect(); + + mState = STATE_CONNECT_SERVER; + break; + + case STATE_SWITCH_CHARACTER: + Log::info("State: SWITCH CHARACTER"); + + // Done with game + Net::getGameHandler()->disconnect(); + + mState = STATE_GET_CHARACTERS; + break; + + case STATE_LOGOUT_ATTEMPT: + Log::info("State: LOGOUT ATTEMPT"); + // TODO + break; + + case STATE_WAIT: + Log::info("State: WAIT"); + break; + + case STATE_EXIT: + Log::info("State: EXIT"); + break; + + case STATE_FORCE_QUIT: + Log::info("State: FORCE QUIT"); + mState = STATE_EXIT; + break; + + case STATE_ERROR: + Log::info("State: ERROR"); + Log::error("%s", errorMessage.c_str()); + showErrorDialog(errorMessage, STATE_CHOOSE_SERVER); + Net::getGameHandler()->disconnect(); + break; + + default: + mState = STATE_FORCE_QUIT; + break; } } - - Net::unload(); - - return 0; } void Client::showOkDialog(const std::string &title, @@ -989,7 +1013,7 @@ void Client::initRootDir() Configuration portable; portable.init(portableName); - logger->log("Portable file: %s", portableName.c_str()); + Log::info("Portable file: %s", portableName.c_str()); if (mOptions.localDataDir.empty()) { @@ -997,8 +1021,8 @@ void Client::initRootDir() if (!dir.empty()) { mOptions.localDataDir = mRootDir + dir; - logger->log("Portable data dir: %s", - mOptions.localDataDir.c_str()); + Log::info("Portable data dir: %s", + mOptions.localDataDir.c_str()); } } @@ -1008,8 +1032,8 @@ void Client::initRootDir() if (!dir.empty()) { mOptions.configDir = mRootDir + dir; - logger->log("Portable config dir: %s", - mOptions.configDir.c_str()); + Log::info("Portable config dir: %s", + mOptions.configDir.c_str()); } } @@ -1019,8 +1043,8 @@ void Client::initRootDir() if (!dir.empty()) { mOptions.screenshotDir = mRootDir + dir; - logger->log("Portable screenshot dir: %s", - mOptions.screenshotDir.c_str()); + Log::info("Portable screenshot dir: %s", + mOptions.screenshotDir.c_str()); } } } @@ -1052,7 +1076,7 @@ void Client::initHomeDir() if (mkdir_r(mLocalDataDir.c_str())) { - logger->error(strprintf(_("%s doesn't exist and can't be created! " + Log::critical(strprintf(_("%s doesn't exist and can't be created! " "Exiting."), mLocalDataDir.c_str())); } @@ -1075,7 +1099,7 @@ void Client::initHomeDir() if (mkdir_r(mConfigDir.c_str())) { - logger->error(strprintf(_("%s doesn't exist and can't be created! " + Log::critical(strprintf(_("%s doesn't exist and can't be created! " "Exiting."), mConfigDir.c_str())); } } @@ -1094,7 +1118,7 @@ void Client::initConfiguration() if (doc.rootNode() && doc.rootNode().name() == "configuration") deserialize(doc.rootNode(), config); else - logger->log("Couldn't read configuration file: %s", configPath.c_str()); + Log::info("Couldn't read configuration file: %s", configPath.c_str()); } /** @@ -1117,7 +1141,7 @@ bool Client::initUpdatesDir() if (mUpdateHost.empty()) { - logger->log("No update host provided"); + Log::info("No update host provided"); mUpdatesDir.clear(); mState = STATE_LOAD_DATA; return false; @@ -1125,8 +1149,8 @@ bool Client::initUpdatesDir() mUpdatesDir = "updates/" + getDirectoryFromURL(mUpdateHost); - logger->log("Update host: %s", mUpdateHost.c_str()); - logger->log("Updates dir: %s", mUpdatesDir.c_str()); + Log::info("Update host: %s", mUpdateHost.c_str()); + Log::info("Updates dir: %s", mUpdatesDir.c_str()); // Verify that the updates directory exists. Create if necessary. if (!FS::isDirectory(mUpdatesDir)) @@ -1146,16 +1170,16 @@ bool Client::initUpdatesDir() if (!CreateDirectory(newDir.c_str(), 0) && GetLastError() != ERROR_ALREADY_EXISTS) { - logger->log("Error: %s can't be made, but doesn't exist!", - newDir.c_str()); + Log::error("%s can't be made, but doesn't exist!", + newDir.c_str()); errorMessage = strprintf(_("Error creating updates directory!\n(%s)"), newDir.c_str()); mState = STATE_ERROR; } #else - logger->log("Error: %s/%s can't be made, but doesn't exist!", - mLocalDataDir.c_str(), mUpdatesDir.c_str()); + Log::error("%s/%s can't be made, but doesn't exist!", + mLocalDataDir.c_str(), mUpdatesDir.c_str()); errorMessage = strprintf(_("Error creating updates directory!\n(%s/%s)"), mLocalDataDir.c_str(), mUpdatesDir.c_str()); @@ -1201,7 +1225,7 @@ void Client::initScreenshotDir() void Client::accountLogin(LoginData *loginData) { - logger->log("Username is %s", loginData->username.c_str()); + Log::info("Username is %s", loginData->username.c_str()); // Send login infos if (loginData->registerLogin) @@ -1224,10 +1248,6 @@ void Client::handleWindowSizeChanged(int width, int height) // Store the new size in the configuration. config.screenWidth = width; config.screenHeight = height; - - mVideo.windowSizeChanged(width, height); - - checkGraphicsSize(); } void Client::checkGraphicsSize() diff --git a/src/client.h b/src/client.h index a3e9c572..b9e16af4 100644 --- a/src/client.h +++ b/src/client.h @@ -150,6 +150,8 @@ public: int exec(); + void update(); + /** * Pops up an OkDialog with the given \a title and \a message, and * switches to the given \a state when Ok is pressed. @@ -236,4 +238,27 @@ private: SDL_TimerID mSecondsCounterId = 0; FpsManager mFpsManager; + +#if defined(_WIN32) || defined(__APPLE__) + /** + * This class triggers an update on the window expose event, which allows + * the application to draw while Windows is in a modal move/resize loop + * as well as while resizing on macOS. + */ + class ExposeEventWatcher + { + public: + ExposeEventWatcher() { SDL_AddEventWatch(&watch, nullptr); } + ~ExposeEventWatcher() { SDL_DelEventWatch(&watch, nullptr); } + + static int watch(void *, SDL_Event *event) + { + if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_EXPOSED) + Client::instance()->update(); + return 0; + } + }; + + ExposeEventWatcher mExposeEventWatcher; +#endif }; diff --git a/src/configuration.cpp b/src/configuration.cpp index b27fbe31..e220f4c6 100644 --- a/src/configuration.cpp +++ b/src/configuration.cpp @@ -107,9 +107,9 @@ VariableData *Configuration::getDefault(const std::string &key, return itdef->second; } - logger->log("%s: No value in registry for key %s", - mConfigPath.c_str(), - key.c_str()); + Log::info("%s: No value in registry for key %s", + mConfigPath.c_str(), + key.c_str()); } return nullptr; @@ -214,13 +214,13 @@ void Configuration::init(const std::string &filename, bool useResManager) if (!rootNode) { - logger->log("Couldn't open configuration file: %s", filename.c_str()); + Log::info("Couldn't open configuration file: %s", filename.c_str()); return; } if (rootNode.name() != "configuration") { - logger->log("Warning: No configuration file (%s)", filename.c_str()); + Log::warn("No configuration file (%s)", filename.c_str()); return; } @@ -351,6 +351,7 @@ void serdeOptions(T option) option("showgender", &Config::showGender); option("showMonstersTakedDamage", &Config::showMonstersTakedDamage); option("showWarps", &Config::showWarps); + option("hideCompletedQuests", &Config::hideCompletedQuests); option("particleMaxCount", &Config::particleMaxCount); option("particleFastPhysics", &Config::particleFastPhysics); option("particleEmitterSkip", &Config::particleEmitterSkip); diff --git a/src/configuration.h b/src/configuration.h index 3ed3c77e..9e00cb74 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -181,6 +181,7 @@ struct Config bool showGender = false; bool showMonstersTakedDamage = false; bool showWarps = true; + bool hideCompletedQuests = false; int particleMaxCount = 3000; int particleFastPhysics = 0; int particleEmitterSkip = 1; diff --git a/src/effectmanager.cpp b/src/effectmanager.cpp index 32280555..d1b3a037 100644 --- a/src/effectmanager.cpp +++ b/src/effectmanager.cpp @@ -38,13 +38,13 @@ EffectManager::EffectManager() // Handle old naming until the 0.5.x versions are obsolete. if (!root || root.name() != "being-effects") { - logger->log("Error loading being effects file: effects.xml"); + Log::info("Error loading being effects file: effects.xml"); return; } } else { - logger->log("Effects are now loading"); + Log::info("Effects are now loading"); } for (auto node : root.children()) @@ -67,7 +67,7 @@ bool EffectManager::trigger(int id, Being *being, int rotation) auto it = mEffects.find(id); if (it == mEffects.end()) { - logger->log("EffectManager::trigger: effect %d not found", id); + Log::warn("EffectManager::trigger: effect %d not found", id); return false; } @@ -90,7 +90,7 @@ bool EffectManager::trigger(int id, int x, int y, int rotation) auto it = mEffects.find(id); if (it == mEffects.end()) { - logger->log("EffectManager::trigger: effect %d not found", id); + Log::warn("EffectManager::trigger: effect %d not found", id); return false; } diff --git a/src/event.h b/src/event.h index d3fdede9..3cbd9123 100644 --- a/src/event.h +++ b/src/event.h @@ -53,7 +53,8 @@ public: ItemChannel, NoticesChannel, NpcChannel, - StorageChannel + StorageChannel, + QuestsChannel }; enum Type @@ -99,7 +100,8 @@ public: UpdateStat, UpdateStatusEffect, Whisper, - WhisperError + WhisperError, + QuestVarsChanged, }; /** diff --git a/src/game.cpp b/src/game.cpp index a6764fcf..7e2496a1 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -21,15 +21,15 @@ #include "game.h" -#include "actorspritemanager.h" #include "actorsprite.h" +#include "actorspritemanager.h" #include "channelmanager.h" #include "client.h" #include "commandhandler.h" #include "configuration.h" #include "effectmanager.h" -#include "event.h" #include "emoteshortcut.h" +#include "event.h" #include "graphics.h" #include "itemshortcut.h" #include "joystick.h" @@ -41,24 +41,24 @@ #include "playerrelations.h" #include "sound.h" +#include "gui/abilitieswindow.h" #include "gui/chatwindow.h" #include "gui/debugwindow.h" #include "gui/equipmentwindow.h" #include "gui/gui.h" #include "gui/helpwindow.h" #include "gui/inventorywindow.h" -#include "gui/shortcutwindow.h" #include "gui/minimap.h" #include "gui/ministatuswindow.h" #include "gui/npcdialog.h" #include "gui/okdialog.h" #include "gui/outfitwindow.h" +#include "gui/questswindow.h" #include "gui/quitdialog.h" -#include "gui/sdlinput.h" #include "gui/setup.h" -#include "gui/socialwindow.h" -#include "gui/abilitieswindow.h" +#include "gui/shortcutwindow.h" #include "gui/skilldialog.h" +#include "gui/socialwindow.h" #include "gui/statuswindow.h" #include "gui/textdialog.h" #include "gui/tradewindow.h" @@ -79,7 +79,6 @@ #include "utils/gettext.h" #include "utils/mkdir.h" -#include <guichan/exception.hpp> #include <guichan/focushandler.hpp> #include <fstream> @@ -96,6 +95,7 @@ StatusWindow *statusWindow; MiniStatusWindow *miniStatusWindow; InventoryWindow *inventoryWindow; SkillDialog *skillDialog; +QuestsWindow *questsWindow; Minimap *minimap; EquipmentWindow *equipmentWindow; TradeWindow *tradeWindow; @@ -152,6 +152,7 @@ static void createGuiWindows() statusWindow = new StatusWindow; inventoryWindow = new InventoryWindow(PlayerInfo::getInventory()); skillDialog = new SkillDialog; + questsWindow = new QuestsWindow; helpWindow = new HelpWindow; debugWindow = new DebugWindow; itemShortcutWindow = new ShortcutWindow("ItemShortcut", @@ -184,6 +185,7 @@ static void destroyGuiWindows() del_0(miniStatusWindow) del_0(inventoryWindow) del_0(skillDialog) + del_0(questsWindow) del_0(minimap) del_0(equipmentWindow) del_0(tradeWindow) @@ -287,7 +289,7 @@ static bool saveScreenshot() if (!screenshot) { serverNotice(_("Could not take screenshot!")); - logger->log("Error: could not take screenshot."); + Log::error("Could not take screenshot."); return false; } @@ -300,9 +302,9 @@ static bool saveScreenshot() if (mkdir_r(screenshotDirectory.c_str()) != 0) { - logger->log("Directory %s doesn't exist and can't be created! " - "Setting screenshot directory to home.", - screenshotDirectory.c_str()); + Log::info("Directory %s doesn't exist and can't be created! " + "Setting screenshot directory to home.", + screenshotDirectory.c_str()); screenshotDirectory = FS::getUserDir(); } @@ -324,13 +326,21 @@ static bool saveScreenshot() if (success) { + std::string screenshotLink; +#if SDL_VERSION_ATLEAST(2, 0, 14) + screenshotLink = strprintf("@@screenshot:%s|%s@@", + filenameSuffix.str().c_str(), + filenameSuffix.str().c_str()); +#else + screenshotLink = filenameSuffix.str(); +#endif serverNotice(strprintf(_("Screenshot saved as %s"), - filenameSuffix.str().c_str())); + screenshotLink.c_str())); } else { serverNotice(_("Saving screenshot failed!")); - logger->log("Error: could not save screenshot."); + Log::error("Could not save screenshot."); } SDL_FreeSurface(screenshot); @@ -387,516 +397,457 @@ static void handleItemPickUp() } /** - * The huge input handling method. + * Handles an SDL_KEYDOWN event and returns whether it was consumed. */ -void Game::handleInput() +bool Game::keyDownEvent(SDL_KeyboardEvent &event) { - if (joystick) - joystick->update(); + gcn::Window *requestedWindow = nullptr; + + // send straight to gui for certain windows + if (quitDialog || TextDialog::isActive() || + PlayerInfo::getNPCPostCount() > 0) + { + return false; + } - // Events - SDL_Event event; - while (SDL_PollEvent(&event)) + if (keyboard.isKeyActive(KeyboardConfig::KEY_EMOTE)) { - bool used = false; + int emotion = keyboard.getKeyEmoteOffset(event.keysym.sym); + if (emotion != -1) + { + emoteShortcut->useEmote(emotion); + return true; + } + } - if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) + if (!chatWindow->isInputFocused() + && !gui->getFocusHandler()->getModalFocused()) + { + NpcDialog *dialog = NpcDialog::getActive(); + if (keyboard.isKeyActive(KeyboardConfig::KEY_OK) + && (!dialog || !dialog->isTextInputFocused())) { - // Let the client deal with this one (it'll pass down from there) - Client::instance()->handleWindowSizeChanged(event.window.data1, - event.window.data2); + // Close the Browser if opened + if (helpWindow->isVisible()) + helpWindow->setVisible(false); + // Close the config window, cancelling changes if opened + else if (setupWindow->isVisible()) + setupWindow->action(gcn::ActionEvent(nullptr, "cancel")); + else if (dialog) + dialog->action(gcn::ActionEvent(nullptr, "ok")); } - // Keyboard events (for discontinuous keys) - else if (event.type == SDL_KEYDOWN) + if (keyboard.isKeyActive(KeyboardConfig::KEY_TOGGLE_CHAT)) { - gcn::Window *requestedWindow = nullptr; + if (chatWindow->requestChatFocus()) + return true; + } + if (dialog) + { + if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_UP)) + dialog->move(1); + else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_DOWN)) + dialog->move(-1); + } + } - if (setupWindow->isVisible() && - keyboard.getNewKeyIndex() > KeyboardConfig::KEY_NO_VALUE) - { - keyboard.setNewKey(event.key.keysym.sym); - keyboard.callbackNewKey(); - keyboard.setNewKeyIndex(KeyboardConfig::KEY_NO_VALUE); - return; - } + if (!chatWindow->isInputFocused() || (event.keysym.mod & KMOD_ALT)) + { + if (keyboard.isKeyActive(KeyboardConfig::KEY_PREV_CHAT_TAB)) + { + chatWindow->prevTab(); + return true; + } + if (keyboard.isKeyActive(KeyboardConfig::KEY_NEXT_CHAT_TAB)) + { + chatWindow->nextTab(); + return true; + } + } - // send straight to gui for certain windows - if (quitDialog || TextDialog::isActive() || - PlayerInfo::getNPCPostCount() > 0) - { - try - { - guiInput->pushInput(event); - } - catch (gcn::Exception e) - { - const char* err = e.getMessage().c_str(); - logger->log("Warning: guichan input exception: %s", err); - } - return; - } + if (!chatWindow->isInputFocused()) + { + bool wearOutfit = false; + bool copyOutfit = false; - // Mode switch to emotes - if (keyboard.isKeyActive(KeyboardConfig::KEY_EMOTE)) - { - // Emotions - int emotion = keyboard.getKeyEmoteOffset(event.key.keysym.sym); - if (emotion != -1) - { - emoteShortcut->useEmote(emotion); - used = true; - return; - } - } - - if (!chatWindow->isInputFocused() - && !gui->getFocusHandler()->getModalFocused()) - { - NpcDialog *dialog = NpcDialog::getActive(); - if (keyboard.isKeyActive(KeyboardConfig::KEY_OK) - && (!dialog || !dialog->isTextInputFocused())) - { - // Close the Browser if opened - if (helpWindow->isVisible()) - helpWindow->setVisible(false); - // Close the config window, cancelling changes if opened - else if (setupWindow->isVisible()) - setupWindow->action(gcn::ActionEvent(nullptr, "cancel")); - else if (dialog) - dialog->action(gcn::ActionEvent(nullptr, "ok")); - } - if (keyboard.isKeyActive(KeyboardConfig::KEY_TOGGLE_CHAT)) - { - if (chatWindow->requestChatFocus()) - used = true; - } - if (dialog) - { - if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_UP)) - dialog->move(1); - else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_DOWN)) - dialog->move(-1); - } - } + if (keyboard.isKeyActive(KeyboardConfig::KEY_WEAR_OUTFIT)) + wearOutfit = true; + if (keyboard.isKeyActive(KeyboardConfig::KEY_COPY_OUTFIT)) + copyOutfit = true; - if (!chatWindow->isInputFocused() || (event.key.keysym.mod & - KMOD_ALT)) + if (wearOutfit || copyOutfit) + { + int outfitNum = -1; + switch (event.keysym.sym) { - if (keyboard.isKeyActive(KeyboardConfig::KEY_PREV_CHAT_TAB)) - { - chatWindow->prevTab(); - return; - } - else if (keyboard.isKeyActive(KeyboardConfig::KEY_NEXT_CHAT_TAB)) - { - chatWindow->nextTab(); - return; - } + case SDLK_1: + case SDLK_2: + case SDLK_3: + case SDLK_4: + case SDLK_5: + case SDLK_6: + case SDLK_7: + case SDLK_8: + case SDLK_9: + outfitNum = event.keysym.sym - SDLK_1; + break; + + case SDLK_0: + outfitNum = 9; + break; + + case SDLK_MINUS: + outfitNum = 10; + break; + + case SDLK_EQUALS: + outfitNum = 11; + break; + + case SDLK_BACKSPACE: + outfitNum = 12; + break; + + case SDLK_INSERT: + outfitNum = 13; + break; + + case SDLK_HOME: + outfitNum = 14; + break; + + default: + break; } - - if (!chatWindow->isInputFocused()) + if (outfitNum >= 0) { - bool wearOutfit = false; - bool copyOutfit = false; + if (wearOutfit) + outfitWindow->wearOutfit(outfitNum); + else if (copyOutfit) + outfitWindow->copyOutfit(outfitNum); + return true; + } + } + } - if (keyboard.isKeyActive(KeyboardConfig::KEY_WEAR_OUTFIT)) - wearOutfit = true; + const int tKey = keyboard.getKeyIndex(event.keysym.sym); + switch (tKey) + { + case KeyboardConfig::KEY_SCROLL_CHAT_UP: + if (chatWindow->isVisible()) + { + chatWindow->scroll(-DEFAULT_CHAT_WINDOW_SCROLL); + return true; + } + break; + case KeyboardConfig::KEY_SCROLL_CHAT_DOWN: + if (chatWindow->isVisible()) + { + chatWindow->scroll(DEFAULT_CHAT_WINDOW_SCROLL); + return true; + } + break; + case KeyboardConfig::KEY_WINDOW_HELP: + // In-game Help + if (helpWindow->isVisible()) + { + helpWindow->setVisible(false); + } + else + { + helpWindow->loadHelp("index"); + helpWindow->requestMoveToTop(); + } + return true; - if (keyboard.isKeyActive(KeyboardConfig::KEY_COPY_OUTFIT)) - copyOutfit = true; + case KeyboardConfig::KEY_QUIT: + { + // Close possible stuck NPC dialogs. + NpcDialog *npcDialog = NpcDialog::getActive(); + if (npcDialog && npcDialog->isWaitingForTheServer()) + { + npcDialog->close(); + return true; + } - if (wearOutfit || copyOutfit) - { - int outfitNum = -1; - switch (event.key.keysym.sym) - { - case SDLK_1: - case SDLK_2: - case SDLK_3: - case SDLK_4: - case SDLK_5: - case SDLK_6: - case SDLK_7: - case SDLK_8: - case SDLK_9: - outfitNum = event.key.keysym.sym - SDLK_1; - break; - - case SDLK_0: - outfitNum = 9; - break; - - case SDLK_MINUS: - outfitNum = 10; - break; - - case SDLK_EQUALS: - outfitNum = 11; - break; - - case SDLK_BACKSPACE: - outfitNum = 12; - break; - - case SDLK_INSERT: - outfitNum = 13; - break; - - case SDLK_HOME: - outfitNum = 14; - break; - - default: - break; - } - if (outfitNum >= 0) - { - used = true; - if (wearOutfit) - outfitWindow->wearOutfit(outfitNum); - else if (copyOutfit) - outfitWindow->copyOutfit(outfitNum); - } - } - } + // Otherwise, show the quit confirmation dialog. + quitDialog = new QuitDialog(&quitDialog); + quitDialog->requestMoveToTop(); + return true; + } + default: + break; + } - const int tKey = keyboard.getKeyIndex(event.key.keysym.sym); - switch (tKey) - { - case KeyboardConfig::KEY_SCROLL_CHAT_UP: - if (chatWindow->isVisible()) - { - chatWindow->scroll(-DEFAULT_CHAT_WINDOW_SCROLL); - used = true; - } - break; - case KeyboardConfig::KEY_SCROLL_CHAT_DOWN: - if (chatWindow->isVisible()) - { - chatWindow->scroll(DEFAULT_CHAT_WINDOW_SCROLL); - used = true; - return; - } - break; - case KeyboardConfig::KEY_WINDOW_HELP: - // In-game Help - if (helpWindow->isVisible()) - helpWindow->setVisible(false); - else - { - helpWindow->loadHelp("index"); - helpWindow->requestMoveToTop(); - } - used = true; - break; - case KeyboardConfig::KEY_QUIT: - { - // Close possible stuck NPC dialogs. - NpcDialog *npcDialog = NpcDialog::getActive(); - if (npcDialog && npcDialog->isWaitingForTheServer()) - { - npcDialog->close(); - return; - } - - // Otherwise, show the quit confirmation dialog. - quitDialog = new QuitDialog(&quitDialog); - quitDialog->requestMoveToTop(); - return; - } - default: - break; - } - if (keyboard.isEnabled() && !chatWindow->isInputFocused() && - !NpcDialog::isAnyInputFocused() && !InventoryWindow::isAnyInputFocused()) + if (keyboard.isEnabled() && !chatWindow->isInputFocused() && + !NpcDialog::isAnyInputFocused() && !InventoryWindow::isAnyInputFocused()) + { + // Do not activate shortcuts if tradewindow is visible + if (!tradeWindow->isVisible() && !setupWindow->isVisible()) + { + // Checks if any item shortcut is pressed. + for (int i = KeyboardConfig::KEY_SHORTCUT_1; + i <= KeyboardConfig::KEY_SHORTCUT_12; + i++) { - // Do not activate shortcuts if tradewindow is visible - if (!tradeWindow->isVisible() && !setupWindow->isVisible()) + if (tKey == i) { - // Checks if any item shortcut is pressed. - for (int i = KeyboardConfig::KEY_SHORTCUT_1; - i <= KeyboardConfig::KEY_SHORTCUT_12; - i++) - { - if (tKey == i && !used) - { - itemShortcut->useItem( - i - KeyboardConfig::KEY_SHORTCUT_1); - break; - } - } - } - - switch (tKey) - { - case KeyboardConfig::KEY_PICKUP: - { - handleItemPickUp(); - - used = true; - } - break; - case KeyboardConfig::KEY_SIT: - // Player sit action - local_player->toggleSit(); - used = true; - break; - case KeyboardConfig::KEY_HIDE_WINDOWS: - // Hide certain windows - if (!chatWindow->isInputFocused()) - { - statusWindow->setVisible(false); - inventoryWindow->setVisible(false); - skillDialog->setVisible(false); - setupWindow->setVisible(false); - equipmentWindow->setVisible(false); - helpWindow->setVisible(false); - debugWindow->setVisible(false); - socialWindow->setVisible(false); - } - break; - case KeyboardConfig::KEY_WINDOW_STATUS: - requestedWindow = statusWindow; - break; - case KeyboardConfig::KEY_WINDOW_INVENTORY: - requestedWindow = inventoryWindow; - break; - case KeyboardConfig::KEY_WINDOW_EQUIPMENT: - requestedWindow = equipmentWindow; - break; - case KeyboardConfig::KEY_WINDOW_SKILL: - requestedWindow = skillDialog; - break; - case KeyboardConfig::KEY_WINDOW_MINIMAP: - minimap->toggle(); - break; - case KeyboardConfig::KEY_WINDOW_CHAT: - requestedWindow = chatWindow; - break; - case KeyboardConfig::KEY_WINDOW_SHORTCUT: - requestedWindow = itemShortcutWindow; - break; - case KeyboardConfig::KEY_WINDOW_SETUP: - requestedWindow = setupWindow; - break; - case KeyboardConfig::KEY_WINDOW_DEBUG: - requestedWindow = debugWindow; - break; - case KeyboardConfig::KEY_WINDOW_SOCIAL: - requestedWindow = socialWindow; - break; - case KeyboardConfig::KEY_WINDOW_EMOTE_SHORTCUT: - requestedWindow = emoteShortcutWindow; - break; - case KeyboardConfig::KEY_WINDOW_OUTFIT: - requestedWindow = outfitWindow; - break; - case KeyboardConfig::KEY_SCREENSHOT: - // Screenshot (picture, hence the p) - saveScreenshot(); - used = true; - break; - case KeyboardConfig::KEY_TRADE: - // Toggle accepting of incoming trade requests - unsigned int deflt = player_relations.getDefault(); - if (deflt & PlayerPermissions::TRADE) - { - serverNotice(_("Ignoring incoming trade requests")); - deflt &= ~PlayerPermissions::TRADE; - } - else - { - serverNotice(_("Accepting incoming trade requests")); - deflt |= PlayerPermissions::TRADE; - } - - player_relations.setDefault(deflt); - - used = true; - break; + itemShortcut->useItem( + i - KeyboardConfig::KEY_SHORTCUT_1); + return true; } } - - if (requestedWindow) - { - requestedWindow->setVisible(!requestedWindow->isVisible()); - if (requestedWindow->isVisible()) - requestedWindow->requestMoveToTop(); - used = true; - } - } - // Quit event - else if (event.type == SDL_QUIT) - { - Client::setState(STATE_EXIT); } - // Push input to GUI when not used - if (!used) + switch (tKey) + { + case KeyboardConfig::KEY_PICKUP: + handleItemPickUp(); + return true; + + case KeyboardConfig::KEY_SIT: + local_player->toggleSit(); + return true; + + case KeyboardConfig::KEY_HIDE_WINDOWS: + // Hide certain windows + statusWindow->setVisible(false); + inventoryWindow->setVisible(false); + skillDialog->setVisible(false); + questsWindow->setVisible(false); + setupWindow->setVisible(false); + equipmentWindow->setVisible(false); + helpWindow->setVisible(false); + debugWindow->setVisible(false); + socialWindow->setVisible(false); + break; + + case KeyboardConfig::KEY_WINDOW_STATUS: + requestedWindow = statusWindow; + break; + case KeyboardConfig::KEY_WINDOW_INVENTORY: + requestedWindow = inventoryWindow; + break; + case KeyboardConfig::KEY_WINDOW_EQUIPMENT: + requestedWindow = equipmentWindow; + break; + case KeyboardConfig::KEY_WINDOW_SKILL: + requestedWindow = skillDialog; + break; + case KeyboardConfig::KEY_WINDOW_QUESTS: + requestedWindow = questsWindow; + break; + case KeyboardConfig::KEY_WINDOW_MINIMAP: + minimap->toggle(); + return true; + case KeyboardConfig::KEY_WINDOW_CHAT: + requestedWindow = chatWindow; + break; + case KeyboardConfig::KEY_WINDOW_SHORTCUT: + requestedWindow = itemShortcutWindow; + break; + case KeyboardConfig::KEY_WINDOW_SETUP: + requestedWindow = setupWindow; + break; + case KeyboardConfig::KEY_WINDOW_DEBUG: + requestedWindow = debugWindow; + break; + case KeyboardConfig::KEY_WINDOW_SOCIAL: + requestedWindow = socialWindow; + break; + case KeyboardConfig::KEY_WINDOW_EMOTE_SHORTCUT: + requestedWindow = emoteShortcutWindow; + break; + case KeyboardConfig::KEY_WINDOW_OUTFIT: + requestedWindow = outfitWindow; + break; + case KeyboardConfig::KEY_SCREENSHOT: + saveScreenshot(); + return true; + + case KeyboardConfig::KEY_TRADE: { - try + // Toggle accepting of incoming trade requests + unsigned int deflt = player_relations.getDefault(); + if (deflt & PlayerPermissions::TRADE) { - guiInput->pushInput(event); + serverNotice(_("Ignoring incoming trade requests")); + deflt &= ~PlayerPermissions::TRADE; } - catch (gcn::Exception e) + else { - const char *err = e.getMessage().c_str(); - logger->log("Warning: guichan input exception: %s", err); + serverNotice(_("Accepting incoming trade requests")); + deflt |= PlayerPermissions::TRADE; } + + player_relations.setDefault(deflt); + + return true; + } + + case KeyboardConfig::KEY_TALK: + if (Being *target = local_player->getTarget()) + if (target->canTalk()) + target->talkTo(); + return true; } + } + + if (requestedWindow) + { + requestedWindow->setVisible(!requestedWindow->isVisible()); + if (requestedWindow->isVisible()) + requestedWindow->requestMoveToTop(); + return true; + } - } // End while + return false; +} + +/** + * Continuous input handling. + */ +void Game::handleInput() +{ + if (joystick) + joystick->update(); // If the user is configuring the keys then don't respond. if (!keyboard.isEnabled()) return; + if (!local_player->isAlive()) + return; + if (PlayerInfo::isTalking()) + return; + if (chatWindow->isInputFocused() || quitDialog || TextDialog::isActive()) + return; // Moving player around - if (local_player->isAlive() && !PlayerInfo::isTalking() && - !chatWindow->isInputFocused() && !quitDialog && !TextDialog::isActive()) - { - // Get the state of the keyboard keys - keyboard.refreshActiveKeys(); - // Ignore input if either "ignore" key is pressed - // Stops the character moving about if the user's window manager - // uses "ignore+arrow key" to switch virtual desktops. - if (keyboard.isKeyActive(KeyboardConfig::KEY_IGNORE_INPUT_1) || - keyboard.isKeyActive(KeyboardConfig::KEY_IGNORE_INPUT_2)) - { - return; - } + // Get the state of the keyboard keys + keyboard.refreshActiveKeys(); - unsigned char direction = 0; + // Ignore input if either "ignore" key is pressed + // Stops the character moving about if the user's window manager + // uses "ignore+arrow key" to switch virtual desktops. + if (keyboard.isKeyActive(KeyboardConfig::KEY_IGNORE_INPUT_1) || + keyboard.isKeyActive(KeyboardConfig::KEY_IGNORE_INPUT_2)) + { + return; + } - // Translate pressed keys to movement and direction - if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_UP) || - (joystick && joystick->isUp())) - { - direction |= Being::UP; - } - else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_DOWN) || - (joystick && joystick->isDown())) - { - direction |= Being::DOWN; - } + unsigned char direction = 0; - if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_LEFT) || - (joystick && joystick->isLeft())) - { - direction |= Being::LEFT; - } - else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_RIGHT) || - (joystick && joystick->isRight())) - { - direction |= Being::RIGHT; - } + // Translate pressed keys to movement and direction + if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_UP) || + (joystick && joystick->isUp())) + { + direction |= Being::UP; + } + else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_DOWN) || + (joystick && joystick->isDown())) + { + direction |= Being::DOWN; + } - if (keyboard.isKeyActive(KeyboardConfig::KEY_EMOTE) && direction != 0) - { - if (local_player->getDirection() != direction) - { - local_player->setDirection(direction); - Net::getPlayerHandler()->setDirection(direction); - } - direction = 0; - } - else - { - local_player->setWalkingDir(direction); - } + if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_LEFT) || + (joystick && joystick->isLeft())) + { + direction |= Being::LEFT; + } + else if (keyboard.isKeyActive(KeyboardConfig::KEY_MOVE_RIGHT) || + (joystick && joystick->isRight())) + { + direction |= Being::RIGHT; + } - // Attacking monsters - if (keyboard.isKeyActive(KeyboardConfig::KEY_ATTACK) || - (joystick && joystick->buttonPressed(0))) + if (keyboard.isKeyActive(KeyboardConfig::KEY_EMOTE) && direction != 0) + { + if (local_player->getDirection() != direction) { - if (local_player->getTarget()) - local_player->attack(local_player->getTarget(), true); + local_player->setDirection(direction); + Net::getPlayerHandler()->setDirection(direction); } + } + else + { + local_player->setWalkingDir(direction); + } - if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_ATTACK)) - { - Being *target = local_player->getTarget(); + // Attacking monsters + if (keyboard.isKeyActive(KeyboardConfig::KEY_ATTACK) || + (joystick && joystick->buttonPressed(0))) + { + if (local_player->getTarget()) + local_player->attack(local_player->getTarget(), true); + } - bool newTarget = !keyboard.isKeyActive(KeyboardConfig::KEY_TARGET); - // A set target has highest priority - if (!target) - { - // Only auto target Monsters - target = actorSpriteManager->findNearestLivingBeing(local_player, - 20, ActorSprite::MONSTER); - } - local_player->attack(target, newTarget); - } + if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_ATTACK)) + { + Being *target = local_player->getTarget(); - // Target the nearest player/monster/npc - if ((keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_PLAYER) || - keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_CLOSEST) || - keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_NPC) || - (joystick && joystick->buttonPressed(3))) && - !keyboard.isKeyActive(KeyboardConfig::KEY_TARGET)) + bool newTarget = !keyboard.isKeyActive(KeyboardConfig::KEY_TARGET); + // A set target has highest priority + if (!target) { - ActorSprite::Type currentTarget = ActorSprite::UNKNOWN; - if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_CLOSEST) || - (joystick && joystick->buttonPressed(3))) - currentTarget = ActorSprite::MONSTER; - else if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_PLAYER)) - currentTarget = ActorSprite::PLAYER; - else if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_NPC)) - currentTarget = ActorSprite::NPC; - - Being *target = actorSpriteManager->findNearestLivingBeing(local_player, - 20, currentTarget); - - if (target && (target != local_player->getTarget() || - currentTarget != mLastTarget)) - { - local_player->setTarget(target); - mLastTarget = currentTarget; - } - } - else - { - mLastTarget = ActorSprite::UNKNOWN; // Reset last target + // Only auto target Monsters + target = actorSpriteManager->findNearestLivingBeing(local_player, + 20, ActorSprite::MONSTER); } + local_player->attack(target, newTarget); + } - // Talk to the nearest NPC if 't' pressed - if (event.type == SDL_KEYDOWN && - keyboard.getKeyIndex(event.key.keysym.sym) == KeyboardConfig::KEY_TALK) + // Target the nearest player/monster/npc + if ((keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_PLAYER) || + keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_CLOSEST) || + keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_NPC) || + (joystick && joystick->buttonPressed(3))) && + !keyboard.isKeyActive(KeyboardConfig::KEY_TARGET)) + { + ActorSprite::Type currentTarget = ActorSprite::UNKNOWN; + if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_CLOSEST) || + (joystick && joystick->buttonPressed(3))) + currentTarget = ActorSprite::MONSTER; + else if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_PLAYER)) + currentTarget = ActorSprite::PLAYER; + else if (keyboard.isKeyActive(KeyboardConfig::KEY_TARGET_NPC)) + currentTarget = ActorSprite::NPC; + + Being *target = actorSpriteManager->findNearestLivingBeing(local_player, + 20, currentTarget); + + if (target && (target != local_player->getTarget() || + currentTarget != mLastTarget)) { - Being *target = local_player->getTarget(); - - if (target) - { - if (target->canTalk()) - target->talkTo(); - } + local_player->setTarget(target); + mLastTarget = currentTarget; } + } + else + { + mLastTarget = ActorSprite::UNKNOWN; // Reset last target + } - // Stop attacking if the right key is pressed - if (!keyboard.isKeyActive(KeyboardConfig::KEY_ATTACK) - && keyboard.isKeyActive(KeyboardConfig::KEY_TARGET)) - { - local_player->stopAttack(); - } + // Stop attacking if the right key is pressed + if (!keyboard.isKeyActive(KeyboardConfig::KEY_ATTACK) + && keyboard.isKeyActive(KeyboardConfig::KEY_TARGET)) + { + local_player->stopAttack(); + } - if (joystick) + if (joystick) + { + if (joystick->buttonPressed(1)) { - if (joystick->buttonPressed(1)) - { - const int x = local_player->getTileX(); - const int y = local_player->getTileY(); - - FloorItem *item = actorSpriteManager->findItem(x, y); + const int x = local_player->getTileX(); + const int y = local_player->getTileY(); - if (item) - local_player->pickUp(item); - } - else if (joystick->buttonPressed(2)) - { - local_player->toggleSit(); - } + if (FloorItem *item = actorSpriteManager->findItem(x, y)) + local_player->pickUp(item); + } + else if (joystick->buttonPressed(2)) + { + local_player->toggleSit(); } } } @@ -931,7 +882,7 @@ void Game::changeMap(const std::string &mapPath) if (!newMap) { - logger->log("Error while loading %s", fullMap.c_str()); + Log::info("Error while loading %s", fullMap.c_str()); new OkDialog(_("Could Not Load Map"), strprintf(_("Error while loading %s"), fullMap.c_str())); } @@ -58,6 +58,8 @@ class Game */ void logic(); + bool keyDownEvent(SDL_KeyboardEvent &event); + void handleInput(); void changeMap(const std::string &mapName); diff --git a/src/graphics.cpp b/src/graphics.cpp index 5455f61a..68fc4e46 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -21,27 +21,11 @@ #include "graphics.h" -#include "resources/image.h" +#include "gui/truetypefont.h" +#include "resources/theme.h" #include <guichan/exception.hpp> -#include <utility> - -ImageRect::ImageRect(ImageRect &&r) -{ - image = std::exchange(r.image, nullptr); - top = r.top; - left = r.left; - bottom = r.bottom; - right = r.right; - fillMode = r.fillMode; -} - -ImageRect::~ImageRect() -{ - delete image; -} - void Graphics::updateSize(int width, int height, float /*scale*/) { @@ -166,7 +150,7 @@ void Graphics::drawImageRect(const ImageRect &imgRect, int x, int y, int w, int switch (imgRect.fillMode) { case FillMode::Stretch: - drawRescaledImage(imgRect.image, + drawRescaledImage(imgRect.image.get(), srcGridX[ix], srcGridY[iy], dstGridX[ix], @@ -175,7 +159,7 @@ void Graphics::drawImageRect(const ImageRect &imgRect, int x, int y, int w, int dstW, dstH); break; case FillMode::Repeat: - drawRescaledImagePattern(imgRect.image, + drawRescaledImagePattern(imgRect.image.get(), srcGridX[ix], srcGridY[iy], srcW, srcH, @@ -189,6 +173,72 @@ void Graphics::drawImageRect(const ImageRect &imgRect, int x, int y, int w, int } } +void Graphics::drawText(const std::string &text, + int x, int y, + gcn::Graphics::Alignment alignment, + const gcn::Color &color, + gcn::Font *font, + bool outline, + bool shadow, + const std::optional<gcn::Color> &outlineColor, + const std::optional<gcn::Color> &shadowColor) +{ + switch (alignment) + { + case gcn::Graphics::LEFT: + break; + case gcn::Graphics::CENTER: + x -= font->getWidth(text) / 2; + break; + case gcn::Graphics::RIGHT: + x -= font->getWidth(text); + break; + default: + throw GCN_EXCEPTION("Unknown alignment."); + } + + auto realOutlineColor = outlineColor; + auto realShadowColor = shadowColor; + + if (shadow && !realShadowColor) + { + auto sc = Theme::getThemeColor(Theme::SHADOW); + sc.a = color.a / 2; + realShadowColor = sc; + } + + if (outline && !realOutlineColor) + { + auto oc = Theme::getThemeColor(Theme::OUTLINE); + oc.a = color.a; + realOutlineColor = oc; + } + + setColor(color); + static_cast<TrueTypeFont*>(font)->drawString(graphics, text, x, y, + realOutlineColor, + realShadowColor); +} + +void Graphics::drawText(const std::string &text, + int x, + int y, + gcn::Graphics::Alignment align, + gcn::Font *font, + const TextFormat &format) +{ + drawText(text, + x, + y, + align, + format.color, + font, + format.outlineColor.has_value(), + format.shadowColor.has_value(), + format.outlineColor, + format.shadowColor); +} + void Graphics::_beginDraw() { pushClipArea(gcn::Rectangle(0, 0, mWidth, mHeight)); diff --git a/src/graphics.h b/src/graphics.h index 038b0c6b..b6508753 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -21,12 +21,17 @@ #pragma once +#include "resources/image.h" + #include <SDL.h> #include <guichan/color.hpp> #include <guichan/graphics.hpp> -class Image; +#include <memory> +#include <optional> + +struct TextFormat; enum class FillMode { @@ -35,47 +40,27 @@ enum class FillMode }; /** - * 9 images defining a rectangle. 4 corners, 4 sides and a middle area. The - * topology is as follows: + * An image reference along with the margins specifying how to render this + * image at different sizes. The margins divide the image into 9 sections as + * follows: * * <pre> - * !-----!-----------------!-----! - * ! 0 ! 1 ! 2 ! - * !-----!-----------------!-----! - * ! 3 ! 4 ! 5 ! - * !-----!-----------------!-----! - * ! 6 ! 7 ! 8 ! - * !-----!-----------------!-----! + * !------!--------------!-------! + * ! ! top ! ! + * !------!--------------!-------! + * ! left ! ! right ! + * !------!--------------!-------! + * ! ! bottom ! ! + * !------!--------------!-------! * </pre> * - * Sections 0, 2, 6 and 8 will remain as is. 1, 3, 4, 5 and 7 will be - * repeated to fit the size of the widget. + * The corner sections will remain as is. The edges and the center sections + * will be repeated or stretched to fit the target size, depending on the fill + * mode. */ -class ImageRect +struct ImageRect { -public: - enum ImagePosition - { - UPPER_LEFT = 0, - UPPER_CENTER = 1, - UPPER_RIGHT = 2, - LEFT = 3, - CENTER = 4, - RIGHT = 5, - LOWER_LEFT = 6, - LOWER_CENTER = 7, - LOWER_RIGHT = 8 - }; - - ImageRect() = default; - ImageRect(const ImageRect &) = delete; - ImageRect(ImageRect &&); - ~ImageRect(); - - ImageRect &operator=(const ImageRect &) = delete; - ImageRect &operator=(ImageRect &&r) = delete; - - Image *image = nullptr; + std::unique_ptr<Image> image; int top = 0; int left = 0; int bottom = 0; @@ -205,6 +190,25 @@ class Graphics : public gcn::Graphics drawImageRect(imgRect, area.x, area.y, area.width, area.height); } + using gcn::Graphics::drawText; + + void drawText(const std::string &text, + int x, int y, + gcn::Graphics::Alignment alignment, + const gcn::Color &color, + gcn::Font *font, + bool outline = false, + bool shadow = false, + const std::optional<gcn::Color> &outlineColor = {}, + const std::optional<gcn::Color> &shadowColor = {}); + + void drawText(const std::string &text, + int x, + int y, + gcn::Graphics::Alignment align, + gcn::Font *font, + const TextFormat &format); + /** * Updates the screen. This is done by either copying the buffer to the * screen or swapping pages. diff --git a/src/gui/abilitieswindow.cpp b/src/gui/abilitieswindow.cpp index 700fa7ff..d8122bf3 100644 --- a/src/gui/abilitieswindow.cpp +++ b/src/gui/abilitieswindow.cpp @@ -145,13 +145,13 @@ void AbilitiesWindow::draw(gcn::Graphics *graphics) void AbilitiesWindow::rebuild(const std::map<int, Ability> &abilityData) { delete_all(mEntries); - + mEntries.clear(); int vPos = 0; //vertical position of next placed element for (auto &[id, ability] : abilityData) { - logger->log("Updating ability GUI for %d", id); + Log::info("Updating ability GUI for %d", id); AbilityInfo *info = AbilityDB::get(id); if (info) @@ -166,7 +166,7 @@ void AbilitiesWindow::rebuild(const std::map<int, Ability> &abilityData) } else { - logger->log("Warning: No info available of ability %d", id); + Log::warn("No info available of ability %d", id); } } } diff --git a/src/gui/buydialog.cpp b/src/gui/buydialog.cpp index 135c2119..fb316722 100644 --- a/src/gui/buydialog.cpp +++ b/src/gui/buydialog.cpp @@ -54,7 +54,7 @@ BuyDialog::BuyDialog(int npcId): setCloseButton(true); setMinWidth(260); setMinHeight(230); - setDefaultSize(260, 230, ImageRect::CENTER); + setDefaultSize(260, 230, WindowAlignment::Center); mShopItems = new ShopItems; diff --git a/src/gui/changeemaildialog.cpp b/src/gui/changeemaildialog.cpp index ce83087b..e10a0d18 100644 --- a/src/gui/changeemaildialog.cpp +++ b/src/gui/changeemaildialog.cpp @@ -112,8 +112,8 @@ void ChangeEmailDialog::action(const gcn::ActionEvent &event) const std::string username = mLoginData->username.c_str(); const std::string newFirstEmail = mFirstEmailField->getText(); const std::string newSecondEmail = mSecondEmailField->getText(); - logger->log("ChangeEmailDialog::Email change, Username is %s", - username.c_str()); + Log::info("ChangeEmailDialog::Email change, Username is %s", + username.c_str()); std::stringstream errorMessage; int error = 0; diff --git a/src/gui/changepassworddialog.cpp b/src/gui/changepassworddialog.cpp index 437a8c90..91d7721d 100644 --- a/src/gui/changepassworddialog.cpp +++ b/src/gui/changepassworddialog.cpp @@ -94,8 +94,8 @@ void ChangePasswordDialog::action(const gcn::ActionEvent &event) const std::string oldPassword = mOldPassField->getText(); const std::string newFirstPass = mFirstPassField->getText(); const std::string newSecondPass = mSecondPassField->getText(); - logger->log("ChangePasswordDialog::Password change, Username is %s", - username.c_str()); + Log::info("ChangePasswordDialog::Password change, Username is %s", + username.c_str()); std::stringstream errorMessage; int error = 0; diff --git a/src/gui/charselectdialog.cpp b/src/gui/charselectdialog.cpp index 2485be69..1ed353dd 100644 --- a/src/gui/charselectdialog.cpp +++ b/src/gui/charselectdialog.cpp @@ -271,7 +271,7 @@ void CharSelectDialog::setCharacters(const Net::Characters &characters) if (characterSlot >= (int)mCharacterEntries.size()) { - logger->log("Warning: slot out of range: %d", character->slot); + Log::warn("Slot out of range: %d", character->slot); continue; } diff --git a/src/gui/chatwindow.cpp b/src/gui/chatwindow.cpp index d006097c..d19231ab 100644 --- a/src/gui/chatwindow.cpp +++ b/src/gui/chatwindow.cpp @@ -102,7 +102,7 @@ ChatWindow::ChatWindow(): setResizable(true); setDefaultVisible(true); setSaveVisible(true); - setDefaultSize(600, 123, ImageRect::LOWER_LEFT); + setDefaultSize(600, 123, WindowAlignment::BottomLeft); setMinWidth(150); setMinHeight(90); diff --git a/src/gui/connectiondialog.cpp b/src/gui/connectiondialog.cpp index 2ff68a15..e6435269 100644 --- a/src/gui/connectiondialog.cpp +++ b/src/gui/connectiondialog.cpp @@ -54,7 +54,7 @@ ConnectionDialog::ConnectionDialog(const std::string &text, void ConnectionDialog::action(const gcn::ActionEvent &) { - logger->log("Cancel pressed"); + Log::info("Cancel pressed"); Client::setState(mCancelState); } diff --git a/src/gui/equipmentwindow.cpp b/src/gui/equipmentwindow.cpp index b9d3b0fd..8523e7b2 100644 --- a/src/gui/equipmentwindow.cpp +++ b/src/gui/equipmentwindow.cpp @@ -63,7 +63,7 @@ EquipmentWindow::EquipmentWindow(Equipment *equipment): setCloseButton(true); setSaveVisible(true); setContentSize(175, 290); - setDefaultSize(getWidth(), getHeight(), ImageRect::CENTER); + setDefaultSize(getWidth(), getHeight(), WindowAlignment::Center); loadWindowState(); mUnequip = new Button(_("Unequip"), "unequip", this); diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 0c3d78eb..768aeb2b 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -71,7 +71,7 @@ Gui::Gui(Graphics *graphics, const std::string &themePath) setTheme(themeIt != mAvailableThemes.end() ? *themeIt : mAvailableThemes.front()); - logger->log("Initializing GUI..."); + Log::info("Initializing GUI..."); // Set graphics setGraphics(graphics); @@ -106,8 +106,8 @@ Gui::Gui(Graphics *graphics, const std::string &themePath) } catch (gcn::Exception e) { - logger->error(std::string("Unable to load '") + fontFile + - std::string("': ") + e.getMessage()); + Log::critical(std::string("Unable to load '") + fontFile + + "': " + e.getMessage()); } // Set bold font @@ -119,8 +119,8 @@ Gui::Gui(Graphics *graphics, const std::string &themePath) } catch (gcn::Exception e) { - logger->error(std::string("Unable to load '") + fontFile + - std::string("': ") + e.getMessage()); + Log::critical(std::string("Unable to load '") + fontFile + + "': " + e.getMessage()); } // Set mono font @@ -132,8 +132,8 @@ Gui::Gui(Graphics *graphics, const std::string &themePath) } catch (gcn::Exception e) { - logger->error(std::string("Unable to load '") + fontFile + - std::string("': ") + e.getMessage()); + Log::critical(std::string("Unable to load '") + fontFile + + "': " + e.getMessage()); } loadCustomCursors(); @@ -288,32 +288,20 @@ void Gui::loadCustomCursors() SDL_Surface *mouseSurface = loadSurface(cursorPath); if (!mouseSurface) { - logger->log("Warning: Unable to load mouse cursor file (%s): %s", - cursorPath.c_str(), SDL_GetError()); + Log::warn("Unable to load mouse cursor file (%s): %s", + cursorPath.c_str(), SDL_GetError()); return; } SDL_SetSurfaceBlendMode(mouseSurface, SDL_BLENDMODE_NONE); -#if SDL_BYTEORDER == SDL_BIG_ENDIAN - const Uint32 rmask = 0xff000000; - const Uint32 gmask = 0x00ff0000; - const Uint32 bmask = 0x0000ff00; - const Uint32 amask = 0x000000ff; -#else - const Uint32 rmask = 0x000000ff; - const Uint32 gmask = 0x0000ff00; - const Uint32 bmask = 0x00ff0000; - const Uint32 amask = 0xff000000; -#endif - constexpr int cursorSize = 40; const int targetCursorSize = cursorSize * mCustomCursorScale; const int columns = mouseSurface->w / cursorSize; - SDL_Surface *cursorSurface = SDL_CreateRGBSurface( + SDL_Surface *cursorSurface = SDL_CreateRGBSurfaceWithFormat( 0, targetCursorSize, targetCursorSize, 32, - rmask, gmask, bmask, amask); + SDL_PIXELFORMAT_RGBA32); for (int i = 0; i < static_cast<int>(Cursor::Count); ++i) { @@ -329,7 +317,7 @@ void Gui::loadCustomCursors() 17 * mCustomCursorScale); if (!cursor) { - logger->log("Warning: Unable to create cursor: %s", SDL_GetError()); + Log::warn("Unable to create cursor: %s", SDL_GetError()); } mCustomMouseCursors.push_back(cursor); diff --git a/src/gui/helpwindow.cpp b/src/gui/helpwindow.cpp index e0e21610..7c7c5d4c 100644 --- a/src/gui/helpwindow.cpp +++ b/src/gui/helpwindow.cpp @@ -45,7 +45,7 @@ HelpWindow::HelpWindow(): setResizable(true); setupWindow->registerWindowForReset(this); - setDefaultSize(500, 400, ImageRect::CENTER); + setDefaultSize(500, 400, WindowAlignment::Center); mBrowserBox = new BrowserBox; mScrollArea = new ScrollArea(mBrowserBox); @@ -98,7 +98,7 @@ void HelpWindow::loadFile(const std::string &file) char *fileContents = (char *) FS::loadFile(fileName, contentsLength); if (!fileContents) { - logger->log("Couldn't load text file: %s", fileName.c_str()); + Log::info("Couldn't load text file: %s", fileName.c_str()); return; } diff --git a/src/gui/inventorywindow.cpp b/src/gui/inventorywindow.cpp index ab2e9c86..0125700c 100644 --- a/src/gui/inventorywindow.cpp +++ b/src/gui/inventorywindow.cpp @@ -65,7 +65,7 @@ InventoryWindow::InventoryWindow(Inventory *inventory): setCloseButton(true); setSaveVisible(true); - setDefaultSize(387, 307, ImageRect::CENTER); + setDefaultSize(387, 307, WindowAlignment::Center); setMinWidth(316); setMinHeight(179); addKeyListener(this); diff --git a/src/gui/npcdialog.cpp b/src/gui/npcdialog.cpp index 16e7db94..033d01cc 100644 --- a/src/gui/npcdialog.cpp +++ b/src/gui/npcdialog.cpp @@ -82,7 +82,7 @@ NpcDialog::NpcDialog(int npcId) setMinWidth(200); setMinHeight(150); - setDefaultSize(260, 200, ImageRect::CENTER); + setDefaultSize(260, 200, WindowAlignment::Center); // Setup output text box mTextBox = new BrowserBox(BrowserBox::AUTO_WRAP); diff --git a/src/gui/popupmenu.cpp b/src/gui/popupmenu.cpp index 4bafc074..16b16af9 100644 --- a/src/gui/popupmenu.cpp +++ b/src/gui/popupmenu.cpp @@ -318,7 +318,7 @@ void PopupMenu::handleLink(const std::string &link) // Unknown actions else if (link != "cancel") { - logger->log("PopupMenu: Warning, unknown action '%s'", link.c_str()); + Log::info("PopupMenu: Warning, unknown action '%s'", link.c_str()); } setVisible(false); diff --git a/src/gui/questswindow.cpp b/src/gui/questswindow.cpp new file mode 100644 index 00000000..6769815e --- /dev/null +++ b/src/gui/questswindow.cpp @@ -0,0 +1,290 @@ +/* + * 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 "questswindow.h" + +#include "configuration.h" + +#include "gui/setup.h" + +#include "gui/widgets/browserbox.h" +#include "gui/widgets/checkbox.h" +#include "gui/widgets/itemlinkhandler.h" +#include "gui/widgets/layout.h" +#include "gui/widgets/listbox.h" +#include "gui/widgets/scrollarea.h" + +#include "net/net.h" +#include "net/playerhandler.h" + +#include "resources/questdb.h" + +#include "utils/gettext.h" + +#include <guichan/font.hpp> + +#include <algorithm> + +class QuestsModel final : public gcn::ListModel +{ +public: + int getNumberOfElements() override + { return mQuests.size(); } + + std::string getElementAt(int i) override + { return mQuests[i].name(); } + + const std::vector<QuestEntry> &getQuests() const + { return mQuests; } + + void setQuests(const std::vector<QuestEntry> &quests) + { mQuests = quests; } + +private: + std::vector<QuestEntry> mQuests; +}; + + +class QuestsListBox final : public ListBox +{ +public: + QuestsListBox(QuestsModel *model) + : ListBox(model) + {} + + unsigned getRowHeight() const override; + + void draw(gcn::Graphics *graphics) override; +}; + +unsigned QuestsListBox::getRowHeight() const +{ + auto rowHeight = ListBox::getRowHeight(); + + if (auto icon = gui->getTheme()->getIcon("complete")) + rowHeight = std::max<unsigned>(rowHeight, icon->getHeight() + 2); + + return rowHeight; +} + +void QuestsListBox::draw(gcn::Graphics *gcnGraphics) +{ + if (!mListModel) + return; + + auto *graphics = static_cast<Graphics *>(gcnGraphics); + auto *model = static_cast<QuestsModel *>(getListModel()); + + const int rowHeight = getRowHeight(); + + auto theme = gui->getTheme(); + auto completeIcon = theme->getIcon("complete"); + auto incompleteIcon = theme->getIcon("incomplete"); + + // Draw filled rectangle around the selected list element + if (mSelected >= 0) + { + auto highlightColor = Theme::getThemeColor(Theme::HIGHLIGHT); + highlightColor.a = gui->getTheme()->getGuiAlpha(); + graphics->setColor(highlightColor); + graphics->fillRectangle(gcn::Rectangle(0, rowHeight * mSelected, + getWidth(), rowHeight)); + } + + // Draw the list elements + graphics->setFont(getFont()); + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + + const int fontHeight = getFont()->getHeight(); + + for (int i = 0, y = 0; i < model->getNumberOfElements(); + ++i, y += rowHeight) + { + if (mSelected == i) + graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT_TEXT)); + else + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + + auto &quest = model->getQuests()[i]; + int x = 1; + + if (const Image *icon = quest.completed ? completeIcon : incompleteIcon) + { + graphics->drawImage(icon, x, y + (rowHeight - icon->getHeight()) / 2); + x += icon->getWidth() + 4; + } + + graphics->drawText(quest.name(), x, y + (rowHeight - fontHeight) / 2); + } +} + + +QuestsWindow::QuestsWindow() + : Window(_("Quests")) + , mQuestsModel(std::make_unique<QuestsModel>()) + , mQuestsListBox(new QuestsListBox(mQuestsModel.get())) + , mHideCompletedCheckBox(new CheckBox(_("Hide completed"), config.hideCompletedQuests)) + , mQuestDetails(new BrowserBox(BrowserBox::AUTO_WRAP)) + , mLinkHandler(std::make_unique<ItemLinkHandler>()) +{ + setWindowName("Quests"); + setupWindow->registerWindowForReset(this); + setResizable(true); + setCloseButton(true); + setSaveVisible(true); + + setDefaultSize(387, 307, WindowAlignment::Center); + setMinWidth(316); + setMinHeight(179); + + mQuestsListBox->addSelectionListener(this); + mHideCompletedCheckBox->setActionEventId("hideCompleted"); + mHideCompletedCheckBox->addActionListener(this); + + auto questListScrollArea = new ScrollArea(mQuestsListBox); + questListScrollArea->setHorizontalScrollPolicy(gcn::ScrollArea::SHOW_NEVER); + + mQuestDetails->setLinkHandler(mLinkHandler.get()); + mQuestDetailsScrollArea = new ScrollArea(mQuestDetails); + mQuestDetailsScrollArea->setHorizontalScrollPolicy(gcn::ScrollArea::SHOW_NEVER); + + auto place = getPlacer(0, 0); + place(0, 0, questListScrollArea, 2, 2).setPadding(2); + place(2, 0, mQuestDetailsScrollArea, 3, 2).setPadding(2); + place = getPlacer(0, 1); + place(0, 0, mHideCompletedCheckBox); + + getLayout().setRowHeight(1, 0); // Don't scale up the bottom row + + listen(Event::QuestsChannel); + + refreshQuestList(); + loadWindowState(); +} + +QuestsWindow::~QuestsWindow() = default; + +void QuestsWindow::action(const gcn::ActionEvent &event) +{ + if (event.getId() == "hideCompleted") + { + config.hideCompletedQuests = mHideCompletedCheckBox->isSelected(); + refreshQuestList(); + } +} + +void QuestsWindow::valueChanged(const gcn::SelectionEvent &event) +{ + if (mSelectedQuestIndex != mQuestsListBox->getSelected()) + updateQuestDetails(); +} + +void QuestsWindow::event(Event::Channel channel, const Event &event) +{ + if (channel == Event::QuestsChannel) + { + if (event.getType() == Event::QuestVarsChanged) + refreshQuestList(); + } +} + +void QuestsWindow::refreshQuestList() +{ + // Store the currently selected quest state and varId to preserve selection + const QuestState *selectedQuestState = nullptr; + int selectedVarId = -1; + if (mSelectedQuestIndex >= 0 && mSelectedQuestIndex < mQuestsModel->getNumberOfElements()) + { + const auto &selectedQuest = mQuestsModel->getQuests().at(mSelectedQuestIndex); + selectedQuestState = selectedQuest.state; + selectedVarId = selectedQuest.varId; + } + + auto &questVars = Net::getPlayerHandler()->getQuestVars(); + auto newQuests = QuestDB::getQuestsEntries(questVars, config.hideCompletedQuests); + + // Put completed quests at the top + std::stable_sort(newQuests.begin(), newQuests.end(), [](const QuestEntry &a, const QuestEntry &b) { + return a.completed > b.completed; + }); + + mQuestsModel->setQuests(newQuests); + + if (!selectedQuestState) + return; + + // Try to find and reselect the same quest, preferring exact state match + int newSelectedIndex = -1; + + for (int i = 0; i < static_cast<int>(newQuests.size()); ++i) + { + if (newQuests[i].state == selectedQuestState) + { + newSelectedIndex = i; + break; + } + else if (newSelectedIndex == -1 && newQuests[i].varId == selectedVarId) + { + newSelectedIndex = i; + // Don't break here - continue looking for exact state match + } + } + + if (mSelectedQuestIndex != newSelectedIndex) + mQuestsListBox->setSelected(newSelectedIndex); + else + updateQuestDetails(); +} + +void QuestsWindow::updateQuestDetails() +{ + mQuestDetails->clearRows(); + + mSelectedQuestIndex = mQuestsListBox->getSelected(); + if (mSelectedQuestIndex < 0 || mSelectedQuestIndex >= mQuestsModel->getNumberOfElements()) + return; + + const QuestEntry &quest = mQuestsModel->getQuests().at(mSelectedQuestIndex); + for (const auto &row : quest.rows()) + { + switch (row.type) + { + case QuestRowType::Text: + mQuestDetails->addRow(row.text); + break; + case QuestRowType::Name: + mQuestDetails->addRow("[" + row.text + "]"); + break; + case QuestRowType::Reward: + mQuestDetails->addRow(strprintf(_("Reward: %s"), row.text.c_str())); + break; + case QuestRowType::Giver: + mQuestDetails->addRow(strprintf(_("Quest Giver: %s"), row.text.c_str())); + break; + case QuestRowType::Coordinates: + mQuestDetails->addRow(strprintf(_("Coordinates: %s (%d, %d)"), + row.text.c_str(), row.x, row.y)); + break; + case QuestRowType::NPC: + mQuestDetails->addRow(strprintf(_("NPC: %s"), row.text.c_str())); + break; + } + } +} diff --git a/src/gui/questswindow.h b/src/gui/questswindow.h new file mode 100644 index 00000000..a479f826 --- /dev/null +++ b/src/gui/questswindow.h @@ -0,0 +1,71 @@ +/* + * 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 "eventlistener.h" + +#include "gui/widgets/window.h" + +#include <guichan/actionlistener.hpp> +#include <guichan/keylistener.hpp> +#include <guichan/selectionlistener.hpp> + +class BrowserBox; +class CheckBox; +class LinkHandler; +class QuestsListBox; +class QuestsModel; +class ScrollArea; + +/** + * Quests window. + * + * \ingroup Interface + */ +class QuestsWindow final : public Window, + public gcn::ActionListener, + public gcn::SelectionListener, + public EventListener +{ +public: + QuestsWindow(); + ~QuestsWindow(); + + void action(const gcn::ActionEvent &event) override; + + void valueChanged(const gcn::SelectionEvent &event) override; + + void event(Event::Channel channel, const Event &event) override; + +private: + void refreshQuestList(); + void updateQuestDetails(); + + int mSelectedQuestIndex = -1; + std::unique_ptr<QuestsModel> mQuestsModel; + QuestsListBox *mQuestsListBox; + CheckBox *mHideCompletedCheckBox; + BrowserBox *mQuestDetails; + ScrollArea *mQuestDetailsScrollArea; + std::unique_ptr<LinkHandler> mLinkHandler; +}; + +extern QuestsWindow *questsWindow; diff --git a/src/gui/recorder.cpp b/src/gui/recorder.cpp index 894e3631..96e458ff 100644 --- a/src/gui/recorder.cpp +++ b/src/gui/recorder.cpp @@ -47,7 +47,7 @@ Recorder::Recorder(ChatWindow *chat, // 123 is the default chat window height. If you change this in Chat, please // change it here as well setDefaultSize(button->getWidth() + offsetX, button->getHeight() + - offsetY, ImageRect::LOWER_LEFT, 0, 123); + offsetY, WindowAlignment::BottomLeft, 0, 123); place(0, 0, button); diff --git a/src/gui/register.cpp b/src/gui/register.cpp index 62114c10..d7924021 100644 --- a/src/gui/register.cpp +++ b/src/gui/register.cpp @@ -147,7 +147,7 @@ void RegisterDialog::action(const gcn::ActionEvent &event) else if (event.getId() == "register" && canSubmit()) { const std::string user = mUserField->getText(); - logger->log("RegisterDialog::register Username is %s", user.c_str()); + Log::info("RegisterDialog::register Username is %s", user.c_str()); std::string errorMessage; int error = 0; diff --git a/src/gui/selldialog.cpp b/src/gui/selldialog.cpp index 4aeacd6f..4fa12e53 100644 --- a/src/gui/selldialog.cpp +++ b/src/gui/selldialog.cpp @@ -56,7 +56,7 @@ SellDialog::SellDialog(int npcId): setCloseButton(true); setMinWidth(260); setMinHeight(230); - setDefaultSize(260, 230, ImageRect::CENTER); + setDefaultSize(260, 230, WindowAlignment::Center); // Create a ShopItems instance, that is aware of duplicate entries. mShopItems = new ShopItems(true); diff --git a/src/gui/serverdialog.cpp b/src/gui/serverdialog.cpp index f5418625..e41c0bbe 100644 --- a/src/gui/serverdialog.cpp +++ b/src/gui/serverdialog.cpp @@ -115,7 +115,10 @@ public: { const ServerInfo &info = model->getServer(i); - graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + if (mSelected == i) + graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT_TEXT)); + else + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); if (!info.name.empty()) { @@ -201,7 +204,7 @@ ServerDialog::ServerDialog(ServerInfo *serverInfo, const std::string &dir): setMinWidth(getWidth()); setMinHeight(getHeight()); - setDefaultSize(getWidth(), getHeight(), ImageRect::CENTER); + setDefaultSize(getWidth(), getHeight(), WindowAlignment::Center); setResizable(true); addKeyListener(this); @@ -361,7 +364,7 @@ void ServerDialog::logic() case DownloadStatus::Canceled: case DownloadStatus::Error: mDownloadDone = true; - logger->log("Error retrieving server list: %s", mDownload->getError()); + Log::info("Error retrieving server list: %s", mDownload->getError()); mDownloadText->setCaption(_("Error retrieving server list!")); break; @@ -406,15 +409,14 @@ void ServerDialog::loadServers() if (!rootNode || rootNode.name() != "serverlist") { - logger->log("Error loading server list!"); + Log::info("Error loading server list!"); return; } int version = rootNode.getProperty("version", 0); if (version != 1) { - logger->log("Error: unsupported online server list version: %d", - version); + Log::error("Unsupported online server list version: %d", version); return; } @@ -440,8 +442,8 @@ void ServerDialog::loadServer(XML::Node serverNode) #endif ) { - logger->log("Ignoring server entry with unknown type: %s", - type.c_str()); + Log::info("Ignoring server entry with unknown type: %s", + type.c_str()); return; } diff --git a/src/gui/setup_audio.cpp b/src/gui/setup_audio.cpp index 43b132d8..0031f9bc 100644 --- a/src/gui/setup_audio.cpp +++ b/src/gui/setup_audio.cpp @@ -108,7 +108,7 @@ void Setup_Audio::apply() catch (const char *err) { new OkDialog(_("Sound Engine"), err); - logger->log("Warning: %s", err); + Log::warn("%s", err); } } else diff --git a/src/gui/shortcutwindow.cpp b/src/gui/shortcutwindow.cpp index 2cffbb81..c33fcf01 100644 --- a/src/gui/shortcutwindow.cpp +++ b/src/gui/shortcutwindow.cpp @@ -49,10 +49,13 @@ ShortcutWindow::ShortcutWindow(const std::string &title, const int border = (getPadding() + content->getFrameSize()) * 2; setMinWidth(content->getBoxWidth() + border); setMinHeight(content->getBoxHeight() + border + GRAB_MARGIN); - setMaxWidth(content->getBoxWidth() * content->getMaxItems() + border); - setMaxHeight(content->getBoxHeight() * content->getMaxItems() + border + GRAB_MARGIN); - setDefaultSize(getMinWidth(), getMaxHeight(), ImageRect::LOWER_RIGHT); + const int maxContentWidth = content->getBoxWidth() * content->getMaxItems(); + const int maxContentHeight = content->getBoxHeight() * content->getMaxItems(); + setMaxWidth(std::max(getMinWidth(), maxContentWidth + border)); + setMaxHeight(std::max(getMinHeight(), maxContentHeight + border + GRAB_MARGIN)); + + setDefaultSize(getMinWidth(), getMaxHeight(), WindowAlignment::BottomRight); place(0, 0, scrollArea, 5, 5).setPadding(0); diff --git a/src/gui/skilldialog.cpp b/src/gui/skilldialog.cpp index 40421daf..49552421 100644 --- a/src/gui/skilldialog.cpp +++ b/src/gui/skilldialog.cpp @@ -147,11 +147,15 @@ public: } // Draw the list elements - graphics->setColor(Theme::getThemeColor(Theme::TEXT)); for (int i = 0, y = 1; i < model->getNumberOfElements(); ++i, y += getRowHeight()) { + if (mSelected == i) + graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT_TEXT)); + else + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + if (SkillInfo *e = model->getSkillAt(i)) e->draw(graphics, y, getWidth()); } @@ -290,7 +294,7 @@ void SkillDialog::loadSkills() if (!root || root.name() != "skills") { - logger->log("Error loading skills file: %s", SKILLS_FILE); + Log::info("Error loading skills file: %s", SKILLS_FILE); if (Net::getNetworkType() == ServerType::TmwAthena) { diff --git a/src/gui/tradewindow.cpp b/src/gui/tradewindow.cpp index e24845b7..6610bd43 100644 --- a/src/gui/tradewindow.cpp +++ b/src/gui/tradewindow.cpp @@ -60,7 +60,7 @@ TradeWindow::TradeWindow(): setWindowName("Trade"); setResizable(true); setCloseButton(true); - setDefaultSize(386, 180, ImageRect::CENTER); + setDefaultSize(386, 180, WindowAlignment::Center); setMinWidth(386); setMinHeight(180); setupWindow->registerWindowForReset(this); diff --git a/src/gui/truetypefont.cpp b/src/gui/truetypefont.cpp index 444641b5..c85d08c0 100644 --- a/src/gui/truetypefont.cpp +++ b/src/gui/truetypefont.cpp @@ -53,6 +53,7 @@ bool operator==(SDL_Color lhs, SDL_Color rhs) lhs.a == rhs.a); } + class TextChunk { public: @@ -60,24 +61,49 @@ public: : text(text) {} - void generate(TTF_Font *font) + void render(Graphics *graphics, + int x, int y, + TTF_Font *font, + std::unique_ptr<Image> &img, + float scale); + + const std::string text; + std::unique_ptr<Image> regular; + std::unique_ptr<Image> outlined; +}; + +void TextChunk::render(Graphics *graphics, + int x, int y, + TTF_Font *font, + std::unique_ptr<Image> &img, + float scale) +{ + if (!img) { // Always render in white, we'll use color modulation when rendering + constexpr SDL_Color white = { 255, 255, 255, 255 }; SDL_Surface *surface = TTF_RenderUTF8_Blended(font, getSafeUtf8String(text), - SDL_Color { 255, 255, 255, 255 }); + white); - if (!surface) - return; + if (surface) + { + img.reset(Image::load(surface)); + SDL_FreeSurface(surface); + } + } - img.reset(Image::load(surface)); + if (img) + { + graphics->drawRescaledImageF(img.get(), 0, 0, x, y, + img->getWidth(), + img->getHeight(), + img->getWidth() / scale, + img->getHeight() / scale, true); - SDL_FreeSurface(surface); } +} - std::unique_ptr<Image> img; - const std::string text; -}; std::list<TrueTypeFont*> TrueTypeFont::mFonts; float TrueTypeFont::mScale = 1.0f; @@ -85,6 +111,7 @@ float TrueTypeFont::mScale = 1.0f; TrueTypeFont::TrueTypeFont(const std::string &filename, int size, int style) : mFilename(filename) , mPointSize(size) + , mStyle(style) { if (TTF_Init() == -1) { @@ -93,14 +120,18 @@ TrueTypeFont::TrueTypeFont(const std::string &filename, int size, int style) } mFont = TTF_OpenFont(filename.c_str(), size * mScale); + mFontOutline = TTF_OpenFont(filename.c_str(), size * mScale); - if (!mFont) + if (!mFont || !mFontOutline) { throw GCN_EXCEPTION("SDLTrueTypeFont::SDLTrueTypeFont: " + std::string(TTF_GetError())); } TTF_SetFontStyle(mFont, style); + TTF_SetFontStyle(mFontOutline, style); + + TTF_SetFontOutline(mFontOutline, static_cast<int>(mScale)); mFonts.push_back(this); } @@ -112,6 +143,9 @@ TrueTypeFont::~TrueTypeFont() if (mFont) TTF_CloseFont(mFont); + if (mFontOutline) + TTF_CloseFont(mFontOutline); + TTF_Quit(); } @@ -123,37 +157,41 @@ void TrueTypeFont::drawString(gcn::Graphics *graphics, return; auto *g = static_cast<Graphics *>(graphics); + TextChunk &chunk = getChunk(text); - bool found = false; + chunk.render(g, x, y, mFont, chunk.regular, mScale); +} - for (auto i = mCache.begin(); i != mCache.end(); ++i) - { - auto &chunk = *i; - if (chunk.text == text) - { - // Raise priority: move it to front - mCache.splice(mCache.begin(), mCache, i); - found = true; - break; - } - } +void TrueTypeFont::drawString(Graphics *graphics, + const std::string &text, + int x, int y, + const std::optional<gcn::Color> &outlineColor, + const std::optional<gcn::Color> &shadowColor) +{ + if (text.empty()) + return; + + auto *g = static_cast<Graphics *>(graphics); + auto color = graphics->getColor(); + TextChunk &chunk = getChunk(text); - if (!found) + if (shadowColor) { - if (mCache.size() >= CACHE_SIZE) - mCache.pop_back(); - mCache.emplace_front(text); - mCache.front().generate(mFont); + g->setColor(*shadowColor); + if (outlineColor) + chunk.render(g, x, y, mFontOutline, chunk.outlined, mScale); + else + chunk.render(g, x + 1, y + 1, mFont, chunk.regular, mScale); } - if (auto img = mCache.front().img.get()) + if (outlineColor) { - g->drawRescaledImageF(img, 0, 0, x, y, - img->getWidth(), - img->getHeight(), - img->getWidth() / mScale, - img->getHeight() / mScale, true); + g->setColor(*outlineColor); + chunk.render(g, x - 1, y - 1, mFontOutline, chunk.outlined, mScale); } + + g->setColor(color); + chunk.render(g, x, y, mFont, chunk.regular, mScale); } void TrueTypeFont::updateFontScale(float scale) @@ -167,9 +205,16 @@ void TrueTypeFont::updateFontScale(float scale) { #if SDL_TTF_VERSION_ATLEAST(2, 0, 18) TTF_SetFontSize(font->mFont, font->mPointSize * mScale); + TTF_SetFontSize(font->mFontOutline, font->mPointSize * mScale); + TTF_SetFontOutline(font->mFontOutline, mScale); #else TTF_CloseFont(font->mFont); + TTF_CloseFont(font->mFontOutline); font->mFont = TTF_OpenFont(font->mFilename.c_str(), font->mPointSize * mScale); + font->mFontOutline = TTF_OpenFont(font->mFilename.c_str(), font->mPointSize * mScale); + TTF_SetFontStyle(font->mFont, font->mStyle); + TTF_SetFontStyle(font->mFontOutline, font->mStyle); + TTF_SetFontOutline(font->mFontOutline, mScale); #endif font->mCache.clear(); @@ -178,19 +223,11 @@ void TrueTypeFont::updateFontScale(float scale) int TrueTypeFont::getWidth(const std::string &text) const { - for (auto i = mCache.begin(); i != mCache.end(); i++) - { - if (i->text == text) - { - // Raise priority: move it to front - // Assumption is that TTF::draw will be called next - mCache.splice(mCache.begin(), mCache, i); - if (i->img) - return std::ceil(i->img->getWidth() / mScale); - return 0; - } - } + TextChunk &chunk = getChunk(text); + if (auto img = chunk.regular.get()) + return std::ceil(img->getWidth() / mScale); + // If the image wasn't created yet, just calculate the width of the text int w, h; TTF_SizeUTF8(mFont, getSafeUtf8String(text), &w, &h); return std::ceil(w / mScale); @@ -205,3 +242,21 @@ int TrueTypeFont::getLineHeight() const { return std::ceil(TTF_FontLineSkip(mFont) / mScale); } + +TextChunk &TrueTypeFont::getChunk(const std::string &text) const +{ + for (auto i = mCache.begin(); i != mCache.end(); i++) + { + if (i->text == text) + { + // Raise priority: move it to front + mCache.splice(mCache.begin(), mCache, i); + return *i; + } + } + + if (mCache.size() >= CACHE_SIZE) + mCache.pop_back(); + + return mCache.emplace_front(text); +} diff --git a/src/gui/truetypefont.h b/src/gui/truetypefont.h index a479537d..f88938ff 100644 --- a/src/gui/truetypefont.h +++ b/src/gui/truetypefont.h @@ -22,13 +22,16 @@ #pragma once +#include <guichan/color.hpp> #include <guichan/font.hpp> #include <SDL_ttf.h> #include <list> +#include <optional> #include <string> +class Graphics; class TextChunk; /** @@ -46,11 +49,13 @@ class TrueTypeFont : public gcn::Font * @param size Font size. */ TrueTypeFont(const std::string &filename, int size, int style = 0); - ~TrueTypeFont() override; - int getWidth(const std::string &text) const override; + const std::string &filename() const { return mFilename; } + int pointSize() const { return mPointSize; } + int style() const { return mStyle; } + int getWidth(const std::string &text) const override; int getHeight() const override; /** @@ -67,13 +72,27 @@ class TrueTypeFont : public gcn::Font const std::string &text, int x, int y) override; + /** + * Extended version of drawString that allows for rendering text with + * outline and/or shadow. + */ + void drawString(Graphics *graphics, + const std::string &text, + int x, int y, + const std::optional<gcn::Color> &outlineColor, + const std::optional<gcn::Color> &shadowColor); + static void updateFontScale(float scale); private: + TextChunk &getChunk(const std::string &text) const; + const std::string mFilename; - TTF_Font *mFont; + TTF_Font *mFont = nullptr; + TTF_Font *mFontOutline = nullptr; const int mPointSize; + const int mStyle; // Word surfaces cache mutable std::list<TextChunk> mCache; diff --git a/src/gui/unregisterdialog.cpp b/src/gui/unregisterdialog.cpp index 94f6ef62..2abbebf7 100644 --- a/src/gui/unregisterdialog.cpp +++ b/src/gui/unregisterdialog.cpp @@ -98,8 +98,8 @@ void UnRegisterDialog::action(const gcn::ActionEvent &event) else if (event.getId() == "unregister") { const std::string &password = mPasswordField->getText(); - logger->log("UnregisterDialog::unregistered, Username is %s", - mLoginData->username.c_str()); + Log::info("UnregisterDialog::unregistered, Username is %s", + mLoginData->username.c_str()); std::stringstream errorMessage; bool error = false; diff --git a/src/gui/updaterwindow.cpp b/src/gui/updaterwindow.cpp index 5cfb45cd..c16c3e04 100644 --- a/src/gui/updaterwindow.cpp +++ b/src/gui/updaterwindow.cpp @@ -60,7 +60,7 @@ std::vector<UpdateFile> loadXMLFile(const std::string &fileName) if (!rootNode || rootNode.name() != "updates") { - logger->log("Error loading update file: %s", fileName.c_str()); + Log::info("Error loading update file: %s", fileName.c_str()); return files; } @@ -110,7 +110,7 @@ std::vector<UpdateFile> loadTxtFile(const std::string &fileName) } else { - logger->log("Error loading update file: %s", fileName.c_str()); + Log::info("Error loading update file: %s", fileName.c_str()); } fileHandler.close(); @@ -128,7 +128,7 @@ UpdaterWindow::UpdaterWindow(const std::string &updateHost, { setWindowName("UpdaterWindow"); setResizable(true); - setDefaultSize(450, 400, ImageRect::CENTER); + setDefaultSize(450, 400, WindowAlignment::Center); setMinWidth(320); setMinHeight(240); @@ -258,9 +258,9 @@ void UpdaterWindow::loadUpdates() mUpdateFiles = loadXMLFile(mUpdatesDir + "/" + xmlUpdateFile); if (mUpdateFiles.empty()) { - logger->log("Warning this server does not have a" - " %s file falling back to %s", xmlUpdateFile, - txtUpdateFile); + Log::warn("This server does not have a" + " %s file falling back to %s", xmlUpdateFile, + txtUpdateFile); mUpdateFiles = loadTxtFile(mUpdatesDir + "/" + txtUpdateFile); } } @@ -336,9 +336,9 @@ void UpdaterWindow::downloadCompleted() mUpdateFiles = loadXMLFile(mUpdatesDir + "/" + xmlUpdateFile); if (mUpdateFiles.empty()) { - logger->log("Warning this server does not have a %s" - " file falling back to %s", - xmlUpdateFile, txtUpdateFile); + Log::warn("This server does not have a %s" + " file falling back to %s", + xmlUpdateFile, txtUpdateFile); // If the resources.xml file fails, fall back onto a older version mDialogState = DialogState::DownloadList; @@ -383,7 +383,7 @@ void UpdaterWindow::downloadCompleted() else { fclose(file); - logger->log("%s already here", thisFile.name.c_str()); + Log::info("%s already here", thisFile.name.c_str()); } mUpdateIndex++; } diff --git a/src/gui/viewport.cpp b/src/gui/viewport.cpp index 9e529063..3d90846e 100644 --- a/src/gui/viewport.cpp +++ b/src/gui/viewport.cpp @@ -278,7 +278,7 @@ void Viewport::_followMouse() mMouseY = static_cast<int>(logicalY); // If the left button is dragged - if (mPlayerFollowMouse && button & SDL_BUTTON(1)) + if (mPlayerFollowMouse && button & SDL_BUTTON_LMASK) { // We create a mouse event and send it to mouseDragged. const Uint8 *keys = SDL_GetKeyboardState(nullptr); diff --git a/src/gui/widgets/avatarlistbox.cpp b/src/gui/widgets/avatarlistbox.cpp index 4a806d04..a5109267 100644 --- a/src/gui/widgets/avatarlistbox.cpp +++ b/src/gui/widgets/avatarlistbox.cpp @@ -76,11 +76,15 @@ void AvatarListBox::draw(gcn::Graphics *gcnGraphics) auto offlineIcon = theme->getIcon("offline"); // Draw the list elements - graphics->setColor(Theme::getThemeColor(Theme::TEXT)); for (int i = 0, y = 0; i < model->getNumberOfElements(); ++i, y += rowHeight) { + if (mSelected == i) + graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT_TEXT)); + else + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + Avatar *a = model->getAvatarAt(i); int x = 1; diff --git a/src/gui/widgets/browserbox.cpp b/src/gui/widgets/browserbox.cpp index 4fe85ae5..bf337a0f 100644 --- a/src/gui/widgets/browserbox.cpp +++ b/src/gui/widgets/browserbox.cpp @@ -23,7 +23,6 @@ #include "gui/widgets/browserbox.h" #include "keyboardconfig.h" -#include "textrenderer.h" #include "gui/gui.h" #include "gui/truetypefont.h" @@ -228,7 +227,7 @@ void BrowserBox::addRow(std::string_view row) void BrowserBox::clearRows() { mTextRows.clear(); - setSize(0, 0); + setSize(mMode == AUTO_SIZE ? 0 : getWidth(), 0); mHoveredLink.reset(); maybeRelayoutText(); } @@ -292,6 +291,8 @@ void BrowserBox::draw(gcn::Graphics *graphics) } } + auto g = static_cast<Graphics*>(graphics); + for (const auto &row : mTextRows) { for (const auto &part : row.parts) @@ -301,16 +302,15 @@ void BrowserBox::draw(gcn::Graphics *graphics) if (part.y > yEnd) return; - TextRenderer::renderText(graphics, - part.text, - part.x, - part.y, - Graphics::LEFT, - part.color, - part.font, - part.outlineColor.has_value() || mOutline, - mShadows, - part.outlineColor); + g->drawText(part.text, + part.x, + part.y, + Graphics::LEFT, + part.color, + part.font, + part.outlineColor.has_value() || mOutline, + mShadows, + part.outlineColor); } } } diff --git a/src/gui/widgets/button.cpp b/src/gui/widgets/button.cpp index 31c3a677..604f5dc8 100644 --- a/src/gui/widgets/button.cpp +++ b/src/gui/widgets/button.cpp @@ -28,7 +28,6 @@ #include "resources/image.h" #include "resources/theme.h" -#include "textrenderer.h" #include <guichan/exception.hpp> #include <guichan/font.hpp> @@ -132,8 +131,9 @@ void Button::draw(gcn::Graphics *graphics) if (isPressed()) widgetState.flags |= STATE_SELECTED; + auto g = static_cast<Graphics *>(graphics); auto &skin = gui->getTheme()->getSkin(SkinType::Button); - skin.draw(static_cast<Graphics *>(graphics), widgetState); + skin.draw(g, widgetState); auto skinState = skin.getState(widgetState.flags); auto font = (skinState && skinState->textFormat.bold) ? boldFont : getFont(); @@ -196,18 +196,16 @@ void Button::draw(gcn::Graphics *graphics) } if (btnIconWidth) - static_cast<Graphics *>(graphics)->drawImage(icon, btnIconX, btnIconY); + g->drawImage(icon, btnIconX, btnIconY); if (auto skinState = skin.getState(widgetState.flags)) { - auto &textFormat = skinState->textFormat; - TextRenderer::renderText(static_cast<Graphics *>(graphics), - getCaption(), - textX, - textY, - getAlignment(), - font, - textFormat); + g->drawText(getCaption(), + textX, + textY, + getAlignment(), + font, + skinState->textFormat); } } diff --git a/src/gui/widgets/chattab.h b/src/gui/widgets/chattab.h index dfc07638..5232392f 100644 --- a/src/gui/widgets/chattab.h +++ b/src/gui/widgets/chattab.h @@ -24,7 +24,6 @@ #include "gui/chatwindow.h" #include "gui/widgets/tab.h" -#include "gui/widgets/textfield.h" class BrowserBox; class Recorder; diff --git a/src/gui/widgets/checkbox.cpp b/src/gui/widgets/checkbox.cpp index e6079f2f..4bb0bb72 100644 --- a/src/gui/widgets/checkbox.cpp +++ b/src/gui/widgets/checkbox.cpp @@ -21,8 +21,6 @@ #include "gui/widgets/checkbox.h" -#include "textrenderer.h" - #include "gui/gui.h" #include "resources/theme.h" @@ -44,19 +42,19 @@ void CheckBox::draw(gcn::Graphics* graphics) if (isSelected()) widgetState.flags |= STATE_SELECTED; + auto g = static_cast<Graphics *>(graphics); auto &skin = gui->getTheme()->getSkin(SkinType::CheckBox); - skin.draw(static_cast<Graphics *>(graphics), widgetState); + skin.draw(g, widgetState); if (auto skinState = skin.getState(widgetState.flags)) { auto &textFormat = skinState->textFormat; - TextRenderer::renderText(static_cast<Graphics *>(graphics), - getCaption(), - skin.getMinWidth() + skin.padding + skin.spacing, - skin.padding, - Graphics::LEFT, - textFormat.bold ? boldFont : getFont(), - textFormat); + g->drawText(getCaption(), + skin.getMinWidth() + skin.padding + skin.spacing, + skin.padding, + Graphics::LEFT, + textFormat.bold ? boldFont : getFont(), + textFormat); } } diff --git a/src/gui/widgets/desktop.cpp b/src/gui/widgets/desktop.cpp index e424beec..b21c235a 100644 --- a/src/gui/widgets/desktop.cpp +++ b/src/gui/widgets/desktop.cpp @@ -113,6 +113,6 @@ void Desktop::setBestFittingWallpaper() } else { - logger->log("Couldn't load %s as wallpaper", wallpaperName.c_str()); + Log::info("Couldn't load %s as wallpaper", wallpaperName.c_str()); } } diff --git a/src/gui/widgets/itemcontainer.cpp b/src/gui/widgets/itemcontainer.cpp index 57e8a973..63388213 100644 --- a/src/gui/widgets/itemcontainer.cpp +++ b/src/gui/widgets/itemcontainer.cpp @@ -155,7 +155,7 @@ void ItemContainer::draw(gcn::Graphics *graphics) if (item->isEquipped()) g->setColor(theme->getColor(Theme::ITEM_EQUIPPED)); else - g->setColor(gcn::Color(0, 0, 0)); + g->setColor(theme->getColor(Theme::TEXT)); g->drawText(caption, itemX + slotSkin.width / 2, itemY + slotSkin.height - 14, gcn::Graphics::CENTER); @@ -354,9 +354,7 @@ void ItemContainer::mouseReleased(gcn::MouseEvent &event) // Show ItemTooltip void ItemContainer::mouseMoved(gcn::MouseEvent &event) { - Item *item = getItemAt(getSlotIndex(event.getX(), event.getY())); - - if (item) + if (Item *item = getItemAt(getSlotIndex(event.getX(), event.getY()))) { mItemPopup->setItem(item->getInfo()); mItemPopup->position(viewport->getMouseX(), viewport->getMouseY()); @@ -394,12 +392,17 @@ void ItemContainer::adjustHeight() int ItemContainer::getSlotIndex(int x, int y) const { + if (x >= getWidth() || y >= getHeight()) + return Inventory::NO_SLOT_INDEX; + auto &slotSkin = gui->getTheme()->getSkin(SkinType::ItemSlot); + const auto row = y / slotSkin.height; + const auto column = x / slotSkin.width; - if (x < getWidth() && y < getHeight()) - return (y / slotSkin.height) * mGridColumns + (x / slotSkin.width); + if (row < 0 || row >= mGridRows || column < 0 || column >= mGridColumns) + return Inventory::NO_SLOT_INDEX; - return Inventory::NO_SLOT_INDEX; + return (row * mGridColumns) + column; } void ItemContainer::keyAction() diff --git a/src/gui/widgets/itemlinkhandler.cpp b/src/gui/widgets/itemlinkhandler.cpp index f596fc82..d9d6f506 100644 --- a/src/gui/widgets/itemlinkhandler.cpp +++ b/src/gui/widgets/itemlinkhandler.cpp @@ -33,6 +33,7 @@ #include "gui/widgets/itemlinkhandler.h" +#include "client.h" #include "resources/iteminfo.h" #include "resources/itemdb.h" #include "utils/gettext.h" @@ -42,6 +43,7 @@ ItemLinkHandler::ItemLinkHandler(Window *parent) : mParent(parent) { mItemPopup = std::make_unique<ItemPopup>(); + mItemPopup->addDeathListener(this); } ItemLinkHandler::~ItemLinkHandler() = default; @@ -55,6 +57,24 @@ static bool isUrl(const std::string &link) void ItemLinkHandler::handleLink(const std::string &link) { +#if SDL_VERSION_ATLEAST(2, 0, 14) + // Handle screenshots by constructing full file path + if (startsWith(link, "screenshot:")) + { + std::string filename = link.substr(11); // Remove "screenshot:" prefix + + // Prevent directory traversal attacks or opening malicious files + if (filename.find("..") == std::string::npos && endsWith(filename, ".png")) + { + std::string fileUrl = "file://" + Client::getScreenshotDirectory() + "/" + filename; + if (SDL_OpenURL(fileUrl.c_str()) == -1) + new OkDialog(_("Open URL Failed"), SDL_GetError(), true, mParent); + } + + return; + } +#endif + if (isUrl(link)) { mLink = link; @@ -97,3 +117,10 @@ void ItemLinkHandler::action(const gcn::ActionEvent &actionEvent) #endif } } + +void ItemLinkHandler::death(const gcn::Event &event) +{ + // If somebody else killed the PopupUp, make sure we don't also try to delete it + if (event.getSource() == mItemPopup.get()) + mItemPopup.release(); +} diff --git a/src/gui/widgets/itemlinkhandler.h b/src/gui/widgets/itemlinkhandler.h index 58202d33..637482bd 100644 --- a/src/gui/widgets/itemlinkhandler.h +++ b/src/gui/widgets/itemlinkhandler.h @@ -24,13 +24,14 @@ #include "gui/widgets/linkhandler.h" #include <guichan/actionlistener.hpp> +#include <guichan/deathlistener.hpp> #include <memory> class ItemPopup; class Window; -class ItemLinkHandler : public LinkHandler, gcn::ActionListener +class ItemLinkHandler : public LinkHandler, gcn::ActionListener, public gcn::DeathListener { public: ItemLinkHandler(Window *parent = nullptr); @@ -42,6 +43,9 @@ class ItemLinkHandler : public LinkHandler, gcn::ActionListener // ActionListener interface void action(const gcn::ActionEvent &actionEvent) override; + // DeathListener interface + void death(const gcn::Event &event) override; + private: std::unique_ptr<ItemPopup> mItemPopup; diff --git a/src/gui/widgets/itemshortcutcontainer.cpp b/src/gui/widgets/itemshortcutcontainer.cpp index b7924a4b..b47fa29d 100644 --- a/src/gui/widgets/itemshortcutcontainer.cpp +++ b/src/gui/widgets/itemshortcutcontainer.cpp @@ -49,6 +49,7 @@ void ItemShortcutContainer::draw(gcn::Graphics *graphics) { auto *g = static_cast<Graphics*>(graphics); auto theme = gui->getTheme(); + auto &skin = theme->getSkin(SkinType::ShortcutBox); graphics->setFont(getFont()); @@ -57,21 +58,22 @@ void ItemShortcutContainer::draw(gcn::Graphics *graphics) WidgetState state; state.x = (i % mGridWidth) * mBoxWidth; state.y = (i / mGridWidth) * mBoxHeight; - theme->drawSkin(g, SkinType::ShortcutBox, state); + skin.draw(g, state); // Draw item keyboard shortcut. const char *key = SDL_GetKeyName( keyboard.getKeyValue(KeyboardConfig::KEY_SHORTCUT_1 + i)); graphics->setColor(Theme::getThemeColor(Theme::TEXT)); - g->drawText(key, state.x + 2, state.y + 2, gcn::Graphics::LEFT); + g->drawText(key, + state.x + skin.padding + 2, + state.y + skin.padding + 2, + gcn::Graphics::LEFT); - if (itemShortcut->getItem(i) < 0) + const int itemId = itemShortcut->getItem(i); + if (itemId < 0) continue; - Item *item = - PlayerInfo::getInventory()->findItem(itemShortcut->getItem(i)); - - if (item) + if (Item *item = PlayerInfo::getInventory()->findItem(itemId)) { // Draw item icon. if (Image *image = item->getImage()) @@ -83,11 +85,13 @@ void ItemShortcutContainer::draw(gcn::Graphics *graphics) caption = "Eq."; image->setAlpha(1.0f); - g->drawImage(image, state.x, state.y); + g->drawImage(image, state.x + skin.padding, state.y + skin.padding); if (item->isEquipped()) g->setColor(Theme::getThemeColor(Theme::ITEM_EQUIPPED)); - g->drawText(caption, state.x + mBoxWidth / 2, - state.y + mBoxHeight - 14, gcn::Graphics::CENTER); + g->drawText(caption, + state.x + mBoxWidth / 2, + state.y + mBoxHeight - 14, + gcn::Graphics::CENTER); } } } @@ -200,15 +204,7 @@ void ItemShortcutContainer::mouseReleased(gcn::MouseEvent &event) // Show ItemTooltip void ItemShortcutContainer::mouseMoved(gcn::MouseEvent &event) { - const int index = getIndexFromGrid(event.getX(), event.getY()); - if (index == -1) - return; - - const int itemId = itemShortcut->getItem(index); - if (itemId < 0) - return; - - if (Item *item = PlayerInfo::getInventory()->findItem(itemId)) + if (Item *item = getItemAt(event.getX(), event.getY())) { mItemPopup->setItem(item->getInfo()); mItemPopup->position(viewport->getMouseX(), viewport->getMouseY()); @@ -219,6 +215,19 @@ void ItemShortcutContainer::mouseMoved(gcn::MouseEvent &event) } } +Item *ItemShortcutContainer::getItemAt(int x, int y) const +{ + const int index = getIndexFromGrid(x, y); + if (index == -1) + return nullptr; + + const int itemId = itemShortcut->getItem(index); + if (itemId < 0) + return nullptr; + + return PlayerInfo::getInventory()->findItem(itemId); +} + // Hide ItemTooltip void ItemShortcutContainer::mouseExited(gcn::MouseEvent &event) { diff --git a/src/gui/widgets/itemshortcutcontainer.h b/src/gui/widgets/itemshortcutcontainer.h index 63d9e0ef..a01857db 100644 --- a/src/gui/widgets/itemshortcutcontainer.h +++ b/src/gui/widgets/itemshortcutcontainer.h @@ -67,6 +67,8 @@ class ItemShortcutContainer : public ShortcutContainer void mouseExited(gcn::MouseEvent &event) override; void mouseMoved(gcn::MouseEvent &event) override; + Item *getItemAt(int x, int y) const; + bool mItemClicked = false; Item *mItemMoved = nullptr; diff --git a/src/gui/widgets/label.cpp b/src/gui/widgets/label.cpp index a2ed8820..9c0fd3cd 100644 --- a/src/gui/widgets/label.cpp +++ b/src/gui/widgets/label.cpp @@ -21,8 +21,6 @@ #include "gui/widgets/label.h" -#include "textrenderer.h" - #include "resources/theme.h" #include <guichan/exception.hpp> @@ -59,15 +57,15 @@ void Label::draw(gcn::Graphics *graphics) throw GCN_EXCEPTION("Unknown alignment."); } - TextRenderer::renderText(static_cast<Graphics *>(graphics), - getCaption(), - textX, - textY, - getAlignment(), - getForegroundColor(), - getFont(), - mOutlineColor.has_value(), - mShadowColor.has_value(), - mOutlineColor, - mShadowColor); + auto g = static_cast<Graphics *>(graphics); + g->drawText(getCaption(), + textX, + textY, + getAlignment(), + getForegroundColor(), + getFont(), + mOutlineColor.has_value(), + mShadowColor.has_value(), + mOutlineColor, + mShadowColor); } diff --git a/src/gui/widgets/listbox.cpp b/src/gui/widgets/listbox.cpp index 112de232..612e785f 100644 --- a/src/gui/widgets/listbox.cpp +++ b/src/gui/widgets/listbox.cpp @@ -56,10 +56,14 @@ void ListBox::draw(gcn::Graphics *graphics) } // Draw the list elements - graphics->setColor(Theme::getThemeColor(Theme::TEXT)); for (int i = 0, y = 0; i < mListModel->getNumberOfElements(); ++i, y += height) { + if (mSelected == i) + graphics->setColor(Theme::getThemeColor(Theme::HIGHLIGHT_TEXT)); + else + graphics->setColor(Theme::getThemeColor(Theme::TEXT)); + graphics->drawText(mListModel->getElementAt(i), 1, y); } } diff --git a/src/gui/widgets/popup.cpp b/src/gui/widgets/popup.cpp index b245b9e6..c4c6f652 100644 --- a/src/gui/widgets/popup.cpp +++ b/src/gui/widgets/popup.cpp @@ -40,7 +40,7 @@ Popup::Popup(const std::string &name, SkinType skinType) , mMaxHeight(graphics->getHeight()) , mSkinType(skinType) { - logger->log("Popup::Popup(\"%s\")", name.c_str()); + Log::debug("Popup::Popup(\"%s\")", name.c_str()); if (!windowContainer) throw GCN_EXCEPTION("Popup::Popup(): no windowContainer set"); @@ -58,7 +58,7 @@ Popup::Popup(const std::string &name, SkinType skinType) Popup::~Popup() { - logger->log("Popup::~Popup(\"%s\")", mPopupName.c_str()); + Log::debug("Popup::~Popup(\"%s\")", mPopupName.c_str()); } void Popup::setWindowContainer(WindowContainer *wc) diff --git a/src/gui/widgets/progressbar.cpp b/src/gui/widgets/progressbar.cpp index 5cf1b05a..502ab686 100644 --- a/src/gui/widgets/progressbar.cpp +++ b/src/gui/widgets/progressbar.cpp @@ -83,11 +83,16 @@ void ProgressBar::draw(gcn::Graphics *graphics) rect.x = 0; rect.y = 0; + Theme::ProgressPalette palette = Theme::THEME_PROG_END; + if (mProgressPalette >= 0) + palette = static_cast<Theme::ProgressPalette>(mProgressPalette); + gui->getTheme()->drawProgressBar(static_cast<Graphics *>(graphics), rect, mColor, mProgress, - mText); + mText, + palette); } void ProgressBar::setProgress(float progress) diff --git a/src/gui/widgets/radiobutton.cpp b/src/gui/widgets/radiobutton.cpp index ceba78eb..3474bbd8 100644 --- a/src/gui/widgets/radiobutton.cpp +++ b/src/gui/widgets/radiobutton.cpp @@ -21,11 +21,11 @@ #include "gui/widgets/radiobutton.h" -#include "textrenderer.h" - #include "gui/gui.h" #include "resources/theme.h" +#include <guichan/font.hpp> + RadioButton::RadioButton(const std::string &caption, const std::string &group, bool marked) @@ -44,19 +44,19 @@ void RadioButton::draw(gcn::Graphics* graphics) if (isSelected()) widgetState.flags |= STATE_SELECTED; + auto g = static_cast<Graphics *>(graphics); auto &skin = gui->getTheme()->getSkin(SkinType::RadioButton); - skin.draw(static_cast<Graphics *>(graphics), widgetState); + skin.draw(g, widgetState); if (auto skinState = skin.getState(widgetState.flags)) { auto &textFormat = skinState->textFormat; - TextRenderer::renderText(static_cast<Graphics *>(graphics), - getCaption(), - skin.getMinWidth() + skin.padding + skin.spacing, - skin.padding, - Graphics::LEFT, - textFormat.bold ? boldFont : getFont(), - textFormat); + g->drawText(getCaption(), + skin.getMinWidth() + skin.padding + skin.spacing, + skin.padding, + Graphics::LEFT, + textFormat.bold ? boldFont : getFont(), + textFormat); } } diff --git a/src/gui/widgets/shoplistbox.cpp b/src/gui/widgets/shoplistbox.cpp index e2313c85..d745d74d 100644 --- a/src/gui/widgets/shoplistbox.cpp +++ b/src/gui/widgets/shoplistbox.cpp @@ -75,6 +75,7 @@ void ShopListBox::draw(gcn::Graphics *gcnGraphics) auto backgroundColor = Theme::getThemeColor(Theme::BACKGROUND); auto warningColor = Theme::getThemeColor(Theme::SHOP_WARNING); auto textColor = Theme::getThemeColor(Theme::TEXT); + auto highlightTextColor = Theme::getThemeColor(Theme::HIGHLIGHT_TEXT); highlightColor.a = alpha; backgroundColor.a = alpha; warningColor.a = alpha; @@ -136,7 +137,7 @@ void ShopListBox::draw(gcn::Graphics *gcnGraphics) } } - graphics->setColor(textColor); + graphics->setColor(i == mSelected ? highlightTextColor : textColor); graphics->drawText(mListModel->getElementAt(i), ITEM_ICON_SIZE + 5, y + (ITEM_ICON_SIZE - fontHeight) / 2); diff --git a/src/gui/widgets/tab.cpp b/src/gui/widgets/tab.cpp index b2779c4f..0f6ca4e5 100644 --- a/src/gui/widgets/tab.cpp +++ b/src/gui/widgets/tab.cpp @@ -73,18 +73,27 @@ void Tab::draw(gcn::Graphics *graphics) if (mTabbedArea && mTabbedArea->isTabSelected(this)) flags |= STATE_SELECTED; - auto &skin = gui->getTheme()->getSkin(SkinType::Tab); + auto theme = gui->getTheme(); + auto &palette = theme->getPalette(0); + auto &skin = theme->getSkin(SkinType::Tab); + if (auto state = skin.getState(flags)) { gcn::Color foregroundColor = state->textFormat.color; + auto outlineColor = state->textFormat.outlineColor; if (mFlash) - foregroundColor = Theme::getThemeColor(Theme::TAB_FLASH); + { + foregroundColor = palette.getColor(Theme::TAB_FLASH); + outlineColor = palette.getOutlineColor(Theme::TAB_FLASH); + } else if (mTabColor) + { foregroundColor = *mTabColor; + } auto label = static_cast<Label*>(mLabel); label->setForegroundColor(foregroundColor); - label->setOutlineColor(state->textFormat.outlineColor); + label->setOutlineColor(outlineColor); label->setShadowColor(state->textFormat.shadowColor); } diff --git a/src/gui/widgets/textbox.cpp b/src/gui/widgets/textbox.cpp index 6cc514fe..7fd7d626 100644 --- a/src/gui/widgets/textbox.cpp +++ b/src/gui/widgets/textbox.cpp @@ -23,7 +23,6 @@ #include "gui/gui.h" #include "resources/theme.h" -#include "textrenderer.h" #include <guichan/font.hpp> @@ -96,7 +95,7 @@ void TextBox::setTextWrapped(const std::string &text, int minDimension) xpos = width; wrappedStream << word; } - else if (xpos != 0 && xpos + getFont()->getWidth(" ") + width <= + else if (xpos != 0 && xpos + getFont()->getWidth(" ") + width <= mMinWidth) { xpos += getFont()->getWidth(" ") + width; @@ -176,18 +175,19 @@ void TextBox::draw(gcn::Graphics *graphics) graphics->setColor(*mTextColor); graphics->setFont(getFont()); + auto g = static_cast<Graphics*>(graphics); + for (i = 0; i < mTextRows.size(); i++) { // Move the text one pixel so we can have a caret before a letter. - TextRenderer::renderText(graphics, - mTextRows[i], - 1, - i * getFont()->getHeight(), - gcn::Graphics::LEFT, - *mTextColor, - getFont(), - mOutlineColor.has_value(), - false, - mOutlineColor); + g->drawText(mTextRows[i], + 1, + i * getFont()->getHeight(), + gcn::Graphics::LEFT, + *mTextColor, + getFont(), + mOutlineColor.has_value(), + false, + mOutlineColor); } } diff --git a/src/gui/widgets/textpreview.cpp b/src/gui/widgets/textpreview.cpp index 2f80bd23..aed04853 100644 --- a/src/gui/widgets/textpreview.cpp +++ b/src/gui/widgets/textpreview.cpp @@ -21,13 +21,7 @@ #include "gui/widgets/textpreview.h" -#include "configuration.h" -#include "textrenderer.h" - #include "gui/gui.h" -#include "gui/truetypefont.h" - -#include <typeinfo> TextPreview::TextPreview(const std::string &text) : mText(text) @@ -36,12 +30,13 @@ TextPreview::TextPreview(const std::string &text) mTextColor = &Theme::getThemeColor(Theme::TEXT); } -void TextPreview::draw(gcn::Graphics* graphics) +void TextPreview::draw(gcn::Graphics *graphics) { - TextRenderer::renderText(graphics, mText, 2, 2, gcn::Graphics::LEFT, - gcn::Color(mTextColor->r, - mTextColor->g, - mTextColor->b, - 255), - mFont, mOutline, mShadow); + auto g = static_cast<Graphics*>(graphics); + g->drawText(mText, 2, 2, gcn::Graphics::LEFT, + gcn::Color(mTextColor->r, + mTextColor->g, + mTextColor->b, + 255), + mFont, mOutline, mShadow); } diff --git a/src/gui/widgets/window.cpp b/src/gui/widgets/window.cpp index e57918b7..14d91af1 100644 --- a/src/gui/widgets/window.cpp +++ b/src/gui/widgets/window.cpp @@ -23,7 +23,6 @@ #include "configuration.h" #include "log.h" -#include "textrenderer.h" #include "gui/gui.h" #include "gui/viewport.h" @@ -53,7 +52,7 @@ Window::Window(SkinType skinType, const std::string &caption, bool modal, Window , mMaxWinWidth(graphics->getWidth()) , mMaxWinHeight(graphics->getHeight()) { - logger->log("Window::Window(\"%s\")", caption.c_str()); + Log::debug("Window::Window(\"%s\")", caption.c_str()); if (!windowContainer) throw GCN_EXCEPTION("Window::Window(): no windowContainer set"); @@ -82,7 +81,7 @@ Window::Window(SkinType skinType, const std::string &caption, bool modal, Window Window::~Window() { - logger->log("Window::~Window(\"%s\")", getCaption().c_str()); + Log::debug("Window::~Window(\"%s\")", getCaption().c_str()); saveWindowState(); @@ -139,13 +138,12 @@ void Window::drawFrame(gcn::Graphics *graphics) if (auto skinState = skin.getState(widgetState.flags)) { auto &textFormat = skinState->textFormat; - TextRenderer::renderText(g, - getCaption(), - getFrameSize() + skin.titleOffsetX, - getFrameSize() + skin.titleOffsetY, - gcn::Graphics::LEFT, - textFormat.bold ? boldFont : getFont(), - textFormat); + g->drawText(getCaption(), + getFrameSize() + skin.titleOffsetX, + getFrameSize() + skin.titleOffsetY, + gcn::Graphics::LEFT, + textFormat.bold ? boldFont : getFont(), + textFormat); } } } @@ -191,52 +189,6 @@ void Window::setLocationRelativeTo(gcn::Widget *widget) getY() + (wy + (widget->getHeight() - getHeight()) / 2 - y)); } -void Window::setLocationRelativeTo(ImageRect::ImagePosition position, - int offsetX, int offsetY) -{ - if (position == ImageRect::UPPER_LEFT) - { - } - else if (position == ImageRect::UPPER_CENTER) - { - offsetX += (graphics->getWidth() - getWidth()) / 2; - } - else if (position == ImageRect::UPPER_RIGHT) - { - offsetX += graphics->getWidth() - getWidth(); - } - else if (position == ImageRect::LEFT) - { - offsetY += (graphics->getHeight() - getHeight()) / 2; - } - else if (position == ImageRect::CENTER) - { - offsetX += (graphics->getWidth() - getWidth()) / 2; - offsetY += (graphics->getHeight() - getHeight()) / 2; - } - else if (position == ImageRect::RIGHT) - { - offsetX += graphics->getWidth() - getWidth(); - offsetY += (graphics->getHeight() - getHeight()) / 2; - } - else if (position == ImageRect::LOWER_LEFT) - { - offsetY += graphics->getHeight() - getHeight(); - } - else if (position == ImageRect::LOWER_CENTER) - { - offsetX += (graphics->getWidth() - getWidth()) / 2; - offsetY += graphics->getHeight() - getHeight(); - } - else if (position == ImageRect::LOWER_RIGHT) - { - offsetX += graphics->getWidth() - getWidth(); - offsetY += graphics->getHeight() - getHeight(); - } - - setPosition(offsetX, offsetY); -} - void Window::setMinWidth(int width) { mMinWinWidth = std::max(getSkin().getMinWidth(), width); @@ -599,41 +551,41 @@ void Window::setDefaultSize() } void Window::setDefaultSize(int defaultWidth, int defaultHeight, - ImageRect::ImagePosition position, + WindowAlignment alignment, int offsetX, int offsetY) { int x = 0; int y = 0; - switch (position) + switch (alignment) { - case ImageRect::UPPER_LEFT: + case WindowAlignment::TopLeft: break; - case ImageRect::UPPER_CENTER: + case WindowAlignment::Top: x = (graphics->getWidth() - defaultWidth) / 2; break; - case ImageRect::UPPER_RIGHT: + case WindowAlignment::TopRight: x = graphics->getWidth() - defaultWidth; break; - case ImageRect::LEFT: + case WindowAlignment::Left: y = (graphics->getHeight() - defaultHeight) / 2; break; - case ImageRect::CENTER: + case WindowAlignment::Center: x = (graphics->getWidth() - defaultWidth) / 2; y = (graphics->getHeight() - defaultHeight) / 2; break; - case ImageRect::RIGHT: + case WindowAlignment::Right: x = graphics->getWidth() - defaultWidth; y = (graphics->getHeight() - defaultHeight) / 2; break; - case ImageRect::LOWER_LEFT: + case WindowAlignment::BottomLeft: y = graphics->getHeight() - defaultHeight; break; - case ImageRect::LOWER_CENTER: + case WindowAlignment::Bottom: x = (graphics->getWidth() - defaultWidth) / 2; y = graphics->getHeight() - defaultHeight; break; - case ImageRect::LOWER_RIGHT: + case WindowAlignment::BottomRight: x = graphics->getWidth() - defaultWidth; y = graphics->getHeight() - defaultHeight; break; diff --git a/src/gui/widgets/window.h b/src/gui/widgets/window.h index 6331a715..2a47b0b9 100644 --- a/src/gui/widgets/window.h +++ b/src/gui/widgets/window.h @@ -36,6 +36,19 @@ class ResizeGrip; class Skin; class WindowContainer; +enum class WindowAlignment +{ + TopLeft, + Top, + TopRight, + Left, + Center, + Right, + BottomLeft, + Bottom, + BottomRight +}; + /** * A window. This window can be dragged around and has a title bar. Windows are * invisible by default. @@ -103,12 +116,6 @@ class Window : public gcn::Window, gcn::WidgetListener void setLocationRelativeTo(gcn::Widget *widget); /** - * Sets the location relative to the given enumerated position. - */ - void setLocationRelativeTo(ImageRect::ImagePosition position, - int offsetX = 0, int offsetY = 0); - - /** * Sets whether or not the window can be resized. */ void setResizable(bool resize); @@ -303,7 +310,7 @@ class Window : public gcn::Window, gcn::WidgetListener * on a relative enumerated position, rather than a coordinate position. */ void setDefaultSize(int defaultWidth, int defaultHeight, - ImageRect::ImagePosition position, + WindowAlignment alignment, int offsetx = 0, int offsetY = 0); /** diff --git a/src/gui/windowmenu.cpp b/src/gui/windowmenu.cpp index 0b2d126f..2c1b6211 100644 --- a/src/gui/windowmenu.cpp +++ b/src/gui/windowmenu.cpp @@ -23,9 +23,10 @@ #include "graphics.h" +#include "gui/abilitieswindow.h" #include "gui/emotepopup.h" +#include "gui/questswindow.h" #include "gui/skilldialog.h" -#include "gui/abilitieswindow.h" #include "gui/widgets/button.h" #include "gui/widgets/window.h" @@ -34,6 +35,8 @@ #include "net/net.h" #include "net/playerhandler.h" +#include "resources/questdb.h" + #include "utils/gettext.h" #include <string> @@ -64,6 +67,9 @@ WindowMenu::WindowMenu() if (abilitiesWindow->hasAbilities()) addButton(N_("Abilities"), x, h, "button-icon-abilities.png"); + if (QuestDB::hasQuests()) + addButton(N_("Quests"), x, h, "button-icon-quests.png"); + addButton(N_("Social"), x, h, "button-icon-social.png", KeyboardConfig::KEY_WINDOW_SOCIAL); addButton(N_("Shortcuts"), x, h, "button-icon-shortcut.png", @@ -122,6 +128,10 @@ void WindowMenu::action(const gcn::ActionEvent &event) { window = skillDialog; } + else if (event.getId() == "Quests") + { + window = questsWindow; + } else if (event.getId() == "Abilities") { window = abilitiesWindow; @@ -166,12 +176,18 @@ static std::string createShortcutCaption(const std::string &text, KeyboardConfig::KeyAction key) { std::string caption = gettext(text.c_str()); + if (key != KeyboardConfig::KEY_NO_VALUE) { - caption += " ("; - caption += SDL_GetKeyName(keyboard.getKeyValue(key)); - caption += ")"; + auto keyValue = keyboard.getKeyValue(key); + if (keyValue > 0) + { + caption += " ("; + caption += SDL_GetKeyName(keyValue); + caption += ")"; + } } + return caption; } @@ -179,7 +195,7 @@ void WindowMenu::addButton(const std::string &text, int &x, int &h, const std::string &iconPath, KeyboardConfig::KeyAction key) { - auto *btn = new Button("", text, this); + auto *btn = new Button(std::string(), text, this); if (!iconPath.empty() && btn->setButtonIcon(iconPath)) { btn->setButtonPopupText(createShortcutCaption(text, key)); @@ -187,7 +203,7 @@ void WindowMenu::addButton(const std::string &text, int &x, int &h, else { btn->setCaption(gettext(text.c_str())); - btn->setButtonPopupText(createShortcutCaption("", key)); + btn->setButtonPopupText(createShortcutCaption(std::string(), key)); } btn->setPosition(x, 0); @@ -204,40 +220,45 @@ void WindowMenu::updatePopUpCaptions() if (!button) continue; - std::string eventId = button->getActionEventId(); + const std::string &eventId = button->getActionEventId(); if (eventId == "Status") { - button->setButtonPopupText(createShortcutCaption("Status", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_STATUS)); } else if (eventId == "Equipment") { - button->setButtonPopupText(createShortcutCaption("Equipment", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_EQUIPMENT)); } else if (eventId == "Inventory") { - button->setButtonPopupText(createShortcutCaption("Inventory", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_INVENTORY)); } else if (eventId == "Skills") { - button->setButtonPopupText(createShortcutCaption("Skills", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SKILL)); } + else if (eventId == "Quests") + { + button->setButtonPopupText( + createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_QUESTS)); + } else if (eventId == "Social") { - button->setButtonPopupText(createShortcutCaption("Social", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SOCIAL)); } else if (eventId == "Shortcuts") { - button->setButtonPopupText(createShortcutCaption("Shortcuts", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SHORTCUT)); } else if (eventId == "Setup") { - button->setButtonPopupText(createShortcutCaption("Setup", + button->setButtonPopupText(createShortcutCaption(eventId, KeyboardConfig::KEY_WINDOW_SETUP)); } } diff --git a/src/inventory.cpp b/src/inventory.cpp index 51eb24e1..e45e094c 100644 --- a/src/inventory.cpp +++ b/src/inventory.cpp @@ -67,7 +67,7 @@ void Inventory::setItem(int index, int id, int quantity) { if (index < 0 || index >= getSize()) { - logger->log("Warning: invalid inventory index: %d", index); + Log::warn("Invalid inventory index: %d", index); return; } diff --git a/src/itemshortcut.cpp b/src/itemshortcut.cpp index 643a594f..4d2cec8a 100644 --- a/src/itemshortcut.cpp +++ b/src/itemshortcut.cpp @@ -29,8 +29,7 @@ ItemShortcut *itemShortcut; -ItemShortcut::ItemShortcut(): - mItemSelected(-1) +ItemShortcut::ItemShortcut() { load(); } @@ -42,8 +41,8 @@ ItemShortcut::~ItemShortcut() void ItemShortcut::load() { - for (int i = 0; i < SHORTCUT_ITEMS; i++) - mItems[i] = -1; + for (int &item : mItems) + item = -1; for (auto &shortcut : config.itemShortcuts) { diff --git a/src/itemshortcut.h b/src/itemshortcut.h index fcec3d7d..0849a8d1 100644 --- a/src/itemshortcut.h +++ b/src/itemshortcut.h @@ -109,7 +109,7 @@ class ItemShortcut void save(); int mItems[SHORTCUT_ITEMS]; /**< The items stored. */ - int mItemSelected; /**< The item held by cursor. */ + int mItemSelected = -1; /**< The item held by cursor. */ }; diff --git a/src/joystick.cpp b/src/joystick.cpp index c6e106bf..070e9dcf 100644 --- a/src/joystick.cpp +++ b/src/joystick.cpp @@ -36,9 +36,9 @@ void Joystick::init() SDL_JoystickEventState(SDL_ENABLE); joystickCount = SDL_NumJoysticks(); - logger->log("%i joysticks/gamepads found", joystickCount); + Log::info("%i joysticks/gamepads found", joystickCount); for (int i = 0; i < joystickCount; i++) - logger->log("- %s", SDL_JoystickNameForIndex(i)); + Log::info("- %s", SDL_JoystickNameForIndex(i)); } Joystick::Joystick(int no) @@ -57,14 +57,14 @@ Joystick::Joystick(int no) // TODO Bail out! if (!mJoystick) { - logger->log("Couldn't open joystick: %s", SDL_GetError()); + Log::info("Couldn't open joystick: %s", SDL_GetError()); return; } - logger->log("Axes: %i ", SDL_JoystickNumAxes(mJoystick)); - logger->log("Balls: %i", SDL_JoystickNumBalls(mJoystick)); - logger->log("Hats: %i", SDL_JoystickNumHats(mJoystick)); - logger->log("Buttons: %i", SDL_JoystickNumButtons(mJoystick)); + Log::info("Axes: %i ", SDL_JoystickNumAxes(mJoystick)); + Log::info("Balls: %i", SDL_JoystickNumBalls(mJoystick)); + Log::info("Hats: %i", SDL_JoystickNumHats(mJoystick)); + Log::info("Buttons: %i", SDL_JoystickNumButtons(mJoystick)); } Joystick::~Joystick() diff --git a/src/keyboardconfig.cpp b/src/keyboardconfig.cpp index ec5b5e37..92d37ace 100644 --- a/src/keyboardconfig.cpp +++ b/src/keyboardconfig.cpp @@ -71,6 +71,7 @@ static KeyData const keyData[KeyboardConfig::KEY_TOTAL] = { { "WindowInventory", SDLK_F3, _("Inventory Window") }, { "WindowEquipment", SDLK_F4, _("Equipment Window") }, { "WindowSkill", SDLK_F5, _("Skill Window") }, + { "WindowQuests", KeyboardConfig::KEY_NO_VALUE, _("Quest Window") }, { "WindowMinimap", SDLK_F6, _("Minimap Window") }, { "WindowChat", SDLK_F7, _("Chat Window") }, { "WindowShortcut", SDLK_F8, _("Item Shortcut Window") }, diff --git a/src/keyboardconfig.h b/src/keyboardconfig.h index 6fc79ced..be643b9b 100644 --- a/src/keyboardconfig.h +++ b/src/keyboardconfig.h @@ -32,9 +32,9 @@ */ struct KeyFunction { - const char* configField; /** Field index that is in the config file. */ - int defaultValue; /** The default key value used. */ - SDL_Keycode value; /** The actual value that is used. */ + const char* configField; /**< Field index that is in the config file. */ + int defaultValue; /**< The default key value used. */ + SDL_Keycode value; /**< The actual value that is used. */ }; class Setup_Keyboard; @@ -191,6 +191,7 @@ class KeyboardConfig KEY_WINDOW_INVENTORY, KEY_WINDOW_EQUIPMENT, KEY_WINDOW_SKILL, + KEY_WINDOW_QUESTS, KEY_WINDOW_MINIMAP, KEY_WINDOW_CHAT, KEY_WINDOW_SHORTCUT, diff --git a/src/log.cpp b/src/log.cpp index 24cb6e9d..eeb26b00 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -1,7 +1,7 @@ /* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team - * Copyright (C) 2009-2012 The Mana Developers + * Copyright (C) 2009-2025 The Mana Developers * * This file is part of The Mana Client. * @@ -23,47 +23,36 @@ #include <SDL.h> -#include <sys/time.h> +#include <cstdarg> +#include <fstream> #include <iostream> #include <sstream> -#include <cstdarg> -#include <cstdio> +#include <sys/time.h> -Logger::Logger() = default; -Logger::~Logger() = default; +static std::ofstream logFile; +static bool logToStandardOut = true; -void Logger::setLogFile(const std::string &logFilename) +static const char *getLogPriorityPrefix(SDL_LogPriority priority) { - mLogFile.open(logFilename, std::ios_base::trunc); - - if (!mLogFile.is_open()) - { - std::cout << "Warning: error while opening " << logFilename << - " for writing.\n"; + switch (priority) { + case SDL_LOG_PRIORITY_WARN: + return "Warning: "; + case SDL_LOG_PRIORITY_ERROR: + return "Error: "; + case SDL_LOG_PRIORITY_CRITICAL: + return "Critical Error: "; + default: + return ""; } } -void Logger::log(const char *log_text, ...) -{ - va_list ap; - va_start(ap, log_text); - vlog(log_text, ap); - va_end(ap); -} - -void Logger::vlog(const char *log_text, va_list ap) +static void logOutputFunction(void *userdata, int category, SDL_LogPriority priority, const char *message) { - const size_t bufSize = 1024; - char* buf = new char[bufSize]; - - // Use a temporary buffer to fill in the variables - vsnprintf(buf, bufSize, log_text, ap); - // Get the current system time timeval tv; gettimeofday(&tv, nullptr); - // Print the log entry + // Create timestamp string std::stringstream timeStr; timeStr << "[" << ((((tv.tv_sec / 60) / 60) % 24 < 10) ? "0" : "") @@ -79,24 +68,71 @@ void Logger::vlog(const char *log_text, va_list ap) << (int)((tv.tv_usec / 10000) % 100) << "] "; - if (mLogFile.is_open()) + const char *prefix = getLogPriorityPrefix(priority); + + if (logToStandardOut) + { + std::cout << timeStr.str() << prefix << message << std::endl; + } + + if (logFile.is_open()) { - mLogFile << timeStr.str() << buf << std::endl; + logFile << timeStr.str() << prefix << message << std::endl; } +} + +void Log::init() +{ + SDL_LogSetOutputFunction(logOutputFunction, nullptr); +} - if (mLogToStandardOut) +void Log::setLogFile(const std::string &logFilename) +{ + logFile.open(logFilename, std::ios_base::trunc); + + if (!logFile.is_open()) { - std::cout << timeStr.str() << buf << std::endl; + std::cout << "Warning: error while opening " << logFilename + << " for writing.\n"; } +} - // Delete temporary buffer - delete[] buf; +void Log::setLogToStandardOut(bool value) +{ + logToStandardOut = value; } -void Logger::error(const std::string &error_text) +#define DEFINE_LOG_FUNCTION(name, priority) \ + void Log::name(const char *fmt, ...) \ + { \ + va_list ap; \ + va_start(ap, fmt); \ + SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, priority, fmt, ap); \ + va_end(ap); \ + } + +DEFINE_LOG_FUNCTION(verbose, SDL_LOG_PRIORITY_VERBOSE) +DEFINE_LOG_FUNCTION(debug, SDL_LOG_PRIORITY_DEBUG) +DEFINE_LOG_FUNCTION(info, SDL_LOG_PRIORITY_INFO) +DEFINE_LOG_FUNCTION(warn, SDL_LOG_PRIORITY_WARN) +DEFINE_LOG_FUNCTION(error, SDL_LOG_PRIORITY_ERROR) + +#undef DEFINE_LOG_FUNCTION + +void Log::vinfo(const char *fmt, va_list ap) +{ + SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, fmt, ap); +} + +void Log::critical(const std::string &message) { - log("Error: %s", error_text.c_str()); - std::cerr << "Error: " << error_text << std::endl; - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", error_text.c_str(), nullptr); + SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "%s", message.c_str()); + + if (!logToStandardOut) + { + std::cerr << getLogPriorityPrefix(SDL_LOG_PRIORITY_CRITICAL) << message << std::endl; + } + + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message.c_str(), nullptr); exit(1); } @@ -1,7 +1,7 @@ /* * The Mana Client * Copyright (C) 2004-2009 The Mana World Development Team - * Copyright (C) 2009-2012 The Mana Developers + * Copyright (C) 2009-2025 The Mana Developers * * This file is part of The Mana Client. * @@ -21,52 +21,51 @@ #pragma once -#include <fstream> +#include <string> +#include <cstdarg> + +#ifdef __GNUC__ +# define LOG_PRINTF_ATTR __attribute__((__format__(__printf__, 1, 2))) +#else +# define LOG_PRINTF_ATTR +#endif /** - * The Log Class : Useful to write debug or info messages + * The Log namespace: Useful to write debug or info messages to the log file + * and/or console. The messages will be timestamped. */ -class Logger +namespace Log { - public: - Logger(); + /** + * Initializes the log system. + */ + void init(); - /** - * Destructor, closes log file. - */ - ~Logger(); + /** + * Sets the file to log to and opens it. + */ + void setLogFile(const std::string &logFilename); - /** - * Sets the file to log to and opens it - */ - void setLogFile(const std::string &logFilename); + /** + * Sets whether the log should be written to standard output. + */ + void setLogToStandardOut(bool value); - /** - * Sets whether the log should be written to standard output. - */ - void setLogToStandardOut(bool value) { mLogToStandardOut = value; } - - /** - * Enters a message in the log. The message will be timestamped. - */ - void log(const char *log_text, ...) -#ifdef __GNUC__ - __attribute__((__format__(__printf__, 2, 3))) -#endif - ; + void verbose(const char *log_text, ...) LOG_PRINTF_ATTR; + void debug(const char *log_text, ...) LOG_PRINTF_ATTR; + void info(const char *log_text, ...) LOG_PRINTF_ATTR; + void warn(const char *log_text, ...) LOG_PRINTF_ATTR; + void error(const char *log_text, ...) LOG_PRINTF_ATTR; - void vlog(const char *log_text, va_list ap); + void vinfo(const char *log_text, va_list ap); - /** - * Log an error and quit. The error will be printed to standard error - * and showm in a simple message box. - */ - __attribute__((noreturn)) - void error(const std::string &error_text); + /** + * Log an error and quit. The error will be printed to standard error + * and shown in a simple message box. + */ + __attribute__((noreturn)) + void critical(const std::string &error_text); - private: - std::ofstream mLogFile; - bool mLogToStandardOut = true; -}; +} // namespace Log -extern Logger *logger; +#undef LOG_PRINTF_ATTR diff --git a/src/map.cpp b/src/map.cpp index 908d6178..8dbc7c28 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -983,7 +983,7 @@ void Map::addAnimation(int gid, TileAnimation animation) auto const [_, inserted] = mTileAnimations.try_emplace(gid, std::move(animation)); if (!inserted) { - logger->error(strprintf("Duplicate tile animation for gid %d", gid)); + Log::warn("Duplicate tile animation for gid %d", gid); } } diff --git a/src/net/download.cpp b/src/net/download.cpp index 7aab3b2f..571af7a4 100644 --- a/src/net/download.cpp +++ b/src/net/download.cpp @@ -108,13 +108,13 @@ bool Download::start() { assert(!mThread); // Download already started - logger->log("Starting download: %s", mUrl.c_str()); + Log::info("Starting download: %s", mUrl.c_str()); mThread = SDL_CreateThread(downloadThread, "Download", this); if (!mThread) { - logger->log("%s", DOWNLOAD_ERROR_MESSAGE_THREAD); + Log::info("%s", DOWNLOAD_ERROR_MESSAGE_THREAD); strncpy(mError, DOWNLOAD_ERROR_MESSAGE_THREAD, CURL_ERROR_SIZE - 1); mState.lock()->status = DownloadStatus::Error; return false; @@ -125,7 +125,7 @@ bool Download::start() void Download::cancel() { - logger->log("Canceling download: %s", mUrl.c_str()); + Log::info("Canceling download: %s", mUrl.c_str()); mCancel = true; } @@ -186,7 +186,7 @@ int Download::downloadThread(void *ptr) if (!curl) break; - logger->log("Downloading: %s", d->mUrl.c_str()); + Log::info("Downloading: %s", d->mUrl.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, d->mHeaders); @@ -236,8 +236,8 @@ int Download::downloadThread(void *ptr) if (res != CURLE_OK) { - logger->log("curl error %d: %s host: %s", - res, d->mError, d->mUrl.c_str()); + Log::info("curl error %d: %s host: %s", + res, d->mError, d->mUrl.c_str()); if (file) { @@ -262,9 +262,9 @@ int Download::downloadThread(void *ptr) // Remove the corrupted file ::remove(outFilename.c_str()); - logger->log("Checksum for file %s failed: (%lx/%lx)", - d->mFileName.c_str(), - adler, *d->mAdler); + Log::info("Checksum for file %s failed: (%lx/%lx)", + d->mFileName.c_str(), + adler, *d->mAdler); continue; // Bail out here to avoid the renaming } diff --git a/src/net/manaserv/charhandler.cpp b/src/net/manaserv/charhandler.cpp index 98591669..cc0ea17e 100644 --- a/src/net/manaserv/charhandler.cpp +++ b/src/net/manaserv/charhandler.cpp @@ -264,10 +264,10 @@ void CharHandler::handleCharacterSelectResponse(MessageIn &msg) chatServer.hostname.assign(msg.readString()); chatServer.port = msg.readInt16(); - logger->log("Game server: %s:%d", gameServer.hostname.c_str(), - gameServer.port); - logger->log("Chat server: %s:%d", chatServer.hostname.c_str(), - chatServer.port); + Log::info("Game server: %s:%d", gameServer.hostname.c_str(), + gameServer.port); + Log::info("Chat server: %s:%d", chatServer.hostname.c_str(), + chatServer.port); // Prevent the selected local player from being deleted local_player = mSelectedCharacter->dummy; diff --git a/src/net/manaserv/chathandler.cpp b/src/net/manaserv/chathandler.cpp index dca556c2..436da820 100644 --- a/src/net/manaserv/chathandler.cpp +++ b/src/net/manaserv/chathandler.cpp @@ -158,8 +158,8 @@ void ChatHandler::handleGameChatMessage(MessageIn &msg) if (!being) { - logger->log("Warning: Received GPMSG_SAY for unknown being with id %i." - " (Message is: %s)", id, chatMsg.c_str()); + Log::warn("Received GPMSG_SAY for unknown being with id %i." + " (Message is: %s)", id, chatMsg.c_str()); return; } @@ -262,7 +262,7 @@ void ChatHandler::handleChatMessage(MessageIn &msg) else { // Can't find channel - logger->log("Couldn't find chat channel id: %hi", channelId); + Log::info("Couldn't find chat channel id: %hi", channelId); } } diff --git a/src/net/manaserv/connection.cpp b/src/net/manaserv/connection.cpp index 1b6f757a..2eb5b4bf 100644 --- a/src/net/manaserv/connection.cpp +++ b/src/net/manaserv/connection.cpp @@ -44,13 +44,13 @@ Connection::~Connection() bool Connection::connect(const std::string &address, enet_uint16 port) { - logger->log("Net::Connection::connect(%s, %i)", address.c_str(), port); + Log::info("Net::Connection::connect(%s, %i)", address.c_str(), port); if (mConnection) disconnect(); if (address.empty()) { - logger->log("Net::Connection::connect() got empty address!"); + Log::info("Net::Connection::connect() got empty address!"); mState = NET_ERROR; return false; } @@ -65,7 +65,7 @@ bool Connection::connect(const std::string &address, enet_uint16 port) if (!mConnection) { - logger->log("Unable to initiate connection to the server."); + Log::info("Unable to initiate connection to the server."); mState = NET_ERROR; return false; } @@ -96,7 +96,7 @@ void Connection::send(const ManaServ::MessageOut &msg) { if (!isConnected()) { - logger->log("Warning: cannot send message to not connected server!"); + Log::warn("Cannot send message to not connected server!"); return; } diff --git a/src/net/manaserv/effecthandler.cpp b/src/net/manaserv/effecthandler.cpp index 22d1f9cf..afd7cd5c 100644 --- a/src/net/manaserv/effecthandler.cpp +++ b/src/net/manaserv/effecthandler.cpp @@ -82,7 +82,7 @@ void EffectHandler::handleCreateEffectBeing(MessageIn &msg) if (b) effectManager->trigger(eid, b); else - logger->log("Warning: CreateEffect called for unknown being #%d", bid); + Log::warn("CreateEffect called for unknown being #%d", bid); } void EffectHandler::handleCreateTextParticle(MessageIn &msg) @@ -120,7 +120,7 @@ void EffectHandler::handleShake(MessageIn &msg) viewport->shakeScreen(intensityX, intensityY, decay, duration); break; default: - logger->log("Warning: Received GPMSG_SHAKE message with unexpected length of %d bytes", msg.getUnreadLength()); + Log::warn("Received GPMSG_SHAKE message with unexpected length of %d bytes", msg.getUnreadLength()); } } diff --git a/src/net/manaserv/guildhandler.cpp b/src/net/manaserv/guildhandler.cpp index 9fdbafc2..bae38c37 100644 --- a/src/net/manaserv/guildhandler.cpp +++ b/src/net/manaserv/guildhandler.cpp @@ -73,7 +73,7 @@ void GuildHandler::handleMessage(MessageIn &msg) { case CPMSG_GUILD_CREATE_RESPONSE: { - logger->log("Received CPMSG_GUILD_CREATE_RESPONSE"); + Log::info("Received CPMSG_GUILD_CREATE_RESPONSE"); if (msg.readInt8() == ERRMSG_OK) { // TODO - Acknowledge guild was created @@ -88,7 +88,7 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_INVITE_RESPONSE: { - logger->log("Received CPMSG_GUILD_INVITE_RESPONSE"); + Log::info("Received CPMSG_GUILD_INVITE_RESPONSE"); const unsigned char response = msg.readInt8(); if (response == ERRMSG_OK) { @@ -111,7 +111,7 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_ACCEPT_RESPONSE: { - logger->log("Received CPMSG_GUILD_ACCEPT_RESPONSE"); + Log::info("Received CPMSG_GUILD_ACCEPT_RESPONSE"); if (msg.readInt8() == ERRMSG_OK) { // TODO - Acknowledge accepted into guild @@ -121,7 +121,7 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_GET_MEMBERS_RESPONSE: { - logger->log("Received CPMSG_GUILD_GET_MEMBERS_RESPONSE"); + Log::info("Received CPMSG_GUILD_GET_MEMBERS_RESPONSE"); if (msg.readInt8() == ERRMSG_OK) { std::string name; @@ -152,7 +152,7 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_UPDATE_LIST: { - logger->log("Received CPMSG_GUILD_UPDATE_LIST"); + Log::info("Received CPMSG_GUILD_UPDATE_LIST"); short guildId = msg.readInt16(); std::string name = msg.readString(); char eventId = msg.readInt8(); @@ -189,14 +189,14 @@ void GuildHandler::handleMessage(MessageIn &msg) break; default: - logger->log("Invalid guild event"); + Log::info("Invalid guild event"); } } } break; case CPMSG_GUILD_INVITED: { - logger->log("Received CPMSG_GUILD_INVITED"); + Log::info("Received CPMSG_GUILD_INVITED"); std::string inviterName = msg.readString(); std::string guildName = msg.readString(); int guildId = msg.readInt16(); @@ -207,7 +207,7 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_PROMOTE_MEMBER_RESPONSE: { - logger->log("Received CPMSG_GUILD_PROMOTE_MEMBER_RESPONSE"); + Log::info("Received CPMSG_GUILD_PROMOTE_MEMBER_RESPONSE"); if (msg.readInt8() == ERRMSG_OK) { @@ -223,14 +223,14 @@ void GuildHandler::handleMessage(MessageIn &msg) case CPMSG_GUILD_REJOIN: { - logger->log("Received CPMSG_GUILD_REJOIN"); + Log::info("Received CPMSG_GUILD_REJOIN"); joinedGuild(msg); } break; case CPMSG_GUILD_QUIT_RESPONSE: { - logger->log("Received CPMSG_GUILD_QUIT_RESPONSE"); + Log::info("Received CPMSG_GUILD_QUIT_RESPONSE"); if (msg.readInt8() == ERRMSG_OK) { @@ -247,7 +247,7 @@ void GuildHandler::handleMessage(MessageIn &msg) } break; case CPMSG_GUILD_KICK_NOTIFICATION: { - logger->log("Received CPMSG_GUILD_KICK_NOTIFICATION"); + Log::info("Received CPMSG_GUILD_KICK_NOTIFICATION"); const int guildId = msg.readInt16(); std::string player = msg.readString(); diff --git a/src/net/manaserv/inventoryhandler.cpp b/src/net/manaserv/inventoryhandler.cpp index 58a495af..fa31b32a 100644 --- a/src/net/manaserv/inventoryhandler.cpp +++ b/src/net/manaserv/inventoryhandler.cpp @@ -88,9 +88,8 @@ void EquipBackend::equip(int inventorySlot, int equipmentSlot) auto slotIt = mSlots.find(equipmentSlot); if (slotIt == mSlots.end()) { - logger->log("ManaServ::EquipBackend: Equipment slot %i" - " is not existing.", - equipmentSlot); + Log::info("ManaServ::EquipBackend: Equipment slot %i" + " is not existing.", equipmentSlot); return; } @@ -115,8 +114,8 @@ void EquipBackend::unequip(int inventorySlot) } } - logger->log("ManaServ::EquipBackend: No equipped item found at inventory " - "slot %i!", inventorySlot); + Log::info("ManaServ::EquipBackend: No equipped item found at inventory " + "slot %i!", inventorySlot); } void EquipBackend::event(Event::Channel, const Event &event) @@ -134,8 +133,8 @@ void EquipBackend::readEquipFile() if (!rootNode || rootNode.name() != "equip-slots") { - logger->log("ManaServ::EquipBackend: Error while reading " - EQUIP_FILE "!"); + Log::info("ManaServ::EquipBackend: Error while reading " + EQUIP_FILE "!"); return; } diff --git a/src/net/manaserv/loginhandler.cpp b/src/net/manaserv/loginhandler.cpp index 1c398990..9828af29 100644 --- a/src/net/manaserv/loginhandler.cpp +++ b/src/net/manaserv/loginhandler.cpp @@ -342,7 +342,7 @@ void LoginHandler::readServerInfo(MessageIn &msg) if (!updateHost.empty()) mLoginData->updateHost = updateHost; else - logger->log("Warning: server does not have an update host set!"); + Log::warn("Server does not have an update host set!"); // Read the client data folder for dynamic data loading. // This is only used by the Qt client. diff --git a/src/net/manaserv/network.cpp b/src/net/manaserv/network.cpp index d69d3397..cb9f76a0 100644 --- a/src/net/manaserv/network.cpp +++ b/src/net/manaserv/network.cpp @@ -48,14 +48,14 @@ void initialize() { if (enet_initialize()) { - logger->error("Failed to initialize ENet."); + Log::critical("Failed to initialize ENet."); } client = enet_host_create(nullptr, 3, 0, 0, 0); if (!client) { - logger->error("Failed to create the local host."); + Log::critical("Failed to create the local host."); } } @@ -66,7 +66,7 @@ void finalize() if (connections) { - logger->error("Tried to shutdown the network subsystem while there " + Log::critical("Tried to shutdown the network subsystem while there " "are network connections left!"); } @@ -78,7 +78,7 @@ Connection *getConnection() { if (!client) { - logger->error("Tried to instantiate a network object before " + Log::critical("Tried to instantiate a network object before " "initializing the network subsystem!"); } @@ -117,14 +117,14 @@ namespace if (iter != mMessageHandlers.end()) { - //logger->log("Received packet %x (%i B)", - // msg.getId(), msg.getLength()); + //Log::info("Received packet %x (%i B)", + // msg.getId(), msg.getLength()); iter->second->handleMessage(msg); } else { - logger->log("Unhandled packet %x (%i B)", - msg.getId(), msg.getLength()); + Log::info("Unhandled packet %x (%i B)", + msg.getId(), msg.getLength()); } // Clean up the packet now that we're done using it. @@ -142,7 +142,7 @@ void flush() switch (event.type) { case ENET_EVENT_TYPE_CONNECT: - logger->log("Connected to port %d.", event.peer->address.port); + Log::info("Connected to port %d.", event.peer->address.port); // Store any relevant server information here. event.peer->data = nullptr; break; @@ -152,7 +152,7 @@ void flush() break; case ENET_EVENT_TYPE_DISCONNECT: - logger->log("Disconnected."); + Log::info("Disconnected."); // Reset the server information. event.peer->data = nullptr; break; diff --git a/src/net/manaserv/partyhandler.cpp b/src/net/manaserv/partyhandler.cpp index e2a021cd..23ea7aa5 100644 --- a/src/net/manaserv/partyhandler.cpp +++ b/src/net/manaserv/partyhandler.cpp @@ -103,7 +103,7 @@ void PartyHandler::handleMessage(MessageIn &msg) "inviter has left the game.")); break; default: - logger->log("Unknown CPMSG_PARTY_INVITE_ANSWER_RESPONSE."); + Log::info("Unknown CPMSG_PARTY_INVITE_ANSWER_RESPONSE."); break; } } break; @@ -161,7 +161,7 @@ void PartyHandler::handleMessage(MessageIn &msg) name.c_str())); break; default: - logger->log("Unknown CPMSG_PARTY_REJECTED."); + Log::info("Unknown CPMSG_PARTY_REJECTED."); break; } } break; diff --git a/src/net/manaserv/playerhandler.cpp b/src/net/manaserv/playerhandler.cpp index 8ee9ed80..edae6be6 100644 --- a/src/net/manaserv/playerhandler.cpp +++ b/src/net/manaserv/playerhandler.cpp @@ -96,7 +96,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) netToken = msg.readString(32); std::string address = msg.readString(); int port = msg.readInt16(); - logger->log("Changing server to %s:%d", address.c_str(), port); + Log::info("Changing server to %s:%d", address.c_str(), port); gameServer.hostname = address; gameServer.port = port; @@ -147,14 +147,14 @@ void PlayerHandler::handleMessage(MessageIn &msg) } break; case ATTRIBMOD_INVALID_ATTRIBUTE: { - logger->log("Warning: Server denied increase of attribute %d (unknown attribute) ", attrNum); + Log::warn("Server denied increase of attribute %d (unknown attribute) ", attrNum); } break; case ATTRIBMOD_NO_POINTS_LEFT: { // when the server says "you got no points" it // has to be correct. The server is always right! // undo attribute change and set points to 0 - logger->log("Warning: Server denied increase of attribute %d (no points left) ", attrNum); + Log::warn("Server denied increase of attribute %d (no points left) ", attrNum); int attrValue = PlayerInfo::getStatBase(attrNum) - 1; PlayerInfo::setAttribute(CHAR_POINTS, 0); PlayerInfo::setStatBase(attrNum, attrValue); @@ -162,7 +162,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) case ATTRIBMOD_DENIED: { // undo attribute change - logger->log("Warning: Server denied increase of attribute %d (reason unknown) ", attrNum); + Log::warn("Server denied increase of attribute %d (reason unknown) ", attrNum); int points = PlayerInfo::getAttribute(CHAR_POINTS) - 1; PlayerInfo::setAttribute(CHAR_POINTS, points); @@ -184,14 +184,14 @@ void PlayerHandler::handleMessage(MessageIn &msg) } break; case ATTRIBMOD_INVALID_ATTRIBUTE: { - logger->log("Warning: Server denied reduction of attribute %d (unknown attribute) ", attrNum); + Log::warn("Server denied reduction of attribute %d (unknown attribute) ", attrNum); } break; case ATTRIBMOD_NO_POINTS_LEFT: { // when the server says "you got no points" it // has to be correct. The server is always right! // undo attribute change and set points to 0 - logger->log("Warning: Server denied reduction of attribute %d (no points left) ", attrNum); + Log::warn("Server denied reduction of attribute %d (no points left) ", attrNum); int attrValue = PlayerInfo::getStatBase(attrNum) + 1; // TODO are these right? PlayerInfo::setAttribute(CHAR_POINTS, 0); @@ -201,7 +201,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) case ATTRIBMOD_DENIED: { // undo attribute change - logger->log("Warning: Server denied reduction of attribute %d (reason unknown) ", attrNum); + Log::warn("Server denied reduction of attribute %d (reason unknown) ", attrNum); int charaPoints = PlayerInfo::getAttribute(CHAR_POINTS) - 1; PlayerInfo::setAttribute(CHAR_POINTS, charaPoints); @@ -245,7 +245,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) BY_SERVER); break; default: - logger->log("0x013b: Unhandled message %i", type); + Log::info("0x013b: Unhandled message %i", type); break; } } @@ -263,7 +263,7 @@ void PlayerHandler::handleMapChangeMessage(MessageIn &msg) Game *game = Game::instance(); const bool sameMap = (game->getCurrentMapName() == mapName); - logger->log("Changing map to %s (%d, %d)", mapName.c_str(), x, y); + Log::info("Changing map to %s (%d, %d)", mapName.c_str(), x, y); // Switch the actual map, deleting the previous one game->changeMap(mapName); @@ -285,8 +285,8 @@ void PlayerHandler::handleMapChangeMessage(MessageIn &msg) local_player->setPosition(x, y); local_player->setDestination(x, y); - logger->log("Adjust scrolling by %d,%d", (int) scrollOffsetX, - (int) scrollOffsetY); + Log::info("Adjust scrolling by %d,%d", (int) scrollOffsetX, + (int) scrollOffsetY); viewport->scrollBy(scrollOffsetX, scrollOffsetY); } @@ -295,7 +295,7 @@ void PlayerHandler::attack(int id) auto ability = AbilityDB::find("Strike"); if (!ability) { - logger->log("PlayerHandler::attack: 'Strike' ability not found."); + Log::info("PlayerHandler::attack: 'Strike' ability not found."); return; } @@ -304,7 +304,7 @@ void PlayerHandler::attack(int id) abilityHandler->useOn(ability->id, id); break; case AbilityInfo::TARGET_POINT: - logger->log("PlayerHandler::attack: Unsupported target mode 'point' for 'Strike' ability."); + Log::info("PlayerHandler::attack: Unsupported target mode 'point' for 'Strike' ability."); break; case AbilityInfo::TARGET_DIRECTION: abilityHandler->useInDirection(ability->id, local_player->getDirection()); @@ -420,8 +420,8 @@ Vector PlayerHandler::getPixelsPerSecondMoveSpeed(const Vector &speed, Map *map) if (!map) { - logger->log("Manaserv::PlayerHandler: Speed wasn't given back" - " because Map not initialized."); + Log::info("Manaserv::PlayerHandler: Speed wasn't given back" + " because Map not initialized."); return speedInPixels; } diff --git a/src/net/net.cpp b/src/net/net.cpp index 1d157b3d..443a739c 100644 --- a/src/net/net.cpp +++ b/src/net/net.cpp @@ -138,7 +138,7 @@ void connectToServer(ServerInfo &server) else if (server.port == 9601) server.type = ServerType::ManaServ; else - logger->error(_("Unknown Server Type! Exiting.")); + Log::critical(_("Unknown Server Type! Exiting.")); } if (networkType == server.type && getGeneralHandler() != nullptr) @@ -160,7 +160,7 @@ void connectToServer(ServerInfo &server) generalHandler = new TmwAthena::GeneralHandler; break; default: - logger->error(_("Server protocol unsupported")); + Log::critical(_("Server protocol unsupported")); break; } @@ -203,4 +203,3 @@ ServerType getNetworkType() } } // namespace Net - diff --git a/src/net/playerhandler.h b/src/net/playerhandler.h index b9cf1abf..e5b86b2e 100644 --- a/src/net/playerhandler.h +++ b/src/net/playerhandler.h @@ -24,6 +24,8 @@ #include "being.h" #include "flooritem.h" +#include "resources/questdb.h" + namespace Net { class PlayerHandler @@ -80,6 +82,11 @@ class PlayerHandler * Return false when tiles-center positions only are to be used. */ virtual bool usePixelPrecision() = 0; + + const QuestVars &getQuestVars() const { return mQuestVars; } + + protected: + QuestVars mQuestVars; }; } // namespace Net diff --git a/src/net/tmwa/abilityhandler.cpp b/src/net/tmwa/abilityhandler.cpp index ab891b40..fea492ef 100644 --- a/src/net/tmwa/abilityhandler.cpp +++ b/src/net/tmwa/abilityhandler.cpp @@ -129,7 +129,7 @@ void AbilityHandler::handleMessage(MessageIn &msg) auto type = msg.readInt8(); if (btype == BSKILL_EMOTE) { - logger->log("Action: %d", btype); + Log::info("Action: %d", btype); } std::string msg; diff --git a/src/net/tmwa/beinghandler.cpp b/src/net/tmwa/beinghandler.cpp index c5979e9f..690b0d87 100644 --- a/src/net/tmwa/beinghandler.cpp +++ b/src/net/tmwa/beinghandler.cpp @@ -33,9 +33,9 @@ #include "playerrelations.h" #include "net/net.h" -#include "net/playerhandler.h" #include "net/tmwa/messagein.h" #include "net/tmwa/messageout.h" +#include "net/tmwa/playerhandler.h" #include "net/tmwa/protocol.h" #include "resources/emotedb.h" @@ -107,6 +107,12 @@ static Being *createBeing(int id, short job) outMsg.writeInt32(id); } + if (type == ActorSprite::NPC) + { + auto playerHandler = static_cast<TmwAthena::PlayerHandler*>(Net::getPlayerHandler()); + playerHandler->applyQuestStatusEffects(being); + } + return being; } @@ -525,8 +531,8 @@ void BeingHandler::handleMessage(MessageIn &msg) dstBeing->setSprite(SPRITE_MISC2, id); break; default: - logger->log("SMSG_BEING_CHANGE_LOOKS2: unsupported type: " - "%d, id: %d", static_cast<int>(type), id); + Log::info("SMSG_BEING_CHANGE_LOOKS2: unsupported type: " + "%d, id: %d", static_cast<int>(type), id); break; } } diff --git a/src/net/tmwa/buysellhandler.cpp b/src/net/tmwa/buysellhandler.cpp index 1fdf1ffe..d7acd674 100644 --- a/src/net/tmwa/buysellhandler.cpp +++ b/src/net/tmwa/buysellhandler.cpp @@ -99,7 +99,7 @@ void BuySellHandler::handleMessage(MessageIn &msg) Item *item = PlayerInfo::getInventory()->getItem(index); - if (item && !(item->isEquipped())) + if (item && !item->isEquipped()) dialog->addItem(item, value); } } diff --git a/src/net/tmwa/charserverhandler.cpp b/src/net/tmwa/charserverhandler.cpp index 0ecbb135..fcd47b74 100644 --- a/src/net/tmwa/charserverhandler.cpp +++ b/src/net/tmwa/charserverhandler.cpp @@ -89,8 +89,8 @@ void CharServerHandler::handleMessage(MessageIn &msg) auto *character = new Net::Character; readPlayerData(msg, character); mCharacters.push_back(character); - logger->log("CharServer: Player: %s (%d)", - character->dummy->getName().c_str(), character->slot); + Log::info("CharServer: Player: %s (%d)", + character->dummy->getName().c_str(), character->slot); } Client::setState(STATE_CHAR_SELECT); diff --git a/src/net/tmwa/gamehandler.cpp b/src/net/tmwa/gamehandler.cpp index 0a3bb9d9..a88e377e 100644 --- a/src/net/tmwa/gamehandler.cpp +++ b/src/net/tmwa/gamehandler.cpp @@ -68,8 +68,8 @@ void GameHandler::handleMessage(MessageIn &msg) msg.readInt32(); // server tick msg.readCoordinates(x, y, direction); msg.skip(2); // unknown - logger->log("Protocol: Player start position: (%d, %d), Direction: %d", - x, y, direction); + Log::info("Protocol: Player start position: (%d, %d), Direction: %d", + x, y, direction); // Switch now or we'll have problems Client::setState(STATE_GAME); // Stores the position until the map is loaded. diff --git a/src/net/tmwa/generalhandler.cpp b/src/net/tmwa/generalhandler.cpp index d6eb3b34..1f52c3db 100644 --- a/src/net/tmwa/generalhandler.cpp +++ b/src/net/tmwa/generalhandler.cpp @@ -117,7 +117,7 @@ void GeneralHandler::handleMessage(MessageIn &msg) { case SMSG_CONNECTION_PROBLEM: code = msg.readInt8(); - logger->log("Connection problem: %i", code); + Log::info("Connection problem: %i", code); switch (code) { diff --git a/src/net/tmwa/inventoryhandler.cpp b/src/net/tmwa/inventoryhandler.cpp index 0d8e3005..73d967a6 100644 --- a/src/net/tmwa/inventoryhandler.cpp +++ b/src/net/tmwa/inventoryhandler.cpp @@ -157,10 +157,10 @@ void InventoryHandler::handleMessage(MessageIn &msg) if (debugInventory) { - logger->log("Index: %d, ID: %d, Type: %d, Identified: %d, " - "Qty: %d, Cards: %d, %d, %d, %d", - index, itemId, itemType, identified, amount, - cards[0], cards[1], cards[2], cards[3]); + Log::info("Index: %d, ID: %d, Type: %d, Identified: %d, " + "Qty: %d, Cards: %d, %d, %d, %d", + index, itemId, itemType, identified, amount, + cards[0], cards[1], cards[2], cards[3]); } if (msg.getId() == SMSG_PLAYER_INVENTORY) @@ -191,10 +191,10 @@ void InventoryHandler::handleMessage(MessageIn &msg) if (debugInventory) { - logger->log("Index: %d, ID: %d, Type: %d, Identified: %d, " - "Qty: %d, Cards: %d, %d, %d, %d", - index, itemId, itemType, identified, amount, - cards[0], cards[1], cards[2], cards[3]); + Log::info("Index: %d, ID: %d, Type: %d, Identified: %d, " + "Qty: %d, Cards: %d, %d, %d, %d", + index, itemId, itemType, identified, amount, + cards[0], cards[1], cards[2], cards[3]); } mInventoryItems.push_back( @@ -419,8 +419,8 @@ void InventoryHandler::handleMessage(MessageIn &msg) } else { - logger->log("Couldn't set attacke range due to the lack" - "of an initialized map."); + Log::info("Couldn't set attacke range due to the lack" + "of an initialized map."); local_player->setAttackRange(-1); } } @@ -434,7 +434,7 @@ void InventoryHandler::handleMessage(MessageIn &msg) index -= INVENTORY_OFFSET; - logger->log("Arrows equipped: %i", index); + Log::info("Arrows equipped: %i", index); mEquips.setEquipment(EQUIP_PROJECTILE_SLOT, index); break; } diff --git a/src/net/tmwa/inventoryhandler.h b/src/net/tmwa/inventoryhandler.h index f5ef4492..cde1235b 100644 --- a/src/net/tmwa/inventoryhandler.h +++ b/src/net/tmwa/inventoryhandler.h @@ -108,9 +108,9 @@ class EquipBackend final : public Equipment::Backend if (!newItem && inventoryIndex >= 0) { - logger->log("EquipBackend: Warning, trying to equip " - "non-existing item from inventory index %i at " - "equipment slot %i.", inventoryIndex, index); + Log::info("EquipBackend: Warning, trying to equip " + "non-existing item from inventory index %i at " + "equipment slot %i.", inventoryIndex, index); return; } diff --git a/src/net/tmwa/loginhandler.cpp b/src/net/tmwa/loginhandler.cpp index a7162ee6..b6e3d518 100644 --- a/src/net/tmwa/loginhandler.cpp +++ b/src/net/tmwa/loginhandler.cpp @@ -108,7 +108,7 @@ void LoginHandler::handleMessage(MessageIn &msg) mUpdateHost = msg.readString(len); loginData.updateHost = mUpdateHost; - logger->log("Received update host \"%s\" from login server.", + Log::info("Received update host \"%s\" from login server.", mUpdateHost.c_str()); break; } @@ -138,10 +138,10 @@ void LoginHandler::handleMessage(MessageIn &msg) msg.readInt16(); // maintenance msg.readInt16(); // is_new - logger->log("Network: Server: %s (%s:%d)", - world->name.c_str(), - ipToString(world->address), - world->port); + Log::info("Network: Server: %s (%s:%d)", + world->name.c_str(), + ipToString(world->address), + world->port); mWorlds.push_back(world); } @@ -150,7 +150,7 @@ void LoginHandler::handleMessage(MessageIn &msg) case SMSG_LOGIN_ERROR: code = msg.readInt8(); - logger->log("Login::error code: %i", code); + Log::info("Login::error code: %i", code); switch (code) { @@ -212,9 +212,9 @@ void LoginHandler::handleMessage(MessageIn &msg) mServerVersion = 0; if (mServerVersion > 0) - logger->log("TMW server version: x%06x", mServerVersion); + Log::info("TMW server version: x%06x", mServerVersion); else - logger->log("Server without version"); + Log::info("Server without version"); mRegistrationEnabled = (options & FLAG_REGISTRATION); diff --git a/src/net/tmwa/messagein.cpp b/src/net/tmwa/messagein.cpp index c0db0fca..2630c511 100644 --- a/src/net/tmwa/messagein.cpp +++ b/src/net/tmwa/messagein.cpp @@ -26,9 +26,11 @@ #include <SDL_endian.h> +#ifndef MAKEWORD #define MAKEWORD(low,high) \ ((unsigned short)(((unsigned char)(low)) | \ ((unsigned short)((unsigned char)(high))) << 8)) +#endif namespace TmwAthena { diff --git a/src/net/tmwa/messageout.cpp b/src/net/tmwa/messageout.cpp index a886fb4d..7758e306 100644 --- a/src/net/tmwa/messageout.cpp +++ b/src/net/tmwa/messageout.cpp @@ -23,6 +23,8 @@ #include "net/tmwa/network.h" +#include "log.h" + #include <SDL_endian.h> #include <cstring> @@ -32,7 +34,7 @@ namespace TmwAthena { MessageOut::MessageOut(uint16_t id) { #ifdef DEBUG - logger->log("Sending %s (0x%x)", Network::mInstance->messageName(id), id); + Log::info("Sending %s (0x%x)", Network::mInstance->messageName(id), id); #endif writeInt16(id); } diff --git a/src/net/tmwa/network.cpp b/src/net/tmwa/network.cpp index b448dc4f..9e010f7c 100644 --- a/src/net/tmwa/network.cpp +++ b/src/net/tmwa/network.cpp @@ -279,7 +279,7 @@ bool Network::connect(const ServerInfo &server) { if (mState != IDLE && mState != NET_ERROR) { - logger->log("Tried to connect an already connected socket!"); + Log::info("Tried to connect an already connected socket!"); assert(false); return false; } @@ -290,8 +290,8 @@ bool Network::connect(const ServerInfo &server) return false; } - logger->log("Network::Connecting to %s:%i", server.hostname.c_str(), - server.port); + Log::info("Network::Connecting to %s:%i", server.hostname.c_str(), + server.port); mServer.hostname = server.hostname; mServer.port = server.port; @@ -376,8 +376,7 @@ void Network::dispatchMessages() auto packetInfoIt = mPacketInfo.find(msgId); if (packetInfoIt == mPacketInfo.end()) { - auto error = strprintf("Unknown packet 0x%x received.", msgId); - logger->error(error); + Log::critical(strprintf("Unknown packet 0x%x received.", msgId)); break; } @@ -395,9 +394,8 @@ void Network::dispatchMessages() if (len < 4) { - auto error = strprintf("Variable length packet 0x%x has invalid length %d.", - msgId, len); - logger->error(error); + Log::critical(strprintf("Variable length packet 0x%x has invalid length %d.", + msgId, len)); break; } } @@ -413,14 +411,14 @@ void Network::dispatchMessages() if (iter != mMessageHandlers.end()) { #ifdef DEBUG - logger->log("Handling %s (0x%x) of length %d", packetInfo->name, msgId, len); + Log::info("Handling %s (0x%x) of length %d", packetInfo->name, msgId, len); #endif iter->second->handleMessage(message); } else { - logger->log("Unhandled %s (0x%x) of length %d", packetInfo->name, msgId, len); + Log::info("Unhandled %s (0x%x) of length %d", packetInfo->name, msgId, len); } skip(len); @@ -474,7 +472,7 @@ bool Network::realConnect() std::string errorMessage = strprintf(_("Unable to resolve host \"%s\""), mServer.hostname.c_str()); setError(errorMessage); - logger->log("SDLNet_ResolveHost: %s", errorMessage.c_str()); + Log::info("SDLNet_ResolveHost: %s", errorMessage.c_str()); return false; } @@ -483,13 +481,13 @@ bool Network::realConnect() mSocket = SDLNet_TCP_Open(&ipAddress); if (!mSocket) { - logger->log("Error in SDLNet_TCP_Open(): %s", SDLNet_GetError()); + Log::info("Error in SDLNet_TCP_Open(): %s", SDLNet_GetError()); setError(SDLNet_GetError()); return false; } - logger->log("Network::Started session with %s:%i", - ipToString(ipAddress.host), ipAddress.port); + Log::info("Network::Started session with %s:%i", + ipToString(ipAddress.host), ipAddress.port); mState = CONNECTED; @@ -522,7 +520,7 @@ void Network::receive() switch (numReady) { case -1: - logger->log("Error: SDLNet_CheckSockets"); + Log::error("SDLNet_CheckSockets"); // FALLTHROUGH case 0: break; @@ -537,7 +535,7 @@ void Network::receive() { // We got disconnected mState = IDLE; - logger->log("Disconnected."); + Log::info("Disconnected."); } else if (ret < 0) { @@ -578,7 +576,7 @@ void Network::receive() if (SDLNet_TCP_DelSocket(set, mSocket) == -1) { - logger->log("Error in SDLNet_DelSocket(): %s", SDLNet_GetError()); + Log::info("Error in SDLNet_DelSocket(): %s", SDLNet_GetError()); } SDLNet_FreeSocketSet(set); @@ -586,7 +584,7 @@ void Network::receive() void Network::setError(const std::string &error) { - logger->log("Network error: %s", error.c_str()); + Log::info("Network error: %s", error.c_str()); mError = error; mState = NET_ERROR; } diff --git a/src/net/tmwa/partyhandler.cpp b/src/net/tmwa/partyhandler.cpp index 2b256cd5..78dfd7a0 100644 --- a/src/net/tmwa/partyhandler.cpp +++ b/src/net/tmwa/partyhandler.cpp @@ -184,7 +184,7 @@ void PartyHandler::handleMessage(MessageIn &msg) partyTab->chatLog(_("Experience sharing not possible."), BY_SERVER); break; default: - logger->log("Unknown party exp option: %d", exp); + Log::info("Unknown party exp option: %d", exp); } switch (item) @@ -208,7 +208,7 @@ void PartyHandler::handleMessage(MessageIn &msg) partyTab->chatLog(_("Item sharing not possible."), BY_SERVER); break; default: - logger->log("Unknown party item option: %d", exp); + Log::info("Unknown party item option: %d", exp); } break; } diff --git a/src/net/tmwa/playerhandler.cpp b/src/net/tmwa/playerhandler.cpp index f6f6ef41..44b0efd3 100644 --- a/src/net/tmwa/playerhandler.cpp +++ b/src/net/tmwa/playerhandler.cpp @@ -21,8 +21,11 @@ #include "net/tmwa/playerhandler.h" +#include "actorspritemanager.h" +#include "being.h" #include "client.h" #include "configuration.h" +#include "effectmanager.h" #include "game.h" #include "localplayer.h" #include "log.h" @@ -55,7 +58,7 @@ const int MAP_TELEPORT_SCROLL_DISTANCE = 8; namespace { /** - * Listener used for handling the overweigth message. + * Listener used for handling the overweight message. */ struct WeightListener : public gcn::ActionListener { @@ -126,7 +129,7 @@ static const char *randomDeathMessage() N_("You're off the twig."), N_("You've kicked the bucket."), N_("You've shuffled off your mortal coil, run down the " - "curtain and joined the bleedin' choir invisibile."), + "curtain and joined the bleedin' choir invisible."), N_("You are an ex-player."), N_("You're pining for the fjords.") }; @@ -152,10 +155,14 @@ PlayerHandler::PlayerHandler() SMSG_PLAYER_STAT_UPDATE_6, SMSG_PLAYER_ARROW_MESSAGE, SMSG_MAP_MASK, + SMSG_QUEST_SET_VAR, + SMSG_QUEST_PLAYER_VARS, 0 }; handledMessages = _messages; playerHandler = this; + + listen(Event::GameChannel); } void PlayerHandler::handleMessage(MessageIn &msg) @@ -179,7 +186,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) int x = msg.readInt16(); int y = msg.readInt16(); - logger->log("Warping to %s (%d, %d)", mapPath.c_str(), x, y); + Log::info("Warping to %s (%d, %d)", mapPath.c_str(), x, y); /* * We must clear the local player's target *before* the call @@ -217,8 +224,8 @@ void PlayerHandler::handleMessage(MessageIn &msg) // Stop movement local_player->setDestination(pos.x, pos.y); - logger->log("Adjust scrolling by %d:%d", (int) scrollOffsetX, - (int) scrollOffsetY); + Log::info("Adjust scrolling by %d:%d", (int) scrollOffsetX, + (int) scrollOffsetY); viewport->scrollBy(scrollOffsetX, scrollOffsetY); } @@ -499,7 +506,7 @@ void PlayerHandler::handleMessage(MessageIn &msg) serverNotice(_("Equip arrows first.")); break; default: - logger->log("0x013b: Unhandled message %i", type); + Log::info("0x013b: Unhandled message %i", type); break; } } @@ -514,6 +521,49 @@ void PlayerHandler::handleMessage(MessageIn &msg) map->setMask(mask); } break; + + case SMSG_QUEST_SET_VAR: + { + int variable = msg.readInt16(); + int value = msg.readInt32(); + int oldValue = mQuestVars.get(variable); + + mQuestVars.set(variable, value); + updateQuestStatusEffects(); + Event::trigger(Event::QuestsChannel, Event::QuestVarsChanged); + + if (effectManager && local_player) + { + switch (QuestDB::questChange(variable, oldValue, value)) + { + case QuestChange::None: + break; + case QuestChange::New: + effectManager->trigger(paths.getIntValue("newQuestEffectId"), local_player); + break; + case QuestChange::Completed: + effectManager->trigger(paths.getIntValue("completeQuestEffectId"), local_player); + break; + } + } + break; + } + + case SMSG_QUEST_PLAYER_VARS: + { + msg.readInt16(); // length + mQuestVars.clear(); + unsigned int count = (msg.getLength() - 4) / 6; + for (unsigned int i = 0; i < count; ++i) + { + int variable = msg.readInt16(); + int value = msg.readInt32(); + mQuestVars.set(variable, value); + } + updateQuestStatusEffects(); + Event::trigger(Event::QuestsChannel, Event::QuestVarsChanged); + break; + } } } @@ -649,8 +699,8 @@ Vector PlayerHandler::getPixelsPerSecondMoveSpeed(const Vector &speed, Map *map) if (!map || speed.x == 0 || speed.y == 0) { - logger->log("TmwAthena::PlayerHandler: Speed set to default: " - "Map not yet initialized or invalid speed."); + Log::info("TmwAthena::PlayerHandler: Speed set to default: " + "Map not yet initialized or invalid speed."); return getDefaultMoveSpeed(); } @@ -664,4 +714,54 @@ Vector PlayerHandler::getPixelsPerSecondMoveSpeed(const Vector &speed, Map *map) return pixelsPerSecond; } +void PlayerHandler::event(Event::Channel channel, const Event &event) +{ + if (channel == Event::GameChannel) + { + if (event.getType() == Event::MapLoaded) + { + updateQuestStatusEffects(); + } + } +} + +void PlayerHandler::applyQuestStatusEffects(Being *npc) +{ + const auto npcId = npc->getSubType(); + const auto effect = mActiveQuestEffects.get(npcId); + if (effect != 0) + npc->setStatusEffect(effect, true); +} + +void PlayerHandler::updateQuestStatusEffects() +{ + auto game = Game::instance(); + if (!game) + return; + + const auto ¤tMapName = game->getCurrentMapName(); + auto updatedQuestEffects = QuestDB::getActiveEffects(mQuestVars, currentMapName); + + // Loop over all NPCs, disabling no longer active effects and enabling new ones + for (auto actor : actorSpriteManager->getAll()) { + if (actor->getType() != ActorSprite::NPC) + continue; + + auto *npc = static_cast<Being *>(actor); + const auto npcId = npc->getSubType(); + const auto oldEffect = mActiveQuestEffects.get(npcId); + const auto newEffect = updatedQuestEffects.get(npcId); + + if (oldEffect != newEffect) + { + if (oldEffect != 0) + npc->setStatusEffect(oldEffect, false); + if (newEffect != 0) + npc->setStatusEffect(newEffect, true); + } + } + + std::swap(mActiveQuestEffects, updatedQuestEffects); +} + } // namespace TmwAthena diff --git a/src/net/tmwa/playerhandler.h b/src/net/tmwa/playerhandler.h index f1a67e94..49990d85 100644 --- a/src/net/tmwa/playerhandler.h +++ b/src/net/tmwa/playerhandler.h @@ -28,7 +28,8 @@ namespace TmwAthena { -class PlayerHandler final : public MessageHandler, public Net::PlayerHandler +class PlayerHandler final : public MessageHandler, public Net::PlayerHandler, + public EventListener { public: PlayerHandler(); @@ -63,6 +64,16 @@ class PlayerHandler final : public MessageHandler, public Net::PlayerHandler bool usePixelPrecision() override { return false; } + + // EventListener + void event(Event::Channel channel, const Event &event) override; + + void applyQuestStatusEffects(Being *npc); + + private: + void updateQuestStatusEffects(); + + QuestEffectMap mActiveQuestEffects; }; } // namespace TmwAthena diff --git a/src/openglgraphics.cpp b/src/openglgraphics.cpp index ecd98ea1..667c91e8 100644 --- a/src/openglgraphics.cpp +++ b/src/openglgraphics.cpp @@ -91,8 +91,8 @@ OpenGLGraphics::OpenGLGraphics(SDL_Window *window, SDL_GLContext glContext) glGetIntegerv(GL_MAX_TEXTURE_SIZE, &texSize); } Image::mTextureSize = texSize; - logger->log("OpenGL texture size: %d pixels%s", Image::mTextureSize, - rectTex ? " (rectangle textures)" : ""); + Log::info("OpenGL texture size: %d pixels%s", Image::mTextureSize, + rectTex ? " (rectangle textures)" : ""); glMatrixMode(GL_TEXTURE); glLoadIdentity(); @@ -542,6 +542,7 @@ void OpenGLGraphics::drawRescaledImagePattern(const Image *image, void OpenGLGraphics::updateScreen() { SDL_GL_SwapWindow(mWindow); + SDL_ShowWindow(mWindow); /* * glFinish flushes all OpenGL commands and makes sure they have been diff --git a/src/particle.cpp b/src/particle.cpp index cb79c86f..5051672b 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -74,7 +74,7 @@ void Particle::setupEngine() Particle::fastPhysics = config.particleFastPhysics; Particle::emitterSkip = config.particleEmitterSkip + 1; Particle::enabled = config.particleEffects; - logger->log("Particle engine set up"); + Log::info("Particle engine set up"); } bool Particle::draw(Graphics *, int, int) const @@ -261,7 +261,7 @@ Particle *Particle::addEffect(const std::string &particleEffectFile, if (!rootNode || rootNode.name() != "effect") { - logger->log("Error loading particle: %s", particleEffectFile.c_str()); + Log::info("Error loading particle: %s", particleEffectFile.c_str()); return nullptr; } diff --git a/src/particleemitter.cpp b/src/particleemitter.cpp index 4954d317..e898b2fb 100644 --- a/src/particleemitter.cpp +++ b/src/particleemitter.cpp @@ -179,9 +179,8 @@ ParticleEmitter::ParticleEmitter(XML::Node emitterNode, Particle *target, } else { - logger->log("Particle Engine: Warning, unknown emitter property \"%s\"", - name.c_str() - ); + Log::info("Particle Engine: Warning, unknown emitter property \"%s\"", + name.c_str()); } } else if (propertyNode.name() == "emitter") diff --git a/src/playerinfo.cpp b/src/playerinfo.cpp index a05ffaaa..a96471a5 100644 --- a/src/playerinfo.cpp +++ b/src/playerinfo.cpp @@ -287,8 +287,8 @@ void clearAbilityStatus(int id) void setAbilityStatus(int id, int current, int max, int recharge) { - logger->log("AbilityUpdate Skill #%d -- (%d/%d) -> %d", id, current, max, - recharge); + Log::info("AbilityUpdate Skill #%d -- (%d/%d) -> %d", id, current, max, + recharge); mAbilities[id].currentMana = current; mAbilities[id].neededMana = max; mAbilities[id].recharge = recharge; 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 0ecc9fd6..136c9334 100644 --- a/src/resources/dye.cpp +++ b/src/resources/dye.cpp @@ -63,8 +63,8 @@ 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; } @@ -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 @@ -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 0fe68f07..1730d2fd 100644 --- a/src/resources/dye.h +++ b/src/resources/dye.h @@ -35,7 +35,7 @@ class DyePalette * 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 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 b36aea01..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; } @@ -117,20 +115,20 @@ Resource *Image::load(SDL_RWops *rw, const Dye &dye) if (!surf) { - logger->log("Error, image load failed: %s", IMG_GetError()); + Log::info("Error, image load failed: %s", IMG_GetError()); return nullptr; } if (surf->format->format != SDL_PIXELFORMAT_RGBA32) { - logger->log("Warning: image format is %s, not SDL_PIXELFORMAT_RGBA32. Converting...", - SDL_GetPixelFormatName(surf->format->format)); + Log::warn("Image format is %s, not SDL_PIXELFORMAT_RGBA32. Converting...", + SDL_GetPixelFormatName(surf->format->format)); SDL_Surface *convertedSurf = SDL_ConvertSurfaceFormat(surf, SDL_PIXELFORMAT_RGBA32, 0); SDL_FreeSurface(surf); if (!convertedSurf) { - logger->log("Error, image convert failed: %s", SDL_GetError()); + Log::info("Error, image convert failed: %s", SDL_GetError()); return nullptr; } surf = convertedSurf; @@ -219,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 @@ -253,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; } @@ -308,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/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..1d217fc2 100644 --- a/src/resources/itemdb.cpp +++ b/src/resources/itemdb.cpp @@ -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; } @@ -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 8e4a07be..0c332902 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" @@ -75,7 +74,7 @@ ThemeInfo::ThemeInfo(const std::string &path) if (rootNode.attribute("name", name) && !name.empty()) this->doc = std::move(doc); else - logger->log("Error: Theme '%s' has no name!", path.c_str()); + Log::error("Theme '%s' has no name!", path.c_str()); } std::string ThemeInfo::getFullPath() const @@ -228,15 +227,14 @@ void Skin::updateAlpha(float alpha) Theme::Theme(const ThemeInfo &themeInfo) : mThemePath(themeInfo.getFullPath()) - , mProgressColors(THEME_PROG_END) { listen(Event::ConfigChannel); readTheme(themeInfo); if (mPalettes.empty()) { - logger->log("Error, theme did not define any palettes: %s", - themeInfo.getPath().c_str()); + Log::info("Error, theme did not define any palettes: %s", + themeInfo.getPath().c_str()); // Avoid crashing mPalettes.emplace_back(THEME_COLORS_END); @@ -377,9 +375,12 @@ void Theme::drawSkin(Graphics *graphics, SkinType type, const WidgetState &state 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(); @@ -408,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); } } @@ -479,7 +484,7 @@ static bool check(bool value, const char *msg, ...) { va_list ap; va_start(ap, msg); - logger->vlog(msg, ap); + Log::vinfo(msg, ap); va_end(ap); } return !value; @@ -487,9 +492,9 @@ static bool check(bool value, const char *msg, ...) bool Theme::readTheme(const ThemeInfo &themeInfo) { - logger->log("Loading %s theme from '%s'...", - themeInfo.getName().c_str(), - themeInfo.getPath().c_str()); + Log::info("Loading %s theme from '%s'...", + themeInfo.getName().c_str(), + themeInfo.getPath().c_str()); XML::Node rootNode = themeInfo.getDocument().rootNode(); @@ -507,10 +512,10 @@ bool Theme::readTheme(const ThemeInfo &themeInfo) else if (childNode.name() == "icon") readIconNode(childNode); else - logger->log("Theme: Unknown node '%s'!", childNode.name().data()); + 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); @@ -588,9 +593,8 @@ static void readSkinStateRectNode(XML::Node node, SkinState &state) node.attribute("fill", rect.filled); } -static void readSkinStateTextNode(XML::Node node, SkinState &state) +static void readTextNode(XML::Node node, TextFormat &textFormat) { - auto &textFormat = state.textFormat; node.attribute("bold", textFormat.bold); node.attribute("color", textFormat.color); node.attribute("outlineColor", textFormat.outlineColor); @@ -625,7 +629,7 @@ 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)); @@ -689,7 +693,7 @@ void Theme::readSkinStateImgNode(XML::Node node, SkinState &state) const border.right = right; border.top = top; border.bottom = bottom; - border.image = image->getSubImage(x, y, width, height); + border.image.reset(image->getSubImage(x, y, width, height)); node.attribute("fill", border.fillMode); } @@ -705,7 +709,7 @@ 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); + Log::info("Error, invalid theme color palette: %s", str); value = gcn::Color(0, 0, 0); return; } @@ -788,6 +792,7 @@ static int readColorId(const std::string &id) "WHISPER_TAB", "BACKGROUND", "HIGHLIGHT", + "HIGHLIGHT_TEXT", "TAB_FLASH", "SHOP_WARNING", "ITEM_EQUIPPED", @@ -873,7 +878,7 @@ void Theme::readPaletteNode(XML::Node node) { int paletteId; if (node.attribute("id", paletteId) && static_cast<size_t>(paletteId) != mPalettes.size()) - logger->log("Theme: Non-consecutive palette 'id' attribute with value %d!", paletteId); + Log::info("Theme: Non-consecutive palette 'id' attribute with value %d!", paletteId); Palette &palette = mPalettes.emplace_back(THEME_COLORS_END); @@ -882,7 +887,7 @@ void Theme::readPaletteNode(XML::Node node) if (childNode.name() == "color") readColorNode(childNode, palette); else - logger->log("Theme: Unknown node '%s'!", childNode.name().data()); + Log::info("Theme: Unknown node '%s'!", childNode.name().data()); } } @@ -916,5 +921,15 @@ void Theme::readProgressBarNode(XML::Node node) if (check(id >= 0, "Theme: 'progress' element has unknown 'id' attribute: '%s'!", idStr.c_str())) return; - mProgressColors[id] = 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 bfd3b33a..fbcb263b 100644 --- a/src/resources/theme.h +++ b/src/resources/theme.h @@ -30,6 +30,7 @@ #include "resources/image.h" #include "utils/xml.h" +#include <array> #include <map> #include <memory> #include <optional> @@ -227,6 +228,7 @@ class Theme : public EventListener WHISPER_TAB, BACKGROUND, HIGHLIGHT, + HIGHLIGHT_TEXT, TAB_FLASH, SHOP_WARNING, ITEM_EQUIPPED, @@ -283,7 +285,7 @@ class Theme : public EventListener static const gcn::Color &getThemeColor(int type); static gcn::Color getProgressColor(int type, float progress); - + const Palette &getPalette(size_t index) const; /** @@ -305,7 +307,8 @@ class Theme : public EventListener 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; @@ -357,5 +360,6 @@ class Theme : public EventListener float mAlpha = 1.0; std::vector<Palette> mPalettes; - std::vector<std::unique_ptr<DyePalette>> mProgressColors; + std::array<std::unique_ptr<DyePalette>, THEME_PROG_END> mProgressColors; + std::array<std::optional<TextFormat>, THEME_PROG_END> mProgressTextFormats; }; diff --git a/src/sdlgraphics.cpp b/src/sdlgraphics.cpp index 9256471a..7d75e7f2 100644 --- a/src/sdlgraphics.cpp +++ b/src/sdlgraphics.cpp @@ -72,16 +72,17 @@ std::unique_ptr<Graphics> SDLGraphics::create(SDL_Window *window, const VideoSet SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, rendererFlags); if (!renderer) { - logger->error(strprintf("Failed to create renderer: %s", + Log::critical(strprintf("Failed to create renderer: %s", SDL_GetError())); return {}; } - return std::make_unique<SDLGraphics>(renderer); + return std::make_unique<SDLGraphics>(window, renderer); } -SDLGraphics::SDLGraphics(SDL_Renderer *renderer) - : mRenderer(renderer) +SDLGraphics::SDLGraphics(SDL_Window *window, SDL_Renderer *renderer) + : mWindow(window) + , mRenderer(renderer) { Image::setRenderer(mRenderer); @@ -90,25 +91,25 @@ SDLGraphics::SDLGraphics(SDL_Renderer *renderer) SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); if (const char *driver = SDL_GetCurrentVideoDriver()) - logger->log("Using video driver: %s", driver); + Log::info("Using video driver: %s", driver); else - logger->log("Using video driver: not initialized"); + Log::info("Using video driver: not initialized"); SDL_RendererInfo info; if (SDL_GetRendererInfo(renderer, &info) == 0) { - logger->log("Using renderer: %s", info.name); - - logger->log("The renderer is a software fallback: %s", - (info.flags & SDL_RENDERER_SOFTWARE) ? "yes" : "no"); - logger->log("The renderer is hardware accelerated: %s", - (info.flags & SDL_RENDERER_ACCELERATED) ? "yes" : "no"); - logger->log("Vsync: %s", - (info.flags & SDL_RENDERER_PRESENTVSYNC) ? "on" : "off"); - logger->log("Renderer supports rendering to texture: %s", - (info.flags & SDL_RENDERER_TARGETTEXTURE) ? "yes" : "no"); - logger->log("Max texture size: %dx%d", - info.max_texture_width, info.max_texture_height); + Log::info("Using renderer: %s", info.name); + + Log::info("The renderer is a software fallback: %s", + (info.flags & SDL_RENDERER_SOFTWARE) ? "yes" : "no"); + Log::info("The renderer is hardware accelerated: %s", + (info.flags & SDL_RENDERER_ACCELERATED) ? "yes" : "no"); + Log::info("Vsync: %s", + (info.flags & SDL_RENDERER_PRESENTVSYNC) ? "on" : "off"); + Log::info("Renderer supports rendering to texture: %s", + (info.flags & SDL_RENDERER_TARGETTEXTURE) ? "yes" : "no"); + Log::info("Max texture size: %dx%d", + info.max_texture_width, info.max_texture_height); } } @@ -245,6 +246,7 @@ void SDLGraphics::drawRescaledImagePattern(const Image *image, void SDLGraphics::updateScreen() { SDL_RenderPresent(mRenderer); + SDL_ShowWindow(mWindow); } void SDLGraphics::windowToLogical(int windowX, int windowY, diff --git a/src/sdlgraphics.h b/src/sdlgraphics.h index 7cdea414..057fcdc3 100644 --- a/src/sdlgraphics.h +++ b/src/sdlgraphics.h @@ -33,7 +33,7 @@ public: static std::unique_ptr<Graphics> create(SDL_Window *window, const VideoSettings &settings); - SDLGraphics(SDL_Renderer *renderer); + SDLGraphics(SDL_Window *window, SDL_Renderer *renderer); ~SDLGraphics() override; void setVSync(bool sync) override; @@ -83,5 +83,6 @@ protected: void updateClipRect() override; private: + SDL_Window *mWindow = nullptr; SDL_Renderer *mRenderer = nullptr; }; diff --git a/src/sound.cpp b/src/sound.cpp index c97951a3..20d3810a 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -64,11 +64,11 @@ void Sound::init() if (mInstalled) return; - logger->log("Sound::init() Initializing sound..."); + Log::info("Sound::init() Initializing sound..."); if (SDL_InitSubSystem(SDL_INIT_AUDIO) == -1) { - logger->log("Sound::init() Failed to initialize audio subsystem"); + Log::info("Sound::init() Failed to initialize audio subsystem"); return; } @@ -78,8 +78,8 @@ void Sound::init() MIX_DEFAULT_CHANNELS, audioBuffer); if (res < 0) { - logger->log("Sound::init Could not initialize audio: %s", - Mix_GetError()); + Log::info("Sound::init Could not initialize audio: %s", + Mix_GetError()); return; } @@ -122,18 +122,18 @@ void Sound::info() case AUDIO_S16MSB: format = "S16MSB"; break; } - logger->log("Sound::info() SDL_mixer: %i.%i.%i (compiled)", + Log::info("Sound::info() SDL_mixer: %i.%i.%i (compiled)", compiledVersion.major, compiledVersion.minor, compiledVersion.patch); - logger->log("Sound::info() SDL_mixer: %i.%i.%i (linked)", + Log::info("Sound::info() SDL_mixer: %i.%i.%i (linked)", linkedVersion->major, linkedVersion->minor, linkedVersion->patch); - logger->log("Sound::info() Driver: %s", driver); - logger->log("Sound::info() Format: %s", format); - logger->log("Sound::info() Rate: %i", rate); - logger->log("Sound::info() Channels: %i", channels); + Log::info("Sound::info() Driver: %s", driver); + Log::info("Sound::info() Format: %s", format); + Log::info("Sound::info() Rate: %i", rate); + Log::info("Sound::info() Channels: %i", channels); } void Sound::setMusicVolume(int volume) @@ -173,7 +173,7 @@ void Sound::stopMusic() if (!mInstalled) return; - logger->log("Sound::stopMusic()"); + Log::info("Sound::stopMusic()"); haltMusic(); } @@ -201,7 +201,7 @@ void Sound::fadeOutMusic(int ms) if (!mInstalled) return; - logger->log("Sound::fadeOutMusic() Fading-out (%i ms)", ms); + Log::info("Sound::fadeOutMusic() Fading-out (%i ms)", ms); if (mMusic) { @@ -260,7 +260,7 @@ void Sound::playSfx(const std::string &path, int x, int y) if (ResourceRef<SoundEffect> sound = resman->getSoundEffect(tmpPath)) { - logger->log("Sound::playSfx() Playing: %s", path.c_str()); + Log::info("Sound::playSfx() Playing: %s", path.c_str()); int vol = 120; if (local_player && (x > 0 || y > 0)) @@ -299,7 +299,7 @@ void Sound::close() return; haltMusic(); - logger->log("Sound::close() Shutting down sound..."); + Log::info("Sound::close() Shutting down sound..."); Mix_CloseAudio(); mInstalled = false; diff --git a/src/text.cpp b/src/text.cpp index 4698aa87..14ffed18 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -23,7 +23,6 @@ #include "text.h" #include "textmanager.h" -#include "textrenderer.h" #include "gui/gui.h" @@ -94,6 +93,7 @@ void Text::adviseXY(int x, int y) void Text::draw(gcn::Graphics *graphics, int xOff, int yOff) { + auto g = static_cast<Graphics *>(graphics); if (mIsSpeech) { WidgetState state; @@ -103,7 +103,7 @@ void Text::draw(gcn::Graphics *graphics, int xOff, int yOff) state.height = mHeight + 10; auto theme = gui->getTheme(); - theme->drawSkin(static_cast<Graphics *>(graphics), SkinType::SpeechBubble, state); + theme->drawSkin(g, SkinType::SpeechBubble, state); /* if (mWidth >= 15) @@ -115,7 +115,7 @@ void Text::draw(gcn::Graphics *graphics, int xOff, int yOff) */ } - TextRenderer::renderText(graphics, mText, + g->drawText(mText, mX - xOff, mY - yOff, gcn::Graphics::LEFT, *mColor, mFont, !mIsSpeech, true); } diff --git a/src/textparticle.cpp b/src/textparticle.cpp index eb477217..4f6f1df4 100644 --- a/src/textparticle.cpp +++ b/src/textparticle.cpp @@ -21,7 +21,7 @@ #include "textparticle.h" -#include "textrenderer.h" +#include "graphics.h" #include <guichan/color.hpp> @@ -47,7 +47,7 @@ bool TextParticle::draw(Graphics *graphics, int offsetX, int offsetY) const gcn::Color color = *mColor; color.a = getCurrentAlpha() * 255; - TextRenderer::renderText(graphics, mText, + graphics->drawText(mText, screenX, screenY, gcn::Graphics::CENTER, color, mTextFont, mOutline, false); diff --git a/src/textrenderer.h b/src/textrenderer.h deleted file mode 100644 index de4fcff2..00000000 --- a/src/textrenderer.h +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Text Renderer - * Copyright (C) 2009 The Mana World Development Team - * Copyright (C) 2009-2012 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 "resources/theme.h" - -#include <guichan/exception.hpp> -#include <guichan/font.hpp> - -/** - * Class for text rendering which can apply an outline and shadow. - */ -class TextRenderer -{ -public: - /** - * Renders a specified text. - */ - static void renderText(gcn::Graphics *graphics, - const std::string &text, - int x, int y, - gcn::Graphics::Alignment alignment, - const gcn::Color &color, - gcn::Font *font, - bool outline = false, - bool shadow = false, - const std::optional<gcn::Color> &outlineColor = {}, - const std::optional<gcn::Color> &shadowColor = {}) - { - switch (alignment) - { - case gcn::Graphics::LEFT: - break; - case gcn::Graphics::CENTER: - x -= font->getWidth(text) / 2; - break; - case gcn::Graphics::RIGHT: - x -= font->getWidth(text); - break; - default: - throw GCN_EXCEPTION("Unknown alignment."); - } - - // Text shadow - if (shadow) - { - if (shadowColor) - { - graphics->setColor(*shadowColor); - } - else - { - auto sc = Theme::getThemeColor(Theme::SHADOW); - sc.a = color.a / 2; - graphics->setColor(sc); - } - - if (outline) - font->drawString(graphics, text, x + 2, y + 2); - else - font->drawString(graphics, text, x + 1, y + 1); - } - - if (outline) - { - /* - graphics->setColor(guiPalette->getColor(Palette::OUTLINE, - alpha/4)); - // TODO: Reanable when we can draw it nicely in software mode - font->drawString(graphics, text, x + 2, y + 2); - font->drawString(graphics, text, x + 1, y + 2); - font->drawString(graphics, text, x + 2, y + 1); - */ - - // Text outline - if (outlineColor) - { - graphics->setColor(*outlineColor); - } - else - { - auto oc = Theme::getThemeColor(Theme::OUTLINE); - oc.a = color.a; - graphics->setColor(oc); - } - - font->drawString(graphics, text, x + 1, y); - font->drawString(graphics, text, x - 1, y); - font->drawString(graphics, text, x, y + 1); - font->drawString(graphics, text, x, y - 1); - } - - graphics->setColor(color); - font->drawString(graphics, text, x, y); - } - - /** - * Renders a specified text. - */ - static void renderText(gcn::Graphics *graphics, - const std::string &text, - int x, - int y, - gcn::Graphics::Alignment align, - gcn::Font *font, - const TextFormat &format) - { - renderText(graphics, - text, - x, - y, - align, - format.color, - font, - format.outlineColor.has_value(), - format.shadowColor.has_value(), - format.outlineColor, - format.shadowColor); - } -}; diff --git a/src/units.cpp b/src/units.cpp index ee8fa6ea..1ec81cb0 100644 --- a/src/units.cpp +++ b/src/units.cpp @@ -123,8 +123,8 @@ void Units::readUnitNode(XML::Node node, const std::string &filename) } else { - logger->log("Error bad unit count: %d for %s in %s", - ul.count, ul.symbol.c_str(), bu.symbol.c_str()); + Log::info("Error bad unit count: %d for %s in %s", + ul.count, ul.symbol.c_str(), bu.symbol.c_str()); } } } @@ -139,7 +139,7 @@ void Units::readUnitNode(XML::Node node, const std::string &filename) else if (type == "currency") units[UNIT_CURRENCY] = ud; else - logger->log("Error unknown unit type: %s in %s", type.c_str(), filename.c_str()); + Log::info("Error unknown unit type: %s in %s", type.c_str(), filename.c_str()); } diff --git a/src/utils/mutex.h b/src/utils/mutex.h index a0c72e95..b6c4e88d 100644 --- a/src/utils/mutex.h +++ b/src/utils/mutex.h @@ -75,13 +75,13 @@ inline Mutex::~Mutex() inline void Mutex::lock() { if (SDL_mutexP(mMutex) == -1) - logger->log("Mutex locking failed: %s", SDL_GetError()); + Log::info("Mutex locking failed: %s", SDL_GetError()); } inline void Mutex::unlock() { if (SDL_mutexV(mMutex) == -1) - logger->log("Mutex unlocking failed: %s", SDL_GetError()); + Log::info("Mutex unlocking failed: %s", SDL_GetError()); } diff --git a/src/utils/sha256.cpp b/src/utils/sha256.cpp index 0e44af7a..a1339d14 100644 --- a/src/utils/sha256.cpp +++ b/src/utils/sha256.cpp @@ -3,7 +3,7 @@ * Copyright (C) 2008-2009 The Mana World Development Team * Copyright (C) 2009-2012 The Mana Developers * - * This file has been slighly modified as part of The Mana Client. + * This file has been slightly modified as 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 version 2, as diff --git a/src/utils/stringutils.cpp b/src/utils/stringutils.cpp index 05b6982c..7b05b365 100644 --- a/src/utils/stringutils.cpp +++ b/src/utils/stringutils.cpp @@ -152,6 +152,32 @@ bool getBoolFromString(std::string text, bool def) return def; } +// Overload for std::vector<int> to parse comma-separated integers +void fromString(const char *str, std::vector<int> &value) +{ + value.clear(); + + const char *p = str; + while (*p) + { + while (*p == ' ' || *p == ',') + ++p; // skip spaces and commas + if (!*p) + break; + char *end = nullptr; + int v = static_cast<int>(strtol(p, &end, 10)); + if (end != p) + { + value.push_back(v); + p = end; + } + else + { + ++p; // skip invalid character to avoid infinite loop + } + } +} + std::string autocomplete(const std::vector<std::string> &candidates, std::string base) { diff --git a/src/utils/stringutils.h b/src/utils/stringutils.h index 53c36bc6..885ddcb8 100644 --- a/src/utils/stringutils.h +++ b/src/utils/stringutils.h @@ -117,9 +117,18 @@ std::string &removeColors(std::string &msg); /** * Returns whether a string starts with a given prefix. */ -inline bool startsWith(const std::string &str, const char *prefix) +inline bool startsWith(std::string_view str, std::string_view prefix) { - return str.rfind(prefix, 0) == 0; + return str.substr(0, prefix.size()) == prefix; +} + +/** + * Returns whether a string ends with a given suffix. + */ +inline bool endsWith(std::string_view str, std::string_view suffix) +{ + return str.size() >= suffix.size() && + str.compare(str.size() - suffix.size(), suffix.size(), suffix) == 0; } /** @@ -193,6 +202,8 @@ inline void fromString(const char *str, bool &value) value = getBoolFromString(str); } +void fromString(const char *str, std::vector<int> &value); + template<typename T> struct FromString<T, std::enable_if_t<std::is_enum_v<T>>> { diff --git a/src/utils/xml.cpp b/src/utils/xml.cpp index c408c9c2..34bcc52b 100644 --- a/src/utils/xml.cpp +++ b/src/utils/xml.cpp @@ -43,13 +43,13 @@ namespace XML auto *context = static_cast<XMLContext*>(ctx); if (context) - logger->log("Error in XML file '%s' on line %d", - context->file.c_str(), error->line); + Log::info("Error in XML file '%s' on line %d", + context->file.c_str(), error->line); else - logger->log("Error in unknown XML file on line %d", - error->line); + Log::info("Error in unknown XML file on line %d", + error->line); - logger->log("%s", error->message); + Log::info("%s", error->message); // No need to keep errors around xmlCtxtResetLastError(error->ctxt); @@ -76,11 +76,11 @@ namespace XML SDL_free(data); if (!mDoc) - logger->log("Error parsing XML file %s", filename.c_str()); + Log::info("Error parsing XML file %s", filename.c_str()); } else { - logger->log("Error loading %s: %s", filename.c_str(), SDL_GetError()); + Log::info("Error loading %s: %s", filename.c_str(), SDL_GetError()); } xmlSetStructuredErrorFunc(nullptr, xmlLogger); @@ -109,7 +109,7 @@ namespace XML mWriter = xmlNewTextWriterFilename(fileName.c_str(), 0); if (!mWriter) { - logger->log("Error creating XML writer for file %s", fileName.c_str()); + Log::info("Error creating XML writer for file %s", fileName.c_str()); return; } diff --git a/src/utils/zlib.cpp b/src/utils/zlib.cpp index f78b235e..08281bcc 100644 --- a/src/utils/zlib.cpp +++ b/src/utils/zlib.cpp @@ -107,19 +107,19 @@ int inflateMemory(unsigned char *in, unsigned int inLength, { if (ret == Z_MEM_ERROR) { - logger->log("Error: Out of memory while decompressing data!"); + Log::error("Out of memory while decompressing data!"); } else if (ret == Z_VERSION_ERROR) { - logger->log("Error: Incompatible zlib version!"); + Log::error("Incompatible zlib version!"); } else if (ret == Z_DATA_ERROR) { - logger->log("Error: Incorrect zlib compressed data!"); + Log::error("Incorrect zlib compressed data!"); } else { - logger->log("Error: Unknown error while decompressing data!"); + Log::error("Unknown error while decompressing data!"); } free(out); diff --git a/src/video.cpp b/src/video.cpp index 7ab21d12..c71a9097 100644 --- a/src/video.cpp +++ b/src/video.cpp @@ -65,7 +65,7 @@ Graphics *Video::initialize(const VideoSettings &settings) if (!initDisplayModes()) { - logger->log("Failed to initialize display modes: %s", SDL_GetError()); + Log::info("Failed to initialize display modes: %s", SDL_GetError()); } SDL_DisplayMode displayMode; @@ -81,7 +81,7 @@ Graphics *Video::initialize(const VideoSettings &settings) if (SDL_GetClosestDisplayMode(mSettings.display, &requestedMode, &displayMode) == nullptr) { - logger->log("SDL_GetClosestDisplayMode failed: %s, falling back to borderless mode", SDL_GetError()); + Log::info("SDL_GetClosestDisplayMode failed: %s, falling back to borderless mode", SDL_GetError()); mSettings.windowMode = WindowMode::WindowedFullscreen; } } @@ -92,6 +92,10 @@ Graphics *Video::initialize(const VideoSettings &settings) switch (mSettings.windowMode) { case WindowMode::Windowed: + // In windowed mode, the window is initially created hidden, we'll show + // it once we have finished setting it up and done an initial paint to + // avoid startup flicker on X11 and Windows. + windowFlags |= SDL_WINDOW_HIDDEN; break; case WindowMode::Fullscreen: windowFlags |= SDL_WINDOW_FULLSCREEN; @@ -99,6 +103,10 @@ Graphics *Video::initialize(const VideoSettings &settings) break; case WindowMode::WindowedFullscreen: windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP; + // On Windows, fullscreen desktop with OpenGL actually flickers worse + // when the window is initially hidden. + if (!mSettings.openGL) + windowFlags |= SDL_WINDOW_HIDDEN; videoMode = "windowed fullscreen"; break; } @@ -106,10 +114,10 @@ Graphics *Video::initialize(const VideoSettings &settings) if (mSettings.openGL) windowFlags |= SDL_WINDOW_OPENGL; - logger->log("Setting video mode %dx%d %s", - mSettings.width, - mSettings.height, - videoMode); + Log::info("Setting video mode %dx%d %s", + mSettings.width, + mSettings.height, + videoMode); mWindow = SDL_CreateWindow("Mana", SDL_WINDOWPOS_UNDEFINED, @@ -120,8 +128,7 @@ Graphics *Video::initialize(const VideoSettings &settings) if (!mWindow) { - logger->error(strprintf("Failed to create window: %s", - SDL_GetError())); + Log::critical(strprintf("Failed to create window: %s", SDL_GetError())); return nullptr; } @@ -131,7 +138,7 @@ Graphics *Video::initialize(const VideoSettings &settings) { if (SDL_SetWindowDisplayMode(mWindow, &displayMode) != 0) { - logger->log("SDL_SetWindowDisplayMode failed: %s", SDL_GetError()); + Log::info("SDL_SetWindowDisplayMode failed: %s", SDL_GetError()); } } @@ -144,7 +151,7 @@ Graphics *Video::initialize(const VideoSettings &settings) mGraphics = OpenGLGraphics::create(mWindow, mSettings); if (!mGraphics) { - logger->log("Failed to create OpenGL context, falling back to SDL renderer: %s", + Log::info("Failed to create OpenGL context, falling back to SDL renderer: %s", SDL_GetError()); mSettings.openGL = false; } @@ -170,7 +177,7 @@ bool Video::apply(const VideoSettings &settings) SDL_DisplayMode displayMode; if (SDL_GetWindowDisplayMode(mWindow, &displayMode) != 0) { - logger->error(strprintf("SDL_GetCurrentDisplayMode failed: %s", SDL_GetError())); + Log::critical(strprintf("SDL_GetCurrentDisplayMode failed: %s", SDL_GetError())); return false; } @@ -188,7 +195,7 @@ bool Video::apply(const VideoSettings &settings) if (SDL_SetWindowDisplayMode(mWindow, &displayMode) != 0) { - logger->error(strprintf("SDL_SetWindowDisplayMode failed: %s", SDL_GetError())); + Log::critical(strprintf("SDL_SetWindowDisplayMode failed: %s", SDL_GetError())); return false; } } @@ -209,7 +216,7 @@ bool Video::apply(const VideoSettings &settings) if (SDL_SetWindowFullscreen(mWindow, windowFlags) != 0) { - logger->error(strprintf("SDL_SetWindowFullscreen failed: %s", SDL_GetError())); + Log::critical(strprintf("SDL_SetWindowFullscreen failed: %s", SDL_GetError())); return false; } @@ -235,12 +242,12 @@ bool Video::apply(const VideoSettings &settings) return true; } -void Video::windowSizeChanged(int width, int height) +void Video::updateWindowSize() { - mSettings.width = width; - mSettings.height = height; - - mGraphics->updateSize(width, height, mSettings.scale()); + SDL_GetWindowSize(mWindow, &mSettings.width, &mSettings.height); + mGraphics->updateSize(mSettings.width, + mSettings.height, + mSettings.scale()); } bool Video::initDisplayModes() diff --git a/src/video.h b/src/video.h index 47aed627..03e0dc4f 100644 --- a/src/video.h +++ b/src/video.h @@ -89,9 +89,9 @@ public: bool apply(const VideoSettings &settings); /** - * Handle a change in window size, possibly adjusting the scale. + * Handles a change in window size, possibly adjusting the scale. */ - void windowSizeChanged(int width, int height); + void updateWindowSize(); const DisplayMode &desktopDisplayMode() const { |