Show HN:自动检测并修复 Sierra 游戏中“死局”状态的工具
Show HN: Automatically detect and patch walking-dead states in Sierra games

原始链接: https://github.com/katiahayati/lucasartsifier/

本项目提供了一个稳健且与引擎无关的静态分析工具,旨在消除 20 世纪 80 年代和 90 年代 Sierra SCI 冒险游戏(如《休闲西装拉瑞 2》和《国王密使 IV》)中导致无法通关的“软锁”状态。 与手动补丁不同,该工具利用抽象解释技术将游戏脚本反编译为包含状态转换、物品移动和剧情标记的图表。它能自动识别“单向”路径,即玩家一旦进入该状态便无法获得胜利,随后推导出并编译相应的“防护机制”,除非玩家拥有必要物品,否则无法通过。该系统智能地将防护点设置在最后可能的时机,允许玩家在进入该路径前纠正库存状态。 由于该工具直接从游戏代码中推导出所有特定游戏数据(获胜条件、死亡触发点、起始房间),因此无需针对新游戏进行手动配置。生成的补丁与原始引擎及 ScummVM 均兼容。该流程使用 Python 3 标准库编写,利用 C# 反编译器和无头 C++ 编译器来生成非破坏性且可移除的补丁。目前已成功分析并测试了四款游戏。

开发者“wkfauna”发布了一款名为“Lucasartsifier”的静态分析工具,旨在消除经典雪乐山(Sierra)冒险游戏中的“死局”(walking-dead states)。这些游戏以允许玩家陷入无法通关的困境而闻名——例如在游戏初期错过关键道具,直到数小时后才发现无法继续。 该工具通过反编译雪乐山资源文件来映射游戏逻辑,并自动注入补丁,防止玩家在未收集必要道具的情况下推进剧情。目前,该工具支持《休闲西装拉瑞2》(Leisure Suit Larry 2)、《国王密使4和6》(King's Quest 4 and 6)以及《劳拉·鲍2》(Laura Bow 2),《国王密使5》正在开发中。 该项目在人工智能(Claude)的协助下构建,用于处理代码和文档。其目的是通过消除“软锁死”带来的挫败感来对这些经典作品进行现代化改造。Hacker News 上的反响非常热烈,用户对这些提升游戏体验的改进表示赞赏,尽管一些参与者对人工智能在开发过程中的作用程度进行了讨论。创作者认为,该项目既是一种保留这些经典作品的方式,也解决了它们广受诟病、有时甚至显得“恶意”的设计缺陷。
相关文章

原文

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 whale, the night clock and seven stranded items, all guarded:

Demo: King's Quest IV, patched

The thirty-second version

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.

What counts as a softlock? (or: Caveat Player)

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 notes
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 in progress (kq5 branch) the village market: matching payments to merchants so everyone can be paid — detection becomes a matching problem
  1. Decompile the game binary to a typed control-flow AST (JSON IR).
  2. Abstract-interpret that AST, composing path conditions into a game graph: guarded movement edges, item acquisitions, item losses, register writes. Room art (PIC/VIEW) and obstacle polygons are read too, since some gates are geometric and exist nowhere in the script.
  3. Condense the graph into strongly-connected components — regions you can wander freely. Only the one-way edges between them can strand you, which is what makes the problem finite.
  4. Find strandings: an item obtainable before a crossing, unavailable after, still needed beyond.
  5. Derive a guard from the winning region — the condition under which the goal is still reachable — and place it at the last point where the player can still comply. Item-wasting dead ends are neutralized separately, with a "Just kidding!" message that prevents you from wasting the needed item, and no score penalty.
  6. Recompile and emit. The patched game is now playable normally (e.g. in ScummVM or DOSBox).

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:

  • Decompilation is sci-tools (sluicebox, MIT). We maintain a fork whose 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.
  • Compilation is SCICompanion's script compiler (Philip Fortier, GPL-2.0+), which we ported to build and run headless on Linux — 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 allsrc/ 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>/.

  • Required actions are not currently modeled. Currently we guard a transition that must not be taken while something it needs is still required and no longer obtainable after the crossing. That covers a room edge, a plot flag advancing, and an event the player does not control — a whale that swallows you, nightfall, an act break. But we do not model actions that, if not taken, lead to a death later. For example, in King's Quest V you have to throw a shoe at a cat to save a mouse who will later save you from bandits.
  • State explosion in Quest For Glory games. QFG games have SO MUCH going on that the analyzer cannot complete. I suspect we can fix that by abstracting away from player stats, combat, and health consumables (rations, etc.), but that work has not been done yet.
  • SCI1.0 / SCI1-middle. SCI0 and SCI1.1 are modeled. King's Quest V — the weird hybrid in between — is in progress on the kq5 branch and most of the way there.
  • Full end-to-end playtesting. All four games have been extensively tested where patched, but none has been played end to end yet. Doing that might uncover bugs.
  • More games! There is no game-specific code in the engine, but Sierra shipped a lot of game-specific code in each game, so every new title has so far required extending the analysis. Hopefully at some point this converges to zero.
  • AGI. AGI games should definitely be included, but that work is not started yet.

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.

联系我们 contact @ memedata.com