mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(db): bbb_autosave table for the client's BBB autosave
The client sends SetBBBAutosave (996) with the model being built every five minutes, before an AFK kick and before quitting, and expects the server to rebuild an unfinished model later (RebuildBBBAutosaveMsg). This keeps the last one per character with the model items that were in the BBB inventory at the time. MySQL 82 / SQLite 65; parity test included. Refs #1632 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,7 @@
|
||||
#include "IContraband.h"
|
||||
#include "IPropertyRent.h"
|
||||
#include "IPropertyReputation.h"
|
||||
#include "IBbbAutosave.h"
|
||||
|
||||
#ifdef _DEBUG
|
||||
# define DLU_SQL_TRY_CATCH_RETHROW(x) do { try { x; } catch (std::exception& ex) { LOG("SQL Error: %s", ex.what()); throw; } } while(0)
|
||||
@@ -63,7 +64,7 @@ class GameDatabase :
|
||||
public IPropertyContents, public IProperty, public IPetNames, public ICharXml,
|
||||
public IMigrationHistory, public IUgc, public IFriends, public ICharInfo,
|
||||
public IAccounts, public IActivityLog, public IAccountsRewardCodes, public IIgnoreList,
|
||||
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation {
|
||||
public IBehaviors, public IUgcModularBuild, public IAuditLog, public IDashboardStats, public IAccountEmails, public IDashboardMaintenance, public IEconomyLedger, public IDashboardAdmin, public IServerConfig, public IScheduledTasks, public ICharacterSnapshots, public IAccountNotes, public IServerHealth, public IRelatedData, public IChatLog, public IAccountStrikes, public ISlashCommands, public IModeration, public IServerOperations, public IPlayerPositions, public IAiSuggestions, public ILiveOps, public IFeaturedProperties, public IMessageCaptures, public IContraband, public IPropertyRent, public IPropertyReputation, public IBbbAutosave {
|
||||
public:
|
||||
virtual ~GameDatabase() = default;
|
||||
// TODO: These should be made private.
|
||||
|
||||
38
dDatabase/GameDatabase/ITables/BbbAutosaveItems.h
Normal file
38
dDatabase/GameDatabase/ITables/BbbAutosaveItems.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#ifndef __BBBAUTOSAVEITEMS__H__
|
||||
#define __BBBAUTOSAVEITEMS__H__
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
// bbb_autosave.source_items: the item ids as a comma separated list
|
||||
namespace BbbAutosaveItems {
|
||||
inline std::string Join(const std::vector<LWOOBJID>& items) {
|
||||
std::string out;
|
||||
for (const auto id : items) {
|
||||
if (!out.empty()) out += ',';
|
||||
out += std::to_string(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<LWOOBJID> Parse(const std::string_view text) {
|
||||
std::vector<LWOOBJID> items;
|
||||
size_t start = 0;
|
||||
while (start < text.size()) {
|
||||
auto end = text.find(',', start);
|
||||
if (end == std::string_view::npos) end = text.size();
|
||||
const std::string part(text.substr(start, end - start));
|
||||
char* parsedEnd = nullptr;
|
||||
const auto id = std::strtoll(part.c_str(), &parsedEnd, 10);
|
||||
if (parsedEnd != part.c_str() && id != 0) items.push_back(id);
|
||||
start = end + 1;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
#endif //!__BBBAUTOSAVEITEMS__H__
|
||||
30
dDatabase/GameDatabase/ITables/IBbbAutosave.h
Normal file
30
dDatabase/GameDatabase/ITables/IBbbAutosave.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef __IBBBAUTOSAVE__H__
|
||||
#define __IBBBAUTOSAVE__H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
// The brick by brick model a character was building when the client last sent SetBBBAutosave (the client's quick save:
|
||||
// every five minutes, before an AFK kick and before quitting). The server rebuilds it into a model when the build ends
|
||||
// without a save (see docs/BuildWorkflow.md).
|
||||
class IBbbAutosave {
|
||||
public:
|
||||
struct Info {
|
||||
std::string lxfml; // sd0 compressed LXFML as the client sent it
|
||||
std::vector<LWOOBJID> sourceItems; // the models that were in the BBB inventory when it was sent
|
||||
int64_t updatedAt{}; // Unix seconds
|
||||
};
|
||||
|
||||
virtual std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) = 0;
|
||||
|
||||
// Replaces the character's autosave
|
||||
virtual void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) = 0;
|
||||
|
||||
virtual void DeleteBbbAutosave(const LWOOBJID characterId) = 0;
|
||||
};
|
||||
|
||||
#endif //!__IBBBAUTOSAVE__H__
|
||||
@@ -215,6 +215,11 @@ public:
|
||||
std::vector<ZoneLimit> GetZoneLimits() override;
|
||||
void SetZoneLimit(const ZoneLimit& limit) override;
|
||||
void DeleteZoneLimit(uint32_t zoneId) override;
|
||||
// IBbbAutosave
|
||||
std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) override;
|
||||
void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override;
|
||||
void DeleteBbbAutosave(const LWOOBJID characterId) override;
|
||||
|
||||
// IPropertyReputation
|
||||
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override;
|
||||
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override;
|
||||
|
||||
30
dDatabase/GameDatabase/MySQL/Tables/BbbAutosave.cpp
Normal file
30
dDatabase/GameDatabase/MySQL/Tables/BbbAutosave.cpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "BbbAutosaveItems.h"
|
||||
|
||||
std::optional<IBbbAutosave::Info> MySQLDatabase::GetBbbAutosave(const LWOOBJID characterId) {
|
||||
auto result = ExecuteSelect("SELECT lxfml, source_items, updated_at FROM bbb_autosave WHERE character_id = ?;", characterId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
|
||||
IBbbAutosave::Info info;
|
||||
std::unique_ptr<std::istream> blob(result->getBlob("lxfml"));
|
||||
std::stringstream data;
|
||||
if (blob) data << blob->rdbuf();
|
||||
info.lxfml = data.str();
|
||||
info.sourceItems = BbbAutosaveItems::Parse(result->getString("source_items").c_str());
|
||||
info.updatedAt = result->getInt64("updated_at");
|
||||
return info;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) {
|
||||
std::istringstream lxfml(info.lxfml);
|
||||
const auto items = BbbAutosaveItems::Join(info.sourceItems);
|
||||
ExecuteInsert(
|
||||
"INSERT INTO bbb_autosave (character_id, lxfml, source_items, updated_at) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE lxfml = VALUES(lxfml), source_items = VALUES(source_items), updated_at = VALUES(updated_at);",
|
||||
characterId, static_cast<const std::istream*>(&lxfml), items, info.updatedAt);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteBbbAutosave(const LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM bbb_autosave WHERE character_id = ?;", characterId);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
|
||||
"Contraband.cpp"
|
||||
"PropertyRent.cpp"
|
||||
"PropertyReputation.cpp"
|
||||
"BbbAutosave.cpp"
|
||||
"MessageCaptures.cpp"
|
||||
"ChatLog.cpp"
|
||||
"RelatedData.cpp"
|
||||
|
||||
@@ -199,6 +199,11 @@ public:
|
||||
std::vector<ZoneLimit> GetZoneLimits() override;
|
||||
void SetZoneLimit(const ZoneLimit& limit) override;
|
||||
void DeleteZoneLimit(uint32_t zoneId) override;
|
||||
// IBbbAutosave
|
||||
std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) override;
|
||||
void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override;
|
||||
void DeleteBbbAutosave(const LWOOBJID characterId) override;
|
||||
|
||||
// IPropertyReputation
|
||||
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override;
|
||||
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override;
|
||||
|
||||
29
dDatabase/GameDatabase/SQLite/Tables/BbbAutosave.cpp
Normal file
29
dDatabase/GameDatabase/SQLite/Tables/BbbAutosave.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "SQLiteDatabase.h"
|
||||
|
||||
#include "BbbAutosaveItems.h"
|
||||
|
||||
std::optional<IBbbAutosave::Info> SQLiteDatabase::GetBbbAutosave(const LWOOBJID characterId) {
|
||||
auto [_, result] = ExecuteSelect("SELECT lxfml, source_items, updated_at FROM bbb_autosave WHERE character_id = ?;", characterId);
|
||||
if (result.eof()) return std::nullopt;
|
||||
|
||||
IBbbAutosave::Info info;
|
||||
int length{};
|
||||
const auto* blob = result.getBlobField("lxfml", length);
|
||||
if (blob && length > 0) info.lxfml.assign(reinterpret_cast<const char*>(blob), length);
|
||||
info.sourceItems = BbbAutosaveItems::Parse(result.getStringField("source_items", ""));
|
||||
info.updatedAt = result.getInt64Field("updated_at");
|
||||
return info;
|
||||
}
|
||||
|
||||
void SQLiteDatabase::SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) {
|
||||
std::istringstream lxfml(info.lxfml);
|
||||
const auto items = BbbAutosaveItems::Join(info.sourceItems);
|
||||
ExecuteInsert(
|
||||
"INSERT INTO bbb_autosave (character_id, lxfml, source_items, updated_at) VALUES (?, ?, ?, ?) "
|
||||
"ON CONFLICT(character_id) DO UPDATE SET lxfml = excluded.lxfml, source_items = excluded.source_items, updated_at = excluded.updated_at;",
|
||||
characterId, static_cast<const std::istream*>(&lxfml), items, info.updatedAt);
|
||||
}
|
||||
|
||||
void SQLiteDatabase::DeleteBbbAutosave(const LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM bbb_autosave WHERE character_id = ?;", characterId);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ set(DDATABASES_DATABASES_SQLITE_TABLES_SOURCES
|
||||
"Contraband.cpp"
|
||||
"PropertyRent.cpp"
|
||||
"PropertyReputation.cpp"
|
||||
"BbbAutosave.cpp"
|
||||
"MessageCaptures.cpp"
|
||||
"ChatLog.cpp"
|
||||
"RelatedData.cpp"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define TESTSQLDATABASE_H
|
||||
|
||||
#include "GameDatabase.h"
|
||||
#include <map>
|
||||
|
||||
class TestSQLDatabase : public GameDatabase {
|
||||
void Connect() override;
|
||||
@@ -245,6 +246,16 @@ class TestSQLDatabase : public GameDatabase {
|
||||
std::vector<ZoneLimit> GetZoneLimits() override { return {}; }
|
||||
void SetZoneLimit(const ZoneLimit& limit) override {}
|
||||
void DeleteZoneLimit(uint32_t zoneId) override {}
|
||||
// IBbbAutosave: kept in memory so the build workflow can be tested
|
||||
std::optional<IBbbAutosave::Info> GetBbbAutosave(const LWOOBJID characterId) override {
|
||||
const auto it = m_BbbAutosaves.find(characterId);
|
||||
if (it == m_BbbAutosaves.end()) return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
void SetBbbAutosave(const LWOOBJID characterId, const IBbbAutosave::Info& info) override { m_BbbAutosaves[characterId] = info; }
|
||||
void DeleteBbbAutosave(const LWOOBJID characterId) override { m_BbbAutosaves.erase(characterId); }
|
||||
std::map<LWOOBJID, IBbbAutosave::Info> m_BbbAutosaves;
|
||||
|
||||
// IPropertyReputation
|
||||
VisitorHistory GetPropertyVisitorHistory(LWOOBJID propertyId, uint32_t accountId, uint32_t day, uint32_t days) override { return {}; }
|
||||
int64_t GetPropertyReputationOnDay(LWOOBJID propertyId, uint32_t day) override { return 0; }
|
||||
|
||||
Reference in New Issue
Block a user