halvorsen/ Smash 3DS Modding / game engine research

Ten months after I was born, the second accident of 2001 came into the world.

Wavedashing is the most famous glitch-that-isn't-a-glitch in fighting games, and Super Smash Bros. Melee is responsible for it. It's absent from every Smash since, so putting it into Smash 3DS means rebuilding three interlocking physics systems inside a completed, compiled game engine, guided by the decompiled source code of Melee itself.

$ tune.ps1 -F 3.1 -K 0.90 -LAG 10 frame 0 momentum killed; launch = 3.1 ·(cosθ, sinθ) along the stick frame 1-27 velocity ×0.90 per frame; gravity cancelled; re-dodge locked frame ~28 |v| < 0.20 → dead stop; dodge ends helpless touchdown slide = airborne X, ×0.92 per frame; actionable at +10

FRAME 0 From somewhere, a transmission

In Melee, an air dodge launches the character in whatever direction the control stick is held. A fixed burst of speed that decays over the life of the dodge, then leaves the character in a helpless free-fall. Dodge diagonally into the ground and the magic happens: the game translates the leftover airborne velocity directly into ground speed, and the character slides across the stage in a standing pose, free to act as if they were standing still, despite being propelled horizontally in real time. Jump, dodge into the floor, slide. That slide is the wavedash, and an entire competitive movement language grew out of it.

No later Smash game has it. Brawl removed directional air dodges entirely; Smash 4, aka the 3DS version if we want to be biblically accurate, gives every character the same direction-agnostic dodge that retains whatever momentum they already had; Ultimate brought the directional dodge back but smothered the actionability of the landing in recovery lag. The point is that a wavedash is not one "thing"; it is an emergent interaction of many separate systems: a stick-directed launch, a landing that keeps horizontal momentum, and ground friction that bleeds it off. All three have to exist before proper wavedashing does.

Smash for 3DS has exactly zero of these. Adding them is therefore not a tweak but a small engineering project where we must beam information into an engine that wasn't looking for it to begin with: find where each mechanism should live, prove how the surrounding machinery behaves, and splice new behaviour in without breaking the thousands of interactions the game already ships with.

FRAME 1 Why the current modding framework can't reach it

Smash 3DS is considered heavily moddable from the outside. Damage, knockback, and character stats live in editable parameter tables. Each move's hitboxes and effects live in a scripting layer called AnimCMD. Models, music, and textures are well-documented, swappable files that might commit the crime of just using an unfamiliar file container. A healthy toolchain exists for all of it.

None of that touches the air dodge's on a structural level. The proof comes from Ultimate, which runs a descendant of the same Havok engine (codenamed Cross2, where Smash 3DS was codenamed Cross): its directional air dodge is driven by a family of data parameters with names like escape_air_slide_speed and escape_air_slide_distance. The 3DS parameter schema was audited against that list, and the result is disappointingly empty. No speed, no distance, no timing field for a directional dodge exists in the 3DS data. The stick isn't even consulted: disassembly shows the 3DS dodge feeding a hard-coded vector (0, 1) into its motion setup. The behaviour is predetermined, not configured.

So the work has to happen in the engine binary itself -- an 11MB file called code.bin that holds all fifty-plus characters' shared logic. (Each character also ships a module of private compiled code, a CRO file. That detail looks like a footnote here and becomes a whole second project later.) Two assets make this viable. First, a built-in symbol table names 6,654 of the engine's functions, turning unintelligible address labels into things like KineticUtility::reset_enable_energy which at least gives me an idea of what the surrounding code does. Second, the deployment path is opportunistic: new code is compiled into unused padding inside the binary, aka a "code cave". A handful of the game's own instructions are redirected into it, and the whole change ships as a small beautiful patch file loaded over LayeredFS. Every experiment in this project consists of one iterative patch sent to an emulator, reboot after reboot.

FRAME 2 A committee of one is called velocity

The first discovery that shapes everything else: a character in this engine does not have a set velocity. It has several, and what appears on screen is the result of their sum.

Each fighter owns a set of kinetic energy objects, independent velocity sources that the engine adds together every frame. One is a control energy: it reads the stick and produces air drift, recomputing itself constantly. One is a gravity energy: it accumulates downward speed frame over frame. Another is a launch energy that holds impulses. Others sit idle until a mechanic needs them.

This architecture partially explains why earlier attempts at a directional dodge didn't quite hit the mark. Write an upward velocity into the control energy and it vanishes. That object recomputes its output from stick drift every frame and simply discards a vertical component. Meanwhile the gravity energy keeps summing downward regardless of what any other object says. Velocity here is not a variable to overwrite; it is a committee of variables to manipulate to act as one cohesive output.

FRAME 5 Ask the jump

The reliable way to persuade it is to find where the engine itself injects velocity and copy the technique exactly. The perfect specimen is the jump, a move that provably sets a deterministic, lasting vertical speed.

Tracing every caller of the engine's jump-speed calculator leads to the canonical recipe. A jump makes two calls to one utility function, reset_enable_energy, which looks an energy object up by number, invokes that object's own initialization method, and switches it on:

reset_enable_energy(1, accessor, 0, {0, +jump_speed}, {0,0,0})   // vertical, into the gravity-integrated energy
reset_enable_energy(2, accessor, 4, {run_speed, 0}, {0,0,0})   // horizontal, into the launch energy

Vertical and horizontal ride different objects. The vertical seed goes into the same energy gravity acts on, which is exactly how a jump works: one upward impulse that gravity then erodes. The directional air dodge is the same two calls with the stick's direction in place of the jump's constants. One subtlety cost a crashed build before it was understood: the accessor handle these functions expect lives one pointer deep inside the kinetic module, not at the address a nearby setup routine happens to pass around. In a game like this, two pointers that look interchangeable rarely are.

FRAME 9 The label that lied

With the launch working in every direction, the next requirement was decay. Melee's dodge bleeds its speed off multiplicatively each frame. Smash 3DS offers an official-looking tool for this: mul_speed, a method that scales every active energy by a chosen factor. Called once per dodge frame, it should have produced perfect exponential decay.

In play, nothing happened. Distances stayed long, momentum never decayed?The playtest was the falsifying instrument, and the disassembly then explained it: mul_speed scales each energy's output fields, but the gravity and launch energies recompute those outputs from internal state on every update. The official mutator writes to a value the engine immediately overwrites. It isn't broken so much as decorative for these object types.

mul_speed({K,K,K}, all) per frame no observable decay // outputs recomputed next update energy.y ×= K (the seeded field) real decay // write what the seed writes

The fix required knowing which fields the seeds actually write, and that came from an unusual source. Each energy object's function table lives at a fixed place in the binary, and live pointers captured during a single earlier debugging session made those tables readable offline. Disassembling each object's initialization method showed precisely where the seeded velocity lands (+0x08 for vertical, +0x04 for horizontal). Multiply those exact fields each frame and the decay is real. Hell yeah, it's all coming together!

Live debugging, for its part, earned a limited role. It could be me, and I could just be stubborn and lazy because the thought of asking Azahar's Discord server or checking its GitHub issues never crossed my mind until now, after the fact. But, the debug interface for Azahar tolerated basically one clean connection per launch and dislikes breakpoints placed before boot. I mentioned it in the other writeup on this site too. Nearly everything after was derived statically from the binary and verified by play.

FRAME 12 Reading the source of a different game

Correct structure still needs correct numbers, and guessing them burned the first few builds. The remedy was to stop approximating and read the original implementation: the Melee decompilation project (doldecomp) has reconstructed the 2001 game's C source, including the air dodge itself. I used AI again here in parallel to mine the decomp, mostly because I expected that Smash's core engine to be nigh unrecognisable when you go as far back as Melee and I didn't want to risk subconsciously blurring the information I learned about the architecture of Smash 3DS into Melee's. But also to see if these tools would really make life easier for this job. One was extracting Melee's dodge code, another auditing Ultimate's modern implementation and checking, name by name, which of its engine calls exist in the 3DS binary.

The decompiled dodge settled every open question at once. The launch is a hard set of a fixed magnitude along the exact stick angle - force × (cosθ, sinθ) - so every direction travels the same distance. A stick inside the deadzone zeroes velocity outright: Melee's neutral dodge is a full-stop hang in the air, which later games abandoned. Decay multiplies both axes identically, with gravity suppressed during the "ballistic" phase. And the wavedash itself turns out to be almost nothing:

// Melee, decompiled: the generic air-to-ground transition
fp->gr_vel = fp->self_vel.x;   // horizontal air speed becomes ground speed; vertical discarded
// ...then ordinary ground friction, scaled up while above walk speed

FRAME 14 A dodge is also a state machine

Physics is half the mechanic. The other half is the game's status system, which is the state machine that decides what a character is doing and what they may do next. Every action is a numbered status with its own entry, per-frame, and exit handlers; the air dodge is status 0x22, internally named EscapeAir, and better known as the air dodge.

Tracing its per-frame handler produced a finding that explained an annoyance along the way. Re-dodging repeatedly in one airtime is how airdodging works in Smash 4 proper. Melee's rules: one dodge, then helpless, are not restorations but modifications to this engine. Turned out to be small: re-entry is gated on a single input bit that the per-frame patch simply clears while the dodge runs.

The landing needed one more excavation. Touching ground mid-dodge routes to a dedicated landing status, and finding its handler meant decoding the table the engine dispatches through: a per-character array pairing each status number with its entry and per-frame functions. Once decoded, that table is a map of every status in the game. The single most reusable artifact of the project. It also revealed the landing's convenient property: the landing setup re-initializes only the control energy, so the dodge's final horizontal speed is still sitting in the launch energy at touchdown, waiting to be picked up. Melee's one-line momentum transfer becomes: read it, re-seed it, let a per-frame multiplier play the role of friction, and after ten frames force the exit into the neutral standing state -- actionable, mid-slide.

FRAME 22 The whole patch

The finished mechanic is all of nine patch records over the stock binary: four redirected function calls, four code caves totaling about 700 bytes of new ARM code, and one changed instruction.

dodge entry    kill drift → read stick → deadzone? full stop : launch F·(cosθ,sinθ)
every frame    v ×= K · both axes; +gravity cancel; |v|<0.20 → snap to 0; block re-dodge
dodge timeout  exit status Fall → FallSpecial (helpless, one dodge per airtime)
touchdown      re-seed airborne X as ground speed
slide frames   X ×= 0.92; after 10 frames → standing, actionable

The default values are Melee's cited values where known: force 3.1, decay 0.90 per frame, ten frames of landing lag.

REPLAY Sixteen builds

This version ledger is a record of how this work actually turned out on each build. Sixteen builds is remarkably low, and I can only thank the existence of AI to bounce ideas off of for making it that low.

v2 launch via control energy vertical silently discarded v3-4 switch kinetic type mid-dodge all momentum dead; state machine corrupted v8 wrong accessor pointer crash on dodge v9 jump-style vertical seed first working up-dodge v11 decay via mul_speed "functionally no decay" — the label that lied v13 decay on the seeded fields real decay v14 landing momentum + friction first slide v16 tail hard-stop + 10f lag exit wavedash, actionable

Now that the ground truth was set in stone, I could finally relax for the easiest part: iterating on the final implementation skeleton into Smash 3DS after the foundation of everything that we need was settled.

TOUCHDOWN +10 What remains

What exists now: a directional air dodge with uniform 360° travel, Melee's neutral full-stop, true multiplicative decay ending in an eventual mid-air stop, helplessness after the dodge, one dodge per airtime, and a landing slide that keeps momentum, decays it through friction, and returns control ten frames after touchdown. A wavedash: jump, dodge into the ground, slide.

What's left is polish, so it isn't ready quite yet. We're basically 99% there given how long it's taken to get to this point since I started working on this game in 2016 as part of my first project with a development team. The exit currently rides along a frame counter rather than the landing state's own interrupt gate. The patch currently uses two instruction encodings the emulator accepts but the handheld's older CPU core does not, so a hardware release needs a small mechanical rewrite of how constants load. And then, in the most sarcastic, deadpan narration voice possible, there is the roster itself.

Aside — why the CRO research exists

A shared-engine patch should be the whole story. The air dodge is common code: one fix in code.bin and the entire cast obeys it. And the entire cast does. Except a few fuckass fighters like Ness and Palutena, who handle the air dodge differently internally and carry their own gravity bounds. The engine exposes per-fighter override hooks for exactly this kind of exception, the hooks resolve into each character's private code module. It is also a design fact of this engine: a “universal” mechanic is universal right up until a character module says otherwise.

Those modules are the CRO files. Bringing the last two fighters in line with everyone else therefore requires the ability to open, modify, and rebuild or patch a relatively unique module format — an entire project of its own, and one that has since gone as far as rebuilding a fighter's whole module from source. That project is the companion write-up, CRO0: loading code into code -- and this aside is the reason it had to happen.