Skip to content

File format

GameScript

The stack language the whole game is written in — and the 1,906 engine operators it calls into.

.gs

In short

Nearly all of Lords of Magic's rules live in plain-text scripts inside the game's archives, in a stack-oriented language derived from PostScript. That is why the community's balance mods exist at all, and it is the single largest modding surface the game has.

Every script in all three studied installs tokenises with a small parser. An experimental interpreter loads the game's own utility library and runs it correctly. Where a script reaches something only the game engine can do, the interpreter stops and names it rather than guessing an answer.

What was found

The engine's whole vocabulary is recoverable

Measured

The game's executable contains a dispatch table naming all 1,906 operators the scripts can call, each with its entry point. Pulling that table out replaced a guess-by-shape heuristic with a real list: about 98.5% of the heuristic's candidates turned out to be genuine, and roughly 33 names per install were coincidence.

It also shows how much engine surface the scripts never touch.

A third of the corpus is pure language

Measured

406 of one install's 1,681 scripts run to completion with no engine support whatsoever. Of the rest, only 124 stop first on something that genuinely requires the engine, and 23 stop on a name that nothing in the scripts or the executable explains.

The two large remaining groups — scripts waiting on a name another script defines, and scripts waiting on a declared constant — are both cheap to satisfy. That ratio is the reason the project's assessment for this workstream is 'continue' rather than 'park'.

Small lexical details that bite

Measured

Definitions end with a keyword, not a semicolon — a community post that implies otherwise is quoting its own trailing comment. Strings have no backslash escaping, so Windows paths in the shipped scripts tokenise as written. And the scripts use a bare carriage return as a line ending, which silently breaks any tool that assumes otherwise.

This project got caught by that last one: a scan for tileset declarations read seven files' worth of commented-out code as live, because it neither honoured comments nor split lines correctly.

A comparison that inverted under correction

We were wrong about this

Classifying what blocks each script first produced a clear-looking result until someone noticed the comparison was case-insensitive. Nothing in the scripts defines the constant GOLD; a different file defines a lowercase variable of the same spelling. Folding case moved that name and 53 others into the wrong column — 75 scripts on one name alone — and flipped which group was larger.

The corrected figures are a near tie. The project's own next-step decision was re-grounded on a third number that the correction did not touch.

One mod ships a real bug

Measured

Running a community balance overhaul's utility library through the interpreter showed its minimum and maximum helpers are swapped relative to the originals. That is a live bug in a mod people play, found by executing the script rather than reading it.

The operator table is ordered by subsystem, and that is testable

Measured

The list of engine operators is fixed at build time, so if neighbours belong to the same subsystem the list is a free outline of the game's internals. That is exactly the sort of thing that looks true when you skim it, so it was tested against a shuffled baseline on signals the list does not itself contain. Neighbouring operators share callers about 5.6 times more often than chance, and share a name stem about 611 times more often.

One measure came back negative, and it is the useful one: grouping operators by the directory their callers live in barely beats chance. Script trees are organised for the people writing them, not along the engine's seams, so anyone inferring subsystems that way gets a plausible map with no way to tell where it is wrong. Name morphology is what labels them.

The positive result is less independent than it first looks, which is recorded too: two of the three measures are the same computation projected two ways and cannot disagree with each other.

Scripts can load other scripts, and it moved the needle

Measured

Making one script pull in another from inside the archive took the share of scripts that run to completion from 19% to 33%. It also required implementing an unrelated-looking operator first: the startup script wraps module loading in a progress meter, and without that operator the name resolves to the wrapper and recurses until it hits the depth limit.

It helped about half as much as predicted, and the reason is recorded rather than glossed: most of the scripts still blocked are blocked because everything that could define what they need stops for the same reason they do.

Still unknown

Named as open rather than guessed at.

  • The engine-side operators themselves. The game exposes over 1,400 of them and the interpreter implements none — it stops at each one by design and says which. Execution today stands on the few dozen that shipped scripts actually reach.
  • A handful of operators that gate everything else: the one that builds dialogs, the one that registers mount types, and two that report table sizes. Most scripts that stop, stop at one of these.

All file formats