Skip to content

Commit

Permalink
Added mechanism for recomps to declare custom mod content types and c…
Browse files Browse the repository at this point in the history
…ontainer formats
  • Loading branch information
Mr-Wiseguy committed Sep 28, 2024
1 parent 46797d6 commit 261b196
Show file tree
Hide file tree
Showing 4 changed files with 495 additions and 215 deletions.
105 changes: 87 additions & 18 deletions librecomp/include/librecomp/mods.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,20 @@ namespace recomp {
Good,
InvalidGame,
MinimumRecompVersionNotMet,
MissingDependency,
WrongDependencyVersion,
FailedToLoadCode,
};

std::string error_to_string(ModLoadError);

enum class CodeModLoadError {
Good,
InternalError,
HasSymsButNoBinary,
HasBinaryButNoSyms,
FailedToParseSyms,
MissingDependencyInManifest,
FailedToLoadNativeCode,
FailedToLoadNativeLibrary,
FailedToFindNativeExport,
Expand All @@ -66,16 +77,13 @@ namespace recomp {
InvalidFunctionReplacement,
FailedToFindReplacement,
ReplacementConflict,
MissingDependencyInManifest,
MissingDependency,
WrongDependencyVersion,
ModConflict,
DuplicateExport,
NoSpecifiedApiVersion,
UnsupportedApiVersion,
};

std::string error_to_string(ModLoadError);
std::string error_to_string(CodeModLoadError);

struct ModFileHandle {
virtual ~ModFileHandle() = default;
Expand Down Expand Up @@ -121,6 +129,7 @@ namespace recomp {
Version version;
std::vector<std::string> authors;
std::vector<Dependency> dependencies;
bool runtime_toggleable;
};

struct ModManifest {
Expand All @@ -133,6 +142,7 @@ namespace recomp {
std::unordered_map<std::string, size_t> dependencies_by_id;
Version minimum_recomp_version;
Version version;
bool runtime_toggleable;

std::vector<NativeLibraryManifest> native_libraries;
std::unique_ptr<ModFileHandle> file_handle;
Expand All @@ -155,10 +165,7 @@ namespace recomp {
ModLoadErrorDetails(const std::string& mod_id_, ModLoadError error_, const std::string& error_param_) :
mod_id(mod_id_), error(error_), error_param(error_param_) {}
};

void scan_mods();
void enable_mod(const std::string& mod_id, bool enabled);
bool is_mod_enabled(const std::string& mod_id);

std::vector<ModDetails> get_mod_details(const std::string& mod_game_id);

// Internal functions, TODO move to an internal header.
Expand All @@ -169,7 +176,38 @@ namespace recomp {

using GenericFunction = std::variant<recomp_func_t*>;

class ModContext;
class ModHandle;
using content_enabled_callback = void(ModContext&, const ModHandle&);
using content_disabled_callback = void(ModContext&, const ModHandle&);

struct ModContentType {
// The file that's used to indicate that a mod contains this content type.
// If a mod contains this file, then it has this content type.
std::string content_filename;
// Whether or not this type of content can be toggled at runtime.
bool allow_runtime_toggle;
// Function to call when an instance of this content type is enabled.
content_enabled_callback* on_enabled;
// Function to call when an instance of this content type is disabled.
content_disabled_callback* on_disabled;
};

// Holds IDs for mod content types, which get assigned as they're registered.
// This is just a wrapper around a number for type safety purposes.
struct ModContentTypeId {
size_t value;
};

struct ModContainerType {
// The types of content that this container is allowed to have.
// Leaving this empty will allow the container to have any type of content.
std::vector<ModContentTypeId> supported_content_types;
// Whether or not this container requires a manifest to be treated as a valid mod.
// If no manifest is present, a default one will be created.
bool requires_manifest;
};

class ModContext {
public:
ModContext();
Expand All @@ -183,22 +221,34 @@ namespace recomp {
std::vector<ModLoadErrorDetails> load_mods(const std::string& mod_game_id, uint8_t* rdram, int32_t load_address, uint32_t& ram_used);
void unload_mods();
std::vector<ModDetails> get_mod_details(const std::string& mod_game_id);
ModContentTypeId register_content_type(const ModContentType& type);
bool register_container_type(const std::string& extension, const std::vector<ModContentTypeId>& content_types, bool requires_manifest);
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);
ModLoadError load_mod(uint8_t* rdram, const std::unordered_map<uint32_t, uint16_t>& section_map, recomp::mods::ModHandle& handle, int32_t load_address, uint32_t& ram_used, std::string& error_param);
ModOpenError open_mod(const std::filesystem::path& mod_path, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest);
ModLoadError load_mod(recomp::mods::ModHandle& mod, std::string& error_param);
void check_dependencies(recomp::mods::ModHandle& mod, std::vector<std::pair<recomp::mods::ModLoadError, std::string>>& errors);
ModLoadError load_mod_code(recomp::mods::ModHandle& mod, std::string& error_param);
ModLoadError resolve_dependencies(recomp::mods::ModHandle& mod, std::string& error_param);
void add_opened_mod(ModManifest&& manifest, std::vector<size_t>&& game_indices);
CodeModLoadError load_mod_code(uint8_t* rdram, const std::unordered_map<uint32_t, uint16_t>& section_vrom_map, recomp::mods::ModHandle& mod, int32_t load_address, uint32_t& ram_used, std::string& error_param);
CodeModLoadError resolve_code_dependencies(recomp::mods::ModHandle& mod, std::string& error_param);
void add_opened_mod(ModManifest&& manifest, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types);

static void on_code_mod_enabled(ModContext& context, const ModHandle& mod);

std::vector<ModContentType> content_types;
std::unordered_map<std::filesystem::path, 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::vector<ModHandle> opened_mods;
std::unordered_map<std::string, size_t> opened_mods_by_id;
std::unordered_set<std::string> mod_ids;
std::unordered_set<std::string> enabled_mods;
std::unordered_map<recomp_func_t*, PatchData> patched_funcs;
std::unordered_map<std::string, size_t> loaded_mods_by_id;
std::vector<size_t> loaded_code_mods;
size_t num_events = 0;
ModContentTypeId code_content_type_id;
size_t active_game = (size_t)-1;
};

class ModCodeHandle {
Expand Down Expand Up @@ -229,8 +279,10 @@ namespace recomp {
std::unique_ptr<ModCodeHandle> code_handle;
std::unique_ptr<N64Recomp::Context> recompiler_context;
std::vector<uint32_t> section_load_addresses;
// Content types present in this mod.
std::vector<ModContentTypeId> content_types;

ModHandle(ModManifest&& manifest, std::vector<size_t>&& game_indices);
ModHandle(const ModContext& context, ModManifest&& manifest, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types);
ModHandle(const ModHandle& rhs) = delete;
ModHandle& operator=(const ModHandle& rhs) = delete;
ModHandle(ModHandle&& rhs);
Expand All @@ -240,16 +292,24 @@ namespace recomp {
size_t num_exports() const;
size_t num_events() const;

ModLoadError populate_exports(std::string& error_param);
CodeModLoadError populate_exports(std::string& error_param);
bool get_export_function(const std::string& export_name, GenericFunction& out) const;
ModLoadError populate_events(size_t base_event_index, std::string& error_param);
CodeModLoadError populate_events(size_t base_event_index, std::string& error_param);
bool get_global_event_index(const std::string& event_name, size_t& event_index_out) const;
ModLoadError load_native_library(const NativeLibraryManifest& lib_manifest, std::string& error_param);
CodeModLoadError load_native_library(const NativeLibraryManifest& lib_manifest, std::string& error_param);

bool is_for_game(size_t game_index) const {
auto find_it = std::find(game_indices.begin(), game_indices.end(), game_index);
return find_it != game_indices.end();
}

bool is_runtime_toggleable() const {
return runtime_toggleable;
}

void disable_runtime_toggle() {
runtime_toggleable = false;
}
private:
// Mapping of export name to function index.
std::unordered_map<std::string, size_t> exports_by_name;
Expand All @@ -261,6 +321,8 @@ namespace recomp {
std::vector<std::unique_ptr<DynamicLibrary>> native_libraries; // Vector of pointers so that implementation can be elsewhere.
// Games that this mod supports.
std::vector<size_t> game_indices;
// Whether this mod can be toggled at runtime.
bool runtime_toggleable;
};

class NativeCodeHandle : public ModCodeHandle {
Expand Down Expand Up @@ -327,7 +389,14 @@ namespace recomp {
void setup_events(size_t num_events);
void register_event_callback(size_t event_index, GenericFunction callback);
void reset_events();
ModLoadError validate_api_version(uint32_t api_version, std::string& error_param);
CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param);


void scan_mods();
void enable_mod(const std::string& mod_id, bool enabled);
bool is_mod_enabled(const std::string& mod_id);
ModContentTypeId register_mod_content_type(const ModContentType& type);
bool register_mod_container_type(const std::string& extension, const std::vector<ModContentTypeId>& content_types, bool requires_manifest);
}
};

Expand Down
119 changes: 89 additions & 30 deletions librecomp/src/mod_manifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ recomp::mods::ModOpenError validate_manifest(const recomp::mods::ModManifest& ma
return ModOpenError::Good;
}

recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesystem::path& mod_path, std::string& error_param) {
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) {
ModManifest manifest{};
std::error_code ec;
error_param = "";
Expand Down Expand Up @@ -408,12 +408,37 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys
bool exists;
std::vector<char> manifest_data = manifest.file_handle->read_file("manifest.json", exists);
if (!exists) {
return ModOpenError::NoManifest;
}
// 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);
}

ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param);
if (parse_error != ModOpenError::Good) {
return parse_error;
manifest.mod_id = mod_path.stem().string();
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;
}
}
else {
ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param);
if (parse_error != ModOpenError::Good) {
return parse_error;
}
}
}

Expand All @@ -439,10 +464,33 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys
}
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);
}
}

// Store the loaded mod manifest in a new mod handle.
manifest.mod_root_path = mod_path;
add_opened_mod(std::move(manifest), std::move(game_indices));
add_opened_mod(std::move(manifest), std::move(game_indices), std::move(detected_content_types));

return ModOpenError::Good;
}
Expand Down Expand Up @@ -493,44 +541,55 @@ std::string recomp::mods::error_to_string(ModLoadError error) {
return "Invalid game";
case ModLoadError::MinimumRecompVersionNotMet:
return "Mod requires a newer version of this project";
case ModLoadError::HasSymsButNoBinary:
case ModLoadError::MissingDependency:
return "Missing dependency";
case ModLoadError::WrongDependencyVersion:
return "Wrong dependency version";
case ModLoadError::FailedToLoadCode:
return "Failed to load mod code";
}
return "Unknown mod loading error " + std::to_string((int)error);
}

std::string recomp::mods::error_to_string(CodeModLoadError error) {
switch (error) {
case CodeModLoadError::Good:
return "Good";
case CodeModLoadError::InternalError:
return "Code mod loading internal error";
case CodeModLoadError::HasSymsButNoBinary:
return "Mod has a symbol file but no binary file";
case ModLoadError::HasBinaryButNoSyms:
case CodeModLoadError::HasBinaryButNoSyms:
return "Mod has a binary file but no symbol file";
case ModLoadError::FailedToParseSyms:
case CodeModLoadError::FailedToParseSyms:
return "Failed to parse mod symbol file";
case ModLoadError::FailedToLoadNativeCode:
case CodeModLoadError::MissingDependencyInManifest:
return "Dependency is present in mod symbols but not in the manifest";
case CodeModLoadError::FailedToLoadNativeCode:
return "Failed to load offline mod library";
case ModLoadError::FailedToLoadNativeLibrary:
case CodeModLoadError::FailedToLoadNativeLibrary:
return "Failed to load mod library";
case ModLoadError::FailedToFindNativeExport:
case CodeModLoadError::FailedToFindNativeExport:
return "Failed to find native export";
case ModLoadError::InvalidReferenceSymbol:
case CodeModLoadError::InvalidReferenceSymbol:
return "Reference symbol does not exist";
case ModLoadError::InvalidImport:
case CodeModLoadError::InvalidImport:
return "Imported function not found";
case ModLoadError::InvalidCallbackEvent:
case CodeModLoadError::InvalidCallbackEvent:
return "Event for callback not found";
case ModLoadError::InvalidFunctionReplacement:
case CodeModLoadError::InvalidFunctionReplacement:
return "Function to be replaced does not exist";
case ModLoadError::FailedToFindReplacement:
case CodeModLoadError::FailedToFindReplacement:
return "Failed to find replacement function";
case ModLoadError::ReplacementConflict:
case CodeModLoadError::ReplacementConflict:
return "Attempted to replace a function that cannot be replaced";
case ModLoadError::MissingDependencyInManifest:
return "Dependency is present in mod symbols but not in the manifest";
case ModLoadError::MissingDependency:
return "Missing dependency";
case ModLoadError::WrongDependencyVersion:
return "Wrong dependency version";
case ModLoadError::ModConflict:
case CodeModLoadError::ModConflict:
return "Conflicts with other mod";
case ModLoadError::DuplicateExport:
case CodeModLoadError::DuplicateExport:
return "Duplicate exports in mod";
case ModLoadError::NoSpecifiedApiVersion:
case CodeModLoadError::NoSpecifiedApiVersion:
return "Mod DLL does not specify an API version";
case ModLoadError::UnsupportedApiVersion:
case CodeModLoadError::UnsupportedApiVersion:
return "Mod DLL has an unsupported API version";
}
return "Unknown mod loading error " + std::to_string((int)error);
}
Loading

0 comments on commit 261b196

Please sign in to comment.