Skip to content
Open
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
20 changes: 19 additions & 1 deletion .specify/memory/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@
- Dependency additions MUST be justified — avoid pulling in large
dependency trees for trivial functionality.

### VI. Code Style & Formatting

- Separate distinct code contexts (imports, struct definitions, impl blocks,
helper functions, tests) with a single blank line.
- Within a function, use blank lines to separate logical steps (setup,
processing, result). Do not insert blank lines between tightly coupled
sequential statements.
- Group `use` statements by origin: std first, then external crates, then
internal crate modules, each group separated by a blank line.
- Comments MUST only be added for code with complex or non-obvious behavior.
Do not document self-explanatory code, simple accessors, or trivial logic.
- Section banners (e.g., `// ── Reading ──`) are allowed to visually separate
major logical regions within a file. Keep them consistent in style.
- Prefer short, focused functions over long ones. If a function exceeds ~50
lines, consider whether it can be decomposed.
- `rustfmt` is authoritative for all brace/indent/whitespace decisions —
do not fight the formatter.

## Error Handling & Safety

- Each crate MUST define its own error type via `thiserror` and a
Expand Down Expand Up @@ -125,4 +143,4 @@
- Runtime development guidance lives in `CLAUDE.md` at the repository
root.

**Version**: 2.0.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-25
**Version**: 2.1.0 | **Ratified**: 2026-03-07 | **Last Amended**: 2026-03-26
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ All crates live under `crates/`. The dependency graph flows upward:

**Math**: All vector/matrix types use `glam` (Vec2, Vec3, Vec4, Mat4, Quat).

**Hashing**: WAD paths are XXHash64 (64-bit) of lowercased paths. Bin object/property names are FNV-1a (32-bit) hashes via `ltk_hash::fnv1a::hash_lower()`.
**Hashing**: WAD paths are XXHash64 (64-bit) of lowercased paths. Bin object/property names are FNV-1a (32-bit) hashes via `ltk_hash::fnv1a::hash_lower()`. Inibin keys are SDBM hashes via `ltk_hash::sdbm::hash_inibin_key(section, property)`.

**I/O utilities**: `ltk_io_ext` provides shared reader/writer extension traits (`ReaderExt`, `WriterExt`) with helpers like `read_str_until_nul()`, `read_padded_string()`, `read_sized_string_u16()`, etc. Format crates MUST use these instead of implementing their own — do not duplicate I/O helpers.

## Crate Layout Convention

Expand Down Expand Up @@ -82,3 +84,9 @@ Shared dependency versions are declared in the root `Cargo.toml` under `[workspa
## Additional Context

The `docs/LTK_GUIDE.md` file contains detailed crate-by-crate API documentation with usage examples, file format references, and hash algorithm details. Consult it for format-specific questions.

## Active Technologies
- `ltk_inibin`: Rust + `thiserror`, `byteorder`, `ltk_io_ext`, `ltk_hash` (SDBM hashing), `glam` (Vec2/Vec3/Vec4), `bitflags` (ValueFlags), `indexmap` (ordered storage) (001-inibin-crate)

## Recent Changes
- 001-inibin-crate: Added `ltk_inibin` crate — inibin/troybin parser with all 14 value types, `ValueFlags` bitfield, unified `as_*()` accessors, `sdbm::hash_inibin_key()` convenience
3 changes: 3 additions & 0 deletions crates/league-toolkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ readme = "../../README.md"
default = [
"anim",
"file",
"inibin",
"mesh",
"meta",
"primitives",
Comment on lines 13 to 19
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says ltk_inibin is re-exported through league-toolkit behind the inibin feature flag, but this change also adds inibin to the default feature set. That makes the dependency enabled by default for all league-toolkit users. Either remove it from default (keeping it truly opt-in) or update the PR/docs to reflect that it’s now part of the default feature set.

Copilot uses AI. Check for mistakes.
Expand All @@ -25,6 +26,7 @@ serde = ["ltk_wad/serde", "ltk_file/serde", "ltk_meta/serde"]

anim = ["dep:ltk_anim"]
file = ["dep:ltk_file"]
inibin = ["dep:ltk_inibin"]
mesh = ["dep:ltk_mesh"]
meta = ["dep:ltk_meta"]
primitives = ["dep:ltk_primitives"]
Expand All @@ -35,6 +37,7 @@ hash = ["dep:ltk_hash"]
[dependencies]
ltk_anim = { version = "0.3.2", path = "../ltk_anim", optional = true }
ltk_file = { version = "0.2.8", path = "../ltk_file", optional = true }
ltk_inibin = { version = "0.1.0", path = "../ltk_inibin", optional = true }
ltk_mesh = { version = "0.4.1", path = "../ltk_mesh", optional = true }
ltk_meta = { version = "0.4.0", path = "../ltk_meta", optional = true }
ltk_primitives = { version = "0.3.2", path = "../ltk_primitives", optional = true }
Expand Down
3 changes: 3 additions & 0 deletions crates/league-toolkit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ pub use ltk_wad as wad;

#[cfg(feature = "hash")]
pub use ltk_hash as hash;

#[cfg(feature = "inibin")]
pub use ltk_inibin as inibin;
1 change: 1 addition & 0 deletions crates/ltk_hash/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Other utilities (hashing, etc)
pub mod elf;
pub mod fnv1a;
pub mod sdbm;
117 changes: 117 additions & 0 deletions crates/ltk_hash/src/sdbm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/// Compute SDBM hash of a lowercased string.
pub fn hash_lower(input: impl AsRef<str>) -> u32 {
let mut hash: u32 = 0;
for c in input.as_ref().chars().flat_map(|c| c.to_lowercase()) {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
for &byte in encoded.as_bytes() {
hash = (byte as u32)
.wrapping_add(hash.wrapping_shl(6))
.wrapping_add(hash.wrapping_shl(16))
.wrapping_sub(hash);
}
}
hash
}

/// Compute SDBM hash of an inibin `section*property` key pair (lowercased, `*` delimiter).
///
/// Convenience wrapper around [`hash_lower_with_delimiter`] with the standard inibin delimiter.
///
/// ```
/// # use ltk_hash::sdbm;
/// let key = sdbm::hash_inibin_key("DATA", "AttackRange");
/// assert_eq!(key, sdbm::hash_lower_with_delimiter("DATA", "AttackRange", '*'));
/// ```
pub fn hash_inibin_key(section: impl AsRef<str>, property: impl AsRef<str>) -> u32 {
hash_lower_with_delimiter(section, property, '*')
}

/// Compute SDBM hash of two strings joined by a delimiter, all lowercased.
///
/// For inibin keys, prefer [`hash_inibin_key`] which defaults the `*` delimiter.
pub fn hash_lower_with_delimiter(a: impl AsRef<str>, b: impl AsRef<str>, delimiter: char) -> u32 {
let mut hash: u32 = 0;

let chars = a
.as_ref()
.chars()
.chain(std::iter::once(delimiter))
.chain(b.as_ref().chars())
.flat_map(|c| c.to_lowercase());

for c in chars {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
for &byte in encoded.as_bytes() {
hash = (byte as u32)
.wrapping_add(hash.wrapping_shl(6))
.wrapping_add(hash.wrapping_shl(16))
.wrapping_sub(hash);
}
}
hash
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_hash_lower_basic() {
// SDBM hash of "test" (all lowercase)
let h = hash_lower("test");
// Verify case-insensitivity
assert_eq!(h, hash_lower("TEST"));
assert_eq!(h, hash_lower("TeSt"));
}
Comment on lines +60 to +67
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ltk_hash::fnv1a has a golden-value test vector, but sdbm tests currently only assert relative properties (case-insensitivity, delimiter equivalence). Adding at least one fixed expected hash value (from a known reference implementation) would better protect against accidental algorithm changes/regressions.

Copilot uses AI. Check for mistakes.

#[test]
fn test_hash_lower_empty() {
assert_eq!(hash_lower(""), 0);
}

#[test]
fn test_hash_lower_with_delimiter() {
let h1 = hash_lower_with_delimiter("DATA", "AttackRange", '*');
let h2 = hash_lower("data*attackrange");
assert_eq!(h1, h2);
}

#[test]
fn test_hash_lower_with_delimiter_case_insensitive() {
let h1 = hash_lower_with_delimiter("DATA", "AttackRange", '*');
let h2 = hash_lower_with_delimiter("data", "attackrange", '*');
assert_eq!(h1, h2);
}

#[test]
fn test_hash_inibin_key() {
let h1 = hash_inibin_key("DATA", "AttackRange");
let h2 = hash_lower_with_delimiter("DATA", "AttackRange", '*');
assert_eq!(h1, h2);
// Case insensitive
assert_eq!(h1, hash_inibin_key("data", "attackrange"));
}

#[test]
fn test_hash_lower_accepts_string() {
let s = String::from("test");
assert_eq!(hash_lower(&s), hash_lower("test"));
assert_eq!(hash_lower(s), hash_lower("test"));
}

#[test]
fn test_hash_lower_with_delimiter_accepts_string() {
let a = String::from("DATA");
let b = String::from("AttackRange");
assert_eq!(
hash_lower_with_delimiter(&a, &b, '*'),
hash_lower_with_delimiter("DATA", "AttackRange", '*')
);
assert_eq!(
hash_lower_with_delimiter(a, b, '*'),
hash_lower_with_delimiter("DATA", "AttackRange", '*')
);
}
}
19 changes: 19 additions & 0 deletions crates/ltk_inibin/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "ltk_inibin"
version = "0.1.0"
edition = "2021"
description = "Inibin/Troybin parser for League Toolkit"
license = "MIT OR Apache-2.0"
readme = "README.md"

[dependencies]
thiserror = { workspace = true }
byteorder = { workspace = true }
bitflags = { workspace = true }
glam = { workspace = true }
indexmap = { workspace = true }
ltk_io_ext = { version = "0.4.1", path = "../ltk_io_ext" }
ltk_hash = { version = "0.2.5", path = "../ltk_hash/" }

[dev-dependencies]
approx = { workspace = true }
Loading
Loading