osb/source/game/StarVersioningDatabase.cpp

220 lines
8.8 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#include "StarVersioningDatabase.hpp"
#include "StarDataStreamExtra.hpp"
#include "StarFormat.hpp"
#include "StarLexicalCast.hpp"
#include "StarFile.hpp"
#include "StarLogging.hpp"
#include "StarWorldLuaBindings.hpp"
#include "StarRootLuaBindings.hpp"
#include "StarUtilityLuaBindings.hpp"
#include "StarAssets.hpp"
#include "StarStoredFunctions.hpp"
#include "StarNpcDatabase.hpp"
#include "StarRoot.hpp"
#include "StarCelestialDatabase.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
char const* const VersionedJson::Magic = "SBVJ01";
size_t const VersionedJson::MagicStringSize = 6;
VersionedJson VersionedJson::readFile(String const& filename) {
DataStreamIODevice ds(File::open(filename, IOMode::Read));
if (ds.readBytes(MagicStringSize) != ByteArray(Magic, MagicStringSize))
throw IOException(strf("Wrong magic bytes at start of versioned json file, expected '%s'", Magic));
return ds.read<VersionedJson>();
}
void VersionedJson::writeFile(VersionedJson const& versionedJson, String const& filename) {
DataStreamBuffer ds;
ds.writeData(Magic, MagicStringSize);
ds.write(versionedJson);
File::overwriteFileWithRename(ds.takeData(), filename);
}
Json VersionedJson::toJson() const {
return JsonObject{
{"id", identifier},
{"version", version},
{"content", content}
};
}
VersionedJson VersionedJson::fromJson(Json const& source) {
// Old versions of VersionedJson used '__' to distinguish between actual
// content and versioned content, but this is no longer necessary or
// relevant.
auto id = source.optString("id").orMaybe(source.optString("__id"));
auto version = source.optUInt("version").orMaybe(source.optUInt("__version"));
auto content = source.opt("content").orMaybe(source.opt("__content"));
return {*id, (VersionNumber)*version, *content};
}
bool VersionedJson::empty() const {
return content.isNull();
}
void VersionedJson::expectIdentifier(String const& expectedIdentifier) const {
if (identifier != expectedIdentifier)
throw VersionedJsonException::format("VersionedJson identifier mismatch, expected '%s' but got '%s'", expectedIdentifier, identifier);
}
DataStream& operator>>(DataStream& ds, VersionedJson& versionedJson) {
ds.read(versionedJson.identifier);
// This is a holdover from when the verison number was optional in
// VersionedJson. We should convert versioned json binary files and the
// celestial chunk database and world storage to a new format eventually
versionedJson.version = ds.read<Maybe<VersionNumber>>().value();
ds.read(versionedJson.content);
return ds;
}
DataStream& operator<<(DataStream& ds, VersionedJson const& versionedJson) {
ds.write(versionedJson.identifier);
ds.write(Maybe<VersionNumber>(versionedJson.version));
ds.write(versionedJson.content);
return ds;
}
VersioningDatabase::VersioningDatabase() {
auto assets = Root::singleton().assets();
for (auto const& pair : assets->json("/versioning.config").iterateObject())
m_currentVersions[pair.first] = pair.second.toUInt();
for (auto const& scriptFile : assets->scan("/versioning/", ".lua")) {
try {
auto scriptParts = File::baseName(scriptFile).splitAny("_.");
if (scriptParts.size() != 4)
throw VersioningDatabaseException::format("Script file '%s' filename not of the form <identifier>_<fromversion>_<toversion>.lua", scriptFile);
String identifier = scriptParts.at(0);
VersionNumber fromVersion = lexicalCast<VersionNumber>(scriptParts.at(1));
VersionNumber toVersion = lexicalCast<VersionNumber>(scriptParts.at(2));
m_versionUpdateScripts[identifier.toLower()].append({scriptFile, fromVersion, toVersion});
} catch (StarException const&) {
throw VersioningDatabaseException::format("Error parsing version information from versioning script '%s'", scriptFile);
}
}
// Sort each set of update scripts first by fromVersion, and then in
// *reverse* order of toVersion. This way, the first matching script for a
// given fromVersion should take the json to the *furthest* toVersion.
for (auto& pair : m_versionUpdateScripts) {
pair.second.sort([](VersionUpdateScript const& lhs, VersionUpdateScript const& rhs) {
if (lhs.fromVersion != rhs.fromVersion)
return lhs.fromVersion < rhs.fromVersion;
else
return lhs.toVersion < rhs.toVersion;
});
}
}
VersionedJson VersioningDatabase::makeCurrentVersionedJson(String const& identifier, Json const& content) const {
RecursiveMutexLocker locker(m_mutex);
return VersionedJson{identifier, m_currentVersions.get(identifier), content};
}
bool VersioningDatabase::versionedJsonCurrent(VersionedJson const& versionedJson) const {
RecursiveMutexLocker locker(m_mutex);
return versionedJson.version == m_currentVersions.get(versionedJson.identifier);
}
VersionedJson VersioningDatabase::updateVersionedJson(VersionedJson const& versionedJson) const {
RecursiveMutexLocker locker(m_mutex);
auto& root = Root::singleton();
CelestialMasterDatabase celestialDatabase;
VersionedJson result = versionedJson;
Maybe<VersionNumber> targetVersion = m_currentVersions.maybe(versionedJson.identifier);
if (!targetVersion)
throw VersioningDatabaseException::format("Versioned JSON has an unregistered identifier '%s'", versionedJson.identifier);
LuaCallbacks celestialCallbacks;
celestialCallbacks.registerCallback("parameters", [&celestialDatabase](Json const& coord) {
return celestialDatabase.parameters(CelestialCoordinate(coord))->diskStore();
});
try {
for (auto const& updateScript : m_versionUpdateScripts.value(versionedJson.identifier.toLower())) {
if (result.version >= *targetVersion)
break;
if (updateScript.fromVersion == result.version) {
auto scriptContext = m_luaRoot.createContext();
scriptContext.load(*root.assets()->bytes(updateScript.script), updateScript.script);
scriptContext.setCallbacks("root", LuaBindings::makeRootCallbacks());
scriptContext.setCallbacks("sb", LuaBindings::makeUtilityCallbacks());
scriptContext.setCallbacks("celestial", celestialCallbacks);
scriptContext.setCallbacks("versioning", makeVersioningCallbacks());
result.content = scriptContext.invokePath<Json>("update", result.content);
if (!result.content) {
throw VersioningDatabaseException::format(
"Could not bring versionedJson with identifier '%s' and version %s forward to current version of %s, conversion script from %s to %s returned null (un-upgradeable)",
versionedJson.identifier, result.version, targetVersion, updateScript.fromVersion, updateScript.toVersion);
}
Logger::debug("Brought versionedJson '%s' from version %s to %s",
versionedJson.identifier, result.version, updateScript.toVersion);
result.version = updateScript.toVersion;
}
}
} catch (std::exception const& e) {
throw VersioningDatabaseException(strf("Could not bring versionedJson with identifier '%s' and version %s forward to current version of %s",
versionedJson.identifier, result.version, targetVersion), e);
}
if (result.version > *targetVersion) {
throw VersioningDatabaseException::format(
"VersionedJson with identifier '%s' and version %s is newer than current version of %s, cannot load",
versionedJson.identifier, result.version, targetVersion);
}
if (result.version != *targetVersion) {
throw VersioningDatabaseException::format(
"Could not bring VersionedJson with identifier '%s' and version %s forward to current version of %s, best version was %s",
versionedJson.identifier, result.version, targetVersion, result.version);
}
return result;
}
Json VersioningDatabase::loadVersionedJson(VersionedJson const& versionedJson, String const& expectedIdentifier) const {
versionedJson.expectIdentifier(expectedIdentifier);
if (versionedJsonCurrent(versionedJson))
return versionedJson.content;
return updateVersionedJson(versionedJson).content;
}
LuaCallbacks VersioningDatabase::makeVersioningCallbacks() const {
LuaCallbacks versioningCallbacks;
versioningCallbacks.registerCallback("loadVersionedJson", [this](String const& storagePath) {
try {
auto& root = Root::singleton();
String filePath = File::fullPath(root.toStoragePath(storagePath));
String basePath = File::fullPath(root.toStoragePath("."));
if (!filePath.beginsWith(basePath))
throw VersioningDatabaseException::format(
"Cannot load external VersionedJson outside of the Root storage path");
auto loadedJson = VersionedJson::readFile(filePath);
return updateVersionedJson(loadedJson).content;
} catch (IOException const& e) {
Logger::debug(
"Unable to load versioned JSON file %s in versioning script: %s", storagePath, outputException(e, false));
return Json();
}
});
return versioningCallbacks;
}
}