mirror of
https://github.com/Modding-Forge/bethkit.git
synced 2026-05-25 21:50:03 -07:00
124 lines
4.8 KiB
Text
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.
|