Play ↗

The making-of · KDFW Decomp Studio

Decompiling a werewolf

Keyboard Drumset Fucking Werewolf is a free game from 2011: one song, seven levels, three minutes. This is how its executable became 9,300 lines of readable C, a runtime that never existed, and builds for the desktop, the browser and phones.

Scroll
01

One song.
Seven deadlines.

The first surprise was in the code, not the binary. Every level ends when the music says so. An invisible object, objTimer, starts the song and then checks, sixty times a second, how many milliseconds have passed since:

if current_time-start>=80960 and current_time-start<93980 {
if room=level3 {
if global.headcount>=6 room=level3D else {global.deathcause=2 game_over() ...}

Reach the goal before the song moves on, or it's game over. Hover the song to see each window.

0:000:441:282:122:56
Hover a level.

This is also why the original could feel bugged: when a 2011 runner stutters on a 2020s machine, the wall clock keeps running and the level's deadline arrives with less of the level played. Our runtime can lock current_time to the music's own playback position.

02

Not the code
you're looking for.

FWA.exe is 12 MB of x86, so the plan was a classic disassembly. Then strings printed \Software\Game Maker\ and d3d_transform_stack_push. The executable is a generic Game Maker 7 runner, written in Delphi, with the actual game glued to its end: sprites, sounds, rooms, and the code as GML source text. Nothing to disassemble — something to unwrap.

offset 1,980,000 · magic 1234321 · version 700 0 bytes
03

Two locks,
one trick.

Lock one: the stream

Inflated, the 10 MB stream starts with 1,579 random dwords, a seed (25249), then 366 more. The seed shuffles a 256-byte table with ten thousand swaps; each byte is then decoded through the inverse table minus its position in the stream, so the same letter never looks the same twice.

Press the button: this is that exact algorithm, running in your browser on the first record of the game.

seed 25249 · position-keyed

Lock two: the scripts

Script bodies came out as noise again — but structured noise. The same 16 bytes opened four different scripts. Guessing they read draw_sprite_ext(, a brute force over all 63,500 possible tables took two seconds and returned a family of candidates. The first one almost worked:

draw_sprite_ext(sprite_indexßimage_indexßx.1ßy …

Commas had become ß, minus signs dots. The right member of the family decoded everything: a = 101, b = 49, which is the seed 49 × 250 + 95 = 12345. A fixed seed, the same for every Game Maker 7 game.

brute.c
// For every table the generator can make, check the known plaintext.
for (int a = 6; a < 256; a++) for (int b = 0; b < 254; b++) {
    shuffle(t, a, b);
    if (matches(t, "draw_sprite_ext(")) printf("a=%d b=%d\n", a, b);
}
a=101 b=0     a=101 b=49    a=101 b=101 …
real  0m2.159s
04

GML in,
C out.

GML is forgiving: no semicolons, = is also a comparison, every variable is created on first use and secretly an array. The decompiler parses all 328 blocks of code, keeps the author's comments and blank lines, infers a C type for every variable, then writes C that a person could have written. Same function names, same variable names, same comments. Here is the eskimo meeting a bat:

original GML · objPlayer, collision with objBat
if hurt>0 exit
times=global.pickups
if times>5 time=5
sound_play(sndBat1)
repeat (times) {
dir=random(360)
my_id=instance_create(x+lengthdir_x(32,dir),y+lengthdir_y(32,dir),objLostPickup)
my_id.direction=dir
my_id.speed=6+random(4)
}
hurt=60
global.pickups-=5
decompiled C · objects/objPlayer.c
if (self->hurt > 0) return;
self->times = global.pickups;
// [studio fix] the original caps a variable named `time`,
// a typo for `times`
if (self->times > 5) {
    if (fix_enabled()) self->times = 5;
    else self->time = 5;
}
sound_play(sndBat1);
repeat (self->times) {
    self->dir = random(360);
    self->my_id = instance_create(self->x + lengthdir_x(32, self->dir),
                                  self->y + lengthdir_y(32, self->dir), objLostPickup);
    set_direction(inst(self->my_id), self->dir);
    set_speed(inst(self->my_id), 6 + random(4));
}
self->hurt = 60;
global.pickups -= 5;

self->

Every variable any object uses lives in one struct Instance, exactly as GM gives every instance one bag of variables. Cross-object access (other.life, objTimer.start) just works.

with (objBlock) { … }

A real C macro: two nested for loops whose inner declaration shadows self and other. The body reads exactly like the GML.

union { double my_x; double my_x_array[16]; }

In GM, my_x and my_x[0] are the same storage. Ten objects use it as a number, one as an array; a C11 anonymous union says precisely that.

set_speed(self, 6)

Speed, direction, hspeed and vspeed are four views of one motion. Writes go through setters so the other three stay in sync; reads stay plain fields.

round(2.5) == 2

GML's round is Delphi's: halves go to the even neighbour. The runtime maps the name onto rint(), keeping the spelling and the behaviour.

keyboard_lastkey = 0; /* GML assigned "" */

When the source is genuinely odd — a string stored in a number — the C does what GM did and says so.

05

A runtime
for 2011.

The decompiled C calls draw_sprite_ext, place_free, d3d_draw_wall… so something has to answer. gmrt is a small Game Maker 7 runtime in C11 on raylib: instances and the GM event order, motion and friction, pixel-precise collisions, rooms and views, fonts, surfaces, blend modes, the 3D mode, sound, and a string garbage collector that the decompiler feeds with the location of every String variable.

One of Game Maker's favourite tricks — d3d_set_fog(1, c_white, 0, 0) to paint a sprite as a flat silhouette — rides along each vertex's normal, so it costs no extra draw calls.

  1. begin step
  2. alarms
  3. keyboard · press · release
  4. step
  5. friction · gravity · motion
  6. outside room / view
  7. collisions
  8. end step
  9. animation
  10. draw

Three bugs worth keeping

The 3D field rendered with every surface plain white.
Everything white. rlgl starts a new batch when the primitive type changes, and silently resets its texture. Mixing triangles and quads lost every texture. Now everything is a quad.
A grey gradient covering the 3D scene.
Lost in the fog. screen_redraw() was drawing into the screen instead of the quarter-size surface the game had selected — the scene vanished behind the vignette.
clawsclaws
Mirror, mirror. I assumed GM's 3D camera mirrors x. The claws in a recording of the original disagreed — so the recording won, and the projection lost its minus sign.
06

The 3D level,
on both sides.

Level four switches Game Maker into 3D, renders the city into a quarter-size surface for a chunky look, scales it up, and leans on SurfaceFix.dll — the DLL shipped next to FWA.exe — to give surfaces a depth buffer. On machines where that patch or Direct3D 8 surfaces misbehave, the level is the first to break. The rebuild needs no DLL: its surfaces simply have depth. Drag to compare with a recording of the original.

That comparison caught two last differences. The claws' invisible pixels were still writing depth, cutting the buildings behind them along the arm; the original discards fully transparent pixels (an alpha test). And its black fog, d3d_set_fog(1, c_black, 600, 700), never shows in the recording: the range behaves like depth-buffer values between 0 and 1, so it never kicks in — and the red sky stays visible.

The original game's 3D level, from a 2011-era recording.
The same level, rebuilt from decompiled C.
rebuilt from C original
07

Better,
not different.

F1

Filters

Pixels, smooth, sharp-bilinear for crisp non-integer scaling, or a gentle CRT.

16:9

Widescreen fill

The sides of wide screens get a blurred, darkened echo of the game instead of black bars.

×2 ×3 ×4

Upscaled sprites

Scale2x / Scale3x smoothing at export time; collisions still use the original pixels.

need(10)

Difficulty

The song can't be slowed, so difficulty scales the goals: fewer cubes, victims, bones, boss HP.

[studio fix]

Bug fixes

Two genuine bugs of the original, each fixed behind fix_enabled() and marked in the C.

1–7

Level select

Rebuilds what earlier rooms leave behind, then jumps the song — and the clock — to the level.

♪

Music-locked time

current_time follows the music's playback position, so a hitch can't steal a deadline.

◉

Touch

On-screen arrows, Z, X and Start for phones, on by default on touch screens.

08

Bring your own
binary.

The studio ships no game — no code, no sprites, no music; the only pictures of it are the screenshots on this page. The studio, a command line and a desktop app, turns your FWA.exe into a self-contained CMake project, then builds whatever your machine can build.

SourceC, PNG/WAV/MP3, CMake, README, CI workflow
Linuxnative build, tested
WebWebAssembly + the themed cabinet page, tested
Androidarm64 NativeActivity builds; APK needs SDK build-tools
Windows · macOSgenerated GitHub workflow, or mingw-w64 locally
iOSneeds Xcode on a Mac — not yet
The studio app: original GML next to the decompiled C. The studio app: build targets with toolchain detection. The studio app: the sprite gallery.