Skip to content

Moveset Edits: AnimCMD Scripts

jugeeya edited this page Oct 4, 2019 · 11 revisions

Replacing AnimCMD functions

AnimCMD controls how each of a character's animations work, and it is split into four categories: game, effect, expression, and sound. AnimCMD is frame-based, and takes care of a lot of the scripting required to make up what we consider a character's moveset. For example, almost all normal hitboxes are defined in a character's game AnimCMD scripts. From here on, "ACMD" and "AnimCMD" will be used interchangeably.

Reading AnimCMD functions

The best resource now for reading AnimCMD functions is ruben_dal's Smash Ultimate Data Viewer. It has all of a character's article's game AnimCMD scripts parsed into a readable C-like syntax, and in some ways close to how we'll write code in this framework.

Roy's forward aerial in the Smash Ultimate Data Viewer We can actually use a Python script to convert this data into something readable by our ACMD framework. If we copy the script on the website into a textfile (for example, named acmd.txt) in our working directory, we can then run: python generate_acmd/data_viewer_to_code_mod.py acmd.txt, we can see the output as:

if (acmd->is_excute()) {
        WorkModule::on_flag(acmd->module_accessor, /*Flag*/ FIGHTER_STATUS_ATTACK_AIR_FLAG_ENABLE_LANDING);
}
MotionModule::set_rate(acmd->module_accessor, 1.0 / /*FSM*/ 0.66);
acmd->frame(12);
MotionModule::set_rate(acmd->module_accessor, 1.0 / /*FSM*/ 1);
acmd->frame(13);
if (acmd->is_excute()) {
        acmd->ATTACK(/*ID*/ 0, /*Part*/ 0, /*Bone*/ hash40("top"), /*Damage*/ 11.0, /*Angle*/ 42, /*KBG*/ 83, /*FKB*/ 0, /*BKB*/ 50, /*Size*/ 4.8, /*X*/ 0.0, /*Y*/ 13.0, /*Z*/ 4.0, /*X2*/ 0.0, /*Y2*/ 10.0, /*Z2*/ 4.0, /*Hitlag*/ 1.3, /*SDI*/ 1.0, /*Clang/Rebound*/ ATTACK_SETOFF_KIND_ON, /*FacingRestrict*/ ATTACK_LR_CHECK_F, /*SetWeight*/ false, /*ShieldDamage*/ 0, /*Trip*/ 0.0, /*Rehit*/ 0, /*Reflectable*/ false, /*Absorbable*/ false, /*Flinchless*/ false, /*DisableHitlag*/ false, /*Direct/Indirect*/ true, /*Ground/Air*/ COLLISION_SITUATION_MASK_GA, /*Hitbits*/ COLLISION_CATEGORY_MASK_ALL, /*CollisionPart*/ COLLISION_PART_MASK_ALL, /*FriendlyFire*/ false, /*Effect*/ hash40("collision_attr_cutup"), /*SFXLevel*/ ATTACK_SOUND_LEVEL_L, /*SFXType*/ COLLISION_SOUND_ATTR_ROY_HIT, 
/*Type*/ ATTACK_REGION_SWORD);
        acmd->ATTACK(/*ID*/ 1, /*Part*/ 0, /*Bone*/ hash40("armr"), /*Damage*/ 11.0, /*Angle*/ 42, /*KBG*/ 83, /*FKB*/ 0, /*BKB*/ 50, /*Size*/ 4.2, /*X*/ 0.0, /*Y*/ 0.0, /*Z*/ 0.0, /*Hitlag*/ 1.3, /*SDI*/ 1.0, /*Clang/Rebound*/ ATTACK_SETOFF_KIND_ON, /*FacingRestrict*/ ATTACK_LR_CHECK_F, /*SetWeight*/ false, /*ShieldDamage*/ 0, /*Trip*/ 0.0, /*Rehit*/ 0, /*Reflectable*/ false, /*Absorbable*/ false, /*Flinchless*/ false, /*DisableHitlag*/ false, /*Direct/Indirect*/ true, /*Ground/Air*/ COLLISION_SITUATION_MASK_GA, /*Hitbits*/ COLLISION_CATEGORY_MASK_ALL, /*CollisionPart*/ COLLISION_PART_MASK_ALL, /*FriendlyFire*/ false, /*Effect*/ hash40("collision_attr_cutup"), /*SFXLevel*/ ATTACK_SOUND_LEVEL_L, /*SFXType*/ COLLISION_SOUND_ATTR_ROY_HIT, /*Type*/ ATTACK_REGION_SWORD);       
        acmd->ATTACK(/*ID*/ 2, /*Part*/ 0, /*Bone*/ hash40("sword1"), /*Damage*/ 11.0, /*Angle*/ 42, /*KBG*/ 83, /*FKB*/ 0, /*BKB*/ 50, /*Size*/ 3.5, /*X*/ 0.0, /*Y*/ 0.0, /*Z*/ 0.7, /*Hitlag*/ 1.3, /*SDI*/ 1.0, /*Clang/Rebound*/ ATTACK_SETOFF_KIND_ON, /*FacingRestrict*/ ATTACK_LR_CHECK_F, /*SetWeight*/ false, /*ShieldDamage*/ 0, /*Trip*/ 0.0, /*Rehit*/ 0, /*Reflectable*/ false, /*Absorbable*/ false, /*Flinchless*/ false, /*DisableHitlag*/ false, /*Direct/Indirect*/ true, /*Ground/Air*/ COLLISION_SITUATION_MASK_GA, /*Hitbits*/ COLLISION_CATEGORY_MASK_ALL, /*CollisionPart*/ COLLISION_PART_MASK_ALL, /*FriendlyFire*/ false, /*Effect*/ hash40("collision_attr_cutup"), /*SFXLevel*/ ATTACK_SOUND_LEVEL_L, /*SFXType*/ COLLISION_SOUND_ATTR_ROY_HIT, /*Type*/ ATTACK_REGION_SWORD);     
        acmd->ATTACK(/*ID*/ 3, /*Part*/ 0, /*Bone*/ hash40("sword1"), /*Damage*/ 7.0, /*Angle*/ 42, /*KBG*/ 80, /*FKB*/ 0, /*BKB*/ 50, /*Size*/ 3.5, /*X*/ 0.0, /*Y*/ 0.0, /*Z*/ 7.2, /*Hitlag*/ 0.7, /*SDI*/ 1.0, /*Clang/Rebound*/ ATTACK_SETOFF_KIND_ON, /*FacingRestrict*/ ATTACK_LR_CHECK_F, /*SetWeight*/ false, /*ShieldDamage*/ 0, /*Trip*/ 0.0, /*Rehit*/ 0, /*Reflectable*/ false, /*Absorbable*/ false, /*Flinchless*/ false, /*DisableHitlag*/ false, /*Direct/Indirect*/ true, /*Ground/Air*/ COLLISION_SITUATION_MASK_GA, /*Hitbits*/ COLLISION_CATEGORY_MASK_ALL, /*CollisionPart*/ COLLISION_PART_MASK_ALL, /*FriendlyFire*/ false, /*Effect*/ hash40("collision_attr_cutup"), /*SFXLevel*/ ATTACK_SOUND_LEVEL_M, /*SFXType*/ COLLISION_SOUND_ATTR_PUNCH, /*Type*/ ATTACK_REGION_SWORD);        
}
acmd->wait(3);
if (acmd->is_excute()) {
        AttackModule::clear_all(acmd->module_accessor);
}
acmd->frame(34);
if (acmd->is_excute()) {
        WorkModule::off_flag(acmd->module_accessor, /*Flag*/ FIGHTER_STATUS_ATTACK_AIR_FLAG_ENABLE_LANDING);
}

This code will be useful in later sections as we learn how to declare ACMD replacements.

Writing AnimCMD functions

ACMD functions are completely controlled by frames, so the most important functions are related to frames. In our replacement of Fox's shine, we can see the first method of declaring which frame we are working on:

// Frame 1
acmd->frame(1);
if (acmd->is_excute()) { ... }

Another method that is frequently used is wait(x), which delays execution until x frames after the frame it is called. This can be seen in our replacement of Squirtle's uptilt.

// This wait occurred after the previous frame(5)
// Frame 7
acmd->wait(2);
if (acmd->is_excute()) { ... }

Using this framework to replace AnimCMD functions

Creating an ACMD function involves using the ACMD constructor to create an ACMD object to place in the acmd_objs global array in acmd_edits.h. The constructor is of the form: ACMD(battle_object_category, battle_object_kind, animation_name, acmd_script_name, acmd_function

In order to specify which character (or weapon, or ...) we would like to replace an ACMD function for, we can use the BATTLE_OBJECT_CATEGORY and KIND of the battle_object associated to the current L2CAgent. Finding which categories correspond to what type of object and which "KIND"s are associated to which characters, search for "BATTLE_OBJECT_CATEGORY" and "FIGHTER_KIND" here. That link tells you what the actual values of the constants are in-game, but since they are also defined in const_value_table.h we can use them freely.

An example for Fox's shine, whose grounded animation is "SpecialLwStart", has the corresponding constructor: ACMD("BATTLE_OBJECT_CATEGORY_FIGHTER", "FIGHTER_KIND_FOX", "special_lw_start", "game_speciallwstart", [] (ACMD*) -> void {/* body of acmd function... */}

Notice the animation name is generally the terms of the animation in lowercase separated by underscores, and the acmd script name is is prepended by the ACMD type (the others being "effect_", "expression_", and "sound_") without any underscores.

What if I want to use a function that isn't yet defined in the framework?

Download NXTool or nx2elf to convert your character NROs to ELF files. In your terminal, you'd type ./nxtool [path/to/fighter/NRO] --elf=[path/to/output/ELF]

For example,

> ./nxtool lua2cpp_pfushigisou.nro --elf=lua2cpp_pfushigisou.elf

Then we can find the function that we want with a program pre-installed with DEVKITPRO. $DEVKITPRO/devkitA64/bin/aarch64-none-elf-readelf -sW [path/to/character/ELF] | grep "[part of the function name]"

If I wanted to find ATTACK_ABS because it's used in many Throw scripts, I'd find:

> $DEVKITPRO/devkitA64/bin/aarch64-none-elf-readelf -sW lua2cpp_pfushigisou.elf | grep "HitModule"
   301: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN3app10sv_animcmd26FT_ATTACK_ABS_CAMERA_QUAKEEP9lua_State
   359: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZN3app10sv_animcmd10ATTACK_ABSEP9lua_State

And we can see that it's the second one. Now we should add that to our app::sv_animcmd namespace in sv_animcmd.h. In order to tell the ELF that it should look for the function with this symbol from the main of Ultimate, all we have to do is type asm("symbol") when declaring the function. So a declaration of this function would look like:

namespace app::sv_animcmd
{
    // other functions...
    u64 ATTACK_ABS(u64) asm("_ZN3app10sv_animcmd10ATTACK_ABSEP9lua_State") LINKABLE;
}
Clone this wiki locally