Skip to content

Commit b56f8fd

Browse files
committed
split validation tests and fix readme
1 parent d313fc7 commit b56f8fd

3 files changed

Lines changed: 302 additions & 332 deletions

File tree

README.md

Lines changed: 80 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
# Halbu
22

3-
A Rust library for reading and modifying Diablo II: Resurrected `.d2s` save files.
3+
A Rust library for parsing, editing, and safely re-encoding Diablo II: Resurrected `.d2s` save files.
44

5-
This library serves as the backend for **[Halbu Editor](https://github.com/feored/halbu-editor)**.
5+
It serves as the backend for **[Halbu Editor](https://github.com/feored/halbu-editor)**.
66

77
---
88

99
## Features
1010

11+
1112
- Parse and modify `.d2s` save files
12-
- Supports both D2R Legacy and RotW save format versions (v99/v105)
13-
- Edit:
13+
- Supports D2R Legacy and RotW formats (`v99`, `v105`)
14+
- Editable sections:
1415
- character data
1516
- attributes
1617
- skills
1718
- quests
1819
- waypoints
19-
- mercenary information
20+
- mercenary data
21+
- Partial parsing via summary API
2022
- Strict or tolerant parsing modes
21-
- Optional validation reports to ensure the game loads the save
23+
- Validation for post-edit sanity checks
24+
- Compatibility checks for format conversion
25+
2226

2327
## Limitations
2428

25-
Some sections of the save format are not yet modeled:
29+
Some parts of the save format are not yet modeled:
2630

2731
- Items
2832
- NPC section
2933

30-
These sections are stored as raw bytes. The library preserves them when writing, but exact round-tripping is not guaranteed.
34+
These sections are preserved as raw bytes when possible, but may not round-trip identically after modifications.
3135

3236

3337
## Installation
@@ -36,10 +40,10 @@ These sections are stored as raw bytes. The library preserves them when writing,
3640
cargo add halbu
3741
```
3842

39-
## Quick start
4043

41-
More examples can be found in `examples/`.
44+
## Basic usage
4245

46+
Parse, modify, and write a save:
4347

4448
```rust
4549
use halbu::{CompatibilityChecks, Save, Strictness};
@@ -49,176 +53,143 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4953

5054
let parsed = Save::parse(&bytes, Strictness::Strict)?;
5155
let mut save = parsed.save;
52-
let target_format = save.format();
5356

5457
save.character.name = "Halbu".to_string();
5558
save.skills.set_all(20);
5659

57-
std::fs::write(
58-
"Halbu.d2s",
59-
save.encode_for(target_format, CompatibilityChecks::Enforce)?,
60-
)?;
60+
let encoded = save.encode_for(save.format(), CompatibilityChecks::Enforce)?;
61+
std::fs::write("Halbu.d2s", encoded)?;
6162

6263
Ok(())
6364
}
6465
```
6566

66-
If you want tolerant parsing with diagnostics:
67+
68+
## Parsing modes
69+
70+
Strict parsing fails on inconsistencies:
6771

6872
```rust
69-
use halbu::{Save, Strictness};
73+
let parsed = Save::parse(&bytes, Strictness::Strict)?;
74+
```
75+
76+
Lax parsing continues and reports issues:
7077

78+
```rust
7179
let parsed = Save::parse(&bytes, Strictness::Lax)?;
7280
if !parsed.issues.is_empty() {
7381
eprintln!("Parse issues: {:?}", parsed.issues);
7482
}
7583
```
7684

7785

78-
Typed attribute access:
79-
80-
```rust
81-
use halbu::attributes::AttributeId;
82-
83-
let strength = save.attributes.stat(AttributeId::Strength).value;
84-
let experience = save.attributes.stat(AttributeId::Experience).value;
85-
```
86-
87-
## Fast summary
86+
## Validation
8887

89-
Use the summary API for file lists and quick metadata reads without a full parse:
88+
Validation is an optional step to check the save for inconsistencies that may prevent the game from loading the save (e.g. invalid character name or mercenary level).
9089

9190
```rust
92-
use halbu::{Save, Strictness};
93-
94-
fn main() -> Result<(), Box<dyn std::error::Error>> {
95-
let bytes = std::fs::read("Hero.d2s")?;
96-
let summary = Save::summarize(&bytes, Strictness::Lax)?;
97-
98-
println!(
99-
"name={:?} class={:?} level={:?} expansion={:?}",
100-
summary.name, summary.class, summary.level, summary.expansion_type
101-
);
102-
103-
Ok(())
91+
let report = save.validate();
92+
if !report.is_valid() {
93+
eprintln!("Validation issues: {:?}", report.issues);
10494
}
10595
```
10696

10797

108-
## Compatibility and forced encode
98+
## Compatibility and encoding
10999

110-
Use compatibility checks before conversion.
111-
They only apply when you are encoding to a target format.
112-
`encode_for(..., CompatibilityChecks::Enforce)` blocks on incompatible conversions.
113-
`encode_for(..., CompatibilityChecks::Ignore)` bypasses those checks and should only be used intentionally.
100+
Compatibility checks apply when converting between formats during encoding.
114101

115102
```rust
116103
use halbu::{CompatibilityChecks, Save, Strictness};
117104
use halbu::format::FormatId;
118105

119-
fn main() -> Result<(), Box<dyn std::error::Error>> {
120-
let bytes = std::fs::read("Hero.d2s")?;
121-
let parsed = Save::parse(&bytes, Strictness::Strict)?;
122-
let save = parsed.save;
106+
let parsed = Save::parse(&bytes, Strictness::Strict)?;
107+
let save = parsed.save;
123108

124-
let target = FormatId::V99;
125-
let issues = save.check_compatibility(target);
126-
if !issues.is_empty() {
127-
eprintln!("Compatibility issues: {issues:?}");
128-
}
109+
let target = FormatId::V99;
110+
let issues = save.check_compatibility(target);
129111

130-
let forced = save.encode_for(target, CompatibilityChecks::Ignore)?;
131-
std::fs::write("Hero.d2s", forced)?;
132-
133-
Ok(())
112+
if !issues.is_empty() {
113+
eprintln!("Compatibility issues: {issues:?}");
134114
}
135-
```
136115

137-
## Validation
116+
// Safe (blocks on incompatibility)
117+
let encoded = save.encode_for(target, CompatibilityChecks::Enforce)?;
138118

139-
Use validation when you want a quick sanity check after editing a save.
119+
// Unsafe (bypasses checks)
120+
let forced = save.encode_for(target, CompatibilityChecks::Ignore)?;
121+
```
140122

141-
`save.validate()` reports issues in the current save model.
142-
Validation is separate from encoding.
143123

144-
```rust
145-
use halbu::Save;
146-
147-
let report = save.validate();
148-
if !report.is_valid() {
149-
eprintln!("Validation issues: {:?}", report.issues);
150-
}
151-
```
124+
## Summary API
152125

153-
If you prefer the free function:
126+
Read metadata without fully parsing the file:
154127

155128
```rust
156-
use halbu::validate_save;
157-
158-
let report = validate_save(&save);
129+
let summary = Save::summarize(&bytes, Strictness::Lax)?;
130+
131+
println!(
132+
"name={:?} class={:?} level={:?} expansion={:?}",
133+
summary.name,
134+
summary.class,
135+
summary.level,
136+
summary.expansion_type
137+
);
159138
```
160139

161-
## Edition guess
140+
## Edition detection
162141

163-
If a file has an unknown/unsupported version, you can still ask Halbu for a best-effort
164-
game edition hint (`D2RLegacy` vs `RotW`).
165-
The hint compares v99/v105 layout coherence (character decode + attributes/skills/items headers)
166-
and uses reserved markers as a tie-breaker.
142+
For unknown versions, Halbu can try to guess which edition the save layout matches most closely:
167143

168144
```rust
169145
use halbu::format::detect_edition_hint;
170146
use halbu::GameEdition;
171147

172148
let hint = detect_edition_hint(&bytes);
149+
173150
if hint == Some(GameEdition::RotW) {
174-
// likely RotW edition (v105-family layout)
151+
// likely RotW (v105-style layout)
175152
}
176153
```
177154

178155

179-
## Documentation
180-
181-
API documentation is available on docs.rs:
156+
## Model overview
182157

183-
https://docs.rs/halbu
158+
Halbu distinguishes between three related concepts:
184159

185-
Project history and release notes are in [CHANGELOG.md](CHANGELOG.md).
186-
187-
## Notes
160+
- `FormatId` — concrete file format (`V99`, `V105`, or unknown)
161+
- `GameEdition` — edition family (`D2RLegacy`, `RotW`)
162+
- `ExpansionType` — in-game expansion mode (`Classic`, `Expansion`, `RotW`)
188163

164+
Typical usage:
189165

190-
Halbu models three related concepts:
166+
- `save.format()` → file format
167+
- `save.game_edition()` → edition family
168+
- `save.expansion_type()` → in-game expansion
191169

192-
- `FormatId`: concrete file format version/layout (`V99`, `V105`, or `Unknown(version)`)
193-
- `GameEdition`: edition family (`D2RLegacy` or `RotW`), derived from known `FormatId` values
194-
- `ExpansionType`: `Classic`, `Expansion`, or `RotW` (canonical on `Save` as `save.expansion_type()`)
195170

196-
Known layout versions mapped by this crate:
171+
## Compatibility rules (examples)
197172

198-
- `v99` -> `D2RLegacy`
199-
- `v105` -> `RotW`
173+
- Warlock requires RotW edition and expansion
174+
- RotW expansion cannot be encoded to non-RotW formats
175+
- Druid/Assassin cannot be encoded as Classic
176+
- Unknown class IDs cannot be safely converted
200177

201-
Use `save.expansion_type()` / `save.set_expansion_type(...)` to read/write expansion mode.
202-
Use `save.game_edition()` to inspect the edition family.
203-
Use `save.character.status()` to inspect status bits, and `save.character.set_hardcore(...)` / `set_ladder(...)` / `set_died(...)` for status mutations.
204-
Use `save.validate()` when you want to check the current save state without writing it.
205178

206-
Current blocking rules (compatibility checks) include:
207-
- Warlock requires RotW edition and RotW expansion type.
208-
- RotW expansion type cannot be encoded to non-RotW editions.
209-
- Druid/Assassin cannot be encoded as Classic.
210-
- Unknown class ids cannot be safely converted to known target formats.
179+
## Notes
211180

212-
Level is stored in both the character section and the attributes section. Use `save.set_level(...)` to keep them in sync.
181+
- Level is stored in multiple sections; use `save.set_level(...)` to keep it consistent
182+
- Additional reverse-engineering notes are available in `NOTES.md`
213183

214-
Additional notes about the format, quest flags, and general reverse-engineering work can be found in `NOTES.md`.
215184

216-
This repository also contains several example .d2s files used in tests to verify that parsing and round-trip encoding work correctly.
185+
## Documentation
217186

187+
API docs: https://docs.rs/halbu
188+
Changelog: [CHANGELOG.md](CHANGELOG.md)
218189

219190
## References
220191

221-
These resources have helped me understand the .d2s format. Many thanks to their authors.
192+
These resources helped me understand the .d2s format. Many thanks to their authors.
222193

223194
* http://user.xmission.com/~trevin/DiabloIIv1.09_File_Format.shtml
224195
* https://github.com/dschu012/D2SLib
@@ -228,4 +199,4 @@ These resources have helped me understand the .d2s format. Many thanks to their
228199
* https://github.com/krisives/d2s-format
229200
* https://github.com/nokka/d2s/
230201
* https://github.com/ThePhrozenKeep/D2MOO
231-
* https://d2mods.info/forum/kb/index?c=4
202+
* https://d2mods.info/forum/kb/index?c=4

0 commit comments

Comments
 (0)