Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarVersioningDatabase.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/game/StarVersioningDatabase.cpp')
-rw-r--r--source/game/StarVersioningDatabase.cpp219
1 files changed, 219 insertions, 0 deletions
diff --git a/source/game/StarVersioningDatabase.cpp b/source/game/StarVersioningDatabase.cpp
new file mode 100644
index 0000000..aa47db7
--- /dev/null
+++ b/source/game/StarVersioningDatabase.cpp
@@ -0,0 +1,219 @@
+#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;
+}
+
+}