Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Save-Game Formats

Captain Bible keeps one nine-entry label index and up to ten independent game states per player prefix. The executable contains no file header, version number, checksum, or compression for either format. Parsing therefore depends on the exact file sizes and fixed block order recovered from the read and write routines.

Player prefixes and filenames

At startup, game_main copies DDGAMES into the active prefix. Every command line argument that does not begin with - replaces it using strcpy. Both save routines then construct a filename by copying that prefix to a local buffer and appending a mutable static suffix initially containing .SV0.

UseSuffixExample with default prefix
Slot-label index.SV0DDGAMES.SV0
Normal slots.SV1 through .SV9DDGAMES.SV4
Quick save.SVQDDGAMES.SVQ

The save-slot menu writes the selected index plus ASCII 1 into the suffix. The main event loop writes Q before an F10 quick save or F9 quick load and restores 0 afterward. Thus quick save is a tenth state without an entry in the nine-label index.

Scene opcode 0x8D reuses this exact active-prefix plus mutable-suffix construction. Its sole shipped site is TITLE.BIN:0x012C, while the suffix is .SV0, so it probes the player’s label index in rb mode. Its missing-file target 0x012F equals the next command, so the shipped title program enters INTRO whether the open succeeds or fails.

The manual requires an extensionless legal DOS name of at most eight characters, or permits a complete path such as d:\players\jimmy. That is a user-facing constraint, not executable validation: the parser copies the argument verbatim and does not check its length, characters, path, or number of non-option arguments. A player prefix separates only filenames; it is not stored inside the state.

SV0 label index

The index is exactly 243 bytes: nine consecutive 27-byte character buffers. Each buffer is interpreted as a CP437 C string.

offset = (slot - 1) * 27

+0x00  char label[27]

If the file cannot be opened, read_save_index initializes all nine buffers with strcpy("(EMPTY)"). Because strcpy stops after the first NUL and does not clear the rest of a 27-byte destination, bytes after that NUL are not part of the label and can retain old data. The supplied index demonstrates this: every visible label is EMPTY, but eight records have nonzero stale-tail bytes. The supplied spelling also lacks the parentheses in the executable’s literal, so it was likely prepared or sanitized separately.

The game writes the complete 243-byte array whenever it writes a state. It does not include slot labels in .SV1 through .SV9 themselves.

Normal-save name editing uses a 27-byte local buffer and permits at most 26 characters. The input routine at 0x2C8B accepts ASCII 20..3B, 3F..5A, and 61..7A, except for ampersand and asterisk. It recognizes Backspace, Enter, and Escape; the blocking routine has no mouse-confirmation path. A selected (EMPTY) label begins as an empty edit field; after Enter, the helper at 0x815A replaces a still-empty label with Game 1 through Game 9 before the index is written.

State-file layout

write_save_state emits 15 fwrite calls totaling exactly 2,752 bytes. read_save_state performs corresponding fread calls in the same order. All nine supplied state files have this size.

File offsetSizeDS offsetInterpretation
0x0002007BF2Checkpoint copy of 100 script-variable words
0x0C8200727ALive 100-word script state
0x190663A66Checkpoint copy of descriptor state bytes
0x1D2660B19466 live ten-byte text descriptors
0x466206EA6Checkpoint resource-name C-string buffer
0x47A20B83ELive resource-name C-string buffer
0x48E208938Checkpoint extension C-string buffer
0x4A220AEFELive extension C-string buffer
0x4B62007CBible translation index
0x4B820048Music enabled flag
0x4BA2004ASound-effects enabled flag
0x4BC29FB0Checkpoint text-bank character
0x4BE20080Live text-bank character
0x4C07685B16Live 16×16×3-byte world map
0x7C076876ECCheckpoint copy of the world map
Total2,752

“Checkpoint” is based on executable copy direction, not just similarity. At new-session initialization and a scene-bytecode checkpoint command, copy_live_state_to_save_buffers copies the live 200-byte block, each descriptor’s byte at offset 4, the two C strings, current text-bank word, and all 16×16 three-byte table cells into the checkpoint buffers. The inverse routine restores those fields and reloads the selected text bank. The normal and quick save paths serialize both versions; they do not take a fresh checkpoint immediately before writing.

The script-state chapter documents the primary block’s 100 signed words, embedded 128-bit flag bank, identified variables, powerups, victim flags, and interpreter commands. The world-map chapter documents the table’s row-major cell layout, packed connection/location byte, scene commands, exploration state, and archive-resource correlation. The detailed semantics of the primary state and several map kinds and parameters remain open.

Runtime text descriptors

The 660-byte block is 66 records of ten bytes:

Record offsetSizeMeaning
+02Far-pointer offset
+22Far-pointer segment
+41Persistent gameplay state byte
+51Text-record selector
+62Offset in the companion DDL stream
+82Span in the companion DDL stream

The preceding 66-byte block is exactly the checkpoint copy of byte +4 from each descriptor. The text-bank loader reconstructs the process-dependent far pointers and structural fields after a load while preserving the state byte. Consequently, pointer values observed on disk are not stable format identifiers.

All supplied saves select bank C. Their first 46 descriptor records match the recovered NIV bank C index exactly in selector, DDL offset, and span; the remaining 20 structural records are zero. All 66 state bytes are also zero. This independently connects the save layout to the verse-index format.

Supplied-save observations

Static comparison of DDGAMES.SV1 through DDGAMES.SV9 found:

  • SV6 and SV8 are byte-for-byte identical.
  • 2,477 of the 2,752 offsets are constant across all nine files.
  • The live and checkpoint 16×16 maps match within every file. They are all zero except in SV3 and SV4, which each have 112 nonzero bytes. Those two grids match CE.MAP except for four field mutations, including two kind changes that exactly follow the executable’s map-normalization rule.
  • The checkpoint/live resource strings decode as LOGO, LOGO, seg, and seg. Bytes after their first NUL can be stale and are not semantic.
  • Translation is 1, music and effects are enabled, and both bank values are ASCII C in every file.
  • Each primary block pair differs at byte 56; SV9 additionally differs at byte 54. This is evidence that both serialized blocks matter, not evidence that either copy is damaged.
  • Descriptor far-pointer segments differ in SV9, consistent with addresses being reconstructed runtime state rather than portable identifiers.

Pairwise comparisons alone do not yet assign gameplay meaning to the 275 offsets that vary across this small corpus. Controlled saves after specific game actions are needed for that stage.

Error behavior

The state reader returns failure if the requested state file cannot be opened. Once open succeeds, it issues all 15 reads but does not check their individual return counts before refreshing audio/text state and returning success. The format has no integrity marker, so a truncated or modified file may partially overwrite memory rather than producing a clean format error. The host-side inspector is intentionally stricter.

That open failure has a surprising F9 consequence. The main input loop stores read_save_state’s return value in the top-level mode word. A missing .SVQ returns 1; mode 1 resets the initial scene strings and calls the same new-session initializer as New Game. Thus F9 with no quick save restarts the game while preserving the current option words. Numbered loading is insulated by its selector, which does not offer a missing state as a loadable row.

Inspection tool

tools/inspect_save.py selects the index or state parser by exact size, decodes C-string buffers, reports snapshot differences and settings, and can list live text descriptors:

tools/inspect_save.py CB/DDGAMES.SV0
tools/inspect_save.py CB/DDGAMES.SV3 --descriptors
tools/inspect_save.py CB/DDGAMES.SV9 --variables

The parser rejects every size other than 243 or 2,752 bytes. Its tests cover all ten supplied files, stale label tails, exact snapshot relationships, scalar meanings, script variables and flags, descriptor state copies, and the full NIV bank C structural match.

Relevant executable functions

Load offsetCurrent nameRole
0x2B6Fchoose_save_slotBuild the nine-label menu and change .SV0 to the chosen slot suffix.
0x7D8Ecopy_live_state_to_save_buffersRefresh checkpoint fields from live state.
0x7E41copy_save_buffers_to_live_stateRestore checkpoint fields and reload text.
0x7F01write_save_indexWrite nine 27-byte label buffers.
0x7F58read_save_indexRead the index or initialize (EMPTY) labels.
0x7FD7write_save_stateWrite the index and the 15 state blocks.
0x815Ainitialize_empty_save_slotReplace an empty label with the Game 1 through Game 9 default.
0x81A5save_selected_slotInitialize the selected label, then write the state.
0x81ACread_save_stateRead the index and all 15 state blocks.