Conversation
Bitfield implementation
Read and write skills.
Basic save file parsing (npcs/items placeholders)
Add serde serialization and reorganize tests/consts files.
Add getters/setters checking for bounds in attributes.
…tion bugfix. When all stats are written, parse one more attribute header to make sure we update byte_position with the position after the 0x1FF trailer.
There was a problem hiding this comment.
Pull request overview
Adds RotW (v105) support alongside legacy (v99) by introducing a format/layout abstraction, compatibility rules, and placeholder section models, plus updated docs/examples and new fixtures/tests.
Changes:
- Introduce
formatmodule with layout detection, decode/encode orchestration, summarize API, and compatibility enforcement/override. - Split/modernize section models (character v99/v105 codecs, attributes bitstream, skills with D2R name mapping, waypoints, NPC/items placeholders).
- Add fixtures + golden/compat/summary tests, update docs/examples, and bump crate metadata/version.
Reviewed changes
Copilot reviewed 44 out of 51 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/save_format_golden.rs | Golden + compatibility + forced-encode tests across v99/v105. |
| src/waypoints/tests.rs | New waypoint tests (currently do not compile as written). |
| src/waypoints/mod.rs | New waypoint model + parsing/encoding logic. |
| src/waypoints/consts.rs | Waypoint section constants and waypoint name tables. |
| src/waypoints.rs | Removed legacy waypoint section builder. |
| src/utils.rs | New shared byte/bit utilities (read_bits/write_bits, endian helpers). |
| src/skills/tests.rs | New skills parsing + name-mapping tests (includes Warlock mapping). |
| src/skills/named_d2r.rs | D2R skill-name tables + normalized lookup helpers. |
| src/skills/mod.rs | Skills section model + raw and named APIs. |
| src/skills.rs | (Context only) legacy file likely replaced by module. |
| src/quests.rs | Removed legacy quests placeholder. |
| src/npcs/mod.rs | New NPC section placeholder preserving fixed-size raw payload. |
| src/npcs.rs | Removed legacy NPC section builder. |
| src/items/mod.rs | New items placeholder + empty-inventory trailer generation by layout/mode. |
| src/header/mod.rs | Removed legacy header code. |
| src/header/mercenary.rs | Removed legacy mercenary variant tables (replaced by simpler model). |
| src/header/character.rs | Removed legacy character code (replaced by codecs). |
| src/format/tests.rs | New format/layout/summary/encoding tests. |
| src/format/summary.rs | New fast summary extraction (header + character fields). |
| src/format/mod.rs | New format module wiring/re-exports. |
| src/format/layout.rs | Layout metadata (offsets/ranges), format detection/fallback, checksum metadata. |
| src/format/encode.rs | Unified encode pipeline with compatibility checks and placeholder section handling. |
| src/format/decode.rs | Unified decode pipeline with strict/lax issues, section parsing, checksum metadata. |
| src/format/compatibility.rs | Compatibility issue reporting and blocking encode validation. |
| src/character/v99.rs | v99 character codec (decode/encode with raw preservation). |
| src/character/v105.rs | v105 character codec (mode marker + raw preservation). |
| src/character/tests.rs | Character codec/unit tests incl. v105 fixtures + title mapping. |
| src/character/mod.rs | Character model, status bitfield, title helpers, canonical invariants. |
| src/character/mercenary/tests.rs | Mercenary parse/write tests. |
| src/character/mercenary/mod.rs | Mercenary model + parse/write helpers. |
| src/character/common.rs | Shared character codec read/write helpers with bounds errors. |
| src/character/codec.rs | Format-dispatched character codec entrypoints. |
| src/attributes/tests.rs | Updated attributes tests targeting new bitstream utilities/types. |
| src/attributes/mod.rs | New typed attributes model + packed bitstream parse/encode (Q8 helpers). |
| src/attributes.rs | Removed legacy attributes implementation. |
| rustfmt.toml | Expand formatting heuristics configuration. |
| notes.md | Removed older notes (replaced by NOTES.md). |
| examples/inspect_issues.rs | Example for strict vs lax parsing and issue inspection. |
| examples/edit_character.rs | Example editing a save and encoding back with compatibility enforcement. |
| assets/test/barbrotw_v105.d2s | New v105 RotW fixture. |
| assets/test/barbexp_v105.d2s | New v105 expansion fixture. |
| assets/test/barbclassic_v105.d2s | New v105 classic fixture. |
| assets/test/Warlock_v105.d2s | New v105 Warlock fixture. |
| assets/test/Test.d2s | Added/updated test fixture. |
| assets/test/Joe.d2s | Added/updated v99 fixture. |
| README.md | New/updated README with API usage and compatibility notes. |
| NOTES.md | New consolidated reverse-engineering notes. |
| LICENSE | Add MIT license file. |
| Cargo.toml | Bump to 0.2.0 and add package metadata/deps (serde/log/etc.). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
src/waypoints/tests.rs
Outdated
| assert_eq!( | ||
| parsed_waypoints.hell.act1[8], | ||
| WaypointInfo { | ||
| id: Waypoint::Catacombs, | ||
| name: "Catacombs", | ||
| act: Act::Act1, | ||
| acquired: true | ||
| } |
src/waypoints/tests.rs
Outdated
| fn generate_waypoints_test() { | ||
| let expected_bytes: [u8; 81] = [ | ||
| 0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, | ||
| ]; | ||
|
|
||
| assert_eq!(expected_bytes, generate(&Waypoints::default())); | ||
| } |
src/waypoints/mod.rs
Outdated
| _ => Err(ParseHardError { | ||
| message: format!("Cannot convert ID > 8 to waypoint: {id:?}"), | ||
| }), |
src/format/decode.rs
Outdated
| } | ||
| } | ||
|
|
||
| let skills_offset = selected_layout.attributes_offset() + byte_position.current_byte + 1; |
src/format/encode.rs
Outdated
| fn empty_items_layout_for_encode(target: FormatId, mode_marker: u8) -> items::EmptyLayout { | ||
| match target { | ||
| FormatId::V99 => items::EmptyLayout::LegacyExpansion, | ||
| FormatId::V105 | FormatId::Unknown(_) => match mode_marker { | ||
| crate::character::v105::MODE_CLASSIC => items::EmptyLayout::V105Classic, | ||
| crate::character::v105::MODE_ROTW => items::EmptyLayout::V105RotW, | ||
| _ => items::EmptyLayout::V105Expansion, | ||
| }, |
| /// Set skill value by raw class-local index. | ||
| pub fn set(&mut self, skill_index: usize, value: u8) { | ||
| self.points[skill_index] = value; | ||
| } | ||
|
|
||
| /// Get skill value by raw class-local index. | ||
| pub fn get(&self, skill_index: usize) -> u8 { | ||
| self.points[skill_index] | ||
| } |
src/waypoints/tests.rs
Outdated
| let parsed_waypoints = match parse(&bytes) { | ||
| Ok(res) => res, | ||
| Err(e) => panic!("parse_waypoints_test: {0}", e), | ||
| }; |
There was a problem hiding this comment.
Pull request overview
This PR adds “RotW” (v105) support and consolidates the crate’s save parsing/encoding around a unified format/layout abstraction, with explicit compatibility checks for unsafe conversions.
Changes:
- Introduces
FormatId(v99/v105/Unknown) layout detection + encode/decode/summarize orchestration undersrc/format/. - Adds new modeled sections (waypoints, skills, NPC/items placeholders) plus shared bit/byte utilities.
- Adds fixtures, golden/compat/summary tests, and updates docs/examples; bumps crate metadata/version.
Reviewed changes
Copilot reviewed 46 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/save_format_golden.rs | Golden roundtrip + compatibility blocker tests (v99/v105/RotW) |
| src/waypoints/mod.rs | New waypoint model with parse/encode + per-act APIs and errors |
| src/waypoints/consts.rs | Waypoint section constants + waypoint name tables |
| src/waypoints/tests.rs | Waypoints parse/roundtrip/unit tests |
| src/waypoints.rs | Removes old waypoint section builder |
| src/utils.rs | Adds shared parsing/bitstream read/write utilities + tests |
| src/skills/mod.rs | New skills section model + D2R name-based helpers |
| src/skills/named_d2r.rs | Default D2R skill name tables + normalized lookup |
| src/skills/tests.rs | Skills section/unit tests (incl. Warlock names) |
| src/quests.rs | Removes old quest section builder (replaced by new quests module elsewhere) |
| src/npcs/mod.rs | NPC section placeholder model with header validation |
| src/npcs.rs | Removes old NPC section builder |
| src/items/mod.rs | Items placeholder + empty-trailer generation by layout/mode |
| src/format/mod.rs | New format orchestration module exports |
| src/format/layout.rs | Layout metadata + FormatId + fallback logic |
| src/format/decode.rs | Unified decode path with strict/lax issues |
| src/format/encode.rs | Unified encode path with compatibility enforcement + checksum |
| src/format/summary.rs | Fast summary extraction (header + character only) |
| src/format/compatibility.rs | Encode compatibility rules + blocker aggregation |
| src/format/edition_hint.rs | Heuristic edition hint detection for unknown versions |
| src/format/tests.rs | Format/layout/summary/encode behavioral tests |
| src/character/mod.rs | New character model + title rules + status bits |
| src/character/codec.rs | Per-format character codec dispatch |
| src/character/common.rs | Shared character field read/write helpers |
| src/character/v99.rs | v99 character codec implementation |
| src/character/v105.rs | v105 character codec + mode marker handling |
| src/character/tests.rs | Character codec/title tests incl. v105 fixture |
| src/character/mercenary/mod.rs | Mercenary model parse/write helpers |
| src/character/mercenary/tests.rs | Mercenary unit tests |
| src/attributes/mod.rs | Attributes model + bitstream parse/encode + typed ids |
| src/attributes/tests.rs | Attributes read/write tests + utils integration |
| examples/waypoints.rs | Example: reading/writing waypoints with explicit errors |
| examples/inspect_issues.rs | Example: strict vs lax parsing + issue reporting |
| examples/edit_character.rs | Example: editing a save and re-encoding with compatibility checks |
| assets/test/*.d2s | Adds/updates v105 fixtures + additional test saves |
| README.md | Updated docs, API usage, compatibility/edition hint explanation |
| NOTES.md | Consolidated/updated format reverse-engineering notes |
| notes.md | Removes old notes file (replaced by NOTES.md) |
| rustfmt.toml | Formatting heuristics update |
| Cargo.toml | Crate metadata updates + version bump + new deps |
| LICENSE | Adds MIT license file |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| /// Set skill value by raw class-local index. | ||
| pub fn set(&mut self, skill_index: usize, value: u8) { | ||
| self.points[skill_index] = value; | ||
| } | ||
|
|
||
| /// Get skill value by raw class-local index. | ||
| pub fn get(&self, skill_index: usize) -> u8 { | ||
| self.points[skill_index] | ||
| } |
| pub(crate) fn layout_for_encode(target: FormatId) -> &'static dyn Layout { | ||
| match target { | ||
| FormatId::V99 => &V99_LAYOUT, | ||
| FormatId::V105 | FormatId::Unknown(_) => &V105_LAYOUT, | ||
| } |
src/waypoints/consts.rs
Outdated
| pub const SECTION_HEADER: [u8; 8] = [0x57, 0x53, 0x01, 0x00, 0x00, 0x00, 0x50, 0x00]; | ||
| pub const DIFFICULTY_HEADER: [u8; 2] = [0x02, 0x01]; | ||
| pub const SECTION_TRAILER: u8 = 0x01; |
| let class_skills = d2r_skills_for_class(class)?; | ||
| class_skills | ||
| .iter() | ||
| .position(|candidate| normalize_name(candidate) == normalized_query) | ||
| .ok_or(NamedSkillError::UnknownSkillName { class, skill_name: skill_name.to_string() }) | ||
| } |
Added support for multiple versions by splitting the model into FormatId (layout)/GameEdition (family)/ExpansionType (mode), with v99/v105 expansion handling done differently on purpose.
Added blocking encode compatibility rules for known bad conversions, with an explicit opt-out (CompatibilityChecks::Ignore).
Unified the core API around one path: Save::parse / Save::summarize / save.check_compatibility / save.encode_for.
Added coverage and docs to match: new v105 fixtures, golden/compat/summary tests, updated README/examples, and bumped crate version to 0.2.0.