mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat: refuse stale character saves with a save generation
Every character gets a save generation in charxml. A world bumps it when it loads the character for play, and every save from that world only goes through while the stored generation is still the one it loaded or last saved (and moves it on). Dashboard edits, restores and maintenance writes bump it too. A world that lost the character to another world (a disconnect noticed late, a zone transfer, an instance migration) or to a dashboard edit can no longer overwrite the newer data: the save is refused, logged and audited as stale_save_refused, the world stops saving that character and a player still connected to it is disconnected with the save failure reason so they reload. Fixes #639 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,26 @@ std::string MySQLDatabase::GetCharacterXml(const LWOOBJID charId) {
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string_view lxfml) {
|
||||
ExecuteUpdate("UPDATE charxml SET xml_data = ? WHERE id = ?;", lxfml, charId);
|
||||
ExecuteUpdate("UPDATE charxml SET xml_data = ?, save_generation = save_generation + 1 WHERE id = ?;", lxfml, charId);
|
||||
}
|
||||
|
||||
std::optional<ICharXml::CharacterXml> MySQLDatabase::ClaimCharacterXml(const LWOOBJID charId) {
|
||||
ExecuteUpdate("UPDATE charxml SET save_generation = save_generation + 1 WHERE id = ?;", charId);
|
||||
// Read both from one row, so a write in between still leaves a matching pair
|
||||
auto result = ExecuteSelect("SELECT xml_data, save_generation FROM charxml WHERE id = ? LIMIT 1;", charId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return CharacterXml{ result->getString("xml_data").c_str(), static_cast<uint64_t>(result->getInt64("save_generation")) };
|
||||
}
|
||||
|
||||
bool MySQLDatabase::SaveCharacterXml(const LWOOBJID charId, const std::string_view lxfml, const uint64_t generation) {
|
||||
// The generation always changes, so a matching row is always counted as affected
|
||||
return ExecuteUpdate("UPDATE charxml SET xml_data = ?, save_generation = ? WHERE id = ? AND save_generation = ?;",
|
||||
lxfml, static_cast<int64_t>(generation + 1), charId, static_cast<int64_t>(generation)) > 0;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::GetCharacterSaveGeneration(const LWOOBJID charId) {
|
||||
auto result = ExecuteSelect("SELECT save_generation FROM charxml WHERE id = ? LIMIT 1;", charId);
|
||||
return result->next() ? static_cast<uint64_t>(result->getInt64("save_generation")) : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
|
||||
Reference in New Issue
Block a user