Static analysis for Sierra adventure games: finds softlocks by abstract interpretation of decompiled SCI scripts, derives and verifies guards, recompiles them into the game
50
stars
594
commits
Python
primary language
Aug 25, 2026
updated
Static analysis for 30-year-old adventure games. This tool decompiles a Sierra SCI game, abstract-interprets the decompiled scripts into a graph of guarded room transitions, item movements and plot-flag writes, finds the softlocks — states where the game still accepts input but victory has quietly become impossible — and derives, verifies, compiles and installs guards that prevent them. Nothing about any title is declared: the start room, the victory room, the death signal and the debug flags are all discovered from the game's own code.
Sierra games, unlike LucasArts ones, let you get stuck. Forget the sunscreen in Los Angeles, board the cruise ship, and you die days later on a raft with no way back. This finds these traps automatically and blocks the crossing that causes them — at the last moment you can still comply.
Four games analyzed and play-tested — Leisure Suit Larry 2 (SCI0, 1988), King's Quest IV (SCI0, 1988), King's Quest VI (SCI1.1, 1992), and Laura Bow 2 (SCI1.1, 1992) — same engine, no game-specific analysis code.
King's Quest IV, patched: the post-whale island.
The tool detects that once you leave the island, you can't come back, and that you need the Golden Bridle later for the unicorn. It injects a "Not yet!" stock refusal if the player attempts to ride the dolphin without the Bridle. Once Rosella gets the Bridle the exit is allowed to happen.
Abridged from a real run on Leisure Suit Larry 2 (python3 -m pipeline <game>):
[2] ANALYZE
anchors: start rm11, victory [86] (discovered)
death signal: global101 == 1001, debug globals: [14, 100] (derived)
101 rooms, 27 strongly-connected components, 40 gating registers
softlocks: 15 items + 1 disjunctive group(s)
- Sunscreen
...
[3] DERIVE
rm38 -> rm131: (and (gEgo has: 11) (gEgo has: 12) (gEgo has: 14) (gEgo has: 15))
rm57 -> rm58: (and (gEgo has: 21) (gEgo has: 24) (gEgo has: 25) (gEgo has: 26))
rm79 -> rm80: (or (gEgo has: 30) (gEgo has: 31))
rm131 -> rm138: (not (gEgo has: 13))
rm63: delete `(gEgo put: 21 -1)` (Hair_Rejuvenator)
verifying against the guarded model...
fixed 15 + 1 group(s); NEW softlocks introduced: none
[4] PATCH
compiled 117/118 scripts
script.000 Main 10790 bytes
script.057 rm57 2938 bytes
...
Done. 10 patch files in build/patch
The analyzer discovered the ship boarding as a one-way crossing, derived which items must cross with you, re-verified the guarded model to prove the guards introduce no new softlocks, and recompiled the touched scripts into Sierra's own loose-patch format.
Note rm131 -> rm138: (not (gEgo has: 13)). Guards carry negative literals too: the Spinach Dip
is fatal to be holding in rm138, so the fix is to refuse the crossing while you still have it —
placed where you can still throw it overboard, because demanding you drop something you can no
longer drop is a wall, which this project treats as worse than the bug. The pipeline refuses to
emit anything if the guards fail verification, or if a script it edited will not compile.
A patched game plays normally — the patch mechanism is how Sierra shipped its own bug fixes, and the originals are never modified (delete the patch files to revert). You can set the guard behavior in-game: Full prevents every dangerous action; Lite prevents it once, then allows it with a warning; Off turns the guards off (currently that picker is not showing in Laura Bow 2 or King's Quest 5, so the guards default to Full).
Some deaths are deliberately left in — the ones you can still avoid from where you are. The analysis distinguishes unwinnable states from avoidable deaths by reachability, not by death conditions. In Leisure Suit Larry 2, walking onto the KGB beach without the full disguise kills you. Some pieces of the disguise exist only on the cruise ship, so the analyzer refuses to let you leave the ship without them. But the rest is obtainable on the island — from the very place the death occurs — so that death stays in: it is how Sierra games hint at what you need to do. As Al Lowe says, "Save Early, Save Often!"
Four games done, spanning the engine's two major eras (SCI0 1988 → SCI1.1 1992), with nothing declared per title — start room, victory room, death signal and debug flags are all derived from each game's own code.
| game | engine | status | notable features |
|---|---|---|---|
| Leisure Suit Larry 2 (1988) | SCI0 | done & tested | the Spinach Dip: fatal to carry, so the guard is a negative literal, placed while you can still ditch it |
| King's Quest IV (1988) | SCI0 | done & tested | the real-time night clock; the whale — random events guarded by arming them only when survivable |
| King's Quest VI (1992) | SCI1.1 | done & tested | the two ending paths massively complicate analysis; guarding the start of the wedding (a timer) until necessary items are in hand |
| Laura Bow 2 (1992) | SCI1.1 | done & tested | the act structure: the plot clock is a register, act breaks are one-way, demands ride the act-flip interceptor |
| King's Quest V (1990) | SCI1-middle | done & tested | the village market (matching payments to merchants, so detection becomes a matching problem); dangerous encounters guarded by allowing them to occur only when survivable; the cat&mouse interception replays until the player gets it right |
Longer version in docs/HOW-IT-WORKS.md; per-file map in
docs/ARCHITECTURE.md; current KQ6 status in
docs/KQ6-STATUS.md; LB2's derivation log in
docs/LB2-ORACLE.md.
Steps 1 and 6 stand on two excellent existing projects, driven headless:
json-ir branch adds a
second emitter beside the .sc source output: the typed control-flow AST as JSON, which is
what the analysis consumes. The decompilation logic itself is untouched.tools/scicompile/ is a small CLI plus a compatibility layer that replaces the MFC/Windows
surface, calling the real parser, class browser, resource map and code generator
(GenerateScriptResource). The vendor tree is cloned at build time and never edited; a
handful of files are patched as a build step for MSVC-only constructs, with every change
documented in tools/scicompile/BUILD_NOTES.md. Each
guarded script the pipeline emits is compiled by the same code paths SCICompanion uses in
its IDE, then wrapped in Sierra's loose-patch header.The analysis is Python 3 with no third-party packages at all — src/ imports only the
standard library. What needs installing is the two external toolchains it drives: the decompiler
(C#) and the SCI compiler (C++), both built here from source.
sudo apt install python3 git cmake g++ make dotnet-sdk-8.0 # Debian/Ubuntu
| what | why | verified against |
|---|---|---|
| Python 3.12 | the analysis and the tests (src/) | 3.12.3 |
| .NET SDK 8 | builds sci-tools, which decompiles the game | 8.0.129 |
| cmake ≥ 3.16, a C++14 compiler, make | builds scicompile, which recompiles the patched scripts | cmake 3.28.3, g++ 13.3 |
| git | both vendored trees are cloned at build time, not bundled | 2.43 |
Verified from scratch in a clean ubuntu:24.04 container: the packages above, the two builds
below, a full pipeline run and the game-independent tests — see the log recipe in
docs/HOW-IT-WORKS.md.
git clone https://github.com/katiahayati/lucasartsifier && cd lucasartsifier
# 1. the decompiler. Clones our sci-tools fork into vendor/, builds it, and decompiles
# GAME into build/ir -- both a .sc source tree and the typed-AST JSON IR.
tools/sci-tools-fork/build.sh /path/to/game
# 2. the compiler: SCICompanion's, ported headless. Its source is cloned and never modified;
# the port lives beside it in tools/scicompile/{compat,patched}.
git clone --depth 1 https://github.com/icefallgames/SCICompanion vendor/SCICompanion
cmake -S tools/scicompile -B tools/scicompile/build
cmake --build tools/scicompile/build -j
Step 1 alone is enough to analyze a game (--report); step 2 is what turns the derived guards
into patch files. vendor/ is gitignored — no third-party source and no game data is
redistributed here.
You supply your own copy of a game; none is included. The commands run from src/:
cd src
python3 -m pipeline /path/to/game # decompile -> analyze -> derive -> patch
python3 -m pipeline /path/to/game --report # analyze only, write nothing
python3 -m pipeline /path/to/game --skip-decompile # reuse the IR under build/ir
Output lands in build/patch/ as loose patch files:
cp build/patch/script.* /copy/of/game/ # install
rm /copy/of/game/script.0* # revert
Loose script.NNN files override the mapped resource, so RESOURCE.MAP and the volumes are never
modified and the patch reverts by deleting files. Point it at a copy of the game, never at
your only one.
python3 tools/run_tests.py # the whole suite (~21 min with every model cold)
docs/TESTING.md has the rest: why some checks are RED on purpose, the three
regression nets and the different questions they answer, how to measure a change against the full
output surface before committing it, and how to drive a patched build under ScummVM with nobody at
the keyboard.
src/ the analysis (Python 3, standard library only)
src/testdata/ the frozen surfaces: two goldens + the watched pair
tools/run_tests.py the test runner (docs/TESTING.md)
tools/drive_scummvm.py play-test a patched build with nobody at the keyboard
tools/kq6_panel_probe.py ... a driver script: cold start -> KQ6's guard control
tools/sci-tools-fork/ build.sh for our JSON-IR fork of sci-tools [C#]
tools/scicompile/ headless Linux port of SCICompanion's compiler [C++, GPL-2.0+]
docs/ how it works, architecture, testing, per-game status, licensing
docs/reviews/ contextless reviews of tagged releases, verbatim
docs/archive/ superseded plans, kept for their measurements [see its README]
vendor/ cloned at build time, never committed (see Install)
Per-game configuration (src/config.py) is filesystem paths and a display name — nothing
about the game itself. Start room, victory rooms, the death signal and the debug flags all
have override fields there, and every game leaves them empty: the pipeline derives all four
from the game's own code (see src/anchors.py). A new title needs no config entry at all —
config.by_name() picks up any game whose decompiled IR sits under build/sweep/<name>/.
MIT, except tools/scicompile/ which is GPL-2.0-or-later — it contains modified SCICompanion source and links its compiler, so it is a derivative work. See
LICENSE, NOTICE, and docs/LICENSING.md.
Built on sci-tools (sluicebox, MIT) and SCICompanion (Philip Fortier, GPL-2.0+). No game data is included in this repository under any terms.
594 commits
Python
72.1%
C++
27.4%
Static analysis for Sierra adventure games: finds softlocks by abstract interpretation of decompiled SCI scripts, derives and verifies guards, recompiles them into the game
50
stars
594
commits
Python
primary language
Aug 25, 2026
updated
Static analysis for 30-year-old adventure games. This tool decompiles a Sierra SCI game, abstract-interprets the decompiled scripts into a graph of guarded room transitions, item movements and plot-flag writes, finds the softlocks — states where the game still accepts input but victory has quietly become impossible — and derives, verifies, compiles and installs guards that prevent them. Nothing about any title is declared: the start room, the victory room, the death signal and the debug flags are all discovered from the game's own code.
Sierra games, unlike LucasArts ones, let you get stuck. Forget the sunscreen in Los Angeles, board the cruise ship, and you die days later on a raft with no way back. This finds these traps automatically and blocks the crossing that causes them — at the last moment you can still comply.
Four games analyzed and play-tested — Leisure Suit Larry 2 (SCI0, 1988), King's Quest IV (SCI0, 1988), King's Quest VI (SCI1.1, 1992), and Laura Bow 2 (SCI1.1, 1992) — same engine, no game-specific analysis code.
King's Quest IV, patched: the post-whale island.
The tool detects that once you leave the island, you can't come back, and that you need the Golden Bridle later for the unicorn. It injects a "Not yet!" stock refusal if the player attempts to ride the dolphin without the Bridle. Once Rosella gets the Bridle the exit is allowed to happen.
Abridged from a real run on Leisure Suit Larry 2 (python3 -m pipeline <game>):
[2] ANALYZE
anchors: start rm11, victory [86] (discovered)
death signal: global101 == 1001, debug globals: [14, 100] (derived)
101 rooms, 27 strongly-connected components, 40 gating registers
softlocks: 15 items + 1 disjunctive group(s)
- Sunscreen
...
[3] DERIVE
rm38 -> rm131: (and (gEgo has: 11) (gEgo has: 12) (gEgo has: 14) (gEgo has: 15))
rm57 -> rm58: (and (gEgo has: 21) (gEgo has: 24) (gEgo has: 25) (gEgo has: 26))
rm79 -> rm80: (or (gEgo has: 30) (gEgo has: 31))
rm131 -> rm138: (not (gEgo has: 13))
rm63: delete `(gEgo put: 21 -1)` (Hair_Rejuvenator)
verifying against the guarded model...
fixed 15 + 1 group(s); NEW softlocks introduced: none
[4] PATCH
compiled 117/118 scripts
script.000 Main 10790 bytes
script.057 rm57 2938 bytes
...
Done. 10 patch files in build/patch
The analyzer discovered the ship boarding as a one-way crossing, derived which items must cross with you, re-verified the guarded model to prove the guards introduce no new softlocks, and recompiled the touched scripts into Sierra's own loose-patch format.
Note rm131 -> rm138: (not (gEgo has: 13)). Guards carry negative literals too: the Spinach Dip
is fatal to be holding in rm138, so the fix is to refuse the crossing while you still have it —
placed where you can still throw it overboard, because demanding you drop something you can no
longer drop is a wall, which this project treats as worse than the bug. The pipeline refuses to
emit anything if the guards fail verification, or if a script it edited will not compile.
A patched game plays normally — the patch mechanism is how Sierra shipped its own bug fixes, and the originals are never modified (delete the patch files to revert). You can set the guard behavior in-game: Full prevents every dangerous action; Lite prevents it once, then allows it with a warning; Off turns the guards off (currently that picker is not showing in Laura Bow 2 or King's Quest 5, so the guards default to Full).
Some deaths are deliberately left in — the ones you can still avoid from where you are. The analysis distinguishes unwinnable states from avoidable deaths by reachability, not by death conditions. In Leisure Suit Larry 2, walking onto the KGB beach without the full disguise kills you. Some pieces of the disguise exist only on the cruise ship, so the analyzer refuses to let you leave the ship without them. But the rest is obtainable on the island — from the very place the death occurs — so that death stays in: it is how Sierra games hint at what you need to do. As Al Lowe says, "Save Early, Save Often!"
Four games done, spanning the engine's two major eras (SCI0 1988 → SCI1.1 1992), with nothing declared per title — start room, victory room, death signal and debug flags are all derived from each game's own code.
| game | engine | status | notable features |
|---|---|---|---|
| Leisure Suit Larry 2 (1988) | SCI0 | done & tested | the Spinach Dip: fatal to carry, so the guard is a negative literal, placed while you can still ditch it |
| King's Quest IV (1988) | SCI0 | done & tested | the real-time night clock; the whale — random events guarded by arming them only when survivable |
| King's Quest VI (1992) | SCI1.1 | done & tested | the two ending paths massively complicate analysis; guarding the start of the wedding (a timer) until necessary items are in hand |
| Laura Bow 2 (1992) | SCI1.1 | done & tested | the act structure: the plot clock is a register, act breaks are one-way, demands ride the act-flip interceptor |
| King's Quest V (1990) | SCI1-middle | done & tested | the village market (matching payments to merchants, so detection becomes a matching problem); dangerous encounters guarded by allowing them to occur only when survivable; the cat&mouse interception replays until the player gets it right |
Longer version in docs/HOW-IT-WORKS.md; per-file map in
docs/ARCHITECTURE.md; current KQ6 status in
docs/KQ6-STATUS.md; LB2's derivation log in
docs/LB2-ORACLE.md.
Steps 1 and 6 stand on two excellent existing projects, driven headless:
json-ir branch adds a
second emitter beside the .sc source output: the typed control-flow AST as JSON, which is
what the analysis consumes. The decompilation logic itself is untouched.tools/scicompile/ is a small CLI plus a compatibility layer that replaces the MFC/Windows
surface, calling the real parser, class browser, resource map and code generator
(GenerateScriptResource). The vendor tree is cloned at build time and never edited; a
handful of files are patched as a build step for MSVC-only constructs, with every change
documented in tools/scicompile/BUILD_NOTES.md. Each
guarded script the pipeline emits is compiled by the same code paths SCICompanion uses in
its IDE, then wrapped in Sierra's loose-patch header.The analysis is Python 3 with no third-party packages at all — src/ imports only the
standard library. What needs installing is the two external toolchains it drives: the decompiler
(C#) and the SCI compiler (C++), both built here from source.
sudo apt install python3 git cmake g++ make dotnet-sdk-8.0 # Debian/Ubuntu
| what | why | verified against |
|---|---|---|
| Python 3.12 | the analysis and the tests (src/) | 3.12.3 |
| .NET SDK 8 | builds sci-tools, which decompiles the game | 8.0.129 |
| cmake ≥ 3.16, a C++14 compiler, make | builds scicompile, which recompiles the patched scripts | cmake 3.28.3, g++ 13.3 |
| git | both vendored trees are cloned at build time, not bundled | 2.43 |
Verified from scratch in a clean ubuntu:24.04 container: the packages above, the two builds
below, a full pipeline run and the game-independent tests — see the log recipe in
docs/HOW-IT-WORKS.md.
git clone https://github.com/katiahayati/lucasartsifier && cd lucasartsifier
# 1. the decompiler. Clones our sci-tools fork into vendor/, builds it, and decompiles
# GAME into build/ir -- both a .sc source tree and the typed-AST JSON IR.
tools/sci-tools-fork/build.sh /path/to/game
# 2. the compiler: SCICompanion's, ported headless. Its source is cloned and never modified;
# the port lives beside it in tools/scicompile/{compat,patched}.
git clone --depth 1 https://github.com/icefallgames/SCICompanion vendor/SCICompanion
cmake -S tools/scicompile -B tools/scicompile/build
cmake --build tools/scicompile/build -j
Step 1 alone is enough to analyze a game (--report); step 2 is what turns the derived guards
into patch files. vendor/ is gitignored — no third-party source and no game data is
redistributed here.
You supply your own copy of a game; none is included. The commands run from src/:
cd src
python3 -m pipeline /path/to/game # decompile -> analyze -> derive -> patch
python3 -m pipeline /path/to/game --report # analyze only, write nothing
python3 -m pipeline /path/to/game --skip-decompile # reuse the IR under build/ir
Output lands in build/patch/ as loose patch files:
cp build/patch/script.* /copy/of/game/ # install
rm /copy/of/game/script.0* # revert
Loose script.NNN files override the mapped resource, so RESOURCE.MAP and the volumes are never
modified and the patch reverts by deleting files. Point it at a copy of the game, never at
your only one.
python3 tools/run_tests.py # the whole suite (~21 min with every model cold)
docs/TESTING.md has the rest: why some checks are RED on purpose, the three
regression nets and the different questions they answer, how to measure a change against the full
output surface before committing it, and how to drive a patched build under ScummVM with nobody at
the keyboard.
src/ the analysis (Python 3, standard library only)
src/testdata/ the frozen surfaces: two goldens + the watched pair
tools/run_tests.py the test runner (docs/TESTING.md)
tools/drive_scummvm.py play-test a patched build with nobody at the keyboard
tools/kq6_panel_probe.py ... a driver script: cold start -> KQ6's guard control
tools/sci-tools-fork/ build.sh for our JSON-IR fork of sci-tools [C#]
tools/scicompile/ headless Linux port of SCICompanion's compiler [C++, GPL-2.0+]
docs/ how it works, architecture, testing, per-game status, licensing
docs/reviews/ contextless reviews of tagged releases, verbatim
docs/archive/ superseded plans, kept for their measurements [see its README]
vendor/ cloned at build time, never committed (see Install)
Per-game configuration (src/config.py) is filesystem paths and a display name — nothing
about the game itself. Start room, victory rooms, the death signal and the debug flags all
have override fields there, and every game leaves them empty: the pipeline derives all four
from the game's own code (see src/anchors.py). A new title needs no config entry at all —
config.by_name() picks up any game whose decompiled IR sits under build/sweep/<name>/.
MIT, except tools/scicompile/ which is GPL-2.0-or-later — it contains modified SCICompanion source and links its compiler, so it is a derivative work. See
LICENSE, NOTICE, and docs/LICENSING.md.
Built on sci-tools (sluicebox, MIT) and SCICompanion (Philip Fortier, GPL-2.0+). No game data is included in this repository under any terms.
594 commits
Python
72.1%
C++
27.4%