2023-06-20 04:33:09 +00:00
|
|
|
#include "StarPlayerStorage.hpp"
|
|
|
|
#include "StarFile.hpp"
|
|
|
|
#include "StarLogging.hpp"
|
|
|
|
#include "StarIterator.hpp"
|
|
|
|
#include "StarTime.hpp"
|
|
|
|
#include "StarConfiguration.hpp"
|
|
|
|
#include "StarPlayer.hpp"
|
|
|
|
#include "StarAssets.hpp"
|
|
|
|
#include "StarEntityFactory.hpp"
|
|
|
|
#include "StarRoot.hpp"
|
2023-07-22 12:31:04 +00:00
|
|
|
#include "StarText.hpp"
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
namespace Star {
|
|
|
|
|
|
|
|
PlayerStorage::PlayerStorage(String const& storageDir) {
|
|
|
|
m_storageDirectory = storageDir;
|
2024-03-08 09:09:27 +00:00
|
|
|
m_backupDirectory = File::relativeTo(m_storageDirectory, "backup");
|
2023-06-20 04:33:09 +00:00
|
|
|
if (!File::isDirectory(m_storageDirectory)) {
|
|
|
|
Logger::info("Creating player storage directory");
|
|
|
|
File::makeDirectory(m_storageDirectory);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto configuration = Root::singleton().configuration();
|
|
|
|
if (configuration->get("clearPlayerFiles").toBool()) {
|
|
|
|
Logger::info("Clearing all player files");
|
|
|
|
for (auto file : File::dirList(m_storageDirectory)) {
|
|
|
|
if (!file.second)
|
|
|
|
File::remove(File::relativeTo(m_storageDirectory, file.first));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
auto versioningDatabase = Root::singleton().versioningDatabase();
|
|
|
|
auto entityFactory = Root::singleton().entityFactory();
|
|
|
|
|
|
|
|
for (auto file : File::dirList(m_storageDirectory)) {
|
|
|
|
if (file.second)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
String filename = File::relativeTo(m_storageDirectory, file.first);
|
|
|
|
if (filename.endsWith(".player")) {
|
|
|
|
try {
|
2023-08-02 12:25:20 +00:00
|
|
|
auto json = VersionedJson::readFile(filename);
|
|
|
|
Uuid uuid(json.content.getString("uuid"));
|
2023-06-20 04:33:09 +00:00
|
|
|
auto& playerCacheData = m_savedPlayersCache[uuid];
|
2023-08-02 12:25:20 +00:00
|
|
|
playerCacheData = entityFactory->loadVersionedJson(json, EntityType::Player);
|
2023-08-02 13:05:30 +00:00
|
|
|
m_playerFileNames.insert(uuid, file.first.rsplit('.', 1).at(0));
|
2023-06-20 04:33:09 +00:00
|
|
|
} catch (std::exception const& e) {
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::error("Error loading player file, ignoring! {} : {}", filename, outputException(e, false));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove all the player entries that are missing player data or fail to
|
|
|
|
// load.
|
|
|
|
auto it = makeSMutableMapIterator(m_savedPlayersCache);
|
|
|
|
while (it.hasNext()) {
|
|
|
|
auto& entry = it.next();
|
|
|
|
if (entry.second.isNull()) {
|
|
|
|
it.remove();
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
auto entityFactory = Root::singleton().entityFactory();
|
|
|
|
auto player = as<Player>(entityFactory->diskLoadEntity(EntityType::Player, entry.second));
|
|
|
|
if (player->uuid() != entry.first)
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("Uuid mismatch in loaded player with filename uuid '{}'", entry.first.hex()));
|
2023-06-20 04:33:09 +00:00
|
|
|
} catch (StarException const& e) {
|
2023-11-26 23:13:21 +00:00
|
|
|
auto& fileName = uuidFileName(entry.first);
|
|
|
|
String uuidHex = entry.first.hex();
|
|
|
|
if (uuidHex == fileName)
|
|
|
|
Logger::error("Failed to validate player with uuid {} : {}", uuidHex, outputException(e, true));
|
|
|
|
else
|
|
|
|
Logger::error("Failed to validate player with uuid {} ({}.player) : {}", uuidHex, fileName, outputException(e, true));
|
2023-06-20 04:33:09 +00:00
|
|
|
it.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
String filename = File::relativeTo(m_storageDirectory, "metadata");
|
|
|
|
m_metadata = Json::parseJson(File::readFileString(filename)).toObject();
|
|
|
|
|
|
|
|
if (auto order = m_metadata.value("order")) {
|
2023-11-26 23:13:21 +00:00
|
|
|
for (auto const& jUuid : order.iterateArray()) {
|
|
|
|
auto entry = m_savedPlayersCache.find(Uuid(jUuid.toString()));
|
|
|
|
if (entry != m_savedPlayersCache.end())
|
|
|
|
m_savedPlayersCache.toBack(entry);
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
} catch (std::exception const& e) {
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::warn("Error loading player storage metadata file, resetting: {}", outputException(e, false));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
PlayerStorage::~PlayerStorage() {
|
|
|
|
writeMetadata();
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t PlayerStorage::playerCount() const {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
return m_savedPlayersCache.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
Maybe<Uuid> PlayerStorage::playerUuidAt(size_t index) {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
if (index < m_savedPlayersCache.size())
|
|
|
|
return m_savedPlayersCache.keyAt(index);
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2023-07-24 07:54:31 +00:00
|
|
|
Maybe<Uuid> PlayerStorage::playerUuidByName(String const& name, Maybe<Uuid> except) {
|
2023-07-22 12:31:04 +00:00
|
|
|
String cleanMatch = Text::stripEscapeCodes(name).toLower();
|
|
|
|
Maybe<Uuid> uuid;
|
|
|
|
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
|
|
|
|
size_t longest = SIZE_MAX;
|
|
|
|
for (auto& cache : m_savedPlayersCache) {
|
2023-07-24 07:54:31 +00:00
|
|
|
if (except && *except == cache.first)
|
|
|
|
continue;
|
|
|
|
else if (auto name = cache.second.optQueryString("identity.name")) {
|
2023-07-22 12:31:04 +00:00
|
|
|
auto cleanName = Text::stripEscapeCodes(*name).toLower();
|
|
|
|
auto len = cleanName.size();
|
|
|
|
if (len < longest && cleanName.utf8().rfind(cleanMatch.utf8()) == 0) {
|
|
|
|
longest = len;
|
|
|
|
uuid = cache.first;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return uuid;
|
|
|
|
}
|
|
|
|
|
|
|
|
Json PlayerStorage::savePlayer(PlayerPtr const& player) {
|
2023-06-20 04:33:09 +00:00
|
|
|
auto entityFactory = Root::singleton().entityFactory();
|
|
|
|
auto versioningDatabase = Root::singleton().versioningDatabase();
|
|
|
|
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
|
|
|
|
auto uuid = player->uuid();
|
|
|
|
|
|
|
|
auto& playerCacheData = m_savedPlayersCache[uuid];
|
2023-08-02 12:25:20 +00:00
|
|
|
if (!m_playerFileNames.hasLeftValue(uuid))
|
|
|
|
m_playerFileNames.insert(uuid, uuid.hex());
|
2023-06-20 04:33:09 +00:00
|
|
|
auto newPlayerData = player->diskStore();
|
|
|
|
if (playerCacheData != newPlayerData) {
|
|
|
|
playerCacheData = newPlayerData;
|
|
|
|
VersionedJson versionedJson = entityFactory->storeVersionedJson(EntityType::Player, playerCacheData);
|
2023-08-02 12:25:20 +00:00
|
|
|
VersionedJson::writeFile(versionedJson, File::relativeTo(m_storageDirectory, strf("{}.player", uuidFileName(uuid))));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2023-07-22 12:31:04 +00:00
|
|
|
return newPlayerData;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
Maybe<Json> PlayerStorage::maybeGetPlayerData(Uuid const& uuid) {
|
2023-06-20 04:33:09 +00:00
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
2023-07-22 12:31:04 +00:00
|
|
|
if (auto cache = m_savedPlayersCache.ptr(uuid))
|
|
|
|
return *cache;
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
Json PlayerStorage::getPlayerData(Uuid const& uuid) {
|
|
|
|
auto data = maybeGetPlayerData(uuid);
|
|
|
|
if (!data)
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
|
2023-07-22 12:31:04 +00:00
|
|
|
else
|
|
|
|
return *data;
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-07-22 12:31:04 +00:00
|
|
|
PlayerPtr PlayerStorage::loadPlayer(Uuid const& uuid) {
|
|
|
|
auto playerCacheData = getPlayerData(uuid);
|
2023-06-20 04:33:09 +00:00
|
|
|
auto entityFactory = Root::singleton().entityFactory();
|
|
|
|
try {
|
|
|
|
auto player = convert<Player>(entityFactory->diskLoadEntity(EntityType::Player, playerCacheData));
|
|
|
|
if (player->uuid() != uuid)
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("Uuid mismatch in loaded player with filename uuid '{}'", uuid.hex()));
|
2023-06-20 04:33:09 +00:00
|
|
|
return player;
|
|
|
|
} catch (std::exception const& e) {
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::error("Error loading player file, ignoring! {}", outputException(e, false));
|
2023-07-22 12:31:04 +00:00
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
2023-06-20 04:33:09 +00:00
|
|
|
m_savedPlayersCache.remove(uuid);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlayerStorage::deletePlayer(Uuid const& uuid) {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
if (!m_savedPlayersCache.contains(uuid))
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
m_savedPlayersCache.remove(uuid);
|
|
|
|
|
2023-08-02 12:56:36 +00:00
|
|
|
auto uuidHex = uuid.hex();
|
|
|
|
auto storagePrefix = File::relativeTo(m_storageDirectory, uuidHex);
|
|
|
|
auto backupPrefix = File::relativeTo(m_backupDirectory, uuidHex);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-08-02 12:56:36 +00:00
|
|
|
auto removeIfExists = [](String const& prefix, String const& suffix) {
|
|
|
|
if (File::exists(prefix + suffix)) {
|
|
|
|
File::remove(prefix + suffix);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-08-02 12:56:36 +00:00
|
|
|
removeIfExists(storagePrefix, ".player");
|
|
|
|
removeIfExists(storagePrefix, ".shipworld");
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
auto configuration = Root::singleton().configuration();
|
|
|
|
unsigned playerBackupFileCount = configuration->get("playerBackupFileCount").toUInt();
|
|
|
|
|
|
|
|
for (unsigned i = 1; i <= playerBackupFileCount; ++i) {
|
2023-08-02 12:56:36 +00:00
|
|
|
removeIfExists(backupPrefix, strf(".player.bak{}", i));
|
|
|
|
removeIfExists(backupPrefix, strf(".shipworld.bak{}", i));
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
WorldChunks PlayerStorage::loadShipData(Uuid const& uuid) {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
if (!m_savedPlayersCache.contains(uuid))
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-08-02 12:25:20 +00:00
|
|
|
String filename = File::relativeTo(m_storageDirectory, strf("{}.shipworld", uuidFileName(uuid)));
|
2023-06-20 04:33:09 +00:00
|
|
|
try {
|
|
|
|
if (File::exists(filename))
|
|
|
|
return WorldStorage::getWorldChunksFromFile(filename);
|
|
|
|
} catch (StarException const& e) {
|
2023-06-27 10:23:44 +00:00
|
|
|
Logger::error("Failed to load shipworld file, removing {} : {}", filename, outputException(e, false));
|
2023-06-20 04:33:09 +00:00
|
|
|
File::remove(filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlayerStorage::applyShipUpdates(Uuid const& uuid, WorldChunks const& updates) {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
if (!m_savedPlayersCache.contains(uuid))
|
2023-06-27 10:23:44 +00:00
|
|
|
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (updates.empty())
|
|
|
|
return;
|
2023-08-02 12:25:20 +00:00
|
|
|
String filePath = File::relativeTo(m_storageDirectory, strf("{}.shipworld", uuidFileName(uuid)));
|
|
|
|
WorldStorage::applyWorldChunksUpdateToFile(filePath, updates);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void PlayerStorage::moveToFront(Uuid const& uuid) {
|
|
|
|
m_savedPlayersCache.toFront(uuid);
|
|
|
|
writeMetadata();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlayerStorage::backupCycle(Uuid const& uuid) {
|
|
|
|
RecursiveMutexLocker locker(m_mutex);
|
|
|
|
|
|
|
|
auto configuration = Root::singleton().configuration();
|
|
|
|
unsigned playerBackupFileCount = configuration->get("playerBackupFileCount").toUInt();
|
2023-08-02 12:25:20 +00:00
|
|
|
auto& fileName = uuidFileName(uuid);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-08-02 12:56:36 +00:00
|
|
|
auto path = [&](String const& dir, String const& extension) {
|
|
|
|
return File::relativeTo(dir, strf("{}.{}", fileName, extension));
|
|
|
|
};
|
|
|
|
|
2024-03-08 09:09:27 +00:00
|
|
|
if (!File::isDirectory(m_backupDirectory))
|
2023-08-02 12:56:36 +00:00
|
|
|
File::makeDirectory(m_backupDirectory);
|
|
|
|
|
|
|
|
File::backupFileInSequence(path(m_storageDirectory, "player"), path(m_backupDirectory, "player"), playerBackupFileCount, ".bak");
|
|
|
|
File::backupFileInSequence(path(m_storageDirectory, "shipworld"), path(m_backupDirectory, "shipworld"), playerBackupFileCount, ".bak");
|
|
|
|
File::backupFileInSequence(path(m_storageDirectory, "metadata"), path(m_backupDirectory, "metadata"), playerBackupFileCount, ".bak");
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void PlayerStorage::setMetadata(String key, Json value) {
|
2024-02-19 15:55:19 +00:00
|
|
|
auto& val = m_metadata[std::move(key)];
|
2023-06-20 04:33:09 +00:00
|
|
|
if (val != value) {
|
2024-02-19 15:55:19 +00:00
|
|
|
val = std::move(value);
|
2023-06-20 04:33:09 +00:00
|
|
|
writeMetadata();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Json PlayerStorage::getMetadata(String const& key) {
|
|
|
|
return m_metadata.value(key);
|
|
|
|
}
|
|
|
|
|
2023-11-24 09:35:21 +00:00
|
|
|
String const& PlayerStorage::uuidFileName(Uuid const& uuid) {
|
2023-08-02 12:25:20 +00:00
|
|
|
if (auto fileName = m_playerFileNames.rightPtr(uuid))
|
|
|
|
return *fileName;
|
2023-11-24 09:35:21 +00:00
|
|
|
else {
|
|
|
|
m_playerFileNames.insert(uuid, uuid.hex());
|
|
|
|
return *m_playerFileNames.rightPtr(uuid);
|
|
|
|
}
|
2023-08-02 12:25:20 +00:00
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
void PlayerStorage::writeMetadata() {
|
|
|
|
JsonArray order;
|
|
|
|
for (auto const& p : m_savedPlayersCache)
|
|
|
|
order.append(p.first.hex());
|
|
|
|
|
2024-02-19 15:55:19 +00:00
|
|
|
m_metadata["order"] = std::move(order);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
String filename = File::relativeTo(m_storageDirectory, "metadata");
|
|
|
|
File::overwriteFileWithRename(Json(m_metadata).printJson(0), filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|