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

summaryrefslogtreecommitdiff
path: root/source/base/StarAssets.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/base/StarAssets.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/base/StarAssets.cpp')
-rw-r--r--source/base/StarAssets.cpp1113
1 files changed, 1113 insertions, 0 deletions
diff --git a/source/base/StarAssets.cpp b/source/base/StarAssets.cpp
new file mode 100644
index 0000000..5066268
--- /dev/null
+++ b/source/base/StarAssets.cpp
@@ -0,0 +1,1113 @@
+#include "StarAssets.hpp"
+#include "StarFile.hpp"
+#include "StarTime.hpp"
+#include "StarDirectoryAssetSource.hpp"
+#include "StarPackedAssetSource.hpp"
+#include "StarJsonBuilder.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarJsonPatch.hpp"
+#include "StarIterator.hpp"
+#include "StarImageProcessing.hpp"
+#include "StarLogging.hpp"
+#include "StarRandom.hpp"
+#include "StarFont.hpp"
+#include "StarAudio.hpp"
+#include "StarCasting.hpp"
+#include "StarLexicalCast.hpp"
+#include "StarSha256.hpp"
+#include "StarDataStreamDevices.hpp"
+
+namespace Star {
+
+static void validatePath(AssetPath const& components, bool canContainSubPath, bool canContainDirectives) {
+ if (components.basePath.empty() || components.basePath[0] != '/')
+ throw AssetException(strf("Path '%s' must be absolute", components.basePath));
+
+ bool first = true;
+ bool slashed = true;
+ bool dotted = false;
+ for (auto c : components.basePath) {
+ if (c == '/') {
+ if (!first) {
+ if (slashed)
+ throw AssetException(strf("Path '%s' contains consecutive //, not allowed", components.basePath));
+ else if (dotted)
+ throw AssetException(strf("Path '%s' '.' and '..' not allowed", components.basePath));
+ }
+ slashed = true;
+ dotted = false;
+ } else if (c == ':') {
+ if (slashed)
+ throw AssetException(strf("Path '%s' has ':' after directory", components.basePath));
+ break;
+ } else if (c == '?') {
+ if (slashed)
+ throw AssetException(strf("Path '%s' has '?' after directory", components.basePath));
+ break;
+ } else {
+ slashed = false;
+ dotted = c == '.';
+ }
+ first = false;
+ }
+ if (slashed)
+ throw AssetException(strf("Path '%s' cannot be a file", components.basePath));
+
+ if (!canContainSubPath && components.subPath)
+ throw AssetException::format("Path '%s' cannot contain sub-path", components);
+ if (!canContainDirectives && !components.directives.empty())
+ throw AssetException::format("Path '%s' cannot contain directives", components);
+}
+
+// The filename is everything after the last slash (excluding directives) and
+// up to the first directive marker.
+static Maybe<pair<size_t, size_t>> findFilenameRange(std::string const& pathUtf8) {
+ size_t firstDirectiveOrSubPath = pathUtf8.find_first_of(":?");
+ size_t filenameStart = 0;
+ while (true) {
+ size_t find = pathUtf8.find('/', filenameStart);
+ if (find >= firstDirectiveOrSubPath)
+ break;
+ filenameStart = find + 1;
+ }
+
+ if (filenameStart == NPos) {
+ return {};
+ } else if (firstDirectiveOrSubPath == NPos) {
+ return {{filenameStart, pathUtf8.size()}};
+ } else {
+ return {{filenameStart, firstDirectiveOrSubPath}};
+ }
+}
+
+AssetPath AssetPath::split(String const& path) {
+ auto i = path.begin();
+ auto end = path.end();
+
+ AssetPath components;
+
+ // base paths cannot have any ':' or '?' characters, stop at the first one.
+ while (i != end) {
+ String::Char c = *i;
+ if (c == ':' || c == '?')
+ break;
+
+ components.basePath += c;
+ ++i;
+ }
+
+ // Sub-paths must immediately follow base paths and must start with a ':',
+ // after this point any further ':' characters are not special.
+ if (i != end && *i == ':') {
+ ++i;
+ while (i != end) {
+ String::Char c = *i;
+ if (c == '?')
+ break;
+
+ if (!components.subPath)
+ components.subPath.emplace();
+
+ *components.subPath += c;
+ ++i;
+ }
+ }
+
+ // Directives must follow the base path and optional sub-path, and each
+ // directive is separated by one or more '?' characters.
+ while (i != end && *i == '?') {
+ ++i;
+ String directive;
+ while (i != end) {
+ String::Char c = *i;
+ if (c == '?')
+ break;
+
+ directive += c;
+ ++i;
+ }
+ if (!directive.empty())
+ components.directives.append(move(directive));
+ }
+
+ starAssert(i == end);
+
+ return components;
+}
+
+String AssetPath::join(AssetPath const& components) {
+ return toString(components);
+}
+
+String AssetPath::setSubPath(String const& path, String const& subPath) {
+ auto components = split(path);
+ components.subPath = subPath;
+ return join(components);
+}
+
+String AssetPath::removeSubPath(String const& path) {
+ auto components = split(path);
+ components.subPath.reset();
+ return join(components);
+}
+
+String AssetPath::getDirectives(String const& path) {
+ size_t firstDirective = path.find('?');
+ if (firstDirective == NPos)
+ return {};
+ return path.substr(firstDirective + 1);
+}
+
+String AssetPath::addDirectives(String const& path, String const& directives) {
+ return String::joinWith("?", path, directives);
+}
+
+String AssetPath::removeDirectives(String const& path) {
+ size_t firstDirective = path.find('?');
+ if (firstDirective == NPos)
+ return path;
+ return path.substr(0, firstDirective);
+}
+
+String AssetPath::directory(String const& path) {
+ if (auto p = findFilenameRange(path.utf8())) {
+ return String(path.utf8().substr(0, p->first));
+ } else {
+ return String();
+ }
+}
+
+String AssetPath::filename(String const& path) {
+ if (auto p = findFilenameRange(path.utf8())) {
+ return String(path.utf8().substr(p->first, p->second));
+ } else {
+ return String();
+ }
+}
+
+String AssetPath::extension(String const& path) {
+ auto file = filename(path);
+ auto lastDot = file.findLast(".");
+ if (lastDot == NPos)
+ return "";
+
+ return file.substr(lastDot + 1);
+}
+
+String AssetPath::relativeTo(String const& sourcePath, String const& givenPath) {
+ if (!givenPath.empty() && givenPath[0] == '/')
+ return givenPath;
+
+ auto path = directory(sourcePath);
+ path.append(givenPath);
+ return path;
+}
+
+bool AssetPath::operator==(AssetPath const& rhs) const {
+ return tie(basePath, subPath, directives) == tie(rhs.basePath, rhs.subPath, rhs.directives);
+}
+
+std::ostream& operator<<(std::ostream& os, AssetPath const& rhs) {
+ os << rhs.basePath;
+ if (rhs.subPath) {
+ os << ":";
+ os << *rhs.subPath;
+ }
+
+ for (auto const& directive : rhs.directives) {
+ os << "?";
+ os << directive;
+ }
+
+ return os;
+}
+
+Maybe<RectU> FramesSpecification::getRect(String const& frame) const {
+ if (auto alias = aliases.ptr(frame)) {
+ return frames.get(*alias);
+ } else {
+ return frames.maybe(frame);
+ }
+}
+
+Assets::Assets(Settings settings, StringList assetSources) {
+ const char* const AssetsPatchSuffix = ".patch";
+
+ m_settings = move(settings);
+ m_stopThreads = false;
+ m_assetSources = move(assetSources);
+
+ for (auto& sourcePath : m_assetSources) {
+ Logger::info("Loading assets from: '%s'", sourcePath);
+ AssetSourcePtr source;
+ if (File::isDirectory(sourcePath))
+ source = make_shared<DirectoryAssetSource>(sourcePath, m_settings.pathIgnore);
+ else
+ source = make_shared<PackedAssetSource>(sourcePath);
+
+ m_assetSourcePaths.add(sourcePath, source);
+
+ for (auto const& filename : source->assetPaths()) {
+ if (filename.endsWith(AssetsPatchSuffix, String::CaseInsensitive)) {
+ auto targetPatchFile = filename.substr(0, filename.size() - strlen(AssetsPatchSuffix));
+ if (auto p = m_files.ptr(targetPatchFile))
+ p->patchSources.append({filename, source});
+ }
+ auto& descriptor = m_files[filename];
+ descriptor.sourceName = filename;
+ descriptor.source = source;
+ }
+ }
+
+ Sha256Hasher digest;
+
+ for (auto const& assetPath : m_files.keys().transformed([](String const& s) {
+ return s.toLower();
+ }).sorted()) {
+ bool digestFile = true;
+ for (auto const& pattern : m_settings.digestIgnore) {
+ if (assetPath.regexMatch(pattern, false, false)) {
+ digestFile = false;
+ break;
+ }
+ }
+
+ auto const& descriptor = m_files.get(assetPath);
+
+ if (digestFile) {
+ digest.push(assetPath);
+ digest.push(DataStreamBuffer::serialize(descriptor.source->open(descriptor.sourceName)->size()));
+ for (auto const& pair : descriptor.patchSources)
+ digest.push(DataStreamBuffer::serialize(pair.second->open(pair.first)->size()));
+ }
+ }
+
+ m_digest = digest.compute();
+
+ for (auto const& filename : m_files.keys())
+ m_filesByExtension[AssetPath::extension(filename).toLower()].append(filename);
+
+ int workerPoolSize = m_settings.workerPoolSize;
+ for (int i = 0; i < workerPoolSize; i++)
+ m_workerThreads.append(Thread::invoke("Assets::workerMain", mem_fn(&Assets::workerMain), this));
+}
+
+Assets::~Assets() {
+ m_stopThreads = true;
+
+ {
+ // Should lock associated mutex to prevent loss of wakeups,
+ MutexLocker locker(m_assetsMutex);
+ // Notify all worker threads to allow them to stop
+ m_assetsQueued.broadcast();
+ }
+
+ // Join them all
+ m_workerThreads.clear();
+}
+
+StringList Assets::assetSources() const {
+ MutexLocker assetsLocker(m_assetsMutex);
+ return m_assetSources;
+}
+
+JsonObject Assets::assetSourceMetadata(String const& sourceName) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+ return m_assetSourcePaths.getRight(sourceName)->metadata();
+}
+
+ByteArray Assets::digest() const {
+ MutexLocker assetsLocker(m_assetsMutex);
+ return m_digest;
+}
+
+bool Assets::assetExists(String const& path) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+ return m_files.contains(path);
+}
+
+String Assets::assetSource(String const& path) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+ if (auto p = m_files.ptr(path))
+ return m_assetSourcePaths.getLeft(p->source);
+ throw AssetException(strf("No such asset '%s'", path));
+}
+
+StringList Assets::scan(String const& suffix) const {
+ if (suffix.beginsWith(".") && !suffix.substr(1).hasChar('.')) {
+ return scanExtension(suffix);
+ } else {
+ StringList result;
+ for (auto const& fileEntry : m_files) {
+ String const& file = fileEntry.first;
+ if (file.endsWith(suffix, String::CaseInsensitive))
+ result.append(file);
+ }
+
+ return result;
+ }
+}
+
+StringList Assets::scan(String const& prefix, String const& suffix) const {
+ StringList result;
+ if (suffix.beginsWith(".") && !suffix.substr(1).hasChar('.')) {
+ StringList filesWithExtension = scanExtension(suffix);
+ for (auto const& file : filesWithExtension) {
+ if (file.beginsWith(prefix, String::CaseInsensitive))
+ result.append(file);
+ }
+ } else {
+ for (auto const& fileEntry : m_files) {
+ String const& file = fileEntry.first;
+ if (file.beginsWith(prefix, String::CaseInsensitive) && file.endsWith(suffix, String::CaseInsensitive))
+ result.append(file);
+ }
+ }
+ return result;
+}
+
+StringList Assets::scanExtension(String const& extension) const {
+ if (extension.beginsWith("."))
+ return m_filesByExtension.value(extension.substr(1));
+ else
+ return m_filesByExtension.value(extension);
+}
+
+Json Assets::json(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, true, false);
+
+ return as<JsonData>(getAsset(AssetId{AssetType::Json, move(components)}))->json;
+}
+
+Json Assets::fetchJson(Json const& v, String const& dir) const {
+ if (v.isType(Json::Type::String))
+ return Assets::json(AssetPath::relativeTo(dir, v.toString()));
+ else
+ return v;
+}
+
+void Assets::queueJsons(StringList const& paths) const {
+ queueAssets(paths.transformed([](String const& path) {
+ auto components = AssetPath::split(path);
+ validatePath(components, true, false);
+
+ return AssetId{AssetType::Json, {components.basePath, {}, {}}};
+ }));
+}
+
+ImageConstPtr Assets::image(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, true, true);
+
+ return as<ImageData>(getAsset(AssetId{AssetType::Image, move(components)}))->image;
+}
+
+void Assets::queueImages(StringList const& paths) const {
+ queueAssets(paths.transformed([](String const& path) {
+ auto components = AssetPath::split(path);
+ validatePath(components, true, true);
+
+ return AssetId{AssetType::Image, move(components)};
+ }));
+}
+
+ImageConstPtr Assets::tryImage(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, true, true);
+
+ if (auto imageData = as<ImageData>(tryAsset(AssetId{AssetType::Image, move(components)})))
+ return imageData->image;
+ else
+ return {};
+}
+
+FramesSpecificationConstPtr Assets::imageFrames(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ MutexLocker assetsLocker(m_assetsMutex);
+ return bestFramesSpecification(path);
+}
+
+AudioConstPtr Assets::audio(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ return as<AudioData>(getAsset(AssetId{AssetType::Audio, move(components)}))->audio;
+}
+
+void Assets::queueAudios(StringList const& paths) const {
+ queueAssets(paths.transformed([](String const& path) {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ return AssetId{AssetType::Audio, move(components)};
+ }));
+}
+
+AudioConstPtr Assets::tryAudio(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ if (auto audioData = as<AudioData>(tryAsset(AssetId{AssetType::Audio, move(components)})))
+ return audioData->audio;
+ else
+ return {};
+}
+
+FontConstPtr Assets::font(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ return as<FontData>(getAsset(AssetId{AssetType::Font, move(components)}))->font;
+}
+
+ByteArrayConstPtr Assets::bytes(String const& path) const {
+ auto components = AssetPath::split(path);
+ validatePath(components, false, false);
+
+ return as<BytesData>(getAsset(AssetId{AssetType::Bytes, move(components)}))->bytes;
+}
+
+IODevicePtr Assets::openFile(String const& path) const {
+ return open(path);
+}
+
+void Assets::clearCache() {
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ // Clear all assets that are not queued or broken.
+ auto it = makeSMutableMapIterator(m_assetsCache);
+ while (it.hasNext()) {
+ auto const& pair = it.next();
+ // Don't clean up queued, persistent, or broken assets.
+ if (pair.second && !pair.second->shouldPersist() && !m_queue.contains(pair.first))
+ it.remove();
+ }
+}
+
+void Assets::cleanup() {
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ double time = Time::monotonicTime();
+
+ auto it = makeSMutableMapIterator(m_assetsCache);
+ while (it.hasNext()) {
+ auto pair = it.next();
+ // Don't clean up broken assets or queued assets.
+ if (pair.second && !m_queue.contains(pair.first)) {
+ double liveTime = time - pair.second->time;
+ if (liveTime > m_settings.assetTimeToLive) {
+ // If the asset should persist, just refresh the access time.
+ if (pair.second->shouldPersist())
+ pair.second->time = time;
+ else
+ it.remove();
+ }
+ }
+ }
+}
+
+bool Assets::AssetId::operator==(AssetId const& assetId) const {
+ return tie(type, path) == tie(assetId.type, assetId.path);
+}
+
+size_t Assets::AssetIdHash::operator()(AssetId const& id) const {
+ return hashOf(id.type, id.path.basePath, id.path.subPath, id.path.directives);
+}
+
+bool Assets::JsonData::shouldPersist() const {
+ return !json.unique();
+}
+
+bool Assets::ImageData::shouldPersist() const {
+ return !alias && !image.unique();
+}
+
+bool Assets::AudioData::shouldPersist() const {
+ return !audio.unique();
+}
+
+bool Assets::FontData::shouldPersist() const {
+ return !font.unique();
+}
+
+bool Assets::BytesData::shouldPersist() const {
+ return !bytes.unique();
+}
+
+FramesSpecification Assets::parseFramesSpecification(Json const& frameConfig, String path) {
+ FramesSpecification framesSpecification;
+
+ framesSpecification.framesFile = move(path);
+
+ if (frameConfig.contains("frameList")) {
+ for (auto const& pair : frameConfig.get("frameList").toObject()) {
+ String frameName = pair.first;
+ RectU rect = RectU(jsonToRectI(pair.second));
+ if (rect.isEmpty())
+ throw AssetException(
+ strf("Empty rect in frame specification in image %s frame %s", framesSpecification.framesFile, frameName));
+ else
+ framesSpecification.frames[frameName] = rect;
+ }
+ }
+
+ if (frameConfig.contains("frameGrid")) {
+ auto grid = frameConfig.get("frameGrid").toObject();
+
+ Vec2U begin(jsonToVec2I(grid.value("begin", jsonFromVec2I(Vec2I()))));
+ Vec2U size(jsonToVec2I(grid.get("size")));
+ Vec2U dimensions(jsonToVec2I(grid.get("dimensions")));
+
+ if (dimensions[0] == 0 || dimensions[1] == 0)
+ throw AssetException(strf("Image %s \"dimensions\" in frameGrid cannot be zero", framesSpecification.framesFile));
+
+ if (grid.contains("names")) {
+ auto nameList = grid.get("names");
+ for (size_t y = 0; y < nameList.size(); ++y) {
+ if (y >= dimensions[1])
+ throw AssetException(strf("Image %s row %s is out of bounds for y-dimension %s",
+ framesSpecification.framesFile,
+ y + 1,
+ dimensions[1]));
+ auto rowList = nameList.get(y);
+ if (rowList.isNull())
+ continue;
+ for (unsigned x = 0; x < rowList.size(); ++x) {
+ if (x >= dimensions[0])
+ throw AssetException(strf("Image %s column %s is out of bounds for x-dimension %s",
+ framesSpecification.framesFile,
+ x + 1,
+ dimensions[0]));
+
+ auto frame = rowList.get(x);
+ if (frame.isNull())
+ continue;
+ auto frameName = frame.toString();
+ if (!frameName.empty())
+ framesSpecification.frames[frameName] =
+ RectU::withSize(Vec2U(begin[0] + x * size[0], begin[1] + y * size[1]), size);
+ }
+ }
+ } else {
+ // If "names" not specified, use auto naming algorithm
+ for (size_t y = 0; y < dimensions[1]; ++y)
+ for (size_t x = 0; x < dimensions[0]; ++x)
+ framesSpecification.frames[strf("%s", y * dimensions[0] + x)] =
+ RectU::withSize(Vec2U(begin[0] + x * size[0], begin[1] + y * size[1]), size);
+ }
+ }
+
+ if (auto aliasesConfig = frameConfig.opt("aliases")) {
+ auto aliases = aliasesConfig->objectPtr();
+ for (auto const& pair : *aliases) {
+ String const& key = pair.first;
+ String value = pair.second.toString();
+
+ // Resolve aliases to aliases by checking to see if the alias value in
+ // the alias map itself. Don't do this more than aliases.size() times to
+ // avoid infinite cycles.
+ for (size_t i = 0; i <= aliases->size(); ++i) {
+ auto it = aliases->find(value);
+ if (it != aliases->end()) {
+ if (i == aliases->size())
+ throw AssetException(strf("Infinite alias loop detected for alias '%s'", key));
+
+ value = it->second.toString();
+ } else {
+ break;
+ }
+ }
+
+ if (!framesSpecification.frames.contains(value))
+ throw AssetException(strf("No such frame '%s' found for alias '%s'", value, key));
+ framesSpecification.aliases[key] = move(value);
+ }
+ }
+
+ return framesSpecification;
+}
+
+void Assets::queueAssets(List<AssetId> const& assetIds) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ for (auto const& id : assetIds) {
+ auto i = m_assetsCache.find(id);
+ if (i != m_assetsCache.end()) {
+ if (i->second)
+ freshen(i->second);
+ } else {
+ auto j = m_queue.find(id);
+ if (j == m_queue.end()) {
+ m_queue[id] = QueuePriority::Load;
+ m_assetsQueued.signal();
+ }
+ }
+ }
+}
+
+shared_ptr<Assets::AssetData> Assets::tryAsset(AssetId const& id) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ auto i = m_assetsCache.find(id);
+ if (i != m_assetsCache.end()) {
+ if (i->second) {
+ freshen(i->second);
+ return i->second;
+ } else {
+ throw AssetException::format("Error loading asset %s", id.path);
+ }
+ } else {
+ auto j = m_queue.find(id);
+ if (j == m_queue.end()) {
+ m_queue[id] = QueuePriority::Load;
+ m_assetsQueued.signal();
+ }
+ return {};
+ }
+}
+
+shared_ptr<Assets::AssetData> Assets::getAsset(AssetId const& id) const {
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ while (true) {
+ auto j = m_assetsCache.find(id);
+ if (j != m_assetsCache.end()) {
+ if (j->second) {
+ auto asset = j->second;
+ freshen(asset);
+ return asset;
+ } else {
+ throw AssetException::format("Error loading asset %s", id.path);
+ }
+ } else {
+ // Try to load the asset in-thread, if we cannot, then the asset has been
+ // queued so wait for a worker thread to finish it.
+ if (!doLoad(id))
+ m_assetsDone.wait(m_assetsMutex);
+ }
+ }
+}
+
+void Assets::workerMain() {
+ while (true) {
+ if (m_stopThreads)
+ break;
+
+ MutexLocker assetsLocker(m_assetsMutex);
+
+ AssetId assetId;
+ QueuePriority queuePriority = QueuePriority::None;
+
+ // Find the highest priority queue entry
+ for (auto const& pair : m_queue) {
+ if (pair.second == QueuePriority::Load || pair.second == QueuePriority::PostProcess) {
+ assetId = pair.first;
+ queuePriority = pair.second;
+ if (pair.second == QueuePriority::Load)
+ break;
+ }
+ }
+
+ if (queuePriority != QueuePriority::Load && queuePriority != QueuePriority::PostProcess) {
+ // Nothing in the queue that needs work
+ m_assetsQueued.wait(m_assetsMutex);
+ continue;
+ }
+
+ bool workIsBlocking;
+ if (queuePriority == QueuePriority::PostProcess)
+ workIsBlocking = !doPost(assetId);
+ else
+ workIsBlocking = !doLoad(assetId);
+
+ if (workIsBlocking) {
+ // We are blocking on some sort of busy asset, so need to wait on
+ // something to complete here, rather than spinning and burning cpu.
+ m_assetsDone.wait(m_assetsMutex);
+ continue;
+ }
+
+ // After processing an asset, unlock the main asset mutex and yield so we
+ // don't starve other threads.
+ assetsLocker.unlock();
+ Thread::yield();
+ }
+}
+
+template <typename Function>
+decltype(auto) Assets::unlockDuring(Function f) const {
+ m_assetsMutex.unlock();
+ try {
+ auto r = f();
+ m_assetsMutex.lock();
+ return r;
+ } catch (...) {
+ m_assetsMutex.lock();
+ throw;
+ }
+}
+
+FramesSpecificationConstPtr Assets::bestFramesSpecification(String const& image) const {
+ if (auto framesSpecification = m_framesSpecifications.maybe(image)) {
+ return *framesSpecification;
+ }
+
+ String framesFile;
+
+ if (auto bestFramesFile = m_bestFramesFiles.maybe(image)) {
+ framesFile = *bestFramesFile;
+
+ } else {
+ String searchPath = AssetPath::directory(image);
+ String filePrefix = AssetPath::filename(image);
+ filePrefix = filePrefix.substr(0, filePrefix.findLast('.'));
+
+ auto subdir = [](String const& dir) -> String {
+ auto dirsplit = dir.substr(0, dir.size() - 1).rsplit("/", 1);
+ if (dirsplit.size() < 2)
+ return "";
+ else
+ return dirsplit[0] + "/";
+ };
+
+ Maybe<String> foundFramesFile;
+
+ // look for <full-path-minus-extension>.frames or default.frames up to root
+ while (!searchPath.empty()) {
+ String framesPath = searchPath + filePrefix + ".frames";
+ if (m_files.contains(framesPath)) {
+ foundFramesFile = framesPath;
+ break;
+ }
+
+ framesPath = searchPath + "default.frames";
+ if (m_files.contains(framesPath)) {
+ foundFramesFile = framesPath;
+ break;
+ }
+
+ searchPath = subdir(searchPath);
+ }
+
+ if (foundFramesFile) {
+ framesFile = foundFramesFile.take();
+ m_bestFramesFiles[image] = framesFile;
+
+ } else {
+ return {};
+ }
+ }
+
+ auto framesSpecification = unlockDuring([&]() {
+ return make_shared<FramesSpecification>(parseFramesSpecification(readJson(framesFile), framesFile));
+ });
+ m_framesSpecifications[image] = framesSpecification;
+
+ return framesSpecification;
+}
+
+IODevicePtr Assets::open(String const& path) const {
+ if (auto p = m_files.ptr(path))
+ return p->source->open(p->sourceName);
+ throw AssetException(strf("No such asset '%s'", path));
+}
+
+ByteArray Assets::read(String const& path) const {
+ if (auto p = m_files.ptr(path))
+ return p->source->read(p->sourceName);
+ throw AssetException(strf("No such asset '%s'", path));
+}
+
+Json Assets::readJson(String const& path) const {
+ ByteArray streamData = read(path);
+ try {
+ Json result = inputUtf8Json(streamData.begin(), streamData.end(), false);
+ for (auto const& pair : m_files.get(path).patchSources) {
+ auto patchStream = pair.second->read(pair.first);
+ auto patchData = inputUtf8Json(patchStream.begin(), patchStream.end(), false).toArray();
+ try {
+ if (patchData.size()) {
+ if (patchData.at(0).type() == Json::Type::Array) {
+ for (auto const& patch : patchData) {
+ try {
+ result = jsonPatch(result, patch.toArray());
+ } catch (JsonPatchTestFail const& e) {
+ Logger::debug("Patch test failure from file %s in source: %s. Caused by: %s", pair.first, m_assetSourcePaths.getLeft(pair.second), e.what());
+ }
+ }
+ } else if (patchData.at(0).type() == Json::Type::Object) {
+ try {
+ result = jsonPatch(result, patchData);
+ } catch (JsonPatchTestFail const& e) {
+ Logger::debug("Patch test failure from file %s in source: %s. Caused by: %s", pair.first, m_assetSourcePaths.getLeft(pair.second), e.what());
+ }
+ } else {
+ throw JsonPatchException(strf("Patch data is wrong type: %s", Json::typeName(patchData.at(0).type())));
+ }
+ }
+ } catch (JsonPatchException const& e) {
+ Logger::error("Could not apply patch from file %s in source: %s. Caused by: %s", pair.first, m_assetSourcePaths.getLeft(pair.second), e.what());
+ }
+ }
+ return result;
+ } catch (std::exception const& e) {
+ throw JsonParsingException(strf("Cannot parse json file: %s", path), e);
+ }
+}
+
+bool Assets::doLoad(AssetId const& id) const {
+ try {
+ // loadAsset automatically manages the queue and freshens the asset
+ // data.
+ return (bool)loadAsset(id);
+ } catch (std::exception const& e) {
+ Logger::error("Exception caught loading asset: %s, %s", id.path, outputException(e, true));
+ } catch (...) {
+ Logger::error("Unknown exception caught loading asset: %s", id.path);
+ }
+
+ // There was an exception, remove the asset from the queue and fill the cache
+ // with null so that getAsset will throw.
+ m_assetsCache[id] = {};
+ m_assetsDone.broadcast();
+ m_queue.remove(id);
+ return true;
+}
+
+bool Assets::doPost(AssetId const& id) const {
+ shared_ptr<AssetData> assetData;
+ try {
+ assetData = m_assetsCache.get(id);
+ if (id.type == AssetType::Audio)
+ assetData = postProcessAudio(assetData);
+ } catch (std::exception const& e) {
+ Logger::error("Exception caught post-processing asset: %s, %s", id.path, outputException(e, true));
+ } catch (...) {
+ Logger::error("Unknown exception caught post-processing asset: %s", id.path);
+ }
+
+ m_queue.remove(id);
+ if (assetData) {
+ assetData->needsPostProcessing = false;
+ m_assetsCache[id] = assetData;
+ freshen(assetData);
+ m_assetsDone.broadcast();
+ }
+
+ return true;
+}
+
+shared_ptr<Assets::AssetData> Assets::loadAsset(AssetId const& id) const {
+ if (auto asset = m_assetsCache.value(id))
+ return asset;
+
+ if (m_queue.value(id, QueuePriority::None) == QueuePriority::Working)
+ return {};
+
+ try {
+ m_queue[id] = QueuePriority::Working;
+ shared_ptr<AssetData> assetData;
+
+ try {
+ if (id.type == AssetType::Json) {
+ assetData = loadJson(id.path);
+ } else if (id.type == AssetType::Image) {
+ assetData = loadImage(id.path);
+ } else if (id.type == AssetType::Audio) {
+ assetData = loadAudio(id.path);
+ } else if (id.type == AssetType::Font) {
+ assetData = loadFont(id.path);
+ } else if (id.type == AssetType::Bytes) {
+ assetData = loadBytes(id.path);
+ }
+
+ } catch (StarException const& e) {
+ if (id.type == AssetType::Image && m_settings.missingImage) {
+ Logger::error("Could not load image asset '%s', using placeholder default.\n%s", id.path, outputException(e, false));
+ assetData = loadImage({*m_settings.missingImage, {}, {}});
+ } else if (id.type == AssetType::Audio && m_settings.missingAudio) {
+ Logger::error("Could not load audio asset '%s', using placeholder default.\n%s", id.path, outputException(e, false));
+ assetData = loadAudio({*m_settings.missingAudio, {}, {}});
+ } else {
+ throw;
+ }
+ }
+
+ if (assetData) {
+ if (assetData->needsPostProcessing)
+ m_queue[id] = QueuePriority::PostProcess;
+ else
+ m_queue.remove(id);
+ m_assetsCache[id] = assetData;
+ m_assetsDone.broadcast();
+ freshen(assetData);
+
+ } else {
+ // We have failed to load an asset because it depends on an asset
+ // currently being worked on. Mark it as needing loading and move it to
+ // the end of the queue.
+ m_queue[id] = QueuePriority::Load;
+ m_assetsQueued.signal();
+ m_queue.toBack(id);
+ }
+
+ return assetData;
+
+ } catch (...) {
+ m_queue.remove(id);
+ m_assetsCache[id] = {};
+ m_assetsDone.broadcast();
+ throw;
+ }
+}
+
+shared_ptr<Assets::AssetData> Assets::loadJson(AssetPath const& path) const {
+ Json json;
+
+ if (path.subPath) {
+ auto topJson =
+ as<JsonData>(loadAsset(AssetId{AssetType::Json, {path.basePath, {}, {}}}));
+ if (!topJson)
+ return {};
+
+ try {
+ auto newData = make_shared<JsonData>();
+ newData->json = topJson->json.query(*path.subPath);
+ return newData;
+ } catch (StarException const& e) {
+ throw AssetException(strf("Could not read JSON value %s", path), e);
+ }
+ } else {
+ return unlockDuring([&]() {
+ try {
+ auto newData = make_shared<JsonData>();
+ newData->json = readJson(path.basePath);
+ return newData;
+ } catch (StarException const& e) {
+ throw AssetException(strf("Could not read JSON asset %s", path), e);
+ }
+ });
+ }
+}
+
+shared_ptr<Assets::AssetData> Assets::loadImage(AssetPath const& path) const {
+ if (!path.directives.empty()) {
+ shared_ptr<ImageData> source =
+ as<ImageData>(loadAsset(AssetId{AssetType::Image, {path.basePath, path.subPath, {}}}));
+ if (!source)
+ return {};
+ List<ImageOperation> operations = path.directives.transformed(imageOperationFromString);
+ StringMap<ImageConstPtr> references;
+ for (auto const& ref : imageOperationReferences(operations)) {
+ auto components = AssetPath::split(ref);
+ validatePath(components, true, false);
+ auto refImage = as<ImageData>(loadAsset(AssetId{AssetType::Image, move(components)}));
+ if (!refImage)
+ return {};
+ references[ref] = refImage->image;
+ }
+
+ return unlockDuring([&]() {
+ auto newData = make_shared<ImageData>();
+ newData->image = make_shared<Image>(processImageOperations(
+ operations, *source->image, [&](String const& ref) { return references.get(ref).get(); }));
+ return newData;
+ });
+
+ } else if (path.subPath) {
+ auto imageData = as<ImageData>(loadAsset(AssetId{AssetType::Image, {path.basePath, {}, {}}}));
+ if (!imageData)
+ return {};
+
+ // Base image must have frames data associated with it.
+ if (!imageData->frames)
+ throw AssetException::format("No associated frames file found for image '%s' while resolving image frame '%s'", path.basePath, path);
+
+ if (auto alias = imageData->frames->aliases.ptr(*path.subPath)) {
+ imageData = as<ImageData>(loadAsset(AssetId{AssetType::Image, {path.basePath, *alias, path.directives}}));
+ if (!imageData)
+ return {};
+
+ auto newData = make_shared<ImageData>();
+ newData->image = imageData->image;
+ newData->alias = true;
+ return newData;
+
+ } else {
+ auto frameRect = imageData->frames->frames.ptr(*path.subPath);
+ if (!frameRect)
+ throw AssetException(strf("No such frame %s in frames spec %s", *path.subPath, imageData->frames->framesFile));
+
+ return unlockDuring([&]() {
+ // Need to flip frame coordinates because frame configs assume top
+ // down image coordinates
+ auto newData = make_shared<ImageData>();
+ newData->image = make_shared<Image>(imageData->image->subImage(
+ Vec2U(frameRect->xMin(), imageData->image->height() - frameRect->yMax()), frameRect->size()));
+ return newData;
+ });
+ }
+
+ } else {
+ auto imageData = make_shared<ImageData>();
+ imageData->image = unlockDuring([&]() {
+ return make_shared<Image>(Image::readPng(open(path.basePath)));
+ });
+ imageData->frames = bestFramesSpecification(path.basePath);
+
+ return imageData;
+ }
+}
+
+shared_ptr<Assets::AssetData> Assets::loadAudio(AssetPath const& path) const {
+ return unlockDuring([&]() {
+ auto newData = make_shared<AudioData>();
+ newData->audio = make_shared<Audio>(open(path.basePath));
+ newData->needsPostProcessing = newData->audio->compressed();
+ return newData;
+ });
+}
+
+shared_ptr<Assets::AssetData> Assets::loadFont(AssetPath const& path) const {
+ return unlockDuring([&]() {
+ auto newData = make_shared<FontData>();
+ newData->font = Font::loadTrueTypeFont(make_shared<ByteArray>(read(path.basePath)));
+ return newData;
+ });
+}
+
+shared_ptr<Assets::AssetData> Assets::loadBytes(AssetPath const& path) const {
+ return unlockDuring([&]() {
+ auto newData = make_shared<BytesData>();
+ newData->bytes = make_shared<ByteArray>(read(path.basePath));
+ return newData;
+ });
+}
+
+shared_ptr<Assets::AssetData> Assets::postProcessAudio(shared_ptr<AssetData> const& original) const {
+ return unlockDuring([&]() -> shared_ptr<AssetData> {
+ if (auto audioData = as<AudioData>(original)) {
+ if (audioData->audio->totalTime() < m_settings.audioDecompressLimit) {
+ auto audio = make_shared<Audio>(*audioData->audio);
+ audio->uncompress();
+
+ auto newData = make_shared<AudioData>();
+ newData->audio = audio;
+ return newData;
+ } else {
+ return audioData;
+ }
+ } else {
+ return {};
+ }
+ });
+}
+
+void Assets::freshen(shared_ptr<AssetData> const& asset) const {
+ asset->time = Time::monotonicTime();
+}
+
+}