From e570944aee0fa517a9167aa8771d69b8075b70ba Mon Sep 17 00:00:00 2001 From: Luan Santos Date: Tue, 12 Dec 2023 16:03:32 -0800 Subject: [PATCH] feat: configurable number of players per account (#2000) New keys on config.lua: maxPlayersOnlinePerAccount = 1 maxPlayersOutsidePZPerAccount = 1 This replaces the old `onePlayerOnlinePerAccount`. Setting those numbers to 1 will effectively be equivalent to 'onePlayerOnlinePerAccount = true' from before. But you can tweak these more flexibly now. By setting 'maxPlayersOnlinePerAccount' to greater than 1 and keeping 'maxPlayersOutsidePZPerAccount' you still effectively prevent players from playing multiple chars at once, but you allow them to use a training dummy, for example. Players will be blocked from leaving PZ or using spells even from within PZ if more than 'maxPlayersOutsidePZPerAccount' are already outside of a protection zone. --- config.lua.dist | 3 +- data-otservbr-global/lib/compat/compat.lua | 10 ------ data/libs/functions/game.lua | 12 ------- src/config/config_definitions.hpp | 2 ++ src/config/configmanager.cpp | 3 +- src/creatures/combat/spells.cpp | 19 +++++++++++ src/creatures/players/player.cpp | 32 ++++++++----------- src/game/game.cpp | 16 +++++++--- src/game/game.hpp | 2 +- src/items/tile.cpp | 21 ++++++++++++ .../functions/creatures/npc/npc_functions.cpp | 10 +++--- src/server/network/protocol/protocolgame.cpp | 24 ++++++++++++-- 12 files changed, 99 insertions(+), 55 deletions(-) diff --git a/config.lua.dist b/config.lua.dist index e7dabfcccaa..fec7fb25e72 100644 --- a/config.lua.dist +++ b/config.lua.dist @@ -62,12 +62,13 @@ statusProtocolPort = 7171 maxPlayers = 0 serverName = "OTServBR-Global" serverMotd = "Welcome to the OTServBR-Global!" -onePlayerOnlinePerAccount = true statusTimeout = 5 * 1000 replaceKickOnLogin = true maxPacketsPerSecond = 25 maxItem = 2000 maxContainer = 100 +maxPlayersOnlinePerAccount = 1 +maxPlayersOutsidePZPerAccount = 1 -- Packet Compression -- Minimize network bandwith and reduce ping diff --git a/data-otservbr-global/lib/compat/compat.lua b/data-otservbr-global/lib/compat/compat.lua index 5691dacdb75..f62945e6b26 100644 --- a/data-otservbr-global/lib/compat/compat.lua +++ b/data-otservbr-global/lib/compat/compat.lua @@ -573,16 +573,6 @@ function getOnlinePlayers() return result end -function getPlayersByAccountNumber(accountNumber) - local result = {} - for _, player in ipairs(Game.getPlayers()) do - if player:getAccountId() == accountNumber then - result[#result + 1] = player:getId() - end - end - return result -end - function getPlayerGUIDByName(name) local player = Player(name) if player then diff --git a/data/libs/functions/game.lua b/data/libs/functions/game.lua index 5682f5c5869..e4d40bef318 100644 --- a/data/libs/functions/game.lua +++ b/data/libs/functions/game.lua @@ -58,18 +58,6 @@ function Game.getHouseByPlayerGUID(playerGUID) return nil end -function Game.getPlayersByAccountNumber(accountNumber) - local result = {} - local players, player = Game.getPlayers() - for i = 1, #players do - player = players[i] - if player:getAccountId() == accountNumber then - result[#result + 1] = player - end - end - return result -end - function Game.getPlayersByIPAddress(ip, mask) if not mask then mask = 0xFFFFFFFF diff --git a/src/config/config_definitions.hpp b/src/config/config_definitions.hpp index 582531a3afd..541da679351 100644 --- a/src/config/config_definitions.hpp +++ b/src/config/config_definitions.hpp @@ -131,6 +131,8 @@ enum ConfigKey_t : uint16_t { MAX_MESSAGEBUFFER, MAX_PACKETS_PER_SECOND, MAX_PLAYERS, + MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT, + MAX_PLAYERS_PER_ACCOUNT, MAX_SPEED_ATTACKONFIST, METRICS_ENABLE_OSTREAM, METRICS_ENABLE_PROMETHEUS, diff --git a/src/config/configmanager.cpp b/src/config/configmanager.cpp index fb2f11d3dea..97d1052ad03 100644 --- a/src/config/configmanager.cpp +++ b/src/config/configmanager.cpp @@ -83,7 +83,8 @@ bool ConfigManager::load() { } loadBoolConfig(L, ALLOW_CHANGEOUTFIT, "allowChangeOutfit", true); - loadBoolConfig(L, ONE_PLAYER_ON_ACCOUNT, "onePlayerOnlinePerAccount", true); + loadIntConfig(L, MAX_PLAYERS_PER_ACCOUNT, "maxPlayersOnlinePerAccount", 1); + loadIntConfig(L, MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT, "maxPlayersOutsidePZPerAccount", 1); loadBoolConfig(L, AIMBOT_HOTKEY_ENABLED, "hotkeyAimbotEnabled", true); loadBoolConfig(L, REMOVE_RUNE_CHARGES, "removeChargesFromRunes", true); loadBoolConfig(L, EXPERIENCE_FROM_PLAYERS, "experienceByKillingPlayers", false); diff --git a/src/creatures/combat/spells.cpp b/src/creatures/combat/spells.cpp index 8460feb722b..d684b5433ff 100644 --- a/src/creatures/combat/spells.cpp +++ b/src/creatures/combat/spells.cpp @@ -20,6 +20,25 @@ Spells::Spells() = default; Spells::~Spells() = default; TalkActionResult_t Spells::playerSaySpell(std::shared_ptr player, std::string &words) { + auto maxOnline = g_configManager().getNumber(MAX_PLAYERS_PER_ACCOUNT, __FUNCTION__); + auto tile = player->getTile(); + if (maxOnline > 1 && player->getAccountType() < account::ACCOUNT_TYPE_GAMEMASTER && tile && !tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + auto maxOutsizePZ = g_configManager().getNumber(MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT, __FUNCTION__); + auto accountPlayers = g_game().getPlayersByAccount(player->getAccount()); + int countOutsizePZ = 0; + for (const auto &accountPlayer : accountPlayers) { + if (accountPlayer == player || accountPlayer->isOffline()) { + continue; + } + if (accountPlayer->getTile() && !accountPlayer->getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) { + ++countOutsizePZ; + } + } + if (countOutsizePZ >= maxOutsizePZ) { + player->sendTextMessage(MESSAGE_FAILURE, fmt::format("You cannot cast spells while you have {} character(s) outside of a protection zone.", maxOutsizePZ)); + return TALKACTION_FAILED; + } + } std::string str_words = words; if (player->hasCondition(CONDITION_FEARED)) { diff --git a/src/creatures/players/player.cpp b/src/creatures/players/player.cpp index 99af9d2fc84..47789f5ba18 100644 --- a/src/creatures/players/player.cpp +++ b/src/creatures/players/player.cpp @@ -5987,32 +5987,28 @@ void Player::clearModalWindows() { } uint16_t Player::getHelpers() const { - uint16_t helpers; - if (guild && party) { - phmap::flat_hash_set> helperSet; + const auto &guildMembers = guild->getMembersOnline(); - const auto guildMembers = guild->getMembersOnline(); - helperSet.insert(guildMembers.begin(), guildMembers.end()); + stdext::vector_set> helperSet; + helperSet.insert(helperSet.end(), guildMembers.begin(), guildMembers.end()); + helperSet.insertAll(party->getMembers()); + helperSet.insertAll(party->getInvitees()); - const auto partyMembers = party->getMembers(); - helperSet.insert(partyMembers.begin(), partyMembers.end()); + helperSet.emplace(party->getLeader()); - const auto partyInvitees = party->getInvitees(); - helperSet.insert(partyInvitees.begin(), partyInvitees.end()); + return static_cast(helperSet.size()); + } - helperSet.insert(party->getLeader()); + if (guild) { + return static_cast(guild->getMemberCountOnline()); + } - helpers = helperSet.size(); - } else if (guild) { - helpers = guild->getMemberCountOnline(); - } else if (party) { - helpers = party->getMemberCount() + party->getInvitationCount() + 1; - } else { - helpers = 0; + if (party) { + return static_cast(party->getMemberCount() + party->getInvitationCount() + 1); } - return helpers; + return 0u; } void Player::sendClosePrivate(uint16_t channelId) { diff --git a/src/game/game.cpp b/src/game/game.cpp index 5b973f3776a..d741b24b1a2 100644 --- a/src/game/game.cpp +++ b/src/game/game.cpp @@ -830,13 +830,19 @@ ReturnValue Game::getPlayerByNameWildcard(const std::string &s, std::shared_ptr< return RETURNVALUE_NOERROR; } -std::shared_ptr Game::getPlayerByAccount(uint32_t acc) { - for (const auto &it : players) { - if (it.second->getAccountId() == acc) { - return it.second; +std::vector> Game::getPlayersByAccount(std::shared_ptr acc, bool allowOffline /* = false */) { + auto [accountPlayers, error] = acc->getAccountPlayers(); + if (error != account::ERROR_NO) { + return {}; + } + std::vector> ret; + for (const auto &[name, _] : accountPlayers) { + auto player = getPlayerByName(name, allowOffline); + if (player) { + ret.push_back(player); } } - return nullptr; + return ret; } bool Game::internalPlaceCreature(std::shared_ptr creature, const Position &pos, bool extendedPos /*=false*/, bool forced /*= false*/, bool creatureCheck /*= false*/) { diff --git a/src/game/game.hpp b/src/game/game.hpp index bbf2e66088c..f446615e29a 100644 --- a/src/game/game.hpp +++ b/src/game/game.hpp @@ -161,7 +161,7 @@ class Game { ReturnValue getPlayerByNameWildcard(const std::string &s, std::shared_ptr &player); - std::shared_ptr getPlayerByAccount(uint32_t acc); + std::vector> getPlayersByAccount(std::shared_ptr acc, bool allowOffline = false); bool internalPlaceCreature(std::shared_ptr creature, const Position &pos, bool extendedPos = false, bool forced = false, bool creatureCheck = false); diff --git a/src/items/tile.cpp b/src/items/tile.cpp index 787c5ed9b31..2b9527095dd 100644 --- a/src/items/tile.cpp +++ b/src/items/tile.cpp @@ -668,6 +668,27 @@ ReturnValue Tile::queryAdd(int32_t, const std::shared_ptr &thing, uint32_ } const auto playerTile = player->getTile(); + // moving from a pz tile to a non-pz tile + if (playerTile && playerTile->hasFlag(TILESTATE_PROTECTIONZONE)) { + auto maxOnline = g_configManager().getNumber(MAX_PLAYERS_PER_ACCOUNT, __FUNCTION__); + if (maxOnline > 1 && player->getAccountType() < account::ACCOUNT_TYPE_GAMEMASTER && !hasFlag(TILESTATE_PROTECTIONZONE)) { + auto maxOutsizePZ = g_configManager().getNumber(MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT, __FUNCTION__); + auto accountPlayers = g_game().getPlayersByAccount(player->getAccount()); + int countOutsizePZ = 0; + for (const auto &accountPlayer : accountPlayers) { + if (accountPlayer == player || accountPlayer->isOffline()) { + continue; + } + if (accountPlayer->getTile() && !accountPlayer->getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) { + ++countOutsizePZ; + } + } + if (countOutsizePZ >= maxOutsizePZ) { + player->sendCreatureSay(player, TALKTYPE_MONSTER_SAY, fmt::format("You can only have {} character{} from your account outside of a protection zone.", maxOutsizePZ == 1 ? "one" : std::to_string(maxOutsizePZ), maxOutsizePZ > 1 ? "s" : ""), &getPosition()); + return RETURNVALUE_NOTPOSSIBLE; + } + } + } if (playerTile && player->isPzLocked()) { if (!playerTile->hasFlag(TILESTATE_PVPZONE)) { // player is trying to enter a pvp zone while being pz-locked diff --git a/src/lua/functions/creatures/npc/npc_functions.cpp b/src/lua/functions/creatures/npc/npc_functions.cpp index 0df0b55041a..c4f447c8823 100644 --- a/src/lua/functions/creatures/npc/npc_functions.cpp +++ b/src/lua/functions/creatures/npc/npc_functions.cpp @@ -384,7 +384,6 @@ int NpcFunctions::luaNpcOpenShopWindowTable(lua_State* L) { lua_pushnil(L); while (lua_next(L, 3) != 0) { const auto tableIndex = lua_gettop(L); - ShopBlock item; auto itemId = getField(L, tableIndex, "clientId"); auto subType = getField(L, tableIndex, "subType"); @@ -397,10 +396,11 @@ int NpcFunctions::luaNpcOpenShopWindowTable(lua_State* L) { auto sellPrice = getField(L, tableIndex, "sell"); auto storageKey = getField(L, tableIndex, "storageKey"); auto storageValue = getField(L, tableIndex, "storageValue"); - auto realName = getFieldString(L, tableIndex, "name"); - g_logger().debug("[{}] item '{}' sell price '{}', buyprice '{}'", __FUNCTION__, realName, sellPrice, buyPrice); - - items.emplace_back(itemId, subType, buyPrice, sellPrice, storageKey, storageValue, std::move(realName)); + auto itemName = getFieldString(L, tableIndex, "itemName"); + if (itemName.empty()) { + itemName = Item::items[itemId].name; + } + items.emplace_back(itemId, subType, buyPrice, sellPrice, storageKey, storageValue, itemName); lua_pop(L, 8); } lua_pop(L, 3); diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 408f6c12904..357c85521fb 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -515,9 +515,11 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS return; } - if (g_configManager().getBoolean(ONE_PLAYER_ON_ACCOUNT, __FUNCTION__) && player->getAccountType() < account::ACCOUNT_TYPE_GAMEMASTER && g_game().getPlayerByAccount(player->getAccountId())) { + auto onlineCount = g_game().getPlayersByAccount(player->getAccount()).size(); + auto maxOnline = g_configManager().getNumber(MAX_PLAYERS_PER_ACCOUNT, __FUNCTION__); + if (player->getAccountType() < account::ACCOUNT_TYPE_GAMEMASTER && onlineCount >= maxOnline) { g_game().removePlayerUniqueLogin(player); - disconnectClient("You may only login with one character\nof your account at the same time."); + disconnectClient(fmt::format("You may only login with {} character{}\nof your account at the same time.", maxOnline, maxOnline > 1 ? "s" : "")); return; } @@ -570,6 +572,24 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS player->setOperatingSystem(operatingSystem); + const auto tile = g_game().map.getOrCreateTile(player->getLoginPosition()); + // moving from a pz tile to a non-pz tile + if (maxOnline > 1 && player->getAccountType() < account::ACCOUNT_TYPE_GAMEMASTER && !tile->hasFlag(TILESTATE_PROTECTIONZONE)) { + auto maxOutsizePZ = g_configManager().getNumber(MAX_PLAYERS_OUTSIDE_PZ_PER_ACCOUNT, __FUNCTION__); + auto accountPlayers = g_game().getPlayersByAccount(player->getAccount()); + int countOutsizePZ = 0; + for (const auto &accountPlayer : accountPlayers) { + if (accountPlayer != player && accountPlayer->getTile() && !accountPlayer->getTile()->hasFlag(TILESTATE_PROTECTIONZONE)) { + ++countOutsizePZ; + } + } + if (countOutsizePZ >= maxOutsizePZ) { + g_game().removePlayerUniqueLogin(player); + disconnectClient(fmt::format("You can only have {} character{} from your account outside of a protection zone.", maxOutsizePZ == 1 ? "one" : std::to_string(maxOutsizePZ), maxOutsizePZ > 1 ? "s" : "")); + return; + } + } + if (!g_game().placeCreature(player, player->getLoginPosition()) && !g_game().placeCreature(player, player->getTemplePosition(), false, true)) { g_game().removePlayerUniqueLogin(player); disconnectClient("Temple position is wrong. Please, contact the administrator.");