Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions config.lua.dist
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ maxPlayersOutsidePZPerAccount = 1
toggleMaxConnectionsByIP = false
maxIPConnections = 3

-- Player Concurrency Lock
-- NOTE: togglePlayerLock blocks character logins during web transactions (marketplace, admin changes, character bazaar)
-- NOTE: playerLockTimeout defines the lock expiry in seconds to prevent deadlock if a web request fails
togglePlayerLock = true
playerLockTimeout = 60

-- Packet Compression
-- Minimize network bandwith and reduce ping
-- Levels: 0 = disabled, 1 = best speed, 9 = best compression
Expand Down
37 changes: 37 additions & 0 deletions data/migrations/66.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local function columnExists(tableName, columnName)
local resultId = db.storeQuery(string.format("SELECT 1 FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = '%s' AND `COLUMN_NAME` = '%s' LIMIT 1;", tableName, columnName))
if resultId then
Result.free(resultId)
return true
end
return false
end

local function indexExists(tableName, indexName)
local resultId = db.storeQuery(string.format("SELECT 1 FROM `information_schema`.`STATISTICS` WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = '%s' AND `INDEX_NAME` = '%s' LIMIT 1;", tableName, indexName))
if resultId then
Result.free(resultId)
return true
end
return false
end

function onUpdateDatabase()
logger.info("Updating database to version 66 (feat: player concurrency lock columns and index)")

if not columnExists("players", "is_locked") then
db.query("ALTER TABLE `players` ADD COLUMN `is_locked` tinyint(1) NOT NULL DEFAULT '0';")
end

if not columnExists("players", "locked_at") then
db.query("ALTER TABLE `players` ADD COLUMN `locked_at` bigint(20) NOT NULL DEFAULT '0';")
end

if not columnExists("players", "lock_reason") then
db.query("ALTER TABLE `players` ADD COLUMN `lock_reason` varchar(64) NOT NULL DEFAULT '';")
end

if not indexExists("players", "idx_players_concurrency_lock") then
db.query("ALTER TABLE `players` ADD INDEX `idx_players_concurrency_lock` (`is_locked`, `locked_at`);")
end
end
6 changes: 5 additions & 1 deletion schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS `server_config` (
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `server_config` (`config`, `value`, `world_id`) VALUES ('db_version', '65', 1), ('motd_hash', '', 1), ('motd_num', '0', 1), ('players_record', '0', 1);
INSERT INTO `server_config` (`config`, `value`, `world_id`) VALUES ('db_version', '66', 1), ('motd_hash', '', 1), ('motd_num', '0', 1), ('players_record', '0', 1);

-- Table structure `accounts`
CREATE TABLE IF NOT EXISTS `accounts` (
Expand Down Expand Up @@ -180,8 +180,12 @@ CREATE TABLE IF NOT EXISTS `players` (
`harmony` int(10) UNSIGNED NOT NULL DEFAULT '0',
`weapon_proficiencies` mediumblob DEFAULT NULL,
`charbazaar` tinyint(1) NOT NULL DEFAULT '0',
`is_locked` tinyint(1) NOT NULL DEFAULT '0',
`locked_at` bigint(20) NOT NULL DEFAULT '0',
`lock_reason` varchar(64) NOT NULL DEFAULT '',
INDEX `account_id` (`account_id`),
INDEX `vocation` (`vocation`),
INDEX `idx_players_concurrency_lock` (`is_locked`, `locked_at`),
CONSTRAINT `players_pk` PRIMARY KEY (`id`),
CONSTRAINT `players_unique` UNIQUE (`name`),
CONSTRAINT `players_account_fk`
Expand Down
2 changes: 2 additions & 0 deletions src/config/config_enums.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ enum ConfigKey_t : uint16_t {
PARTY_SHARE_LOOT_BOOSTS,
PARTY_SHARE_LOOT_BOOSTS_DIMINISHING_FACTOR,
PARTY_SHARE_RANGE_MULTIPLIER,
PLAYER_LOCK_TIMEOUT,
PREMIUM_DEPOT_LIMIT,
PREY_BONUS_REROLL_PRICE,
PREY_BONUS_TIME,
Expand Down Expand Up @@ -366,6 +367,7 @@ enum ConfigKey_t : uint16_t {
TOGGLE_MAP_CUSTOM,
TOGGLE_MAX_CONNECTIONS_BY_IP,
TOGGLE_MOUNT_IN_PZ,
TOGGLE_PLAYER_LOCK,
TOGGLE_RECEIVE_REWARD,
TOGGLE_SAVE_ASYNC,
TOGGLE_SAVE_INTERVAL,
Expand Down
2 changes: 2 additions & 0 deletions src/config/configmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ bool ConfigManager::load() {
loadBoolConfig(L, TOGGLE_IMBUEMENT_SHRINE_STORAGE, "toggleImbuementShrineStorage", true);
loadBoolConfig(L, TOGGLE_MAX_CONNECTIONS_BY_IP, "toggleMaxConnectionsByIP", false);
loadBoolConfig(L, TOGGLE_MOUNT_IN_PZ, "toggleMountInProtectionZone", false);
loadBoolConfig(L, TOGGLE_PLAYER_LOCK, "togglePlayerLock", true);
loadBoolConfig(L, TOGGLE_RECEIVE_REWARD, "toggleReceiveReward", false);
loadBoolConfig(L, TOGGLE_SAVE_ASYNC, "toggleSaveAsync", false);
loadBoolConfig(L, TOGGLE_SAVE_INTERVAL, "toggleSaveInterval", false);
Expand Down Expand Up @@ -369,6 +370,7 @@ bool ConfigManager::load() {
loadIntConfig(L, PARALYZE_DELAY_INTERVAL, "timeAfterParalyze", 200);
loadIntConfig(L, PARALLELISM, "parallelism", 2);
loadIntConfig(L, PARTY_LIST_MAX_DISTANCE, "partyListMaxDistance", 0);
loadIntConfig(L, PLAYER_LOCK_TIMEOUT, "playerLockTimeout", 60);
loadIntConfig(L, PREMIUM_DEPOT_LIMIT, "premiumDepotLimit", 8000);
loadIntConfig(L, PREY_BONUS_REROLL_PRICE, "preyBonusRerollPrice", 1);
loadIntConfig(L, PREY_BONUS_TIME, "preyBonusTime", 7200);
Expand Down
14 changes: 13 additions & 1 deletion src/io/functions/iologindata_load_player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ void IOLoginDataLoad::loadItems(ItemsMap &itemsMap, const DBResult_ptr &result,
bool IOLoginDataLoad::preLoadPlayer(const std::shared_ptr<Player> &player, const std::string &name) {
Database &db = Database::getInstance();

std::string query = fmt::format("SELECT `id`, `account_id`, `group_id`, `deletion`, `world_id` FROM `players` WHERE `name` = {}", db.escapeString(name));
std::string query = fmt::format("SELECT `id`, `account_id`, `group_id`, `deletion`, `world_id`, `is_locked`, `locked_at`, `lock_reason`, UNIX_TIMESTAMP() AS `db_time` FROM `players` WHERE `name` = {}", db.escapeString(name));
DBResult_ptr result = db.storeQuery(query);
if (!result) {
return false;
Expand All @@ -94,6 +94,18 @@ bool IOLoginDataLoad::preLoadPlayer(const std::shared_ptr<Player> &player, const
return false;
}

if (g_configManager().getBoolean(TOGGLE_PLAYER_LOCK)) {
if (result->getNumber<uint8_t>("is_locked") == 1) {
auto lockedAt = result->getNumber<int64_t>("locked_at");
auto dbTime = result->getNumber<int64_t>("db_time");
auto timeout = std::max<int32_t>(1, g_configManager().getNumber(PLAYER_LOCK_TIMEOUT));
if (lockedAt > 0 && lockedAt <= dbTime + timeout && (dbTime - lockedAt < timeout)) {
g_logger().warn("Player {} login rejected: character is currently locked for a web/market transaction (reason: {})", name, result->getString("lock_reason"));
return false;
}
}
}

player->worldId = result->getNumber<uint8_t>("world_id");

player->setGUID(result->getNumber<uint32_t>("id"));
Expand Down
Loading