Skip to content

The Development Plugin

blu-dev edited this page Apr 19, 2021 · 3 revisions

The Problem

It is a great understatement to say that Ultimate is not a light-weight game, especially on startup. A single iteration for your code mod has to go through numerous steps:

  1. Notice that there is a problem with the current implementation.
  2. Figure out what that problem is and where it is in the code, then change it.
  3. Recompile the plugin, fixing errors and sending it back to the switch.
  4. Closing and reopening Smash. Depending on how many plugins you have, and how many mods, this can take up to minutes.
  5. Go into training mode or a match and test to see if the problem has been fixed.

This iteration time can get very lengthy and is quite discouraging while trying to fine-tune/balance your mod.

The Solution

Smashline's solution to this is to include support for what I call a "development plugin". The development plugin is different from the average skyline plugin in a couple of ways:

  • skyline_main goes unused, as Skyline is not the loader of the plugin
  • They are both installed AND have the potential to be uninstalled at runtime
  • There can only be one at a time

The development plugin is loaded from rom:/smashline/development.nro and is supposed to contain whatever mods are currently being developed. It is required to export smashline_install, which is called by Smashline to install the mods for your plugin. They can also optionally export smashline_uninstall, which can be used to perform any "cleanup" that the plugin needs to do, such as freeing dynamic memory.

Smashline automatically uninstalls every ACMD script, status script, once-per-frame callback, and agent reset callback that points to a function in the development plugin when it gets unloaded.

Using the Development Plugin

There are a few guidelines when it comes to developing a mod and using the development plugin to do it.

  • No kind of function hooking (skyline hooks, smashline hooks, etc.) should be used from within the development plugin. If the plugin gets reloaded at runtime (the whole point of this), the references will be to garbage addresses and will cause crashes. If you must hook a function, see the sections below.
  • For the same reason function hooks aren't allowed, common status replacements aren't allowed if they also provide a symbol to hook.
  • Once-per-agent-frame replacements should not be used. They are faster, as there is no iteration or locking of mutexes, but they also operate similar to function hooks and can't be used. Instead, use the once-per-agent-frame callbacks explained elsewhere in the wiki.

In order to send your plugin to the right location, make sure that you have updated cargo-skyline (cargo install cargo-skyline) and build your plugin with the following command

cargo skyline install --install-path rom:/smashline/development.nro

This will build/install your plugin to the right location for smashline to read. In order to reload the plugin at runtime, press L + R + Dpad Up

If you are confused, please see this video below where I demonstrate the development plugin.

Working around restrictions

These restrictions are unfortunate, I'll admit. Part of the reason that this kind of development option hasn't been made available yet is because of these restrictions. It is difficult to police what people do to prevent them from encountering difficult to trace bugs. However, in this case I believe the benefits far outweigh the costs. While I can't prepare you for every scenario, I will provide some solutions to those developing larger mods.

Parent and child plugins

This is an important concept for the other sections, so listen up!

In this instance, a "parent" plugin is a plugin which is installed via skyline, meaning it is installed and is available for the lifetime of the software. This means that it is a safe place to install your hooks, both skyline and smashline style, and also to store your static variables/other important items.

The "child" plugin is going to be the development plugin. This plugin should only know what the parent tells it. If the parent plugin hooks a function, send the important information to the child plugin. I know that this sounds simple, but it's at the core of how to work around the restrictions that come from non-static lifetime plugins.

Dealing with function hooks

Dealing with function hooks is pretty simple, and there are a couple of ways to do it. One of which is already implemented in plugins like libnro_hook.nro and even libsmashline_hook.nro (crazy, I know). This method is using callbacks. Now, since you will need removable callbacks, one option is to implement these callbacks as if there is only ever one plugin that will be using them:

// This is in the parent plugin
use smash::app::BattleObjectModuleAccessor;

type GetIntCallback = fn(boma: *mut BattleObjectModuleAccessor, what: i32, value: i32);

static mut CHILD_FUNCTION: Option<GetIntCallback> = None;
#[skyline::hook(replace = WorkModule_get_int)]
unsafe fn get_int_hook(boma: *mut BattleObjectModuleAccessor, what: i32) -> i32 {
  let value = original!()(boma, what);
  if let Some(child_func) = CHILD_FUNCTION.as_ref() {
    child_func(boma, what, value);
  }
  value
}

#[no_mangle]
pub extern "Rust" set_child_function(child: Option<GetIntCallback>) {
  unsafe {
    CHILD_FUNCTION = child;
  }
}

// This is in the child plugin
use smash::app::BattleObjectModuleAccessor;

type GetIntCallback = fn(boma: *mut BattleObjectModuleAccessor, what: i32, value: i32);
extern "Rust" {
  fn set_child_function(child: Option<GetIntCallback>);
}

fn get_int_hook(boma: *mut BattleObjectModuleAccessor, what: i32, value: i32) {
  println!("WorkModule::get_int called for {:#x}: {:#x}", what, value);
}

#[smashline::installer]
pub fn install() {
  set_child_function(Some(get_int_hook));
}

#[smashline::uninstaller]
pub fn uninstall() {
  set_child_function(None);
}

Static variables

Unfortunately, most mod developers don't have the tools to replicate what the game developers do in order to access variables between multiple scripts without the use of static variables. This has become a staple in most mods in order to store long-lasting information. Since this information only lasts for as the plugin is loaded, it is not the best idea to store these variables inside of the child plugin. There is a simple, albeit somewhat annoying, solution to this: use getter and setter functions.

// This is the parent plugin
use smash::app::BattleObjectModuleAccessor;

mod mario {
  pub static mut IS_LARGE: [bool; 8] = [false; 8];
}

mod snake {
  pub static mut GRENADES_OUT: [i32; 8] = [0; 8];
  pub static mut JAB_USED_FRAME: [f32; 8] = [0.0; 8];
  pub static mut LAST_DAMAGE_DEALT: [f32; 8] = [0.0; 8];
}


pub enum GlobalVariables {
  Mario_IsLarge,
  Snake_GrenadesOut,
  Snake_JabUsedFrame,
  Snake_LastDamageDealt
}

#[no_mangle]
pub unsafe extern "Rust" fn get_int(boma: *mut BattleObjectModuleAccessor, which: GlobalVariables) -> i32 {
  let id = WorkModule::get_int(boma, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID);
  match which {
    Snake_GrenadesOut => snake::GRENADES_OUT[id],
    _ => 0
  }
}

#[no_mangle]
pub unsafe extern "Rust" fn set_float(boma: *mut BattleObjectModuleAccessor, which: GlobalVariables, value: f32) {
  let id = WorkModule::get_int(boma, *FIGHTER_INSTANCE_WORK_ID_INT_ENTRY_ID);
  match which {
    Snake_JabUsedFrame => snake::JAB_USED_FRAME[id],
    Snake_LastDamageDealt => snake::LAST_DAMAGE_DEALT[id],
    _ => 0.0
  }
}

This is just one way to handle static variables, I'm sure that there are better and more creative ways to handle them

Other issues

There are bound to be other issues that I didn't predict when developing this utility. I apologize if something seems "impossible" to do with a development plugin, or if unexpected things crash. This isn't a perfect system, but was designed to be helpful in developing cool mods without wasting away on the smash loading screen :)