Skip to content

Commit

Permalink
Improve doc (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
rukai authored Apr 5, 2024
1 parent a7b6da2 commit 070ace5
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 42 deletions.
75 changes: 75 additions & 0 deletions docs/actions.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
digraph G {
fontname="Helvetica,Arial,sans-serif"
node [fontname="Helvetica,Arial,sans-serif"]
edge [fontname="Helvetica,Arial,sans-serif"]

subgraph clusterwait {
node [style=filled];
wait1 -> wait1[label="Subaction completes"]
wait1[label="idle Subaction"]
label = "Idle action";
color=red;
fontcolor=red;
}

subgraph clusterattackS4start {
node [style=filled];
attackS4start[label="Side Smash start subaction"];
label = "Side Smash Start action";
color=red;
fontcolor=red;
}

subgraph clusterattackS4Hold {
node [style=filled];
attackS4hold[label="Side Smash hold subaction"];
label = "Side Smash hold action";
color=red;
fontcolor=red;
}

subgraph clusterattackS4 {
node [style=filled];
attackS4[label="Side Smash subaction"];
label = "Side Smash action";
color=red;
fontcolor=red;
}

subgraph clusterattack11 {
node [style=filled];
attack11[label="Jab subaction"];
label = "Jab action";
color=red;
fontcolor=red;
}

subgraph clusterspecialS {
node [style=filled];
specialSStart -> specialSLoop[label="subaction completes"]
specialSLoop -> specialSEnd[label="Projectile collides\nand is destroyed"]
specialSStart[label="startup subaction"]
specialSLoop[label="projectile control subaction"]
specialSEnd[label="winddown subaction"]
label = "Side special action";
color=red;
fontcolor=red;
}

wait1 -> attackS4start[label="press A and ← or →"]
attackS4start -> attackS4hold[label="subaction completes"]
attackS4hold -> attackS4[label="release A"]
wait1 -> attack11[label="Press A"]
wait1 -> specialSStart[label="press B and ← or →"]

attack11 -> end[label="subaction completes"]
attackS4 -> end[label="subaction completes"]
specialSEnd -> end[label="subaction completes"]

end [shape=Msquare,label="return to idle"];


// ruins positioning so add in manually with an image editor
//attack11 -> wait1[label="Subaction completes"]
//specialSEnd -> wait1[label="Subaction completes"]
}
Binary file added docs/actions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/actions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

dot actions.dot -Tpng -o actions.png
155 changes: 113 additions & 42 deletions docs/writeup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,87 @@ rukaidata is a website that displays framedata on the characters in Super Smash
Previously framedata was manually compiled in large forum posts. e.g. https://smashboards.com/threads/squirtle-hitboxes-and-frame-data-3-6.395456/
However with a new version of Project M (P+) in the works, I wanted to automate this process.
In this article I'm going to run through technical details of how brawl works to explain how I did this.
I will assume you are familiar with programming, basic game dev concepts and basic understanding of the game itself.
This is not an exhasutive document, the aim is to give a high level overview while giving enough technical details so that a reader can investigate further in a specific area.

With a modded Wii you can rip your brawl disk to an ISO file.
Then using a tool like dolphin you can extract the files from the ISO to your own filesystem.
Then using a tool like [dolphin](https://dolphin-emu.org/) you can extract the files from the ISO to your own filesystem.

![Brawl files shown in dolphin](fs_marth.png)

And this is where rukaidata starts.
It reads the file containing the hitbox and hurtbox data it wants and then produces html tables + a 3D model.
Pretty straightforward, right? Nope.
I'm going to show you just how far this rabbit hole goes.
It reads the file containing the [hitbox](https://en.wiktionary.org/wiki/hitbox) and [hurtbox](https://en.wiktionary.org/wiki/hurtbox) data it wants and then produces html tables + a 3D model.
Presumably there would be a data structure that defines what hitboxes and hurtboxes occur on each frame.
Something like this?
```json
{
"frames": [
{
"hitboxes": []
},
{
"hitboxes": []
},
{
"hitboxes": [
{
"x": 15.4,
"y": 15.4,
"radius": 1.0
"damage": 5,
}
]
},
{
"hitboxes": [
{
"x": 15.4,
"y": 15.4,
"radius": 1.0
"damage": 5,
},
{
"x": 180.0,
"y": 15.4,
"radius": 1.0
"damage": 10,
}
]
}
]
}
```

This would define 1 hitbox on the 3rd frame and 2 on the 4th frame of the characters move.
While I think this is a good idea, and wrote a [game](https://canoncollision.com/) / [engine](https://github.com/rukai/PF_Sandbox) around this approach, the developers of brawl instead chose to have hitboxes and hurtboxes derived from the animation of the characters 3D models at runtime.

Brawl's approach has the advantage that:
* Its quicker to define a characters moves and hitbox definitions will track animations if they are tweaked.
* Hitbox placement is robust to dynamic changes in the game e.g. a character moves faster or increases in size due to an in game mechanic.

If you want a TLDR; jump to the "Putting it all together" section.
However, for rukaidata, this turns a simple excercise in parsing and drawing some circles in webgl into a hugely complex endeavor involving the parsing of character models, animations and scripts.
The rest of this article will walk through all of this.

## Brawl character files

Each Brawl character has many files, but there are 3 main types.
For Mario these are:
For Zelda these are:

* FitMario.pac - Contains constants and scripts for the characters actions.
* FitMario00.pac - Contains the 3D models for the 1st costume
* FitMario01.pac - Contains the 3D models for the 2nd costume ... and so on ...
* FitMarioMotionEtc - Contains the animations for all of the characters actions.
* FitZelda.pac - Contains constants and scripts for the characters actions.
* FitZelda00.pac - Contains the 3D models for the 1st costume
* FitZelda01.pac - Contains the 3D models for the 2nd costume ... and so on ...
* FitZeldaMotionEtc - Contains the animations for all of the characters actions.

Brawl characters have a number of actions which are further split into subactions.
There is generally one subaction per action, except for special moves and smash attacks.
The subactions are where most of the interesting stuff happens.
There is generally one subaction per action, with a few exceptions such as for special moves.
Most of the games logic is defined at the subaction level, actions are mostly glue between subactions.

![](actions.png)

The names used here for actions/subactions are their common names, not the actual internal names which are given names like "wait1", "attack11", or "attackS4".
Moves you might expect to be a single action such as smash attacks are actually multiple actions.

### The costume file (FitMario00.pac)
### The costume file (FitZelda00.pac)

The main components of a brawl 3d model (known as MDL0) are:
* Vertices - A list of (x, y, z) points in 3D space defining the surface of the model.
Expand All @@ -58,7 +111,7 @@ Each costume file duplicates all of this data, even for data like bones where th

Relevant source code: MDL0 parser: https://github.com/rukai/brawllib_rs/tree/master/src/mdl0

### The animation file (FitMarioMotionEtc.pac)
### The animation file (FitZeldaMotionEtc.pac)

Contains the animations (known as CHR0) for every subaction.

Expand All @@ -68,7 +121,7 @@ Each animation consists of:

Each CHR0Child consists of:
* The name of the bone this CHR0Child is specifying the animation for.
* A list of keyframes in one of 6 formats.
* A list of [keyframes](https://en.wikipedia.org/wiki/Key_frame) in one of 6 formats.

No matter which format is used each keyframe can be processed into:
* The frame the keyframe specifies
Expand All @@ -93,25 +146,32 @@ Relevant source code:
* CHR0 parser: https://github.com/rukai/brawllib_rs/blob/master/src/chr0.rs
* Applying CHR0 to the bones: https://github.com/rukai/brawllib_rs/blob/27b7aca33ca111635863d7c41eb83c7e2db04f7d/src/high_level_fighter.rs#L413

### The constants and scripts file (FitMario00.pac)
### The constants and scripts file (FitZelda.pac)

The game logic for each character is split between:
* C++ of which we can only see the compiled PPC assembly. - This is very difficult to modify and understand.
* C++ of which we can only see the compiled PowerPC machine code. - This is very difficult to modify and understand.
* Brawls custom binary format scripting language. - This is comparatively easy to modify and understand.

#### Hurtboxes

The C++ code lives elsewhere but refers to many constants and data structures in this file.
Hurtboxes are one such data structure.
They are really spheres that get stretched into cylinders.
There is a list of hurtboxes with each one containing:
Zelda's list of hurtboxes are defined in a hurtboxes section in the FitZelda.pac file.
Brawl's C++ engine reads from each characters version of this file when determining collisions.

Hurtboxes are spheres that are optionally stretched into spherical cylinders.
Each hurtbox is defined with these properties:
* Bone index - The bone the hurtbox is attached to.
* Offset - Offsets the hurtboxes position from the bone its attached to.
* Stretch - An offset to stretch the sphere into a cylinder with hemispherical ends. A value of [0, 0, 0] results in a perfect sphere with no stretching.
* Radius - The radius of the sphere.

"Attaching" the hurtbox to a bone means we apply the bones transformation matrix resulting from the current animation + frame to the hurtbox.

As an example say that zelda's hurtbox definitions included a hurtbox attached to zelda's left forearm bone, with enough stretch to cover her forearm, then it would be active in the area drawn yellow:
![](zelda_hurtbox.png)

This single list of hurtboxes are the same for all actions but some actions will turn on/off specific hurtboxes.
For example sonic will disable all body hurtboxes and enable the spin hurtbox when using spin attacks.

Relevant source code: fighter data parser: https://github.com/rukai/brawllib_rs/blob/master/src/sakurai/fighter_data/mod.rs

#### Hitboxes
Expand Down Expand Up @@ -163,6 +223,9 @@ SyncWait(4.0)
DeleteAllHitBoxes
```

As an example say a script attached a hitbox to zelda's left handbone then it would be active in the area circled red:
![](zelda_hitbox.png)

Relevant source code:
* script parser: https://github.com/rukai/brawllib_rs/blob/master/src/script_ast/mod.rs
* script runner: https://github.com/rukai/brawllib_rs/blob/master/src/script_runner.rs
Expand Down Expand Up @@ -216,58 +279,66 @@ An FSM of 2, would move a hitbox normally occurring on frame 5 to occur on frame
In order to get this crucial data I read the contents of WiiRD emulated Wii memory at the offset this table is stored at.
I then parse the data and make it available to the script runner.

This is easily the hackiest, most hardcoded thing in rukaidata but the extra accuracy it provides is invaluable.
This is easily the hackiest, most hardcoded thing in rukaidata but IMO the extra accuracy it provides is worth the hack.

## Putting it all together

The process I use to display the hurtboxes and hitboxes for a brawl mod looks like this:
1. Run the WiiRD codeset, modifying Fighter.pac and keeping the WiiRD Wii Memory.
2. Retrieve the animation engine FSMs from the WiiRD Wii memory
3. Parse the animations from FitMarioMotionEtc.pac, the bones from FitMario00.pac and the hurtboxes and the scripts from FitMario.pac and Fighter.pac.
3. Parse the animations from FitZeldaMotionEtc.pac, the bones from FitZelda00.pac and the hurtboxes and the scripts from FitZelda.pac and Fighter.pac.
4. Iterate over every frame, of every subaction, of every character.
1. Apply the animation of the current subaction and frame to the bones.
2. Run the subaction scripts for this frame.
* This needs to maintain state such as: hurtbox vulnerability, hitboxes, character movement and variables.
* It also needs to handle control flow of if/else statements and loops.
3. Apply any relevant animation engine FSMs
4. Take all the data that will need to be accessed in the webpage for rendering and store it in high level structs.
5. Generate a .html file for each subaction, serializing all the high level data structures to JSON.
6. The .html pages call a common javascript renderer that reads the JSON and renders it via three.js
5. Generate a .html file for each subaction, serializing all the high level data structures to bincode in a seperate file.
6. The .html pages initialize a renderer written in rust with wgpu compiled to wasm that displays the bincode encoded data.

I believe the most important factor leading to rukaidata's success is my insistence on automating everything and keeping everything reproducible.
I have built a consistent pipeline that reads in raw brawl files + raw brawl mod files and produces static html files.
As well as keeping things sane for my own development, it means Brawl modders can run rukaidata locally to rapidly test their changes.
The resulting render:

![](https://rukaidata.com/PM3.6/Marth/subactions/AttackAirF.gif)

I believe the most important factor leading to rukaidata's success is my insistence on automating everything and keeping everything reproducible.
I have built a consistent pipeline that reads in raw brawl files + raw brawl mod files and produces static html/bincode/wasm files.
As well as keeping things sane for my own development, it means Brawl modders can run rukaidata locally to rapidly test their changes.

## Prior work

When modders want to modify character files they use a tool called Brawlcrate for model/animation files or PSAC for scripts files.
95% of the reverse engineering of these files was already done for me in open source projects such as Brawlcrate.
I just had to port it to rust, improving the architecture along the way.
However I did have to dig through C# reflection hell to figure it all out...

## Where next?

I currently have a second renderer written in rust using wgpu.
I use this renderer to generate gifs listed in the metadata of each page that gets embedded in discord when the page gets linked to.
wgpu is an implementation of the webgpu spec, the new version of webgl.
wgpu provides a rust and C API.
Rust for use by rust desktop applications and C to be used in Firefox as its implementation of webgpu.
## gif rendering

I would absolutely love to replace my javascript renderer with my rust renderer.
However there is a lot standing in the way of that.
1. The webgpu spec will need to be available on all browsers.
2. wgpu will need to support compiling to wasm
This is obviously a problem far bigger than me, but I am doing my part by contributing to wgpu.
When users link a page of the website in discord I need to include a gif in the embedded preview.
Since the web renderer is written in wgpu, the renderer can also be run natively over vulkan/metal/dx12.
Making use of this I also run the renderer at website generation time and store the output in gif files.
One gif file is created per subaction page allowing the preview gif to be served statically along with the webpage.

## Cool things I'll probably never get around to

Currently rukaidata mostly ignores actions, instead it handles everything at the subaction level.
It would really nice to handle everything at the action level instead.
It would be really nice to handle everything at the action level instead.
To make this work properly though, I would have to have some system for running the same action with different variables.
This could occur with hardcoded values used at page generation time, or I would have to move to move the script runner into the browser and allow users to specify their own variables.
This could occur with hardcoded values used at page generation time, or I would have to move the script runner into the browser and allow users to specify their own variables.

Currently rukaidata displays hitboxes, hurtboxes, ledge grab boxes and environment collision boxes.
It would be really cool to render the character along with these visualisations.
It would give the user an in-game grounding to what the data is showing.

## Reflections on the overall approach

If I were to start this project over from scratch I would take a completely different approach.
To be truly accurate I should have extracted data from a live running game.
There are a fair few actions that cannot be extracted because of the limitations in this approach to emulating brawl.
This logic could probably either be done at the emulation level (within dolphin) or within Brawl itself.

One big downside to this approach is I would need to manually define the inputs to reach each move.
For a large portion of each characters moves I could just have a common set of inputs that would work across all characters.
But there are still a lot of character specific moves that would need to be uniquely specified.

However, I have no desire to rewrite rukaidata to do this, the current approach is good enough.
Binary file added docs/zelda_hitbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/zelda_hurtbox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 070ace5

Please sign in to comment.