AnimCMD Scripts - blu-dev/smashline GitHub Wiki

What are they?

AnimCMD (short for Animation Command) are simple scripts that the game uses to manipulate properties of a character's moves. AnimCMD (ACMD) scripts are the less powerful of the two kind of scripts which govern the behavior of character's moves (for reasons I'll get into later), but they do allow for convenient script-like interacting with the game's internal systems.

There are four kinds of ACMD scripts:

  • game scripts. They control the positioning, size, spawning, etc. of hitboxes, spawning of articles, shifting of kinetic energy, etc. Not all game scripts are very complex, in fact most of them are only there to spawn hitboxes. These scripts are the most commonly edited scripts.
  • effect scripts. As the name implies, effect scripts control the spawning and manipulation of visual effects (explosions, fire, electricity, etc) on the character during the specified move.
  • sound scripts. sound scripts control the timing of voice clips and other sound-related things (can you tell I don't know very much about scripts that aren't game scripts).
  • expression scripts. expression scripts are the least used, but they contain instructions on how to handle specific interactions with stage geometry, controller rumble, and other edge-cases.

How do I get them?

Luckily, ACMD scripts have been dumped numerous times and are often kept up-to-date with the game's current version. To get a list of all of the game scripts, you can visit Rubendal's data viewer, where many of the game scripts are sorted by character and further by agent. If you are interested in other kinds of scripts, though, you can visit WuBoy's repository containing the majority of the game's scripts dumped with the same tool as Rubendal's data viewer.

How do I edit them?

Smashline provides the acmd_script macro to generate scripts which can be installed at runtime.

use smash::lua2cpp::L2CFighterCommon;
use smashline::*;

#[acmd_script(agent = "snake", script = "game_attack11", category = ACMD_GAME)]
pub fn snake_jab1(fighter: &mut L2CFighterCommon) {
  println!("Hello from snake jab 1!");
  original!(fighter);
}

If this is a lot to digest for you, don't worry, you are definitely not alone. Let's walk through the macro declaration.

  • "agent = "snake""

The agent of a script represents what in-game object is going to be able to call this modified script. In our case, we are replacing the this script of the agent called "snake", which is the in-game name for Snake (convenient, huh).

  • "script = "game_attack11""

The script of a script (heh) is just the name of function the game calls. Unfortunately, these don't always match up nicely with what we have come to know the moves as. "game_attack11" is the internal name for jab 1, "game_attackhi3" is the internal name for up tilt, etc.

Sometimes, the name of an agent (or the script) is unknown and all we will know is the Hash40. Don't worry, the macro also accepts this as input:

#[acmd_script(agent = 0x5516d6b9e, script = 0xd04731edc, category = ACMD_GAME)]

while this is certainly less readable, don't worry, it's the same macro declaration as above! The Hash40 of "snake" is 0x5516d6b9e and the Hash40 of "game_attack11" is 0xd04731edc. (A "Hash40" is basically a way to convert strings to numbers. Smash uses hashes in a lot of its systems)

For convenience, you can also replace multiple scripts with the same modded script by changing script = "script_name" to a bracketed list of script names:

#[acmd_script(agent = "mario", scripts = ["game_specialn", "game_specialairn"], category = ACMD_GAME)]
pub fn mario_fireball(fighter: &mut L2CFighterCommon) {
  // insert some reimagined super cool fireball code here
}
  • "category = ACMD_GAME"

There are four categories: ACMD_GAME, ACMD_EFFECT, ACMD_SOUND, and ACMD_EXPRESSION. They correspond to the four different kinds of ACMD scripts. This is a required part of the macro declaration to make sure that the Smashline plugin knows where to place our modded script.

  • "original!(fighter);"

Unlike skyline-acmd and smash-script (the predecessor of Smashline), Smashline supports calling the original ACMD script from the modded one. The use cases for this have been outlined below, and it isn't recommended that you deviate too far from these but I ultimately can't stop you.

There are instructions on how to actually write the "move" part of the script in PiNE's guide elsewhere in this wiki.

low_priority - an optional macro argument

One issue that has been noticeable in the past with moveset mods and script replacements is that larger modpacks, such as HDR Soul Remix, Championship Edition, and the Bor Patch, tend to overwrite a lot of the game's ACMD. This makes it very difficult for smaller mod developers to release mods that are compatible, as there are often conflicts where both mods overwrite the same ACMD script. The low_priority argument aims to solve this.

There is no way to automagically merge two different mods into one without messing something up horribly, so instead low_priority allows other mods to replace that specific script. For example:

#[acmd_script(agent = "snake", script = "game_attackhi3", category = ACMD_GAME, low_priority)]
pub fn new_snake_up_tilt(fighter: &mut L2CFighterCommon) {
  println!("I'm not actually cool enough to write good mods :(");
  original!(fighter);
}

is a mod that I made for Snake up tilt, and I'd say it works pretty well. But, it is a part of my larger modpack "Super Epicer Smash Bros." that replaces basically every script in the game. Because of this, I should mark it with low_priority, so that if someone else wanted to mod Snake and also use my incredible modpack, both would be compatible. For example, if someone installed either of the following scripts:

#[acmd_script(agent = "snake", script = "game_attackhi3", category = ACMD_GAME, low_priority)]
pub fn snake_up_tilt_with_flashbang(fighter: &mut L2CFighterCommon) {
  println!("This mod is pretty cool but I'm not confident that it is the best");
  original!(fighter);
}

// OR

#[acmd_script(agent = "snake", script = "game_attackhi3", category = ACMD_GAME)]
pub fn new_snake_up_tilt_with_animation_change(fighter: &mut L2CFighterCommon) {
  println!("there is a really cool animation here I promise!");
  original!(fighter);
}

the installed script would overwrite new_snake_up_tilt. Anything with low_priority will get overwritten by anything else, but if a mod does not have low_priority it can not get overwritten.

For more information on when you should/shouldn't use low_priority, please see PiNE's guide.

original! use cases

Without getting into the gory, technical details of how script replacement/ACMD works, it should be noted that ACMD scripts will only ever be called once throughout the course of the move, and will hang out in the function until it's time to continue execution. Because of this, my recommended use case for original! would be if you are putting a custom moveset over one slot of a character, or you only want to do something really quickly at the beginning of a script. For example, both of the following would be fine:

#[acmd_script(agent = "mario", script = "game_specialn", category = ACMD_GAME)]
pub fn mario_fireball(fighter: &mut L2CFighterCommon) {
  WorkModule::enable_transition_term(fighter.module_accessor, *FIGHTER_STATUS_TRANSITION_TERM_ID_CONT_JUMP_SQUAT);
  original!();
}

#[acmd_script(agent = "ryu", script = "game_specials", category = ACMD_GAME)]
pub fn ryu_hadouken(fighter: &mut L2CFighterCommon) {
  if WorkModule::get_int(fighter.module_accessor, *FIGHTER_INSTANCE_WORK_ID_INT_COLOR) != 7 {
    original!();
    return;
  }
  // some move edit that only works on the 8th costume slot
}

Any kind of system where you wait multiple frames before calling original! is not recommended, although feel free to experiment.

It should also be noted that any attempt to call original! inside of a modded script that replaces multiple ACMD scripts simultaneously will result in a compiler error: Compiler error for using the original macro with multiple scripts

Calling subroutines from within scripts

For the same gory, technical reasons I already didn't want to get into about the original! macro, calling subroutines that call any of the smash::app::sv_animcmd::frame or smash::app::sv_animcmd::wait variants will cause undefined behavior. If you need to move blocks of code out of the main script for whatever reason, my recommendation is to either mark the functions with #[inline(always)] or rewrite them as a macro:

macro_rules! wait_10_frames {
  ($fighter:ident) => {
    smash::app::sv_animcmd::wait($fighter.lua_state_agent, 10.0);
  }
}

#[acmd_script(agent = "luigi", script = "effect_catch", category = ACMD_EFFECT)]
pub fn luigi_grab(fighter: &mut L2CFighterCommon) {
  wait_10_frames!(fighter);
  println!("why are you playing this horrible character.");
  wait_10_frames!(fighter);
}