Skip to content

Weapons and pickups ​

Five weapons, one button to put them away, and a table of pickups that is read off the art rather than off the box. Taking a gun makes you a different player, which is why the guns are here and not with the classes that carry them.

The pickups are one table and one test ​

A hundred and forty stat* records across the sixteen levels, and they are the one system the whole game shares: one creator, 0x45b160, and one collector, 0x45b270, called from the player's own think every frame.

Each chapter's init reads its own list of names and hands the creator a NEGATIVE code — 0x45b19a dispatches on code + 9, so the nine are −9 to −1 — and the cel and script it picks are all in PLAYER.SBK rather than in the level's book, which is what lets one table cover every level. Two of the nine are in no book at all (18000 and 18062), and those two are exactly the two no level places: statpunch and statshield.

statscoreup is one name and three pickups. 0x451420 switches on the record's own param and hands −6, −5 or −4 — worth 2000, 5000 and 10000 — which is why the eleven levels that place one place it with a param.

Collecting is two tests and no button: 0x434140 for a rect overlap and then 0x40e680, which compares the two sprites pixel by pixel. No facing, no range band, no action key — you walk into it. And the reach is the record's own rect, filed at user+4 by the creator; the art is only what is drawn.

0x42827a's table is what each one does, and every sound comes out of the CHARACTER's bank (skulz.snd) rather than the level's:

  -1  stathealth    0x402b20(0x190)      four hundred health, clamped
  -2  statlife      0x40d400(lives + 1)  one life, and 0x40d400 caps five
  -4  statscoreup   0x40d450(0x2710)     ten thousand
  -5  statscoreup   0x40d450(0x1388)     five thousand
  -6  statscoreup   0x40d450(0x7d0)      two thousand
  -8  stattimer     0x40d350(-850)       eight hundred and fifty back on the clock
  -9  (unnamed)     walks the level's `initplayer` records for the one whose
                    rect holds it and stores that index — a CHECKPOINT

STREETS' first four are on the roofs. They sit at y914…994 where the street is 1223 and the top of a jump is 1099, so twenty jumps from the pavement reach none of them. The level's ladder is not a shortcut, it is the way to the pickups.

The other creator is the guns, and it is below.

The guns are the other creator, and taking one makes you a different player ​

0x45af60 takes the POSITIVE codes, and nothing about it is the pickups' shape.

It has a button. 0x4298a1 is the gate — the idle state calls the reach handler 0x42edd0 only while S is held — and 0x42f081 ducks instead when the probe comes back empty. One key, two jobs, and which one you get depends entirely on what is in front of you.

It has a facing, and a band rather than a rect. 0x42f017 shifts the player's own point 35 pixels the way they are looking, and 0x45ae90 asks three things of it: that it falls inside x ± 55 — a band the creator writes at user+6 and user+0xa, unrelated to the record's rect — that the two are within 150 pixels vertically, and that obj+0x30 is set, which a weapon still bouncing after a swap is not.

Code 2 is invisible. The class's draw (0x45ae23) skips any pickup whose code is 2, which is the one a Coke machine's can files when it comes to rest. The can lying on top is the picture, and taking it pays 150 health, not a weapon.

And taking one changes the player. 0x45eed0 sets 0x479438 and the pickup case installs the weapon's own script; that script's kind becomes player+0x18, and 0x4284ed's table sends the entire state machine somewhere else. Five weapons, five kinds, five handlers of about 2,400 bytes each, and every one of them re-implements the idle, the walk, the run, the jump, the fall, the landing and the duck in its own cels:

  6   blaster    0x471458 kind 22  0x42dbd0   4000s   icon 10304  max 160
  9   flaregun   0x470a78 kind 20  0x42cb80   2700s   icon 10306  max  16
  10  flamer     0x470f98 kind 18  0x42b860   1200s   icon 10307  max 160
  12  soaker     0x471260 kind 19  0x42c1e0   3200s   icon 10311  max 160
  16  scepter    0x470c40 kind 21  0x42d2b0   3300s   icon 10308  max 160

The whole table lives at 0x4a7f10 + id * 12 as { icon, max, rounds, fire }, and it is written by the chapter's own entry function rather than by anything in the pickup code. 0x40d681 draws the icon and 0x40d6a2 makes the panel's bar out of rounds * 64 / max, so the bar is that table read twice.

The CHAPTER is the unit, not the level. The four entry functions — 0x4511f0 flamer, 0x43bb00 flare gun, 0x421aa0 soaker, 0x416110 blaster — each zero all 21 rounds counts and name their own weapon, leaving the hands empty. That is why SEWER places two statflare and no gun to fire them with: you are expected to still be holding SERVICE's. It is also why every statflamer in the rip is in WOODS or CITY and every statflare is in MALL, SERVICE, SEWER or ARCADE — the placement scan and the four init functions agree exactly.

Every callback is three instructions and all seven are the same three: the weapon, the rounds, the panel. What separates a gun from a refill is one line — statblaster and statblasterpack both give 40 rounds, and only the base weapon's pickup case goes on to call 0x45eed0. Which is why picking up a tank with nothing in your hands leaves you with nothing in your hands.

Swapping throws the old one away: 0x42f0dc calls 0x45b060 the moment you press S at a gun that is not the one you are holding, spawning your weapon as a falling object with the player's own gravity, before the reach has even played. You can only ever carry one. No single level places two kinds, so this only happens across a chapter.

The flare is a shot, and it does a number. Its fire function is 0x436d40:

  436d43  cmp [0x4a7f82], 0        ; rounds left, or nothing happens at all
  436d5e  0x40ef30(mall.snd, 0x49) ; "#0700 flare gun"
  436d6d  0x45ef00(1)              ; one round
  436df0  x = player.x -+ 0x3c     ; sixty pixels ahead, by facing
  436db0  0x430d40(0x474cb8, …)    ; and there it is, at 0x1a = 100

The flight is a corkscrew. The spawner files 0x434540(17) + 13 — 14…30 — at user+2 and 0x43ac3b reads it down two at a time, each frame adding that value to the flare's vertical velocity and flipping its sign — written outright from 7 up and added below it. So a flare leaves the barrel thrashing and straightens out over about seven frames. It is not aimed and it is not flat. Its strength of 100 is a percentage: 0x42f910 makes the blow out of the flight cel's own pair (7207's dx 8) plus the flare's velocity, forty a frame across and whatever the corkscrew is doing, so a flare hits for about fifty. Whatever it hits or lands on, it burns out (tag 4) and slides to a stop before it goes.

The other four fire functions differ. The flamer's 0x44dae0 is a held stream rather than a shot — its modes −1 and −2 reach into every live flame to stop it — and the flame's blow strength is 0xfff7, −9. That is a code and not a number: it is the same −9 the kragg tests for, and what it means is each class handler's own business. The soaker's 0x41f820 is the same shape, but its droplet does carry a real number — 0x4217ba writes the same hundred the flare has — so what stops that one is the stream rather than the damage. The scepter fires one of the same objects but does not hold it: 0x424620 frees it the frame its six cels end, and its fire function spends forty a press. All three are STREAMS in src/guns.ts, one contract with three sets of numbers; the blaster's bolt is BOLT, and fireGun is where the five meet.

The blaster and the flamer spend no ammunition at all. 0x45ef00 appears once in the flare gun's fire function and twice each in the soaker's and the scepter's, and not once in either of the other two.

The panel's special-weapon window at 290,305–380,450 paints its plate and its four gauge rows (0x40d691); the icon appears while 0x479438 is set, and the gauge is the weapon record's own rounds * 64 / max.

Five weapons, and three of them pour ​

Each weapon owns a state machine and a fire function, and the table at 0x4a7f10 + id * 12 is how they meet: {dword icon, word max, word rounds, dword fire}, with the fire pointer at +8.

   6  blaster   state 0x42dbd0   fire 0x412a70   script 0x471458   cels 4000s
   9  flaregun  state 0x42cb80   fire 0x436d40   script 0x470a78   cels 2700s
  10  flamer    state 0x42b860   fire 0x44dae0   script 0x470f98   cels 1200s
  12  soaker    state 0x42c1e0   fire 0x41f820   script 0x471260   cels 3200s
  16  scepter   state 0x42d2b0   fire 0x41f6b0   script 0x470c40   cels 3300s

A state machine dispatches on the TAG the player is on (movsx eax, [eax+0x44]) and calls its weapon's fire function with a small number — the VARIANT — which every fire function reads its own way. Tag 2 is the standing shot and tag 6 the ducking one across all five, and the flamer and soaker send -2 from the tags either side of those, which is how a stream knows to stop.

The three that pour ​

The flamer, the soaker and the scepter do not launch anything. Each adds an object to a list the player owns, and 0x421700 plants it fresh every frame:

  42170a  if (player mirrored)  x = player.x - user[+4]
  42171e  else                   x = player.x + user[+4]
  42172f  x += player.vx          ; ...and it leads your own motion
  42173b  y = user[+2] + player.y + player.vy

So a stream is not fired and forgotten, it is redrawn where you are — which is why walking while you hold the button sweeps it across a room. (The flamer's own copy of that arithmetic is 0x453c70, the think its class files at 0x453ba8; it reads the same two user fields the same way.)

The offset belongs to a pose, and the pose is in a second script. Each fire function files a muzzle offset chosen by the variant the state machine passes — the flamer's four are at 0x44db90 and the three after it, (135, -35), (63, 15), (49, -22), (66, 5) — and the flame cel is anchored at its NOZZLE (9508 is 255x56 with posX 1), so the offset has to land on the barrel. It only does for one cel. Measuring the blue muzzle spark every armed cel carries, against that cel's own anchor:

  1240  (73,-14)    1241  (86,-34)    1242  (97,-54)    1243  (132,-37)
  1222..1227 (crouching)  (~61,+14)   ->  variant 1 to the pixel

0x470f98 tag 2 is a single frame of cel 1240, and it is not the firing pose: drawn with it, the player holds the gun low with the flame hanging 135 pixels away and nothing in between. The sustained pose is a different script: 0x46faf8, installed by 0x423726 (tag 0) and 0x42370d (tag 1, for the second character), whose tag 0 is 1240 1241 1242 1243 1244 1243 1244 — the gun RISES and the arm extends, settling on 1243/1244, whose spark is variant 0's offset to four pixels. The offset is right for that pose.

Two negative variants end it: -2 walks the list installing the shutting-off tag, and -1 writes the expire kind straight into every member. The player's own hit handler sends that -1 before it has even looked at the blow (0x448c19, 0x448c40), so being hit puts your flamethrower out.

They cost a round an engine frame (0x45ef00(1)), which makes a full hundred and sixty about eleven seconds of flame — against the scepter's forty a shot, four shots and no more. RAVECAVE's statscepter files no rounds at all, so what you get is the single round 0x45eed0 gives for arming you, one beam, and an empty gauge.

They do not all hit with the same thing ​

  0x4217ba   soaker    mov word ptr [edi+0x1a], 0x64    ; a hundred, every frame
  0x424630   scepter   mov word ptr [ecx+0x1a], 0x64    ; a hundred
  0x453b9b   flamer    mov word ptr [esi+0x1a], 0xfff7  ; MINUS NINE

Two of the three are ordinary damage; the flame is a code. And -9 is the one code that is not in the player's own table — it falls below 0x448c84's range test — so what reads it is a −9 arm in the victim's own hit handler (Fighting). Like the blaster's bolt — which only chapter four's own handlers read, Boggs', the TCop's and the test tube's — the flamethrower is a key rather than a weapon. What a full gauge of it kills is three kinds of thing: MOLITOV (initwerec), whose −9 arm at 0x45296e ends in its death; the dog, whose arm at 0x4550d3 installs its death script 0x478208 and pays its 200 (0x455115); and CITY's crows, which burn for eight frames and tumble (The nameless handler is the crow's). This page carries all three.

The damage, where there is any, is small and comes from the art: 0x42f910 scales the striking cel's own (dy, dx) by the object's strength, and the soaker's 9806 carries dx 8. Eight a frame, so a two-hundred-health zombie takes about twenty-five frames of water.

INV puts the gun away and gets it out again ​

There is no inventory screen anywhere in SC.EXE, and only one weapon slot, 0x479434: taking a different gun throws yours down (0x42f0dc). What INV does is switch between that one gun and your fists.

Two things say "armed", and they are not the same. 0x479438 is carried: the panel's icon (0x40d663) and the swap at a different gun (0x42f0a8) read it. player+0x18 being one of the five armed kinds 0x12..0x16 is out (0x448bf0): the fire button, the armed moveset, the knockdown's disarm (0x44911b) and the surge's recharge (0x402ee0) ask that.

Every player state answers the button with the same instruction, mov word ptr [eax+0x18], 0xf — the unarmed idle unconditionally (0x4298c0), the unarmed walk and run only while you carry a gun (0x429af4), and all five guns' handlers. State 15 is 0x428975:

  428975  cmp word ptr [0x4ac386], 0
  42897d  jne 0x428c46                   ; down: install the unarmed idle 0x471648
  428983  ...0x479434 through 0x429624   ; up: dispatch on the weapon
  4289c1  cmp dword ptr [eax+0x3e], 0x471458
  4289c8  je  0x4289ea                   ; the gun's script is already in: put it away
  4289ca  cmp word ptr [0x479438], 0
  4289d2  je  0x4289ea                   ; nothing carried: nothing to get out
  4289d6  push 0x471458 / call 0x45d090  ; get it out
  4289ea  ...0x4717c8 with S held, else 0x471648

So it is a toggle, decided on the release. The release has to land on a state 15 frame, and a hold alternates: 0x45d090 writes the new script's kind into player+0x18 (0x45d0a7), so the unarmed idle the held branch installs is kind 0, and kind 0's handler sees the button still down and goes back to 15. A tap — in on one engine frame, out on the next — flips cleanly. A hold ends on whichever of the two its last frame was, and a release on kind 0's frame leaves the idle standing with the gun away.

Nothing is spent and 0x479438 is never touched. With the gun away the player is on the unarmed state machine, so P punches and K kicks again.

The gun also goes away, still carried, whenever an unarmed script goes in: every level opens on the idle 0x475c88 (0x448bc7) and a save load only sets 0x479438 (0x45e041), a respawn installs 0x471648 (0x42950f), the ladder and the bar end on the unarmed fall 0x471b28 (0x42af84, 0x42b597), the flail 0x472350 ends on the idle, a knockdown is 0x476890, and a reach that took anything but one of the five guns ends on 0x471648 (0x42884d). A knockdown throws the gun down only if it was out. INV brings it back from any of these.

A pickup is taken on the ART, not the box ​

0x45b270 intersects the two rects with 0x434140 and only then calls 0x40e680, which is the second test and the real one. That walks the intersection looking for a row where BOTH cels have an opaque span — and it is spans rather than pixels because that is how the SHP stores a row, which is why 0x4320c0 compares span lists and never touches a pixel value.

The player's cel is a tall rectangle with a great deal of nothing in it: STREETS' statlife runs 3629..3729, and standing at 3600 or 3760 overlaps that rect by a pixel or two while touching none of the art. The rect alone hands you the life; the art does not, and the suite asserts exactly that pair of positions.

The point 0x40e680 hands back is the centre of the RECT intersection (0x40e782 reads back what 0x434140 wrote), not the centre of the pixels it found. So the pixel walk only ever answers yes or no, and can stop at the first row that touches.

Docs licensed GPL-3.0 (the decoder is ported from DFET). Game data © CyberFlix — not included.