bethkit/docs/modules/ROOT/pages/architecture.adoc

124 lines
4.8 KiB
Text

= Architecture & Crate Layout
bethkit is a Cargo workspace.
Each crate has a single, well-bounded responsibility.
They can be used independently or combined.
== Crate overview
[cols="1,1,3",options="header"]
|===
| Crate | Status | Purpose
| `bethkit-io`
| ✅ Complete
| Low-level primitives shared by all other crates: memory-mapped file access via `memmap2`,
a `SliceCursor` for sequential byte reading, zlib and LZ4 decompression.
| `bethkit-core`
| ✅ Complete
| ESP/ESL/ESM parser and writer, string-table I/O, streaming `PluginPatcher`,
load-order utilities (`LoadOrder`, `PluginCache`), and the declarative record schema
(`SchemaRegistry`, `RecordView`).
| `bethkit-bsa`
| ✅ Complete
| BSA (TES3/TES4/FO3/SSE) and BA2 (GNRL/DX10) archive reader and writer.
`Archive::extract` is `&self` and safe to call concurrently; rayon is used internally
for parallel compression when *writing* new archives.
| `bethkit-ffi`
| ✅ Complete
| Stable `extern "C"` ABI and matching `bethkit.h` header generated by `cbindgen`.
Enables Python, C#, and C++ callers without a Rust toolchain.
|===
== Dependency graph
----
bethkit-ffi
└── bethkit-core
└── bethkit-io
bethkit-bsa
└── bethkit-io
----
`bethkit-bsa` and `bethkit-ffi` are intentionally separate from `bethkit-core`
so consumers who only need the plugin parser do not pull in archive or FFI code.
== Source layout inside `bethkit-core`
[source]
----
crates/bethkit-core/src/
lib.rs Public API surface — re-exports from all modules
error.rs CoreError enum and Result alias
types.rs FormId, Signature, RecordFlags, GameContext, Game, PluginKind
plugin.rs Plugin, PluginHeader
group.rs Group, GroupHeader, GroupType, GroupLabel
record.rs Record, RecordHeader, SubRecord, SubRecordData
encoding.rs CodePage, code_page_for_language
strings.rs StringTable, StringFileKind
localized.rs LocalizationSet, extract_strings, apply_edits
patcher.rs PluginPatcher, RecordPatch
writer.rs PluginWriter and writable node types
load_order.rs LoadOrder, LoadOrderEntry, GlobalFormId
cache.rs PluginCache, CacheEntry
implicits.rs ImplicitRecords (engine-hardcoded FormID constants)
schema/
mod.rs RecordSchema, SubRecordDef, FieldDef, FieldType,
EnumDef, FlagsDef, ArrayCount, SchemaRegistry
view.rs RecordView, FieldEntry, FieldValue
enums.rs 27 static EnumDef / FlagsDef instances (SSE)
sse/
mod.rs Registry builder — registers all 126 SSE schemas
common.rs 25 reusable SubRecordDef statics (EDID, FULL, VMAD, …)
simple.rs 32 simple record schemas (KYWD, GLOB, GMST, …)
items.rs 15 item schemas (WEAP, ARMO, BOOK, ALCH, …)
actors.rs 13 actor / NPC schemas (NPC_, RACE, PACK, FACT, …)
magic.rs 16 magic schemas (SPEL, MGEF, ENCH, PROJ, …)
world.rs 23 world schemas (CELL, WRLD, LAND, STAT, LIGH, …)
quests.rs 9 quest / dialogue schemas (QUST, DIAL, INFO, …)
audio.rs 10 audio schemas (SOUN, SNDR, MUSC, …)
projectiles.rs 8 placed-projectile schemas (PARW, PBEA, …)
----
== Design principles
=== Zero-cost schema data
All record schema definitions — `RecordSchema`, `SubRecordDef`, `FieldDef`, `EnumDef`, `FlagsDef` —
are `Copy + Clone` and stored as `static` items.
`SchemaRegistry::sse()` returns a `&'static SchemaRegistry` built once via `OnceLock` on first call.
No heap allocation is needed for the definitions themselves at runtime.
=== Zero-copy parsing
`Plugin::open` memory-maps the file with `memmap2`.
`Record` and `SubRecord` hold `Arc<[u8]>` slices that reference the mapping directly.
Subrecords are parsed lazily on first access.
Decompression allocates only when a record is actually compressed.
=== Streaming writes
`PluginPatcher` reads the source plugin sequentially and writes each group/record to the output
as it passes through.
Only the records that need changing are held in memory simultaneously.
The cost is proportional to the number of changed records, not the plugin size.
=== Error handling
Every public function that can fail returns `Result<T, CoreError>`.
`CoreError` uses `thiserror` and provides structured context on every variant.
See xref:error-handling.adoc[Error Handling] for the full list of variants.
=== Multi-game via `GameContext`
`GameContext` bundles all binary-format differences between games into one struct:
header signature, HEDR version, light-flag bit position, and FormID layout.
There is no feature-flag or conditional compilation involved;
the correct `GameContext` is simply passed at the call site.
NOTE: Game-specific *record schema* definitions currently exist only for Skyrim SE.
Adding them for other games is straightforward and follows the same pattern as the SSE schemas.