Part of #41: the comments of `benilla-formats` (the library, its extract tools, examples and tests) are rewritten to the rule in `docs/METHOD.md`, which is what the code does and the reference fact behind it, in a line. 19,654 comment lines become 7,264. Fixes #105: the WMO collision comments now name walking's box-query leaf `0x6bca50` and the camera and line-of-sight segment-query leaf `0x6bc700`, in `models/collision.rs`, `examples/wmo_bsp.rs` and `tests/chain.rs`. - **The mechanical strip.** The citations that were only a private record number go first, and each edit is shown to be a pure removal. - **Wrong facts corrected.** The rewrite corrects the comments that stated wrong facts. Among them: - A placed doodad's material alpha reads file slot 0 on its spawn clock, while the reference reads bone 0's armed sequence, re-rolled every play window. The old text said doodads are armed once at load. - The fog end is stored ×36 and scaled once at load (`0x6d6090`). It is not raw. - The MCSH bit picks a map doodad's sun scale, 1.0 lit or 0.5 shadowed. 2.5 belongs to an entity's light node. - `SpellRec` is the row as stored, column × 4, not a translated copy. - A global sequence's clock is anchored at the instance's attach time (`0x714352`). - Particle twinkle is a gated size multiplier (`0x7b2adc`). - The blob shadow sizes from the Stand box. - Data claims are checked against the install: the weapon-trail kits' parameters and alphas, the beam rows' noise, and the maps' `field_16`. - Dozens more turned up on the way: renamed or deleted items, docs sitting on the wrong item, stale counts and links. - **Deviation labels.** "Deviation:" now marks only a choice with a reason. An unbuilt behaviour says so plainly. - **Verification.** Every file strips to identical code, and `cargo fmt --check` is clean. Four second readers compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference, the code and the shipped data. String literals are untouched; they are #92. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
159 lines
5.9 KiB
Rust
159 lines
5.9 KiB
Rust
//! `DurabilityCosts.dbc` and `DurabilityQuality.dbc`, behind the repair price the reference
|
||
//! computes itself (`0x5da330`, wrapped by `0x4faf30`): points lost × the quality row
|
||
//! `2·Quality + 1` × the item level's weapon or armor column by subclass, rounded half away from
|
||
//! zero to at least 1, then × `(1 - discount)` rounded half to even. vmangos keys the quality row
|
||
//! `(Quality + 1)·2` (`Player.cpp:4996`), so its charge may differ from the price shown.
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use anyhow::{Context, Result};
|
||
use benilla_dbc::{FieldType, Schema, SchemaField};
|
||
|
||
use crate::chain::Chain;
|
||
use crate::dbc::{f32_at, parse, u32_at};
|
||
|
||
const DURABILITY_COSTS: &str = "DBFilesClient\\DurabilityCosts.dbc";
|
||
const DURABILITY_QUALITIES: &str = "DBFilesClient\\DurabilityQuality.dbc";
|
||
|
||
/// The two repair tables, id-keyed.
|
||
pub struct DurabilityTables {
|
||
/// Item level → the 29 per-point cost columns (21 weapon, then 8 armor).
|
||
costs: HashMap<u32, Vec<u32>>,
|
||
/// Row id → the quality multiplier (the client keys this with `2·Quality + 1`).
|
||
qualities: HashMap<u32, f32>,
|
||
}
|
||
|
||
impl DurabilityTables {
|
||
/// The per-point cost for a class and subclass at `item_level`, `None` when unrepairable.
|
||
fn column(&self, item_level: u32, class: u32, subclass: u32) -> Option<u32> {
|
||
let row = self.costs.get(&item_level)?;
|
||
let idx = match class {
|
||
2 if subclass < 21 => subclass as usize,
|
||
4 if subclass < 8 => 21 + subclass as usize,
|
||
_ => return None,
|
||
};
|
||
row.get(idx).copied()
|
||
}
|
||
|
||
/// The displayed repair cost of one item in copper, after the vendor's reputation `discount`.
|
||
pub fn repair_cost(
|
||
&self,
|
||
points_lost: u32,
|
||
item_level: u32,
|
||
quality: u32,
|
||
class: u32,
|
||
subclass: u32,
|
||
discount: f32,
|
||
) -> u32 {
|
||
if points_lost == 0 {
|
||
return 0;
|
||
}
|
||
let Some(col) = self.column(item_level, class, subclass) else {
|
||
return 0;
|
||
};
|
||
let Some(&mult) = self.qualities.get(&(2 * quality + 1)) else {
|
||
return 0;
|
||
};
|
||
let base = f64::from(points_lost) * f64::from(mult) * f64::from(col);
|
||
// Half away from zero, as the reference's ±0.5 then `_ftol`, then at least 1.
|
||
let per = ((base + 0.5).floor() as i64).max(1);
|
||
// The discount multiply rounds half to even.
|
||
let discounted = per as f64 * f64::from(1.0 - discount);
|
||
let f = discounted.floor();
|
||
let frac = discounted - f;
|
||
let up = frac > 0.5 || (frac == 0.5 && (f as i64) % 2 != 0);
|
||
let rounded = if up { f + 1.0 } else { f };
|
||
rounded.max(0.0) as u32
|
||
}
|
||
}
|
||
|
||
fn costs_schema() -> Schema {
|
||
let mut s = Schema::new("DurabilityCosts");
|
||
s.add_field(SchemaField::new("id", FieldType::UInt32));
|
||
s.add_field(SchemaField::new_array("weapon", FieldType::UInt32, 21));
|
||
s.add_field(SchemaField::new_array("armor", FieldType::UInt32, 8));
|
||
s.set_key_field("id");
|
||
s
|
||
}
|
||
|
||
fn qualities_schema() -> Schema {
|
||
let mut s = Schema::new("DurabilityQuality");
|
||
s.add_field(SchemaField::new("id", FieldType::UInt32));
|
||
s.add_field(SchemaField::new("mult", FieldType::Float32));
|
||
s.set_key_field("id");
|
||
s
|
||
}
|
||
|
||
/// Load both durability tables off the patch chain.
|
||
pub fn load_durability_tables(chain: &mut Chain) -> Result<DurabilityTables> {
|
||
let bytes = chain
|
||
.read_file(DURABILITY_COSTS)
|
||
.with_context(|| format!("reading {DURABILITY_COSTS}"))?;
|
||
let rs = parse(&bytes, costs_schema(), "DurabilityCosts")?;
|
||
let mut costs = HashMap::new();
|
||
for r in rs.records() {
|
||
let Some(id) = u32_at(r, 0) else { continue };
|
||
let row: Vec<u32> = (1..30).map(|i| u32_at(r, i).unwrap_or(0)).collect();
|
||
costs.insert(id, row);
|
||
}
|
||
|
||
let bytes = chain
|
||
.read_file(DURABILITY_QUALITIES)
|
||
.with_context(|| format!("reading {DURABILITY_QUALITIES}"))?;
|
||
let rs = parse(&bytes, qualities_schema(), "DurabilityQuality")?;
|
||
let mut qualities = HashMap::new();
|
||
for r in rs.records() {
|
||
let (Some(id), Some(mult)) = (u32_at(r, 0), f32_at(r, 1)) else {
|
||
continue;
|
||
};
|
||
qualities.insert(id, mult);
|
||
}
|
||
|
||
Ok(DurabilityTables { costs, qualities })
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn tables() -> DurabilityTables {
|
||
let mut costs = HashMap::new();
|
||
// Item level 23: weapon subclass 7 (sword) costs 4/point, armor subclass 3 (mail) 2/point.
|
||
let mut row = vec![0u32; 29];
|
||
row[7] = 4;
|
||
row[21 + 3] = 2;
|
||
costs.insert(23, row);
|
||
let mut qualities = HashMap::new();
|
||
qualities.insert(3, 0.6f32); // the client's row for quality 1 (2·1+1)
|
||
DurabilityTables { costs, qualities }
|
||
}
|
||
|
||
#[test]
|
||
fn per_item_cost_is_the_clients_arithmetic() {
|
||
let t = tables();
|
||
assert_eq!(t.repair_cost(10, 23, 1, 2, 7, 0.0), 24);
|
||
// Mail gloves, 7 points: 7 · 0.6 · 2 = 8.4, rounded to 8.
|
||
assert_eq!(t.repair_cost(7, 23, 1, 4, 3, 0.0), 8);
|
||
// A base under 1 still charges 1: 1 · 0.1 · 2 = 0.2.
|
||
let mut t2 = tables();
|
||
t2.qualities.insert(3, 0.1);
|
||
assert_eq!(t2.repair_cost(1, 23, 1, 4, 3, 0.0), 1, "min 1 copper");
|
||
assert_eq!(t.repair_cost(0, 23, 1, 2, 7, 0.0), 0);
|
||
assert_eq!(t.repair_cost(10, 23, 1, 15, 0, 0.0), 0, "not weapon/armor");
|
||
assert_eq!(t.repair_cost(10, 99, 1, 2, 7, 0.0), 0, "no level row");
|
||
}
|
||
|
||
#[test]
|
||
fn discount_rounds_to_nearest_even() {
|
||
let mut t = tables();
|
||
t.qualities.insert(3, 1.0);
|
||
let mut row = vec![0u32; 29];
|
||
row[7] = 1; // 1 copper/point so points == the pre-discount price
|
||
t.costs.insert(23, row);
|
||
// Half off: 20.5 rounds to 20 and 21.5 to 22; 21.0 is no tie.
|
||
assert_eq!(t.repair_cost(41, 23, 1, 2, 7, 0.5), 20);
|
||
assert_eq!(t.repair_cost(43, 23, 1, 2, 7, 0.5), 22);
|
||
assert_eq!(t.repair_cost(42, 23, 1, 2, 7, 0.5), 21);
|
||
}
|
||
}
|