Resource, texture and overlay services

This commit is contained in:
Luke Street
2026-07-07 13:55:55 -06:00
parent c876ea558f
commit af5635dd42
15 changed files with 1322 additions and 48 deletions
+351
View File
@@ -0,0 +1,351 @@
#include "registry.hpp"
#include "aurora/dvd.h"
#include "aurora/lib/logging.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/overlay.h"
#include <cstdint>
#include <cstring>
#include <mutex>
#include <string_view>
#include <unordered_map>
#include <utility>
using namespace std::string_literals;
namespace dusk::mods::svc {
namespace {
aurora::Module Log("dusk::mods::overlay");
struct OverlayFileData {
std::string bundlePath;
std::shared_ptr<ModBundle> bundle;
std::shared_ptr<const std::vector<u8> > buffer;
};
// Keyed by the id passed to Aurora as per-file userdata. Guarded by s_overlayMutex: Aurora may
// call cbOpen from a DVD thread while the game thread replaces the set in overlay_sync_files.
// The shared bundle/buffer pointer keeps a disabled/reloaded mod's data readable until the last
// open completes.
std::unordered_map<uintptr_t, OverlayFileData> s_overlayFiles;
uintptr_t s_nextOverlayId = 1;
std::mutex s_overlayMutex;
struct RuntimeOverlayEntry {
uint64_t handle = 0;
std::string discPath;
std::string bundlePath; // bundle-backed if non-empty
std::shared_ptr<const std::vector<u8>> buffer; // buffer-backed otherwise
size_t size = 0;
};
std::unordered_map<const LoadedMod*, std::vector<RuntimeOverlayEntry>> s_runtimeOverlays;
uint64_t s_nextRuntimeHandle = 1;
bool s_overlaysDirty = false;
// Aurora matches overlay paths against the disc case-insensitively and later entries win, so
// claims are tracked by lowercased path and re-claims by a different mod warn.
void claim_overlay_path(std::unordered_map<std::string, const LoadedMod*>& claims,
const std::string& discPath, const LoadedMod& mod) {
std::string key = discPath;
for (auto& ch : key) {
if (ch >= 'A' && ch <= 'Z') {
ch += 'a' - 'A';
}
}
const auto [it, inserted] = claims.try_emplace(std::move(key), &mod);
if (!inserted && it->second != &mod) {
Log.warn("Overlay conflict: '{}' is provided by both '{}' and '{}'; '{}' wins.", discPath,
it->second->metadata.id, mod.metadata.id, mod.metadata.id);
it->second = &mod;
}
}
void find_overlay_files(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
std::unordered_map<std::string, const LoadedMod*>& claims) {
for (const auto& file : mod.bundle->getFileNames()) {
if (!file.starts_with("overlay/")) {
continue;
}
auto overlayPath = file.substr("overlay/"s.size());
assert(!overlayPath.starts_with('/'));
overlayPath.insert(0, "/");
const auto size = mod.bundle->getFileSize(file);
const auto id = s_nextOverlayId++;
s_overlayFiles.emplace(id, OverlayFileData{file, mod.bundle, nullptr});
claim_overlay_path(claims, overlayPath, mod);
files.emplace_back(strdup(overlayPath.c_str()), reinterpret_cast<void*>(id), size);
}
}
void append_runtime_overlays(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
std::unordered_map<std::string, const LoadedMod*>& claims) {
const auto it = s_runtimeOverlays.find(&mod);
if (it == s_runtimeOverlays.end()) {
return;
}
for (const auto& entry : it->second) {
const auto id = s_nextOverlayId++;
if (entry.buffer != nullptr) {
s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, entry.buffer});
} else {
s_overlayFiles.emplace(id, OverlayFileData{entry.bundlePath, mod.bundle, nullptr});
}
claim_overlay_path(claims, entry.discPath, mod);
files.emplace_back(strdup(entry.discPath.c_str()), reinterpret_cast<void*>(id), entry.size);
}
}
struct OpenOverlayFile {
std::vector<u8> ownedData;
std::shared_ptr<const std::vector<u8> > shared;
size_t pos = 0;
[[nodiscard]] const std::vector<u8>& data() const {
return shared != nullptr ? *shared : ownedData;
}
};
void* cbOpen(void* userdata) {
const auto id = reinterpret_cast<uintptr_t>(userdata);
OverlayFileData fileData;
{
std::lock_guard lock{s_overlayMutex};
const auto it = s_overlayFiles.find(id);
if (it == s_overlayFiles.end()) {
// The overlay set was re-pushed between the FST lookup and this call.
return nullptr;
}
fileData = it->second;
}
if (fileData.buffer != nullptr) {
return new OpenOverlayFile{.shared = std::move(fileData.buffer)};
}
try {
auto fileContents = fileData.bundle->readFile(fileData.bundlePath);
return new OpenOverlayFile{.ownedData = std::move(fileContents)};
} catch (const std::runtime_error& e) {
Log.error("Failed to read overlay file {}: {}", fileData.bundlePath, e.what());
return nullptr;
}
}
void cbClose(void* handle) {
const auto openFile = static_cast<OpenOverlayFile*>(handle);
delete openFile;
}
int64_t cbRead(void* handle, uint8_t* buf, const size_t len) {
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
const auto remainingSpace = openFile.data().size() - openFile.pos;
const auto toRead = std::min(remainingSpace, len);
std::memcpy(buf, openFile.data().data() + openFile.pos, toRead);
openFile.pos += toRead;
return static_cast<int64_t>(toRead);
}
int64_t cbSeek(void* handle, int64_t offset, int32_t whence) {
if (whence != 0) {
Log.fatal("Invalid seek mode from aurora: {}", whence);
}
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
const auto posSigned =
std::clamp(offset, static_cast<int64_t>(0), static_cast<int64_t>(openFile.data().size()));
openFile.pos = static_cast<size_t>(posSigned);
return posSigned;
}
constexpr AuroraOverlayCallbacks s_overlayCallbacks = {
.open = cbOpen,
.close = cbClose,
.read = cbRead,
.seek = cbSeek,
};
void overlay_sync_files() {
static bool callbacksRegistered = false;
if (!callbacksRegistered) {
aurora_dvd_overlay_callbacks(&s_overlayCallbacks);
callbacksRegistered = true;
}
s_overlaysDirty = false;
std::vector<AuroraOverlayFile> files;
std::unordered_map<std::string, const LoadedMod*> claims;
{
std::lock_guard lock{s_overlayMutex};
s_overlayFiles.clear();
for (auto& mod : ModLoader::instance().active_mods()) {
find_overlay_files(files, mod, claims);
append_runtime_overlays(files, mod, claims);
}
}
Log.debug("Registering {} overlay file(s).", files.size());
aurora_dvd_overlay_files(files.data(), files.size(), nullptr);
for (const auto& file : files) {
std::free(const_cast<char*>(file.fileName));
}
}
uint64_t overlay_add_file(
LoadedMod& mod, std::string discPath, std::string bundlePath, size_t size) {
const auto handle = s_nextRuntimeHandle++;
s_runtimeOverlays[&mod].push_back({
.handle = handle,
.discPath = std::move(discPath),
.bundlePath = std::move(bundlePath),
.size = size,
});
s_overlaysDirty = true;
return handle;
}
uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector<u8> data) {
const auto handle = s_nextRuntimeHandle++;
const auto size = data.size();
s_runtimeOverlays[&mod].push_back({
.handle = handle,
.discPath = std::move(discPath),
.buffer = std::make_shared<const std::vector<u8>>(std::move(data)),
.size = size,
});
s_overlaysDirty = true;
return handle;
}
bool overlay_remove(LoadedMod& mod, uint64_t handle) {
const auto it = s_runtimeOverlays.find(&mod);
if (it == s_runtimeOverlays.end()) {
return false;
}
if (std::erase_if(it->second, [&](const auto& entry) { return entry.handle == handle; }) == 0) {
return false;
}
if (it->second.empty()) {
s_runtimeOverlays.erase(it);
}
s_overlaysDirty = true;
return true;
}
void overlay_remove_mod(LoadedMod& mod) {
if (s_runtimeOverlays.erase(&mod) != 0) {
s_overlaysDirty = true;
}
}
bool consume_overlays_dirty() {
return std::exchange(s_overlaysDirty, false);
}
constexpr size_t kMaxOverlayFileSize = UINT32_MAX;
bool is_valid_disc_path(const char* discPath) {
if (discPath == nullptr) {
return false;
}
const std::string_view path{discPath};
return path.starts_with('/') && is_safe_resource_path(path.substr(1));
}
ModResult overlay_add_file(
ModContext* context, const char* discPath, const char* bundlePath, OverlayHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || !is_valid_disc_path(discPath) || bundlePath == nullptr ||
!is_safe_resource_path(bundlePath))
{
return MOD_INVALID_ARGUMENT;
}
size_t size = 0;
try {
size = mod->bundle->getFileSize(bundlePath);
} catch (const std::exception& e) {
Log.error(
"[{}] overlay add_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what());
return MOD_UNAVAILABLE;
}
if (size > kMaxOverlayFileSize) {
Log.error("[{}] overlay add_file '{}' failed: file too large ({} bytes)",
mod->metadata.id, bundlePath, size);
return MOD_INVALID_ARGUMENT;
}
const auto handle = overlay_add_file(*mod, discPath, bundlePath, size);
if (outHandle != nullptr) {
*outHandle = handle;
}
return MOD_OK;
}
ModResult overlay_add_buffer(ModContext* context, const char* discPath, const void* data,
size_t size, OverlayHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || !is_valid_disc_path(discPath) || (data == nullptr && size != 0) ||
size > kMaxOverlayFileSize)
{
return MOD_INVALID_ARGUMENT;
}
const auto* bytes = static_cast<const u8*>(data);
const auto handle = overlay_add_buffer(*mod, discPath, std::vector<u8>{bytes, bytes + size});
if (outHandle != nullptr) {
*outHandle = handle;
}
return MOD_OK;
}
ModResult overlay_remove(ModContext* context, OverlayHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr || handle == 0) {
return MOD_INVALID_ARGUMENT;
}
if (!overlay_remove(*mod, handle)) {
Log.error("[{}] overlay remove failed: unknown handle {}", mod->metadata.id, handle);
return MOD_INVALID_ARGUMENT;
}
return MOD_OK;
}
constexpr OverlayService s_overlayService{
.header = SERVICE_HEADER(OverlayService, OVERLAY_SERVICE_MAJOR, OVERLAY_SERVICE_MINOR),
.add_file = overlay_add_file,
.add_buffer = overlay_add_buffer,
.remove = overlay_remove,
};
} // namespace
constinit const ServiceModule g_overlayModule{
.id = OVERLAY_SERVICE_ID,
.majorVersion = OVERLAY_SERVICE_MAJOR,
.minorVersion = OVERLAY_SERVICE_MINOR,
.service = &s_overlayService,
.modDetached = overlay_remove_mod,
.lifecycleApplied = overlay_sync_files,
.frameEnd =
[] {
if (consume_overlays_dirty()) {
overlay_sync_files();
}
},
};
} // namespace dusk::mods::svc
+3
View File
@@ -199,6 +199,9 @@ void ModLoader::init_services() {
{
&svc::g_hostModule,
&svc::g_logModule,
&svc::g_resourceModule,
&svc::g_overlayModule,
&svc::g_textureModule,
&svc::g_configModule,
})
{
+3
View File
@@ -64,6 +64,9 @@ void modules_shutdown();
extern const ServiceModule g_hostModule;
extern const ServiceModule g_logModule;
extern const ServiceModule g_resourceModule;
extern const ServiceModule g_overlayModule;
extern const ServiceModule g_textureModule;
extern const ServiceModule g_configModule;
} // namespace dusk::mods::svc
+102
View File
@@ -0,0 +1,102 @@
#include "registry.hpp"
#include "aurora/lib/logging.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/resource.h"
#include <fmt/format.h>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unordered_map>
namespace dusk::mods::svc {
namespace {
aurora::Module Log("dusk::mods::resource");
// Allocations by owning mod, so buffers still live when a mod detaches can be freed.
std::unordered_map<void*, const LoadedMod*> s_buffers;
void resource_remove_mod(LoadedMod& mod) {
size_t reclaimed = 0;
std::erase_if(s_buffers, [&](const auto& entry) {
if (entry.second != &mod) {
return false;
}
std::free(entry.first);
++reclaimed;
return true;
});
if (reclaimed != 0) {
Log.warn("[{}] reclaimed {} resource buffer(s) that were never freed", mod.metadata.id,
reclaimed);
}
}
ModResult resource_load(ModContext* context, const char* relativePath, ResourceBuffer* outBuffer) {
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(ResourceBuffer)) {
return MOD_INVALID_ARGUMENT;
}
outBuffer->data = nullptr;
outBuffer->size = 0;
auto* mod = mod_from_context(context);
if (mod == nullptr || relativePath == nullptr || !is_safe_resource_path(relativePath)) {
return MOD_INVALID_ARGUMENT;
}
const auto entry = fmt::format("res/{}", relativePath);
std::vector<u8> data;
try {
data = mod->bundle->readFile(entry);
} catch (const std::runtime_error& e) {
Log.error("[{}] resource load '{}' failed: {}", mod->metadata.id, entry, e.what());
return MOD_UNAVAILABLE;
}
if (!data.empty()) {
void* copy = std::malloc(data.size());
if (copy == nullptr) {
return MOD_ERROR;
}
std::memcpy(copy, data.data(), data.size());
s_buffers.emplace(copy, mod);
outBuffer->data = copy;
outBuffer->size = data.size();
}
return MOD_OK;
}
void resource_free(ModContext* context, ResourceBuffer* buffer) {
if (buffer == nullptr || buffer->struct_size < sizeof(ResourceBuffer) ||
buffer->data == nullptr)
{
return;
}
if (s_buffers.erase(buffer->data) == 0) {
Log.error("[{}] resource free: not a live loaded buffer", mod_id_from_context(context));
return;
}
std::free(buffer->data);
buffer->data = nullptr;
buffer->size = 0;
}
constexpr ResourceService s_resourceService{
.header = SERVICE_HEADER(ResourceService, RESOURCE_SERVICE_MAJOR, RESOURCE_SERVICE_MINOR),
.load = resource_load,
.free = resource_free,
};
} // namespace
constinit const ServiceModule g_resourceModule{
.id = RESOURCE_SERVICE_ID,
.majorVersion = RESOURCE_SERVICE_MAJOR,
.minorVersion = RESOURCE_SERVICE_MINOR,
.service = &s_resourceService,
.modDetached = resource_remove_mod,
};
} // namespace dusk::mods::svc
+455
View File
@@ -0,0 +1,455 @@
#include "registry.hpp"
#include "aurora/lib/logging.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "mods/svc/texture.h"
#include <aurora/texture.hpp>
#include <fmt/format.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>
#include <unordered_map>
using namespace std::string_literals;
static_assert(TEXTURE_HASH_WILDCARD == aurora::texture::kWildcardTextureHash);
static_assert(TEXTURE_TLUT_WILDCARD == aurora::texture::kWildcardTlutHash);
namespace dusk::mods::svc {
namespace {
struct TextureRawData {
std::vector<u8> data;
uint32_t width = 0;
uint32_t height = 0;
uint32_t mipCount = 1;
uint32_t gxFormat = 0;
};
aurora::Module Log("dusk::mods::textures");
// Referenced by Aurora's lazy virtual-file reads (from arbitrary threads, under Aurora's registry
// lock) and by raw-entry spans. Immutable after construction; freed only after the corresponding
// unregister_replacement returns, at which point Aurora guarantees no further reads.
struct TextureKeepalive {
std::shared_ptr<ModBundle> bundle;
std::string bundlePath;
std::vector<u8> ownedData;
};
// Called with Aurora's registry lock held: must not take any Dusk lock or re-enter
// aurora::texture. ModBundle reads are documented thread-safe.
bool texture_read_cb(void* userData, const char* path, std::vector<uint8_t>& outBytes) {
auto* keepalive = static_cast<TextureKeepalive*>(userData);
try {
outBytes = keepalive->bundle->readFile(path);
return true;
} catch (...) {
return false;
}
}
struct RuntimeTextureEntry {
uint64_t handle = 0;
aurora::texture::ReplacementRegistration registration;
std::shared_ptr<TextureKeepalive> keepalive;
// Original inputs, kept for re-registration when the mod's priority changes.
bool isVirtual = false;
aurora::texture::ReplacementKey key; // raw entries only
uint32_t width = 0;
uint32_t height = 0;
uint32_t mipCount = 1;
uint32_t gxFormat = 0;
std::string label;
};
struct ModTextureRecord {
int32_t appliedPriority = 0;
bool staticRegistered = false;
aurora::texture::ReplacementGroup staticGroup;
std::vector<std::shared_ptr<TextureKeepalive>> staticKeepalives;
std::vector<RuntimeTextureEntry> runtime;
};
// Game thread only: all mutations happen in service calls made from mod code (init/update/hooks
// run inside ModLoader::tick), in the loader's sync/deactivate paths, or at shutdown.
std::unordered_map<const LoadedMod*, ModTextureRecord> s_modTextures;
uint64_t s_nextTextureHandle = 1;
// Position in m_mods (dependency-sorted load order) + 1; later-loaded mods win. The user
// texture_replacements directory uses kUserTextureReplacementPriority, below any mod.
int32_t compute_mod_priority(const LoadedMod& mod) {
int32_t index = 0;
for (const auto& other : ModLoader::instance().mods()) {
++index;
if (&other == &mod) {
return index;
}
}
return index + 1;
}
bool is_sidecar_mip(std::string_view stem) {
constexpr std::string_view tag = "_mip";
size_t i = stem.size();
while (i > 0 && stem[i - 1] >= '0' && stem[i - 1] <= '9') {
--i;
}
if (i == stem.size() || i < tag.size()) {
return false;
}
return stem.substr(i - tag.size(), tag.size()) == tag;
}
bool has_replacement_extension(std::string_view filename) {
const auto dot = filename.rfind('.');
if (dot == std::string_view::npos) {
return false;
}
std::string ext{filename.substr(dot)};
std::ranges::transform(ext, ext.begin(),
[](char ch) { return ch >= 'A' && ch <= 'Z' ? static_cast<char>(ch + 'a' - 'A') : ch; });
return ext == ".dds" || ext == ".png";
}
std::string_view final_path_component(std::string_view path) {
const auto slash = path.rfind('/');
return slash == std::string_view::npos ? path : path.substr(slash + 1);
}
const LoadedMod* find_static_conflict(
const aurora::texture::ReplacementKey& key, const LoadedMod* exclude) {
for (const auto& [mod, record] : s_modTextures) {
if (mod == exclude) {
continue;
}
for (const auto& registration : record.staticGroup.registrations) {
if (registration.key == key) {
return mod;
}
}
}
return nullptr;
}
void register_static_textures(LoadedMod& mod, ModTextureRecord& record) {
std::vector<std::string> candidates;
for (const auto& file : mod.bundle->getFileNames()) {
if (!file.starts_with("textures/") || !has_replacement_extension(file)) {
continue;
}
auto filename = final_path_component(file);
if (is_sidecar_mip(filename.substr(0, filename.rfind('.')))) {
continue;
}
candidates.push_back(file);
}
// Deterministic order; with the first parse of a key winning, this mirrors Aurora's
// load_replacement_directory dedupe semantics.
std::ranges::sort(candidates);
std::vector<aurora::texture::ReplacementKey> seenKeys;
for (const auto& path : candidates) {
const auto parsed = aurora::texture::parse_replacement_filename(final_path_component(path));
if (!parsed.has_value()) {
Log.warn(
"[{}] '{}' does not follow the texture replacement naming convention; skipped.",
mod.metadata.id, path);
continue;
}
const aurora::texture::ReplacementKey key{*parsed};
if (std::ranges::find(seenKeys, key) != seenKeys.end()) {
continue;
}
seenKeys.push_back(key);
if (const auto* other = find_static_conflict(key, &mod); other != nullptr) {
const auto& winner =
s_modTextures.find(other)->second.appliedPriority > record.appliedPriority ?
*other :
mod;
Log.warn(
"Texture replacement conflict: '{}' is replaced by both '{}' and '{}'; '{}' wins.",
path, other->metadata.id, mod.metadata.id, winner.metadata.id);
}
auto keepalive = std::make_shared<TextureKeepalive>(mod.bundle, path);
const auto registration = aurora::texture::register_virtual_replacement(path,
{.read = texture_read_cb, .userData = keepalive.get()},
{.priority = record.appliedPriority});
if (registration.id == 0) {
continue;
}
record.staticGroup.registrations.push_back(registration);
record.staticKeepalives.push_back(std::move(keepalive));
}
record.staticRegistered = true;
if (!record.staticGroup.registrations.empty()) {
Log.info("[{}] registered {} texture replacement(s).", mod.metadata.id,
record.staticGroup.registrations.size());
}
}
void register_runtime_entry(RuntimeTextureEntry& entry, int32_t priority) {
if (entry.isVirtual) {
entry.registration = aurora::texture::register_virtual_replacement(
entry.keepalive->bundlePath,
{.read = texture_read_cb, .userData = entry.keepalive.get()}, {.priority = priority});
} else {
entry.registration = aurora::texture::register_replacement(entry.key,
{
.bytes = {entry.keepalive->ownedData.data(), entry.keepalive->ownedData.size()},
.width = entry.width,
.height = entry.height,
.mipCount = entry.mipCount,
.gxFormat = entry.gxFormat,
.label = entry.label,
},
{.priority = priority});
}
}
void unregister_record(ModTextureRecord& record) {
aurora::texture::unregister_replacements(record.staticGroup);
record.staticGroup.registrations.clear();
record.staticKeepalives.clear();
record.staticRegistered = false;
for (auto& entry : record.runtime) {
aurora::texture::unregister_replacement(entry.registration);
entry.registration = {};
}
}
void textures_sync_replacements() {
// Module detach removes records eagerly, but a record whose mod is no
// longer active must not linger with stale priority.
std::erase_if(s_modTextures, [&](auto& item) {
if (item.first->active) {
return false;
}
unregister_record(item.second);
return true;
});
for (auto& mod : ModLoader::instance().active_mods()) {
const auto priority = compute_mod_priority(mod);
auto& record = s_modTextures[&mod];
if (record.staticRegistered && record.appliedPriority == priority) {
continue;
}
if (record.staticRegistered) {
// A reload re-sorted m_mods and changed this mod's priority: re-register everything
// at the new priority. Cheap, since file-backed entries decode lazily.
aurora::texture::unregister_replacements(record.staticGroup);
record.staticGroup.registrations.clear();
record.staticKeepalives.clear();
record.appliedPriority = priority;
register_static_textures(mod, record);
for (auto& entry : record.runtime) {
aurora::texture::unregister_replacement(entry.registration);
register_runtime_entry(entry, priority);
}
} else {
record.appliedPriority = priority;
register_static_textures(mod, record);
}
}
}
uint64_t texture_register_raw(
LoadedMod& mod, const aurora::texture::ReplacementKey& key, TextureRawData data) {
auto& record = s_modTextures[&mod];
if (record.appliedPriority == 0) {
record.appliedPriority = compute_mod_priority(mod);
}
auto& entry = record.runtime.emplace_back();
entry.handle = s_nextTextureHandle++;
entry.keepalive = std::make_shared<TextureKeepalive>();
entry.keepalive->ownedData = std::move(data.data);
entry.isVirtual = false;
entry.key = key;
entry.width = data.width;
entry.height = data.height;
entry.mipCount = data.mipCount;
entry.gxFormat = data.gxFormat;
entry.label = fmt::format("mod {} texture {}", mod.metadata.id, entry.handle);
register_runtime_entry(entry, record.appliedPriority);
return entry.handle;
}
uint64_t texture_register_file(LoadedMod& mod, std::string bundlePath) {
auto& record = s_modTextures[&mod];
if (record.appliedPriority == 0) {
record.appliedPriority = compute_mod_priority(mod);
}
auto& entry = record.runtime.emplace_back();
entry.handle = s_nextTextureHandle++;
entry.keepalive = std::make_shared<TextureKeepalive>(mod.bundle, std::move(bundlePath));
entry.isVirtual = true;
register_runtime_entry(entry, record.appliedPriority);
if (entry.registration.id == 0) {
record.runtime.pop_back();
return 0;
}
return entry.handle;
}
bool texture_unregister(LoadedMod& mod, uint64_t handle) {
const auto it = s_modTextures.find(&mod);
if (it == s_modTextures.end()) {
return false;
}
auto& runtime = it->second.runtime;
const auto entry =
std::ranges::find_if(runtime, [&](const auto& e) { return e.handle == handle; });
if (entry == runtime.end()) {
return false;
}
aurora::texture::unregister_replacement(entry->registration);
runtime.erase(entry);
return true;
}
void textures_remove_mod(LoadedMod& mod) {
const auto it = s_modTextures.find(&mod);
if (it == s_modTextures.end()) {
return;
}
unregister_record(it->second);
s_modTextures.erase(it);
}
std::optional<aurora::texture::ReplacementKey> translate_key(const TextureKey* key) {
if (key == nullptr || key->struct_size < sizeof(TextureKey)) {
return std::nullopt;
}
switch (key->kind) {
case TEXTURE_KEY_POINTER:
if (key->pointer == nullptr) {
return std::nullopt;
}
return aurora::texture::ReplacementKey{aurora::texture::TexturePointerKey{key->pointer}};
case TEXTURE_KEY_SOURCE:
if (key->width == 0 || key->height == 0) {
return std::nullopt;
}
return aurora::texture::ReplacementKey{aurora::texture::TextureSourceKey{
.textureHash = key->texture_hash,
.tlutHash = key->tlut_hash,
.width = key->width,
.height = key->height,
.format = key->gx_format,
.hasTlut = key->has_tlut,
}};
default:
return std::nullopt;
}
}
ModResult texture_register_data(ModContext* context, const TextureKey* key, const TextureData* data,
TextureReplacementHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
const auto translatedKey = translate_key(key);
if (mod == nullptr || !translatedKey.has_value() || data == nullptr ||
data->struct_size < sizeof(TextureData) || data->data == nullptr || data->size == 0 ||
data->width == 0 || data->height == 0 || data->mip_count == 0)
{
return MOD_INVALID_ARGUMENT;
}
const auto* bytes = static_cast<const u8*>(data->data);
const auto handle = texture_register_raw(*mod, *translatedKey,
{
.data = std::vector<u8>{bytes, bytes + data->size},
.width = data->width,
.height = data->height,
.mipCount = data->mip_count,
.gxFormat = data->gx_format,
});
if (outHandle != nullptr) {
*outHandle = handle;
}
return MOD_OK;
}
ModResult texture_register_file(
ModContext* context, const char* bundlePath, TextureReplacementHandle* outHandle) {
if (outHandle != nullptr) {
*outHandle = 0;
}
auto* mod = mod_from_context(context);
if (mod == nullptr || bundlePath == nullptr || !is_safe_resource_path(bundlePath)) {
return MOD_INVALID_ARGUMENT;
}
const std::string_view path{bundlePath};
const auto slash = path.rfind('/');
const auto filename = slash == std::string_view::npos ? path : path.substr(slash + 1);
if (!aurora::texture::parse_replacement_filename(filename).has_value()) {
Log.error("[{}] texture register_file '{}' failed: "
"filename does not follow the replacement naming convention",
mod->metadata.id, bundlePath);
return MOD_INVALID_ARGUMENT;
}
try {
mod->bundle->getFileSize(bundlePath);
} catch (const std::exception& e) {
Log.error(
"[{}] texture register_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what());
return MOD_UNAVAILABLE;
}
const auto handle = texture_register_file(*mod, bundlePath);
if (handle == 0) {
return MOD_INVALID_ARGUMENT;
}
if (outHandle != nullptr) {
*outHandle = handle;
}
return MOD_OK;
}
ModResult texture_unregister(ModContext* context, TextureReplacementHandle handle) {
auto* mod = mod_from_context(context);
if (mod == nullptr || handle == 0) {
return MOD_INVALID_ARGUMENT;
}
if (!texture_unregister(*mod, handle)) {
Log.error(
"[{}] texture unregister failed: unknown handle {}", mod->metadata.id, handle);
return MOD_INVALID_ARGUMENT;
}
return MOD_OK;
}
constexpr TextureService s_textureService{
.header = SERVICE_HEADER(TextureService, TEXTURE_SERVICE_MAJOR, TEXTURE_SERVICE_MINOR),
.register_data = texture_register_data,
.register_file = texture_register_file,
.unregister = texture_unregister,
};
} // namespace
constinit const ServiceModule g_textureModule{
.id = TEXTURE_SERVICE_ID,
.majorVersion = TEXTURE_SERVICE_MAJOR,
.minorVersion = TEXTURE_SERVICE_MINOR,
.service = &s_textureService,
.modDetached = textures_remove_mod,
.lifecycleApplied = textures_sync_replacements,
};
} // namespace dusk::mods::svc
+2 -1
View File
@@ -20,7 +20,8 @@ void reload() {
}
const auto root = ConfigPath / "texture_replacements";
s_directoryGroup = aurora::texture::load_replacement_directory(root);
s_directoryGroup = aurora::texture::load_replacement_directory(
root, {.priority = kUserTextureReplacementPriority});
DuskLog.info("Texture replacement directory loaded: {} registration(s)",
s_directoryGroup.registrations.size());
}