osb/source/base/StarAssets.cpp

1121 lines
34 KiB
C++
Raw Normal View History

2023-06-20 04:33:09 +00:00
#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 patchJson = inputUtf8Json(patchStream.begin(), patchStream.end(), false);
if (patchJson.isType(Json::Type::Array)) {
auto patchData = patchJson.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) {
2023-06-20 04:33:09 +00:00
try {
result = jsonPatch(result, patchData);
2023-06-20 04:33:09 +00:00
} 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())));
2023-06-20 04:33:09 +00:00
}
}
} 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());
2023-06-20 04:33:09 +00:00
}
}
else if (patchJson.isType(Json::Type::Object)) { //Kae: Do a good ol' json merge instead if the .patch file is a Json object
auto patchData = patchJson.toObject();
result = jsonMerge(result, patchData);
2023-06-20 04:33:09 +00:00
}
}
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();
}
}