Skip to content

Code injection plugin/module system #3

@tech-ticks

Description

@tech-ticks

Current state

Currently, we are shipping a "monolithic" binary with patches for common use-cases, which imposes the following problems:

  • Users can't selectively enable or disable patches, so if a patch causes issues, the only option is to disable code injection entirely.
  • Adding custom functionality is difficult and annoying, users have to fork this repo and build their own binary to make changes. If users want to include upstream changes in their forks, they have to merge the changes manually.

Implementation

Instead of shipping code in one binary, hyperbeam should act as a "plugin manager" which loads other NROs and provide some useful functionality for patches. Skyline can be used as a reference on how to implement this: https://github.com/skyline-dev/skyline/blob/master/source/skyline/plugin/PluginManager.cpp

Challenges

  • Code hooking is currently implemented by patching the binary by generating an IPS file (see https://github.com/tech-ticks/hyperbeam/blob/master/patches/codehook.slpatch). This will no longer be possible because plugins can be dynamically loaded. Instead, dynamic runtime hooks can be used (see https://github.com/skyline-dev/skyline/blob/master/source/skyline/inlinehook/And64InlineHook.cpp#L645)
  • Functions from the game binary need to be called in a different way. The symbols are currently linked with a linker script, e.g. PegasusActDatabase_ActorData_get_Name = 0x061F1A0 - 0x5caf000;. This works because we know where both the game binary and the hyperbeam binary are loaded in memory. Some ideas on how to work around it:
    • Research whether absolute jumps can be used in linker scripts (easiest option; should work because the game binary is always loaded in the same address)
    • Attempt to re-export the symbols in the "main" hyperbeam binary to make them accessible in plugins via dynamic linking. Preferred option because it would allow most plugins to work on all game versions without compiling them twice
    • Generate a header file with function pointers to absolute addresses
    • Keep the current linker script approach with relative jumps and "move" every plugin to the same specific address in memory when its code is executed, e.g. by reordering the virtual memory pages. Complicated and error-prone, probably a bad idea

API proposal

Callbacks for common hooks, e.g. when the game starts or on every frame, should be provided. After a plugin was loaded, an initialization function of the plugin is called, which should return a struct with information like the plugin name. There are separate callbacks when plugins are enabled or disabled to allow users or plugin developers to dynamically enable and disable plugins in the future. A simple plugin could look like this:

#include <hyperbeam/plugin.h>

extern "C" hb::PluginInfo hbPluginOnInit(void* reserved) {
  return hb::PluginInfo {
    .id = "authorname.exampleplugin",
    .name = "Example plugin",
    .author = "Author name",
    .version = hb::Version(1, 0, 0), // 1.0.0
    .flags = hb::PluginFlags::SOME_FLAG | hb::PluginFlags::SOME_OTHER_FLAG,
    .result = hb::Result::OK // Allows returning errors if initialization failed
  }
}

void onUpdate(float deltaTime) {}
void onDungeonUpdate(float deltaTime) {}
void onSpecialFunc(int specialFuncId) {}

extern "C" hb::Result hbPluginOnEnable(void* reserved) {
  if ((hb::registerCallback(hb::Callbacks::UPDATE, onUpdate) != hb::Result::OK) // Every frame
    return hb::Result::ERROR; 
  if ((hb::registerCallback(hb::Callbacks::DUNGEON_UPDATE, onDungeonUpdate) != hb::Result::OK) // Every frame in dungeons
    return hb::Result::ERROR;
  if ((hb::registerCallback(hb::Callbacks::SPECIALFUNC, onSpecialFunc) != hb::Result::OK) // When SpecialFunc is called in a script
    return hb::Result::ERROR;
  // ...

  // Custom hooks that are not supported through callbacks can be added with A64HookFunction(), see the link above
  return hb::Result::OK;
}

extern "C" hb::Result hbPluginOnDisable(void* reserved) {
  return hb::Result::OK;
}

Integration in DreamNexus

Plugins should not be directly supported in the DreamNexus UI; instead, the existing mod system should be leveraged to create "wrapper mods" that simply copy the plugin file to the expected directory at build time. Additional files that are required by the plugin can also be generated in the mod.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions