Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ jobs:
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo test --locked
- run: cargo run --locked --example console -- --walkthrough
- run: cargo clippy --locked --all-targets --all-features -- -D warnings
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/target
*.save.json
82 changes: 81 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ schemars = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_path_to_error = "0.1"
sha2 = "0.10"
tempfile = "3"

[features]
default = []
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A minimal data-driven point-and-click adventure runtime. **The Last Light** is an
original, hand-authored three-room fixture with pickups, inventory-gated actions,
branching dialogue, room transitions and a reachable ending.
branching dialogue, room transitions, versioned save/load and a reachable ending.

## Play

Expand All @@ -13,17 +13,31 @@ API and platform setup. The old Bevy 0.6 scaffold has been updated.
```sh
cargo run --locked --features desktop --example last_light
# Optional path to another v1 scene:
cargo run --locked --features desktop --example last_light -- path/to/game.json
cargo run --locked --features desktop --example last_light -- path/to/game.json path/to/slot.json
```

Click the labeled rectangles, or press their numbers (1–9). Items are used
automatically by matching hotspots. Dialogue is modal. **N** starts a new game.
**F5** saves; **F9** loads (also available as buttons).
The desktop player uses native Bevy UI; on Linux it needs X11 and a Vulkan-capable
driver (Mesa software rendering also works). Audio, gamepads and 3D are disabled.

For a terminal player or a complete self-checking demonstration (no GPU):

```sh
cargo run --locked --example console
cargo run --locked --example console -- --walkthrough
```

Follow the [playable walkthrough](docs/walkthrough.md). The automated run saves
and restores an active conversation, a repaired lantern and the ending, using an
isolated temporary slot. Normal play saves to `last_light.save.json` in the
current directory. Failed loads preserve the running game.

## Editor integration

- [Versioned scene contract](docs/scene-format.md)
- [Save format and compatibility](docs/save-format.md)
- [Generated JSON Schema](docs/scene-v1.schema.json)
- [Fixture source](assets/last-light/game.json) and [asset license](assets/last-light/LICENSE.md)

Expand Down
22 changes: 22 additions & 0 deletions assets/last-light/walkthrough.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# These are also playable commands for the interactive console.
click crate
click workshop
click keeper
choose help
# Save mid-conversation while carrying the fuse, then replace the running game.
save
new
load
choose accept
click stairs
click panel
# Save after consuming an item; used hotspots and dialogue history must persist.
save
new
load
click beacon
# Endings must also survive a fresh instance.
save
new
load
quit
Binary file added docs/images/dialogue-restored.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/ending-restored.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 52 additions & 0 deletions docs/save-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Save format v1

`Game::save_json` / `load_json` handle UTF-8 JSON; `save_file` / `load_file` add
filesystem persistence. The desktop and terminal players share these APIs.

The envelope has required fields `save_version` (1), `game_id`,
`scene_fingerprint` and `state`. The fingerprint is lowercase SHA-256 of the typed
scene serialized by `serde_json::to_vec`: fixed struct field order, expanded
optional defaults, array order preserved. Source whitespace and object key order
do not matter. Any content change, including text, intentionally invalidates old
saves. No migrations are implemented. A future serializer/format change that
alters canonicalization must also change the save version.

`state` contains:

| Field | Representation |
| --- | --- |
| `room` | Current room ID |
| `inventory` | Sorted set of item IDs |
| `flags` | Sorted set of true flag IDs; absent means false |
| `used_hotspots` | Sorted set of `{room, hotspot}` for successful one-shot actions |
| `dialogue` | `{dialogue, node}` for the active conversation, or null |
| `visited_nodes` | Sorted set of `{dialogue, node}` |
| `chosen_choices` | Sorted set of `{dialogue, node, choice}` |
| `ending` | Ending ID, or null |

Loading checks version, game identity, fingerprint, all state references,
one-shot hotspot markers, dialogue/choice history consistency, and terminal
state consistency. The active dialogue/ending must be exposed by the current
room. Unknown fields are rejected. Loading is transactional: the running state
is replaced only after every check succeeds. Error messages include the save
path (file API) and offending JSON field. Saves are local state snapshots, not
signed proofs of play; validation does not prove that arbitrary edited states
are reachable or enforce every game-specific puzzle invariant.

Saving writes and syncs a temporary file beside the destination, then atomically
replaces the slot using `tempfile::persist`. Failed serialization, write, or
replacement leaves an existing slot intact. Parent directories must already
exist. Temp files are cleaned up on error. Concurrent writers use independent
temporary files; the last successful replacement wins. Directory metadata is not
synced, so power-loss durability depends on the platform/filesystem.

The desktop player uses `<game_id>.save.json` in its current directory by default;
its optional second positional argument chooses a different slot. F5/button
saves, F9/button loads, N/button starts a new game without deleting the slot.
Saving/loading is allowed during dialogue and after the ending. Missing,
corrupt, mismatched and inaccessible saves are shown as errors without resetting
the current game. The terminal fixture uses `last_light.save.json`.

The status line, hover state, window configuration and UI entities are transient;
loading rebuilds the interface from the restored state. Item selection is not
needed: inventory-gated hotspots use matching items automatically.
Loading
Loading