This document states design intent and the boundaries to preserve. It is written in "should" form and stays stable; for what is actually built today, see ROADMAP.md and docs/PARSER_COVERAGE.md.
Build a Rust-based, CLI-first Infinity Engine inspection tool that can:
- locate installed game resources
- read resources from
override,KEY, andBIF - resolve
dialog.tlkstrings - decode selected binary formats
- export stable JSON for humans, scripts, and AI agents
The design should optimize for correctness, testability, and gradual expansion across Infinity Engine games such as BGEE, BG2EE, EET, IWD, and PST.
The system should be divided into a few clear layers:
- Environment discovery
- Resource lookup and byte loading
- Format decoding
- Text resolution
- Serialization and CLI presentation
Each layer should have a narrow responsibility.
crates/
ie-core/ # shared types, errors, resource ids
ie-io/ # installation discovery, KEY/BIF/TLK/DLC loading
ie-formats/ # typed decoders and scoped write paths
ie-cli/ # command frontend, one module per concern
skills/ # canonical agent skills (mirrored to .claude/skills/)
scripts/ # repo maintenance tooling
docs/
Tests live in crates/<crate>/tests/, with committed goldens under tests/goldens/ and
real-resource expectations under crates/ie-cli/tests/expectations/. There is no top-level
tests/ directory and no committed game data. The four crates are already split; keep the
boundaries rather than merging or re-cutting them mid-feature.
Holds shared domain types:
GamePathResourceTypeResRefStrRef- shared error types
- common binary parsing helpers
- source metadata types
This crate should stay small and dependency-light.
Responsible for reading the game installation:
- locate
chitin.key - locate language folder and
dialog.tlk - resolve override precedence
- parse
KEY - locate and read from
BIF - return raw resource bytes with metadata
This crate should not know format-specific details like the structure of CRE or ITM.
Responsible for typed decoding of resource bytes:
ITMSPLCRESTODLGBCSARECHR(a wrapper around a complete embeddedCRE)GAM/SAV(save containers)
Each format should have:
- a raw parser
- typed structs
- JSON-ready export structs or serializers
- format-specific tests
Cross-cutting modules also live here where they operate on decoded resources rather than bytes:
effects (variant-aware opcode tables) and verify (ARE cross-resource integrity checks).
Scoped write paths (patch_cre_scalars, patch_chr_scalars, patch_are_scalars,
add_item_to_save_gam) belong to this
crate too. They must preserve every byte outside the declared edit, repair only an explicit offset
set, and refuse layouts they have not been validated against.
Responsible for user-facing commands:
locatedump-rawdumplisttlk,tlk-appendpatchoverride-diffverifysave-list,save-info,save-add-item
It should not contain parsing logic beyond argument handling and output formatting. As the command
surface grew, main.rs was split into per-concern modules (dialog_graph, override_diff,
patch_input, resource_links, save_support, verify_command); new commands should follow that
shape rather than accreting in main.rs.
Expected request path:
- CLI receives
--gameand--resource - loader resolves where the resource lives
- raw bytes are returned with metadata
- format decoder parses bytes into typed structures
- TLK resolver enriches
strreffields if needed - serializer emits stable JSON
Keep these steps separately testable.
Purpose:
- resolve the winning source for a resource
- report whether the source is
override,bif, or loose file
Expected output:
- resource name
- resource type
- physical source
- path or archive location
Purpose:
- read bytes from the resolved source
Should return:
- raw bytes
- source metadata
Purpose:
- turn bytes into typed resources
Avoid giant enum-driven parser functions. Prefer per-format modules with a shared trait only if it helps, for example:
trait DecodeResource {
type Output;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodeError>;
}Purpose:
- resolve
StrRefinto localized text
Keep resolution separate from parsing. Parsed resources should still be meaningful without live text resolution.
Use explicit exported structures rather than serializing internal parser structs directly.
Reason:
- internal structs often reflect offsets and parsing concerns
- exported structs should reflect inspection concerns
- the public JSON shape should remain stable even if parser internals evolve
Recommended output shape:
- raw ids preserved
- decoded labels where available
- nested structures named clearly
- unknown values kept visible
Use a parsing style that makes offsets and field widths obvious.
Requirements:
- little-endian helpers
- fixed-width string/resref helpers
- strict bounds checking
- clear errors on truncated input
Avoid “parse by side effect” code that mutates many cursors implicitly.
Implement one format at a time.
Suggested order:
ITMSPLCRESTODLGBCS
Rationale:
ITMandSPLare structurally rich but still manageableCREandSTOunlock practical modding and party-analysis use casesDLGis high value but larger in scopeBCSshould wait until its exported representation is agreed
String references should be modeled as explicit value objects, not plain integers.
Suggested model:
struct ResolvedStrRef {
strref: u32,
text: Option<String>,
}This avoids losing the original id and makes unresolved strings explicit.
Do not hardcode assumptions for a single title.
The design should tolerate:
- different resource populations
- different language layouts
- title-specific quirks
- Enhanced Edition vs older layout differences when relevant
Variant-specific behavior should live in documented decision points, not spread through the codebase.
Implemented shape: GameVariant (standard / iwd / pst) is detected once at discovery from stable
root files rather than folder names, rides on ResourceMetadata, and is consumed at the specific points
that diverge — currently effect-opcode decoding and PST inventory slot maps. Add new variant branches at
that granularity; do not thread per-game conditionals through parsers wholesale.
Errors should be actionable.
Prefer:
resource not foundunsupported resource typeinvalid KEY entry offsettruncated CRE header
Over vague generic failures.
Preserve enough context to debug:
- game path
- resource name
- source kind
- format type
- failing field or offset if known
Tests exist at four levels. See docs/TESTING.md for how to run them and docs/GOLDENS.md for the two golden tiers.
For:
- fixed-width readers
- endian helpers
- resref parsing
- TLK field parsing
For:
- real resources from installed games
- parser correctness
- JSON stability
For:
- full command execution against a synthetic install
- override precedence
- TLK resolution in exported JSON
For:
- exact-value goldens over synthetic fixtures, covering every decoded format and every human-readable output mode, running in CI
- normalized JSON shape goldens checked against real installs, which pin structure without committing values
- real-resource expectations with stated provenance
- deterministic adversarial-input coverage proving no decoder panics
No game data is committed. cargo test --workspace must pass on a clean checkout with no
installs and no environment variables set, so the default suite runs on synthetic fixtures and a
synthetic installation. Real installs are reached only through env-gated tests that no-op when
their variable is unset — see docs/TESTING.md.
Real-resource facts are recorded as expectations rather than checked-in files. Each case should document:
- source game and install (its provenance)
- original resource name
- why it exists
- what edge or behavior it covers
Keep expectations to individual non-localized facts, never whole dumps. Do not avoid weird real-world files — reach them through an env-gated test.
IESDP is the layout authority: where each field sits, how wide it is, what its bits and enums mean. It is not a data source — what any particular resource contains can only be read from the file, so expected values always come from real resources across several installs. When a real file's layout disagrees with IESDP, the file wins. Neither dictates architecture: decode from the spec, then shape the output for this tool's consumers.
See docs/FORMAT_REFERENCES.md for the workflow.
Default to JSON for all machine-readable output.
Requirements:
- deterministic field ordering where feasible
- no debug-only internals
- stable naming
- support compact and pretty output
Do not leak parser offsets unless they are intentionally exposed.
Prefer explicit subcommands.
Minimal command surface (all shipped):
locatedump-rawdumptlklist
Later:
difffor decoded resourcessearch
Shared flags should stay shared: --game and --resource mean the same thing everywhere, and
--source auto|override|bif / --skip-override apply uniformly to lookup-based commands.
CLI flags should be stable and unsurprising.
Future growth should be possible in these directions:
- new resource formats
- diffing
- patch planning support
- machine-readable cross-reference graphs
- optional writeback tooling
- optional GUI or TUI frontends
Do not build those now, but avoid blocking them accidentally.
ie-climust not parse binary formats directlyie-iomust not know game-mechanics semantics ofITMorCREie-formatsmust not depend on terminal output concerns- text resolution must remain swappable
The architecture is working if the project can reliably:
- locate
VICONI.CRE - dump an
ITMas stable JSON - resolve
strreftext fromdialog.tlk - prove override precedence with a test
- pin that output with a real-resource expectation that states its provenance
That is enough to make the project useful before it becomes large.