Add support for embedded mods. (#108)
Some checks failed
validate / ubuntu (x64, Debug) (push) Has been cancelled
validate / ubuntu (x64, Release) (push) Has been cancelled
validate / ubuntu (arm64, Debug) (push) Has been cancelled
validate / ubuntu (arm64, Release) (push) Has been cancelled
validate / windows (x64, Debug) (push) Has been cancelled
validate / windows (x64, Release) (push) Has been cancelled
validate / macos (arm64, Debug) (push) Has been cancelled
validate / macos (arm64, Release) (push) Has been cancelled
validate / macos (x64, Debug) (push) Has been cancelled
validate / macos (x64, Release) (push) Has been cancelled

* Add support for embedded mods.

* Fix autogenerated mod manifests

---------

Co-authored-by: Mr-Wiseguy <mrwiseguyromhacking@gmail.com>
This commit is contained in:
Darío 2025-04-23 00:53:43 -03:00 committed by GitHub
parent 4b57f50722
commit 02d797aedc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 158 additions and 103 deletions

View file

@ -145,6 +145,7 @@ namespace recomp {
ZipModFileHandle() = default;
ZipModFileHandle(const std::filesystem::path& mod_path, ModOpenError& error);
ZipModFileHandle(std::span<const uint8_t> mod_bytes, ModOpenError& error);
~ZipModFileHandle() final;
std::vector<char> read_file(const std::string& filepath, bool& exists) const final;
@ -331,6 +332,7 @@ namespace recomp {
~ModContext();
void register_game(const std::string& mod_game_id);
void register_embedded_mod(const std::string& mod_id, std::span<const uint8_t> mod_bytes);
std::vector<ModOpenErrorDetails> scan_mod_folder(const std::filesystem::path& mod_folder);
void close_mods();
void load_mods_config();
@ -361,7 +363,9 @@ namespace recomp {
ModContentTypeId get_code_content_type() const { return code_content_type_id; }
bool is_content_runtime_toggleable(ModContentTypeId content_type) const;
private:
ModOpenError open_mod(const std::filesystem::path& mod_path, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest);
ModOpenError open_mod_from_manifest(ModManifest &manifest, std::string &error_param, const std::vector<ModContentTypeId> &supported_content_types, bool requires_manifest);
ModOpenError open_mod_from_path(const std::filesystem::path& mod_path, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest);
ModOpenError open_mod_from_memory(std::span<const uint8_t> mod_bytes, std::string &error_param, const std::vector<ModContentTypeId> &supported_content_types, bool requires_manifest);
ModLoadError load_mod(ModHandle& mod, std::string& error_param);
void check_dependencies(ModHandle& mod, std::vector<std::pair<ModLoadError, std::string>>& errors);
CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map<uint32_t, uint16_t>& section_vrom_map, ModHandle& mod, int32_t load_address, bool hooks_available, uint32_t& ram_used, std::string& error_param);
@ -381,6 +385,7 @@ namespace recomp {
std::unordered_map<std::string, ModContainerType> container_types;
// Maps game mod ID to the mod's internal integer ID.
std::unordered_map<std::string, size_t> mod_game_ids;
std::unordered_map<std::string, std::span<const uint8_t>> embedded_mod_bytes;
std::vector<ModHandle> opened_mods;
std::unordered_map<std::string, size_t> opened_mods_by_id;
std::unordered_map<std::filesystem::path::string_type, size_t> opened_mods_by_filename;
@ -586,6 +591,7 @@ namespace recomp {
CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param);
void initialize_mods();
void register_embedded_mod(const std::string &mod_id, std::span<const uint8_t> mod_bytes);
void scan_mods();
void close_mods();
std::filesystem::path get_mods_directory();

View file

@ -69,6 +69,16 @@ recomp::mods::ZipModFileHandle::ZipModFileHandle(const std::filesystem::path& mo
error = ModOpenError::Good;
}
recomp::mods::ZipModFileHandle::ZipModFileHandle(std::span<const uint8_t> mod_bytes, ModOpenError& error) {
archive = std::make_unique<mz_zip_archive>();
if (!mz_zip_reader_init_mem(archive.get(), mod_bytes.data(), mod_bytes.size(), 0)) {
error = ModOpenError::InvalidZip;
return;
}
error = ModOpenError::Good;
}
std::vector<char> recomp::mods::ZipModFileHandle::read_file(const std::string& filepath, bool& exists) const {
std::vector<char> ret{};
@ -728,8 +738,116 @@ bool parse_mod_config_storage(const std::filesystem::path &path, const std::stri
return true;
}
recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesystem::path& mod_path, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest) {
recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModManifest& manifest, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest) {
{
bool exists;
std::vector<char> manifest_data = manifest.file_handle->read_file("mod.json", exists);
if (!exists) {
// If this container type requires a manifest then return an error.
if (requires_manifest) {
return ModOpenError::NoManifest;
}
// Otherwise, create a default manifest.
else {
// Take the file handle from the manifest before clearing it so that it can be reassigned afterwards.
std::unique_ptr<ModFileHandle> file_handle = std::move(manifest.file_handle);
std::filesystem::path root_path = std::move(manifest.mod_root_path);
manifest = {};
manifest.file_handle = std::move(file_handle);
manifest.mod_root_path = std::move(root_path);
for (const auto &[key, val] : mod_game_ids) {
manifest.mod_game_ids.emplace_back(key);
}
manifest.mod_id = manifest.mod_root_path.stem().string();
manifest.display_name = manifest.mod_id;
manifest.description.clear();
manifest.short_description.clear();
manifest.authors = { "Unknown" };
manifest.minimum_recomp_version.major = 0;
manifest.minimum_recomp_version.minor = 0;
manifest.minimum_recomp_version.patch = 0;
manifest.version.major = 0;
manifest.version.minor = 0;
manifest.version.patch = 0;
manifest.enabled_by_default = true;
}
}
else {
ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param);
if (parse_error != ModOpenError::Good) {
return parse_error;
}
}
}
// Check for this being a duplicate of another opened mod.
if (mod_ids.contains(manifest.mod_id)) {
error_param = manifest.mod_id;
return ModOpenError::DuplicateMod;
}
mod_ids.emplace(manifest.mod_id);
// Check for this mod's game ids being valid.
std::vector<size_t> game_indices;
for (const auto &mod_game_id : manifest.mod_game_ids) {
auto find_id_it = mod_game_ids.find(mod_game_id);
if (find_id_it == mod_game_ids.end()) {
error_param = mod_game_id;
return ModOpenError::WrongGame;
}
game_indices.emplace_back(find_id_it->second);
}
// Scan for content types present in this mod.
std::vector<ModContentTypeId> detected_content_types;
auto scan_for_content_type = [&detected_content_types, &manifest](ModContentTypeId type_id, std::vector<ModContentType> &content_types) {
const ModContentType &content_type = content_types[type_id.value];
if (manifest.file_handle->file_exists(content_type.content_filename)) {
detected_content_types.emplace_back(type_id);
}
};
// If the mod has a list of specific content types, scan for only those.
if (!supported_content_types.empty()) {
for (ModContentTypeId content_type_id : supported_content_types) {
scan_for_content_type(content_type_id, content_types);
}
}
// Otherwise, scan for all content types.
else {
for (size_t content_type_index = 0; content_type_index < content_types.size(); content_type_index++) {
scan_for_content_type(ModContentTypeId{ .value = content_type_index }, content_types);
}
}
// Read the mod config if it exists.
ConfigStorage config_storage;
std::filesystem::path config_path = mod_config_directory / (manifest.mod_id + ".json");
parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema);
// Read the mod thumbnail if it exists.
static const std::string thumbnail_dds_name = "thumb.dds";
static const std::string thumbnail_png_name = "thumb.png";
bool exists = false;
std::vector<char> thumbnail_data = manifest.file_handle->read_file(thumbnail_dds_name, exists);
if (!exists) {
thumbnail_data = manifest.file_handle->read_file(thumbnail_png_name, exists);
}
// Store the loaded mod manifest in a new mod handle.
add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data));
return ModOpenError::Good;
}
recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_path(const std::filesystem::path& mod_path, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest) {
ModManifest manifest{};
manifest.mod_root_path = mod_path;
std::error_code ec;
error_param = "";
@ -764,108 +882,18 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys
return handle_error;
}
{
bool exists;
std::vector<char> manifest_data = manifest.file_handle->read_file("mod.json", exists);
if (!exists) {
// If this container type requires a manifest then return an error.
if (requires_manifest) {
return ModOpenError::NoManifest;
}
// Otherwise, create a default manifest.
else {
// Take the file handle from the manifest before clearing it so that it can be reassigned afterwards.
std::unique_ptr<ModFileHandle> file_handle = std::move(manifest.file_handle);
manifest = {};
manifest.file_handle = std::move(file_handle);
for (const auto& [key, val] : mod_game_ids) {
manifest.mod_game_ids.emplace_back(key);
}
return open_mod_from_manifest(manifest, error_param, supported_content_types, requires_manifest);
}
manifest.mod_id = mod_path.stem().string();
manifest.display_name = manifest.mod_id;
manifest.description.clear();
manifest.short_description.clear();
manifest.authors = { "Unknown" };
manifest.minimum_recomp_version.major = 0;
manifest.minimum_recomp_version.minor = 0;
manifest.minimum_recomp_version.patch = 0;
manifest.version.major = 0;
manifest.version.minor = 0;
manifest.version.patch = 0;
manifest.enabled_by_default = true;
}
}
else {
ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param);
if (parse_error != ModOpenError::Good) {
return parse_error;
}
}
recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_memory(std::span<const uint8_t> mod_bytes, std::string &error_param, const std::vector<ModContentTypeId> &supported_content_types, bool requires_manifest) {
ModManifest manifest{};
ModOpenError handle_error;
manifest.file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(mod_bytes, handle_error);
if (handle_error != ModOpenError::Good) {
return handle_error;
}
// Check for this being a duplicate of another opened mod.
if (mod_ids.contains(manifest.mod_id)) {
error_param = manifest.mod_id;
return ModOpenError::DuplicateMod;
}
mod_ids.emplace(manifest.mod_id);
// Check for this mod's game ids being valid.
std::vector<size_t> game_indices;
for (const auto& mod_game_id : manifest.mod_game_ids) {
auto find_id_it = mod_game_ids.find(mod_game_id);
if (find_id_it == mod_game_ids.end()) {
error_param = mod_game_id;
return ModOpenError::WrongGame;
}
game_indices.emplace_back(find_id_it->second);
}
// Scan for content types present in this mod.
std::vector<ModContentTypeId> detected_content_types;
auto scan_for_content_type = [&detected_content_types, &manifest](ModContentTypeId type_id, std::vector<ModContentType>& content_types) {
const ModContentType& content_type = content_types[type_id.value];
if (manifest.file_handle->file_exists(content_type.content_filename)) {
detected_content_types.emplace_back(type_id);
}
};
// If the mod has a list of specific content types, scan for only those.
if (!supported_content_types.empty()) {
for (ModContentTypeId content_type_id : supported_content_types) {
scan_for_content_type(content_type_id, content_types);
}
}
// Otherwise, scan for all content types.
else {
for (size_t content_type_index = 0; content_type_index < content_types.size(); content_type_index++) {
scan_for_content_type(ModContentTypeId{.value = content_type_index}, content_types);
}
}
// Read the mod config if it exists.
ConfigStorage config_storage;
std::filesystem::path config_path = mod_config_directory / (manifest.mod_id + ".json");
parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema);
// Read the mod thumbnail if it exists.
static const std::string thumbnail_dds_name = "thumb.dds";
static const std::string thumbnail_png_name = "thumb.png";
bool exists = false;
std::vector<char> thumbnail_data = manifest.file_handle->read_file(thumbnail_dds_name, exists);
if (!exists) {
thumbnail_data = manifest.file_handle->read_file(thumbnail_png_name, exists);
}
// Store the loaded mod manifest in a new mod handle.
manifest.mod_root_path = mod_path;
add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data));
return ModOpenError::Good;
return open_mod_from_manifest(manifest, error_param, supported_content_types, requires_manifest);
}
std::string recomp::mods::error_to_string(ModOpenError error) {

View file

@ -627,6 +627,10 @@ void recomp::mods::ModContext::register_game(const std::string& mod_game_id) {
mod_game_ids.emplace(mod_game_id, mod_game_ids.size());
}
void recomp::mods::ModContext::register_embedded_mod(const std::string &mod_id, std::span<const uint8_t> mod_bytes) {
embedded_mod_bytes.emplace(mod_id, mod_bytes);
}
void recomp::mods::ModContext::close_mods() {
std::unique_lock lock(opened_mods_mutex);
opened_mods_by_id.clear();
@ -802,10 +806,10 @@ std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mo
std::error_code ec;
close_mods();
static const std::vector<ModContentTypeId> empty_content_types{};
for (const auto& mod_path : std::filesystem::directory_iterator{mod_folder, std::filesystem::directory_options::skip_permission_denied, ec}) {
bool is_mod = false;
bool requires_manifest = true;
static const std::vector<ModContentTypeId> empty_content_types{};
std::reference_wrapper<const std::vector<ModContentTypeId>> supported_content_types = std::cref(empty_content_types);
if (mod_path.is_regular_file()) {
auto find_container_it = container_types.find(mod_path.path().extension().string());
@ -821,7 +825,7 @@ std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mo
if (is_mod) {
printf("Opening mod " PATHFMT "\n", mod_path.path().stem().c_str());
std::string open_error_param;
ModOpenError open_error = open_mod(mod_path, open_error_param, supported_content_types, requires_manifest);
ModOpenError open_error = open_mod_from_path(mod_path, open_error_param, supported_content_types, requires_manifest);
if (open_error != ModOpenError::Good) {
ret.emplace_back(mod_path.path(), open_error, open_error_param);
@ -832,6 +836,18 @@ std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mo
}
}
for (const auto &mod_bytes : embedded_mod_bytes) {
if (opened_mods_by_id.contains(mod_bytes.first)) {
continue;
}
std::string open_error_param;
ModOpenError open_error = open_mod_from_memory(mod_bytes.second, open_error_param, empty_content_types, true);
if (open_error != ModOpenError::Good) {
ret.emplace_back(mod_bytes.first, open_error, open_error_param);
}
}
return ret;
}

View file

@ -90,6 +90,11 @@ void recomp::mods::initialize_mods() {
mod_context->set_mod_config_directory(config_path / mod_config_directory);
}
void recomp::mods::register_embedded_mod(const std::string &mod_id, std::span<const uint8_t> mod_bytes) {
std::lock_guard<std::mutex> lock(mod_context_mutex);
mod_context->register_embedded_mod(mod_id, mod_bytes);
}
void recomp::mods::scan_mods() {
std::vector<recomp::mods::ModOpenErrorDetails> mod_open_errors;
{