diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/base/StarAssets.cpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/base/StarAssets.cpp')
-rw-r--r-- | source/base/StarAssets.cpp | 1113 |
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(); +} + +} |