diff --git a/data-otservbr-global/lib/quests/soul_war.lua b/data-otservbr-global/lib/quests/soul_war.lua
index a9e9d920e91..668c67f92b9 100644
--- a/data-otservbr-global/lib/quests/soul_war.lua
+++ b/data-otservbr-global/lib/quests/soul_war.lua
@@ -1112,9 +1112,9 @@ function MonsterType:calculateBagYouDesireChance(player, itemChance)
itemChance = itemChance + (playerTaintLevel * SoulWarQuest.bagYouDesireChancePerTaint)
end
- logger.info("Player {} killed {} with {} taints, loot chance {}", player:getName(), monsterName, playerTaintLevel, itemChance)
+ logger.debug("Player {} killed {} with {} taints, loot chance {}", player:getName(), monsterName, playerTaintLevel, itemChance)
- if math.random(1, 100000) <= totalChance then
+ if math.random(1, 100000) <= itemChance then
logger.debug("Player {} killed {} and got a bag you desire with drop chance {}", player:getName(), monsterName, itemChance)
if monsterName == "Goshnar's Megalomania" then
-- Reset kill count on successful drop
diff --git a/data/items/items.xml b/data/items/items.xml
index 8cec8f05ec6..0afe5ebe858 100644
--- a/data/items/items.xml
+++ b/data/items/items.xml
@@ -4027,6 +4027,9 @@
+
+
+
-
@@ -4342,6 +4345,9 @@
+
+
+
-
@@ -20590,6 +20596,9 @@
+
+
+
-
@@ -26310,6 +26319,9 @@
+
+
+
-
@@ -26319,6 +26331,9 @@
+
+
+
-
@@ -26328,6 +26343,9 @@
+
+
+
@@ -45774,6 +45792,9 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
+
+
-
@@ -45781,6 +45802,9 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
+
+
-
@@ -45788,6 +45812,9 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
+
+
-
@@ -45796,6 +45823,9 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
+
+
@@ -58245,6 +58275,9 @@ hands of its owner. Granted by TibiaRoyal.com"/>
+
+
+
-
diff --git a/qodana.yml b/qodana.yml
index 1621c979a9d..4857043fe5d 100644
--- a/qodana.yml
+++ b/qodana.yml
@@ -3,6 +3,8 @@ version: "1.0"
profile:
name: qodana.recommended
+linter: jetbrains/qodana-clang:latest
+
bootstrap: |
set -e
sudo apt-get update && sudo apt-get -y dist-upgrade
diff --git a/src/account/account_repository.hpp b/src/account/account_repository.hpp
index 0d4dcc7abcf..6dfe2c65668 100644
--- a/src/account/account_repository.hpp
+++ b/src/account/account_repository.hpp
@@ -27,6 +27,8 @@ class AccountRepository {
virtual bool loadBySession(const std::string &email, AccountInfo &acc) = 0;
virtual bool save(const AccountInfo &accInfo) = 0;
+ virtual bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) = 0;
+
virtual bool getPassword(const uint32_t &id, std::string &password) = 0;
virtual bool getCoins(const uint32_t &id, const uint8_t &type, uint32_t &coins) = 0;
diff --git a/src/account/account_repository_db.cpp b/src/account/account_repository_db.cpp
index b150a636a97..81da30c08c5 100644
--- a/src/account/account_repository_db.cpp
+++ b/src/account/account_repository_db.cpp
@@ -64,6 +64,16 @@ bool AccountRepositoryDB::save(const AccountInfo &accInfo) {
return successful;
};
+bool AccountRepositoryDB::getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) {
+ auto result = g_database().storeQuery(fmt::format("SELECT `id` FROM `players` WHERE `account_id` = {} AND `name` = {}", id, g_database().escapeString(name)));
+ if (!result) {
+ g_logger().error("Failed to get character: [{}] from account: [{}]!", name, id);
+ return false;
+ }
+
+ return result->countResults() == 1;
+}
+
bool AccountRepositoryDB::getPassword(const uint32_t &id, std::string &password) {
auto result = g_database().storeQuery(fmt::format("SELECT * FROM `accounts` WHERE `id` = {}", id));
if (!result) {
diff --git a/src/account/account_repository_db.hpp b/src/account/account_repository_db.hpp
index 651600e3bc4..e34d864a090 100644
--- a/src/account/account_repository_db.hpp
+++ b/src/account/account_repository_db.hpp
@@ -20,6 +20,8 @@ class AccountRepositoryDB final : public AccountRepository {
bool loadBySession(const std::string &esseionKey, AccountInfo &acc) override;
bool save(const AccountInfo &accInfo) override;
+ bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) override;
+
bool getPassword(const uint32_t &id, std::string &password) override;
bool getCoins(const uint32_t &id, const uint8_t &type, uint32_t &coins) override;
diff --git a/src/game/game.cpp b/src/game/game.cpp
index f356df5aa6a..a44b75d285c 100644
--- a/src/game/game.cpp
+++ b/src/game/game.cpp
@@ -3932,7 +3932,7 @@ void Game::playerUseWithCreature(uint32_t playerId, const Position &fromPos, uin
}
const std::shared_ptr monster = creature->getMonster();
- if (monster && monster->isFamiliar() && creature->getMaster()->getPlayer() == player && (it.isRune() || it.type == ITEM_TYPE_POTION)) {
+ if (monster && monster->isFamiliar() && creature->getMaster() && creature->getMaster()->getPlayer() == player && (it.isRune() || it.type == ITEM_TYPE_POTION)) {
player->setNextPotionAction(OTSYS_TIME() + g_configManager().getNumber(EX_ACTIONS_DELAY_INTERVAL, __FUNCTION__));
if (it.isMultiUse()) {
diff --git a/src/io/iologindata.cpp b/src/io/iologindata.cpp
index 132cead6412..fce5d0dc293 100644
--- a/src/io/iologindata.cpp
+++ b/src/io/iologindata.cpp
@@ -19,7 +19,7 @@
#include "enums/account_type.hpp"
#include "enums/account_errors.hpp"
-bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, const std::string &password, std::string &characterName, uint32_t &accountId, bool oldProtocol) {
+bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor, const std::string &password, std::string &characterName, uint32_t &accountId, bool oldProtocol, const uint32_t ip) {
Account account(accountDescriptor);
account.setProtocolCompat(oldProtocol);
@@ -38,6 +38,11 @@ bool IOLoginData::gameWorldAuthentication(const std::string &accountDescriptor,
}
}
+ if (!g_accountRepository().getCharacterByAccountIdAndName(account.getID(), characterName)) {
+ g_logger().warn("IP [{}] trying to connect into another account character", convertIPToString(ip));
+ return false;
+ }
+
if (AccountErrors_t::Ok != enumFromValue(account.load())) {
g_logger().error("Failed to load account [{}]", accountDescriptor);
return false;
diff --git a/src/io/iologindata.hpp b/src/io/iologindata.hpp
index 1451cf89778..79fa3b59ad7 100644
--- a/src/io/iologindata.hpp
+++ b/src/io/iologindata.hpp
@@ -17,7 +17,7 @@ using ItemBlockList = std::list>>;
class IOLoginData {
public:
- static bool gameWorldAuthentication(const std::string &accountDescriptor, const std::string &sessionOrPassword, std::string &characterName, uint32_t &accountId, bool oldProcotol);
+ static bool gameWorldAuthentication(const std::string &accountDescriptor, const std::string &sessionOrPassword, std::string &characterName, uint32_t &accountId, bool oldProcotol, const uint32_t ip);
static uint8_t getAccountType(uint32_t accountId);
static void updateOnlineStatus(uint32_t guid, bool login);
static bool loadPlayerById(std::shared_ptr player, uint32_t id, bool disableIrrelevantInfo = true);
diff --git a/src/lua/functions/core/game/config_functions.cpp b/src/lua/functions/core/game/config_functions.cpp
index d0e77a69352..b839f720057 100644
--- a/src/lua/functions/core/game/config_functions.cpp
+++ b/src/lua/functions/core/game/config_functions.cpp
@@ -70,12 +70,21 @@ int ConfigFunctions::luaConfigManagerGetBoolean(lua_State* L) {
}
int ConfigFunctions::luaConfigManagerGetFloat(lua_State* L) {
- auto key = getNumber(L, -1);
+ // configManager.getFloat(key, shouldRound = true)
+
+ // Ensure the first argument (key) is provided and is a valid enum
+ auto key = getNumber(L, 1);
if (!key) {
reportErrorFunc("Wrong enum");
return 1;
}
- lua_pushnumber(L, g_configManager().getFloat(key, __FUNCTION__));
+ // Check if the second argument (shouldRound) is provided and is a boolean; default to true if not provided
+ bool shouldRound = getBoolean(L, 2, true);
+ float value = g_configManager().getFloat(key, __FUNCTION__);
+ double finalValue = shouldRound ? static_cast(std::round(value * 100.0) / 100.0) : value;
+
+ g_logger().debug("[{}] key: {}, finalValue: {}, shouldRound: {}", __METHOD_NAME__, magic_enum::enum_name(key), finalValue, shouldRound);
+ lua_pushnumber(L, finalValue);
return 1;
}
diff --git a/src/lua/functions/core/game/config_functions.hpp b/src/lua/functions/core/game/config_functions.hpp
index ae4952e9643..9806a35f426 100644
--- a/src/lua/functions/core/game/config_functions.hpp
+++ b/src/lua/functions/core/game/config_functions.hpp
@@ -17,6 +17,33 @@ class ConfigFunctions final : LuaScriptInterface {
static void init(lua_State* L);
private:
+ /**
+ * @brief Retrieves a float configuration value from the configuration manager, with an optional rounding.
+ *
+ * This function is a Lua binding used to get a float value from the configuration manager. It requires
+ * a key as the first argument, which should be a valid enumeration. An optional second boolean argument
+ * specifies whether the retrieved float should be rounded to two decimal places.
+ *
+ * @param L Pointer to the Lua state. The first argument must be a valid enum key, and the second argument (optional)
+ * can be a boolean indicating whether to round the result.
+ *
+ * @return Returns 1 after pushing the result onto the Lua stack, indicating the number of return values.
+ *
+ * @exception reportErrorFunc Throws an error if the first argument is not a valid enum.
+ *
+ * Usage:
+ * local result = ConfigManager.getFloat(ConfigKey.SomeKey)
+ * local result_rounded = ConfigManager.getFloat(ConfigKey.SomeKey, false)
+ *
+ * Detailed behavior:
+ * 1. Extracts the key from the first Lua stack argument as an enumeration of type `ConfigKey_t`.
+ * 2. Checks if the second argument is provided; if not, defaults to true for rounding.
+ * 3. Retrieves the float value associated with the key from the configuration manager.
+ * 4. If rounding is requested, rounds the value to two decimal places.
+ * 5. Logs the method call and the obtained value using the debug logger.
+ * 6. Pushes the final value (rounded or original) back onto the Lua stack.
+ * 7. Returns 1 to indicate a single return value.
+ */
static int luaConfigManagerGetFloat(lua_State* L);
static int luaConfigManagerGetBoolean(lua_State* L);
static int luaConfigManagerGetNumber(lua_State* L);
diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp
index d8cdda05f45..909eef1e92e 100644
--- a/src/server/network/protocol/protocolgame.cpp
+++ b/src/server/network/protocol/protocolgame.cpp
@@ -841,7 +841,7 @@ void ProtocolGame::onRecvFirstMessage(NetworkMessage &msg) {
}
uint32_t accountId;
- if (!IOLoginData::gameWorldAuthentication(accountDescriptor, password, characterName, accountId, oldProtocol)) {
+ if (!IOLoginData::gameWorldAuthentication(accountDescriptor, password, characterName, accountId, oldProtocol, getIP())) {
ss.str(std::string());
if (authType == "session") {
ss << "Your session has expired. Please log in again.";
diff --git a/tests/fixture/account/in_memory_account_repository.hpp b/tests/fixture/account/in_memory_account_repository.hpp
index 40dbda38e08..8a294992274 100644
--- a/tests/fixture/account/in_memory_account_repository.hpp
+++ b/tests/fixture/account/in_memory_account_repository.hpp
@@ -120,6 +120,17 @@ namespace tests {
return true;
}
+ bool getCharacterByAccountIdAndName(const uint32_t &id, const std::string &name) final {
+ for (auto it = accounts.begin(); it != accounts.end(); ++it) {
+ if (it->second.id == id) {
+ if (it->second.players.find(name) != it->second.players.end()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
InMemoryAccountRepository &reset() {
accounts.clear();
coins_.clear();
diff --git a/tests/unit/account/account_test.cpp b/tests/unit/account/account_test.cpp
index 9259703c77c..c0c3ebbb832 100644
--- a/tests/unit/account/account_test.cpp
+++ b/tests/unit/account/account_test.cpp
@@ -592,4 +592,32 @@ suite<"account"> accountTest = [] {
expect(acc.load() == enumToValue(AccountErrors_t::Ok));
expect(acc.authenticate());
};
+
+ test("Account::getCharacterByAccountIdAndName using an account with the given character.") = [&injectionFixture] {
+ auto [accountRepository] = injectionFixture.get();
+
+ Account acc { 1 };
+ accountRepository.addAccount(
+ "session-key",
+ AccountInfo { 1, 1, 1, AccountType::ACCOUNT_TYPE_GOD, { { "Canary", 1 }, { "Canary2", 2 } }, false, getTimeNow() + 24 * 60 * 60 * 1000 }
+ );
+
+ const auto hasCharacter = accountRepository.getCharacterByAccountIdAndName(1, "Canary");
+
+ expect(hasCharacter);
+ };
+
+ test("Account::getCharacterByAccountIdAndName using an account without the given character.") = [&injectionFixture] {
+ auto [accountRepository] = injectionFixture.get();
+
+ Account acc { 1 };
+ accountRepository.addAccount(
+ "session-key",
+ AccountInfo { 1, 1, 1, AccountType::ACCOUNT_TYPE_GOD, { { "Canary", 1 }, { "Canary2", 2 } }, false, getTimeNow() + 24 * 60 * 60 * 1000 }
+ );
+
+ const auto hasCharacter = accountRepository.getCharacterByAccountIdAndName(1, "Invalid");
+
+ expect(!hasCharacter);
+ };
};