From b7387f653d24ab5f93c91735b8d91fa6d629d489 Mon Sep 17 00:00:00 2001 From: Andrew <47818697+Nyeriah@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:31:00 -0300 Subject: [PATCH] feat(Core/Player): restore additional saves behind a config bitmask (#26825) --- .../apps/worldserver/worldserver.conf.dist | 14 ++++++ .../game/Achievements/AchievementMgr.cpp | 2 + src/server/game/Entities/Player/Player.h | 4 +- .../game/Entities/Player/PlayerStorage.cpp | 12 +++++ .../game/Entities/Player/PlayerUpdates.cpp | 48 +++++++++++++++++++ src/server/game/World/WorldConfig.cpp | 1 + src/server/game/World/WorldConfig.h | 1 + 7 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index d3b9db4c1..ff176fa0c 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -1838,6 +1838,20 @@ PlayerSave.Stats.MinLevel = 0 PlayerSave.Stats.SaveOnlyOnLogout = 1 +# +# PlayerSave.AdditionalSaves +# Description: Bitmask of player data to save to the database a few seconds after +# important changes instead of waiting for the next periodic save +# (PlayerSaveInterval). Reduces the progress lost on a server crash. +# Combine the values to enable multiple triggers. +# Values: 1 - Inventory and gold (after looting an item of rare or better quality) +# 2 - Quest status (after quest status changes) +# 4 - Achievements (after completing an achievement) +# Default: 0 - (Disabled) +# 7 - (All of the above) + +PlayerSave.AdditionalSaves = 0 + # # CleanCharacterDB # Description: Clean out deprecated achievements, skills, spells and talents from the db. diff --git a/src/server/game/Achievements/AchievementMgr.cpp b/src/server/game/Achievements/AchievementMgr.cpp index 55cb41b42..210d7818e 100644 --- a/src/server/game/Achievements/AchievementMgr.cpp +++ b/src/server/game/Achievements/AchievementMgr.cpp @@ -2333,6 +2333,8 @@ void AchievementMgr::CompletedAchievement(AchievementEntry const* achievement) } } + _player->AdditionalSavingAddMask(ADDITIONAL_SAVING_ACHIEVEMENTS); + if (achievement->flags & (ACHIEVEMENT_FLAG_REALM_FIRST_REACH | ACHIEVEMENT_FLAG_REALM_FIRST_KILL) && !_player->GetSession()->HasPermission(rbac::RBAC_PERM_CANNOT_EARN_REALM_FIRST_ACHIEVEMENTS)) sAchievementMgr->SetRealmCompleted(achievement); diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index fe93e67b3..f62853f9e 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -988,6 +988,7 @@ enum AdditionalSaving ADDITIONAL_SAVING_NONE = 0x00, ADDITIONAL_SAVING_INVENTORY_AND_GOLD = 0x01, ADDITIONAL_SAVING_QUEST_STATUS = 0x02, + ADDITIONAL_SAVING_ACHIEVEMENTS = 0x04, }; enum PlayerCommandStates @@ -2618,7 +2619,7 @@ public: bool IsFreeFlying() const { return HasAuraType(SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED) || HasAuraType(SPELL_AURA_FLY); } // saving - void AdditionalSavingAddMask(uint8 mask) { m_additionalSaveTimer = 2000; m_additionalSaveMask |= mask; } + void AdditionalSavingAddMask(uint8 mask); // arena spectator [[nodiscard]] bool IsSpectator() const { return m_ExtraFlags & PLAYER_EXTRA_SPECTATOR_ON; } void SetIsSpectator(bool on); @@ -2814,6 +2815,7 @@ protected: void _SaveCharacter(bool create, CharacterDatabaseTransaction trans); void _SaveInstanceTimeRestrictions(CharacterDatabaseTransaction trans); void _SavePlayerSettings(CharacterDatabaseTransaction trans); + void UpdateAdditionalSaves(uint32 p_time); /*********************************************************/ /*** ENVIRONMENTAL SYSTEM ***/ diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index 7083bfbd3..58cd4196e 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -7254,6 +7254,18 @@ void Player::SaveToDB(CharacterDatabaseTransaction trans, bool create, bool logo pet->SavePetToDB(PET_SAVE_AS_CURRENT); } +// flag data to be saved by UpdateAdditionalSaves a moment after an important change, +// filtered by the PlayerSave.AdditionalSaves config mask +void Player::AdditionalSavingAddMask(uint8 mask) +{ + mask &= sWorld->getIntConfig(CONFIG_ADDITIONAL_SAVES); + if (!mask) + return; + + m_additionalSaveTimer = 2000; + m_additionalSaveMask |= mask; +} + // fast save function for item/money cheating preventing - save only inventory and money state void Player::SaveInventoryAndGoldToDB(CharacterDatabaseTransaction trans) { diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index 1ba0916f2..3aa4894e9 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -332,6 +332,8 @@ void Player::Update(uint32 p_time) } } + UpdateAdditionalSaves(p_time); + // Handle Water/drowning HandleDrowning(p_time); @@ -2399,3 +2401,49 @@ void Player::ProcessSpellQueue() break; } } + +// save only the data flagged by AdditionalSavingAddMask shortly after +// important changes, so a crash loses at most a few seconds of them +void Player::UpdateAdditionalSaves(uint32 p_time) +{ + if (!m_additionalSaveTimer || GetSession()->isLogingOut()) + return; + + if (m_additionalSaveTimer > p_time) + { + m_additionalSaveTimer -= p_time; + return; + } + + uint8 mask = m_additionalSaveMask; + m_additionalSaveTimer = 0; + m_additionalSaveMask = 0; + + CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); + + if (mask & ADDITIONAL_SAVING_INVENTORY_AND_GOLD) + SaveInventoryAndGoldToDB(trans); + + if (mask & ADDITIONAL_SAVING_QUEST_STATUS) + { + _SaveQuestStatus(trans); + + // if nothing changed, nothing will happen + _SaveDailyQuestStatus(trans); + _SaveWeeklyQuestStatus(trans); + _SaveSeasonalQuestStatus(trans); + _SaveMonthlyQuestStatus(trans); + } + + if (mask & ADDITIONAL_SAVING_ACHIEVEMENTS) + { + m_achievementMgr->SaveToDB(trans); + + // achievements are often earned together with skill or gold changes + // (professions, riding, wealth), save those too to keep the DB consistent + _SaveSkills(trans); + SaveGoldToDB(trans); + } + + CharacterDatabase.CommitTransaction(trans); +} diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index bd6f0a300..0990520f7 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -168,6 +168,7 @@ void WorldConfig::BuildConfigCache() SetConfigValue(CONFIG_INTERVAL_SAVE, "PlayerSaveInterval", 900000); SetConfigValue(CONFIG_INTERVAL_DISCONNECT_TOLERANCE, "DisconnectToleranceInterval", 0); SetConfigValue(CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, "PlayerSave.Stats.SaveOnlyOnLogout", true); + SetConfigValue(CONFIG_ADDITIONAL_SAVES, "PlayerSave.AdditionalSaves", 0); SetConfigValue(CONFIG_VALIDATE_SKILL_LEARNED_BY_SPELLS, "ValidateSkillLearnedBySpells", true); SetConfigValue(CONFIG_MIN_LEVEL_STAT_SAVE, "PlayerSave.Stats.MinLevel", 0, ConfigValueCache::Reloadable::Yes, [](uint32 const& value) { return value < MAX_LEVEL; }, "< MAX_LEVEL"); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index d4e3c79fa..56792c005 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -27,6 +27,7 @@ enum ServerConfigs CONFIG_ALLOW_PLAYER_COMMANDS, CONFIG_CLEAN_CHARACTER_DB, CONFIG_STATS_SAVE_ONLY_ON_LOGOUT, + CONFIG_ADDITIONAL_SAVES, CONFIG_ALLOW_TWO_SIDE_ACCOUNTS, CONFIG_ALLOW_TWO_SIDE_INTERACTION_CALENDAR, CONFIG_ALLOW_TWO_SIDE_INTERACTION_CHAT,