Various Callbacks - blu-dev/smashline GitHub Wiki
Once-per-frame Functions
One of the most powerful tools that has been used in Ultimate modding so far is the concept of "once-per-frame" callbacks. This code gets called immediately after the EXEC_STATUS
script has run and gives the developer lots of control over the fighter.
Be careful though, if you are developing a larger mod, putting lots of important code in these callbacks can cause the game to slowdown/lag.
There are two kinds of once-per-frame callbacks that are provided by Smashline: actual callbacks and "replacements". Callbacks are safe for use with the development plugin, but replacements function somewhat similarly to multi-hooking/symbol hooks and should be avoided when working with the development plugin.
Callbacks
In Smashline, these functions are referred to as "global callbacks" since they get called for every fighter/every weapon (depending on which you choose). They have no return value (since they are callbacks) and are called after the EXEC_STATUS
script is called.
#[smashline::fighter_frame_callback]
fn global_fighter_frame(fighter: &mut L2CFighterCommon) {
let boma = smash::app::sv_system::battle_object_module_accessor(fighter.lua_state);
let category = smash::app::utility::get_category(boma);
let kind = smash::app::utility::get_kind(boma);
if category == BATTLE_OBJECT_CATEGORY_FIGHTER && kind == FIGHTER_KIND_MARIO {
let motion_kind = MotionModule::motion_kind(boma);
if motion_kind == hash40("special_n") {
println!("mario fireball!");
}
}
}
#[smashline::weapon_frame_callback]
fn global_weapon_frame(weapon: &mut L2CFighterBase) {
let boma = smash::app::sv_system::battle_object_module_accessor(fighter.lua_state);
let category = smash::app::utility::get_category(boma);
let kind = smash::app::utility::get_kind(boma);
if category == BATTLE_OBJECT_CATEGORY_WEAPON && kind == WEAPON_KIND_MARIO_FIREBALL {
println!("fireball!");
}
}
#[smashline::installer]
pub fn install() {
smashline::install_agent_frame_callbacks!(
global_fighter_frame,
global_weapon_frame
);
}
Replacements
Once-per-frame replacements function similarly to multi-hooks/smashline-style hooks. Unlike callbacks, these actually have to return a value because they replace the original function call.
The original function is called implicitly for you, so there is no need to worry about calling the original!
macro, with one exception explained below.
#[smashline::fighter_frame(agent = FIGHTER_KIND_MARIO)]
fn mario_frame(fighter: &mut L2CFighterCommon) {
let motion_kind = MotionModule::motion_kind(fighter.module_accessor);
if motion_kind == hash40("special_n") {
println!("Mario fireball!");
}
}
#[smashline::weapon_frame(agent = WEAPON_KIND_MARIO_FIREBALL)]
fn fireball_frame(weapon: &mut L2CFighterBase){
println!("Fireball frame!");
}
#[skyline::main(name = "smashline-example")]
pub fn main() {
// For these, the install functions go in main since they are not safe to be uninstalled at runtime.
smashline::install_agent_frames!(
mario_frame,
fireball_frame
);
}
global
and override
keywords
These agent frames have the huge benefit of only ever being run on the specified agent, but that can also be a downside. For example, occasionally you want to have global changes which impact the entire cast. For that, you need to use either a global callback or a global replacement. Global callbacks have already been introduced to you earlier on this page. Global replacements function identically to the agent frames just explained, but also happen for every fighter.
#[smashline::fighter_frame(global)]
pub fn global_fighter_frame(fighter: &mut L2CFighterCommon) {
// put some cool code here
}
#[skyline::main(name = "smashline-example")]
pub fn main() {
smashline::install_agent_frames!(
global_fighter_frame
);
}
In addition, since frame replacements operate similarly to hooks, they also have an original!
macro which you can call. This macro is available all the time, but it should only really be used when you specify that you want to override the original function. In this case, you are required to return an L2CValue and either re-implement the entire function or call original!
. While it is a feature, it isn't horribly useful outside of running code both immediately before and after the EXEC_STATUS
status script runs.
#[smashline::weapon_frame(agent = WEAPON_KIND_LINK_BOOMERANG, override)]
pub fn boomerang_frame(weapon: &mut L2CFighterBase) -> L2CValue {
println!("This is before the exec status");
let result = original!(weapon);
println!("This is after the exec status");
result
}
#[skyline::main(name = "smashline-example")]
pub fn main() {
smashline::install_agent_frames!(
boomerang_frame
);
}
Multiple Replacements
Unlike smash-script
, you can chain multiple once-per-frame replacements together. Just declare two the same way and install them. There is no guarantee of order when they are called, however.
#[smashline::fighter_frame(agent = FIGHTER_KIND_MARIO)]
fn mario_frame1(fighter: &mut L2CFighterCommon) {
println!("this is the first");
}
#[smashline::fighter_frame(agent = FIGHTER_KIND_MARIO)]
fn mario_frame2(fighter: &mut L2CFighterCommon) {
println!("this is the second");
}
#[skyline::main(name = "smashline-example")]
pub fn main() {
smashline::install_agent_frames!(
mario_frame1,
mario_frame2
);
}
Reset Callbacks
Reset callbacks are another one of Smashline's brand-new features! These are very useful when dealing with any kind of information that becomes invalid whenever training mode is reset or the fighter/agent is re-initialized (different from respawning).
It can be helpful to understand when these callbacks will be called.
Fighter reset callbacks will be called at the beginning of a game, whenever training mode is reset, if the fighter switches out (like PT or Pyra/Mythra), and (possibly) when they spawn on the results stage (not 100% sure on this one).
Agent resets are similar in that they are called whenever something is re-initialized, but that can happen at any point in the game. Any time you "generate" or "spawn" something, chances are the agent reset callbacks will get called. If you use Mario's neutral special, the agent reset will be called when the fireball spawns.
I think in this case a little bit of technical explanation will go a long way. Every L2CFighterCommon
and L2CFighterBase
object have a table of L2CValue
s called the global_table
. This global table stores information about what frame the fighter is currently on in their status, how many frames they were in their last status, the current controller inputs, and a few other goodies. Any time this global table is reset, the reset callbacks will be called. There are probably instances that I didn't explain, but it's best to just experiment with these. They can be very powerful utilities.
#[smashline::fighter_reset]
fn fighter_reset(fighter: &mut L2CFighterCommon) {
let boma = &mut *fighter.module_accessor;
let kind = smash::app::utility::get_kind(boma);
if kind == FIGHTER_KIND_MARIO {
println!("Mario is being reset. What a loser!");
}
}
#[smashline::agent_reset]
fn agent_reset(agent: &mut L2CFighterBase) {
let boma = &mut *agent.module_accessor;
let category = smash::app::utility::get_category(boma);
let kind = smash::app::utility::get_kind(boma);
// we need both a category and kind check, since everything calls this function
if category == BATTLE_OBJECT_CATEGORY_WEAPON && kind == WEAPON_KIND_SAMUSD_BUNSHIN {
println!("Resetting dark samus's clone weapon!");
}
}
#[smashline::installer]
fn install() {
smashline::install_agent_resets!(
fighter_reset,
agent_reset
);
}
agent
instead of weapon
this time?
Why I struggled between whether or not I should stay consistent with the naming for this reset, but the reality is that I don't know for sure what does/does not call these resets. I know that all fighters and weapons will call them, but everything else in the game could/could not. For that reason I opted to call these agent
resets instead of weapon
resets.