20 October 2024
I was directed recently to a utility that laid bare the maps, items and other structures of the classic Bitmap Brothers' game Cadaver on Amiga.
It was fascinating to be able to dig into how the game was put together, not least because I'm working on something very similar with my as-yet-unnamed Amiga isometric game.
Much of the interaction in Cadaver is written in a custom scripting language known as "Adventure Creation Language" (ACL), created Bitmap Brothers' Steve Kelly. Other than a single image in a magazine article, not much is publicly known about this language though.
=> [IMG: Adventure Creation Language sample]
In a previous unfinished Amiga game I was working on ("Space Zelda"), I implemented event handling by defining C macros that could be entered in the map editor and ended up as actual compiled code in the game. Fast, but awkward and not terribly flexible. The code was in the game not the map data so it would have been very difficult to provide updates or expansions with this approach.
Delving into the source code of Cadaver Explorer, I was able to quickly grok the shape of the game's scripting system. ACL is clearly not a general-purpose language interfaced to the game engine. Scripts are based around a set of fixed events (eg: player touches an object), and each event has it's own fixed parameters as well as optional 'pre-event' instructions. The compiled bytecode is a series of very specialised and quite high-level 8-bit opcodes followed by a variable parameter list.
For example, to unlock an exit if the player is carrying a particular item:
0x22 0x33 : value = rucksack.containsItem(0x33)
0x30 : if (value == 0) {
0x0a 0x66 : exit[0x66].unlock()
Seeing this got me thinking about interaction in my own game. In its present state there is very little, and I have so far used dedicated properties on objects to link them with events.
For example, a "zone" has a property that identifies a global flag to set if the player leaves the area, and a separate property to identify a flag to clear on the same event. Clearly, this could be scripted into a single "OnPlayerExit" event, but how? An Amiga doesn't really have the resources to run a modern scripting engine, but are there other options?
I was initially pointed towards the Pawn language. A C-like language aimed at microcontrollers, it is efficient and can run in a few kB of memory. However, it was not straightforward to get it running on my macOS development laptop so I quickly moved on.
After some searching, these are the most interesting possible alternatives I came across.
After all that though, I'm wondering if the Bitmap Brothers had it right with the highly specialised opcodes? I don't think I need a full language, rather a few commands to set and clear flags globally and on specific objects in response to events. It could be a little as five or six operations, and at that I would likely get away with a basic string matching parser.
A simple, almost assembly level, language might well suffice.
setglobal i // set, clear, invert global flag 'i' clrglobal i notglobal i move i,j // copy global flag i to object flag j nmove i,j // copy global flag i to object flag j and invert disable i // disable object 'i' (which can be 'self')
I'm making this up as I go here, but this would cover everything in the isometric game at the moment. The zone example from earlier could be implemented as follows. Zone, Key and Gate are objects in the game.
// set flag when player moves away from gate Zone.OnPlayerExit: setglobal 33 disable self // clear flag when player collects a gate key Key.OnPlayerCollect: clrglobal 33 // object flag 0 represts gate locked state // gate locks behind player, unlocks when they collect the key Gate.OnSpawn: move 33,0 Gate.OnGlobalFlagChange: move 33,0
There are different ways to approach this. "disable self" prevents the gate from locking a second time after the key is collected, but could also be implemented by adding another command to not-and a second global flag for "key collected" with the gate lock flag. OnGlobalFlagChange could instead take a parameter of the flag number (33) that changed, but that would require comparisons and control flow ("if param == 33...") in the language.
If it turns out I absolutely need a full featured language, I'm tending towards zForth or microvium from the list above, or perhaps AngelScript depending on footprint. An intermediate step would be a custom, assembly-like language with a fixed set of "variables" (registers) and comparisons with conditional execution or jumps. Maybe I should just bake in a 6502 emulator and memory map the flag array! I don't think it will come to this, though.
I wonder how common scripting like this was in Amiga games? The famous examples of ScummVM and Another World use complex virtual machines to drive the bulk of the action, and from some limited reverse engineering I did on Zeewolf I'm fairly sure it's using a script of some kind to orchestrate its missions, but beyond that I don't know. Lua and JavaScript post-date most Amiga games so I imagine any that use scripting will have individual tightly-bound languages along the lines of ACL.
This has been an interesting diversion. I should be able to start working on the isometric game again shortly and scripting events properly will be the first thing to tackle.
=> Amiga Game Scripting Languages - Update
=> Kroah's Game Reverse Engineering Page
=> Kroah's Cadaver Explorer
=> Cadaver | Zeewolf | ScummVM | Another World 101
=> Cadaver - Diary of a Game (PDF)
=> Pawn
=> zForth
=> microvium
=> AngelScript
=> Elk
=> eLua
=> EmbedVM
=> TinyBasic
=> Some others (StackOverflow)
=> amiga This content has been proxied by September (ba2dc).
=> gamedev
=> isometric
=> retro
=> scriptingProxy Information
text/gemini;lang=en