Does Helious 2 have an ending? And reverse engineering part of it to find out.

alien tentacle throwing gang signs

Some backstory

In 1993, Shae Puckett (back then going by a different first name) published the DOS game Helious. The in-game backstory describes how they acquired the game: a visit from aliens. Late one night, Shae was woken up by a green light in their window. After tripping on a stack of phonebooks they managed to get out of the house to meet an identical clone of themself stretching out its hand. Shae passes out and once they wake up they realize that the only thing the aliens touched was their computer. The aliens had dropped off the source code as well as binaries to a mysterious game. As anyone in this situation would do, Shae decided to sell it as their own. Obviously, this backstory is 100% true.

The game was split into two pieces in the typical shareware style. The first piece was free and the second piece cost $20.

The game didn’t sell.

In 2014, Ross Scott released an episode of Ross’s Game Dungeon about Helious. It’s worth watching but isn’t necessary to understand the rest of this post. This is where I first heard of Helious.

The game(s)

The game can charitably be called an action puzzle game. The player controls a blue balloon-like sphere containing a certain amount of air. The arrow keys control the balloon’s acceleration, not velocity. Changing your speed (except by the “friction brake” bound to the Z key) reduces the amount of air you have, and if you run out the balloon explodes, so there’s a soft time limit. You can tell roughly how much air you have by looking at the balloon’s size, since it shrinks when you lose air. You also lose air when colliding with enemies or their projectiles.

You also have to deal with puzzle elements like annoying teleporters and sections that are locked off until you shoot the matching unlocking symbol. There’s 9 levels per game and the objective of all the levels is to collect the blue gems. Once all the blue gems in the level have been collected you can grab the flag. In the first game it reveals an alien symbol, which you need to see the ending. In the second game it just disappears.

The symbols are used on the level select screen, which has a tentacle for a cursor.

When all the correct symbols are entered the ending plays.

The first game has a somewhat reasonable difficulty curve going clockwise around the level select circle. The second game wants you dead. I’m not gonna bother recording gameplay footage myself, but Ross explains it decently well. Actually he kind of understates it, I would argue it’s harder than he says it is, but maybe I’m just bad at games. I’m not saying it’s impossible to beat these levels, but I am saying that nobody has claimed to have beaten any of them except the one “easy” one.

The “easy” level has no enemies and is only about movement and air management. This isn’t actually easy, but only requires planning and precision, there’s no need to do projectile dodging like you’re playing a Touhou game (at least in bullet hell games you’re controlling the character’s speed, not acceleration). Ross managed to beat this level by making a map, and after finishing and exiting out of the level he noticed that there was an additional knuckle joint added on his tentacle thing.

Ross thought this indicated progress, which would make sense, but it turns out it’s actually a bug related to sprite Z-coordinates and drawing orders, there’s no connection to level completion. The rest of the levels remained unconquered, and as far as I can tell nobody has stepped up to claim that they’ve beaten any of the other levels (at least legitimately, see the footnote).

Some time in 2020 I started my attempt at reverse engineering enough of the game to make a proper cheat for it.

Why not just use Cheat Engine?

Partially me being a little overly ambitious, but it turned out to be useful later. Cheat Engine is good enough in some cases, but it’s the completely wrong tool to use when working with an emulated game. I wanted something that searches the emulated system’s memory directly.

Just a plain old memory searcher

Turns out all you need to make a memory searcher is an array the size of your memory and a bitmap with a bit for every byte in the array. The bitmap says if the byte is a valid candidate or not. Start out with all the bits indicating validity, then when you take a new snapshot, go through and compare the new values with the old ones you have stored, turning off the validity bits for changes that don’t match your criteria. Once you have a low enough number of valid candidates, list the addresses that have their valid bits set. That’s all.

So I hacked one together and used it on memory dumps made with DOSBox’s debugger to find out where the air supply value is kept (turns out the game’s data is in a single 64KiB segment—convenient!). It was typical Cheat Engine-like stuff, take a snapshot, waste some air, discard values that didn’t change, you know the drill (or maybe you don’t, but I don’t have time to write a more thorough explanation if I’m gonna get this out before cohost goes read-only and still have time for my final chost). Once I had the memory address I had what I needed to make the only cheat I’d need.

If I could freeze that address at zero, I’d have the smallest size balloon possible (so it fits in all the crevices) but it will never go negative and run out. Invincibility! Problem is, at the time DOSBox didn’t have any “freeze memory” command, so I had to make my own.

DOSBox memory freeze

DOSBox’s debugger already had breakpoints that trigger when a value in memory changes. I just had to add another type that behaves the same, except instead of stopping, it silently changes the value to zero and continues. It wasn’t very complicated to implement, and some time later someone else had the same thought, but actually bothered to submit a pull request to DOSBox-X.

Once I had a memory freeze command I used it to lock the air supply to zero, and now all that was left to do was play the game. So I did.

This game is actually impossible

Turns out one of the levels is missing the goal flag. And I’ve already shown it, it’s this one!

There’s just no flag anywhere around here. There isn’t even a particularly natural spot for a flag to be. I went around the whole level several times after all the enemies were dead and there’s just no flag. This isn’t the last level, it’s something like half-way around the circle! This is where I had to bust out the proper reverse engineering software. I didn’t have any desire to figure out IDA Pro’s pricing model (genuinely what the hell are they doing) nor am I willing to pay those prices, so I just went for Ghidra.

Summary of the reverse engineering

Using the game engine’s error messages as a guide I managed to find the SPAWN function (which creates objects), as well as the LEVEL function (which loads levels). Interestingly enough LEVEL doesn’t use SPAWN. Objects are stored in levels in the exact same way as their live states are stored in the game. So instead of having a separate format for levels, it just loads in the object list as a big block of bytes from disk. From those two functions I found the the memory location of the object list, and through the SPAWN function, its callees, and a few other functions I won’t bother explaining, I figured out some parts of the object format, including object type ID and sprite ID.

For whatever reason SPAWN assembles the new object in an array allocated on the stack via a pointer to the end of the array, and then it copies it into place, which is a little odd, but what made it annoying was that Ghidra couldn’t figure it out and kept putting stupid names on those stack locations and I couldn’t get it to do the right thing so it was just a mess.

Through strategic trial and error I managed to figure out the type and sprite IDs of the flags (all the flags are identical). With that in hand, I turned to the LEVEL function again. I couldn’t manage to figure out how the level data is stored in the EXE because it looked messy and I don’t hate myself, so instead I put a breakpoint immediately after it finished copying the object list into memory.

I did this with a few different levels, but the most important one is obviously the broken level. Turns out that it’s just actually missing a flag object. There’s not a bug that causes it not to load, it’s just not in the level data. It was likely just never placed into the level. Why would the aliens do this? Are they stupid?

What if we try skipping to the ending?

By putting a breakpoint at the start of the LEVEL function and peeking at its one parameter (the purpose of which is pretty obvious given the context) we can find out which level IDs lead where. The levels themselves have IDs from 0 to 8 (going clockwise from the top), the level select and title screen have 9 and 10, and the first game’s ending has 11. So what’s level 11 in Helious 2? Let’s enter a level, and then, when the breakpoint hits, we’ll switch out the value in the parameter with 11.

Oh. In case you’re curious, this happens for every number greater than 10. So if there’s an ending then it’s certainly not a level.

Fixing the broken level

The ending doesn’t have to be its own level, it might be something else. So let’s just repair the broken level. This took some doing.

In the end I ended up writing a launcher program that installs itself as an interrupt handler in DOS before launching the game so I can put pseudo-breakpoints into the game’s code. When triggered, they check where they came from and choose some special code to run based on that. That way I managed to sneak in a little bit of code that, when the right level loads, replaces one enemy with a flag. I also reimplemented the cheat into this program.


Click here to see the code!
;;;  -*- mode: asm; indent-tabs-mode: t; tab-width: 8 -*-
%define FARCODE
bits 16
org 0x0100

%macro patch 2
mov byte [%1], %2
%endmacro

%macro dispatch 2
cmp ax, (%1 + 1) ; To let you type the address of the patched
; instruction instead of the one after it.
je %2
%endmacro


entry:
;; Shrink our memory usage so we can load other things.
;; DOS gives us all of memory by default.
mov sp, end_of_memory
mov ah, 0x4a
mov bx, (end_of_memory - entry + 0x10F) / 16
int 0x21

mov si, 0
.loop:
mov byte [uninit+si], 0
inc si
cmp si, (end_of_memory - uninit)
jne .loop

mov ax, 0x2503 ; Set interrupt 3 vector
mov dx, int3handler
int 0x21

;; We're going to let DOS load the game into memory and set it up to
;; run, but we're not going to actually let it run. We're instead
;; going to use the undocumented "load but don't run" subfunction of
;; EXEC. It puts the few details we need to find and run the loaded
;; program in the parameter block, so we're making sure to have
;; space for that, and that we make sure to use it.
mov ax, cs
mov es, ax
mov ax, 0x4b01
mov dx, exec_path
mov bx, paramblock
int 0x21
jc exit

mov ax, word [paramblock+18] ; IP
mov [game_ip], ax
mov ax, word [paramblock+14] ; SP
mov [game_sp], ax
mov ax, word [paramblock+16] ; SS
mov [game_ss], ax
mov ax, word [paramblock+20] ; CS
mov [game_cs], ax
push ds
mov ds, ax

;;; Patches (relies on the code right above for setting and saving DS):
;; Cheat patch
patch 0x7cfd, 0xcc ; Int 3 dispatch
patch 0x7cfe, 0x90 ; NOP
patch 0x7cff, 0x90 ; NOP

;; Level number watch routine
patch 0x63bd, 0xcc
patch 0x63be, 0x90
patch 0x63bf, 0x90

;; Flag patch
patch 0x6526, 0xcc
patch 0x6527, 0x90
patch 0x6528, 0x90

pop ds

;;; Prepping for launch:

;; Exit fixing
xor ax,ax
mov es,ax
mov word [es:(4 * 0x22 + 0)], exit
mov word [es:(4 * 0x22 + 2)], cs

;; Set up the data and extra segments for the game
mov ah, 0x51
int 0x21
;; Pushing and popping like this saves one byte, why did I bother?
push bx
push bx
pop es
pop ds

;; Set up the exit
mov word [es:0x0a], exit

;; Now I'm going to stack the stack so that when I snap my fingers,
;; we're going to "return" into the game!
mov ss, [cs:game_ss]
mov sp, [cs:game_sp]
pushf
push word [cs:game_cs]
push word [cs:game_ip]
iret ; And shonk! We're dropping into the game.

exit:
mov ax, 0x4c00
int 0x21

int3handler:
pusha
mov bp,sp
mov ax, [bp+16]

dispatch 0x7cfd, cheat
dispatch 0x63bd, level_changed
dispatch 0x6526, flag_hack

int3done:
popa
int3nopop:
iret

;; Well well well, if I ain't the grand daddy of all liars.
;; There wasn't just one "air supply address", in reality
;; there's three, with two levels having different ones
;; from normal. This is because the player is in fact an
;; object placed into the level in the level editor, so
;; sometimes it just ends up at a different real memory
;; address depending on when in the process the player
;; was added in. Just one of those little white lies you
;; have to tell to not bloat your blog post or add a
;; bunch of poorly written footnotes. Let this be our
;; little secret.
exception_level_1: equ 8
exception_level_2: equ 4

object_store: equ 0x8c93
replace_object: equ 21
freeze_addresses: dw 0x8cb1, 0x90cb, 0x8d83

cheat:
popa

push ax
push bx
push cx

cmp byte cs:[level_number], 0x08
jg .skip3 ; Skip if we're not in a gameplay level

mov cx, bx
mov bx, freeze_addresses
cmp byte cs:[level_number], exception_level_1
jne .skip1
add bx, 2
.skip1:
cmp byte cs:[level_number], exception_level_2
jne .skip2
add bx, 4
.skip2:
cmp cs:[bx], cx
jne .skip3
mov ax, 0
.skip3:
pop cx
pop bx
mov word es:[bx], ax ; Instruction from the game that was patched.
pop ax
iret

flag_hack:
cmp byte cs:[level_number], 0x05
jne .done
;; Replace the type with the flag type
mov byte [object_store + (replace_object * 0x2a) + 0x16], 0xB
;; Replace the sprite with the flag sprite
mov byte [object_store + (replace_object * 0x2a) + 0x18], 0x51
.done:
popa
mov si, word [0x56f5] ; Replaced instruction
iret

level_changed:
popa
mov cs:[level_number], ax
mov dx, 0x1a ; This is the instruction we patched out to get here.
iret

;;; Data:
exec_path:
db "HELIOUS2.EXE", 0

dw 32 dup (?)
stack: equ $
uninit: equ $

ABSOLUTE uninit

;; Game process data
game_cs: resw 1
game_ip: resw 1
game_ss: resw 1
game_sp: resw 1

level_number: resw 1

alignb 16
paramblock: resb (32 + 0x200)

end_of_memory: equ $


So let’s see the result!

That’s a flag alright. But does it work? Yes, it does! Or at least as well as any other flag in the game, it disappears when collected and can only be collected when all the blue gems in the level have been collected.

So what happens when we get all the flags now that all the levels have one?

Conclusion

Nothing. Absolutely nothing.

There seems to be no ending, which makes sense since the README more or less says there isn’t one. Maybe I should have mentioned that earlier…

This might however not be an accident. The aliens (which, as I said at the start, 100% do exist) intended the game to be in one piece, but Shae split it in half. The ending in the first game might have been the one intended for the whole game, and splitting it off caused the alien signalling system to break as a result. The level that’s missing a flag might have been intended to be the waiting room to stay in as the aliens come and get you after you’ve passed their test. Unfortunately I have not been contacted by any aliens yet. Perhaps a new government came in and cancelled the human contact project? Or maybe they forgot about us. Or maybe Shae fucked it up! Who knows.

Anyway, please check out Shae’s art, it’s good stuff. And if you’re one of the people that believe that Shae was replaced by their alien clone, that technically makes all their works alien artifacts. That’s pretty neat!

Please also check out Stop Killing Games (which Ross is leading) if you haven’t already. If you’re an EU citizen, please sign the citizens’ initiative!

This is the fastest I’ve ever written something of this length, but to be fair it is attempt number 7.

If you have any more questions, send me an email and I’ll try to respond unless the aliens have already gotten to me.