- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module - the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder - the glue screens get a capture shutter, and the engine stops depending on the protocol crate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
121 lines
4.5 KiB
Rust
121 lines
4.5 KiB
Rust
//! `TaxiPath.dbc` — the direct-hop fare table between two flight-master nodes: `(from, to, cost)`
|
|
//! triples the flight-master UI walks to price a route (decision 0484 phase 1). Distinct from
|
|
//! `TaxiPathNode.dbc` (`crate::taxi`), which carries a path's actual waypoints — this table is
|
|
//! the coarse "does a direct hop from A to B exist, and what does it cost" lookup phase 2's route
|
|
//! computation needs; a multi-hop trip chains several of these rows.
|
|
//!
|
|
//! **4 fields (verified this session, matching vmangos's own `TaxiPathEntry`,
|
|
//! `DBCStructure.h:688-694`):** `ID(0), FromTaxiNode(1), ToTaxiNode(2), Price(3)`.
|
|
//!
|
|
//! Verified against the live table: id 6 is the direct hop `TaxiNodes` 2 ("Stormwind, Elwynn")
|
|
//! → 4 ("Sentinel Hill, Westfall"), cost 110 copper.
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use anyhow::{Context, Result};
|
|
use benilla_dbc::{FieldType, Schema, SchemaField};
|
|
|
|
use crate::dbc::{parse, u32_at};
|
|
use crate::Chain;
|
|
|
|
const TAXI_PATH: &str = "DBFilesClient\\TaxiPath.dbc";
|
|
|
|
/// One `TaxiPath.dbc` row — a direct hop between two flight-master nodes.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct TaxiPath {
|
|
pub id: u32,
|
|
pub from: u32,
|
|
pub to: u32,
|
|
/// Fare in copper for this one hop, undiscounted — vmangos applies the reputation discount
|
|
/// server-side before charging; the wire's `TaxiPath.dbc` copy never carries a discounted
|
|
/// figure.
|
|
pub cost: u32,
|
|
}
|
|
|
|
/// `TaxiPath.dbc` rows keyed by `ID`, plus a `(from, to)` lookup for the route computation
|
|
/// phase 2 needs.
|
|
pub struct TaxiPaths {
|
|
rows: HashMap<u32, TaxiPath>,
|
|
by_pair: HashMap<(u32, u32), u32>,
|
|
}
|
|
|
|
impl TaxiPaths {
|
|
pub fn get(&self, id: u32) -> Option<&TaxiPath> {
|
|
self.rows.get(&id)
|
|
}
|
|
|
|
/// The direct hop from `from` to `to`, if `TaxiPath.dbc` carries one. The table is
|
|
/// directed — a two-way route is two separate rows, one per direction — so this only ever
|
|
/// matches the exact order asked for.
|
|
pub fn between(&self, from: u32, to: u32) -> Option<&TaxiPath> {
|
|
self.by_pair
|
|
.get(&(from, to))
|
|
.and_then(|id| self.rows.get(id))
|
|
}
|
|
|
|
/// Every direct hop leaving `node`, in no particular order — a route search expands a node
|
|
/// through this (phase 2).
|
|
pub fn paths_from(&self, node: u32) -> impl Iterator<Item = &TaxiPath> {
|
|
self.rows.values().filter(move |p| p.from == node)
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.rows.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.rows.is_empty()
|
|
}
|
|
}
|
|
|
|
/// 4 fields per the module doc.
|
|
fn schema() -> Schema {
|
|
let mut s = Schema::new("TaxiPath");
|
|
for name in ["ID", "FromTaxiNode", "ToTaxiNode", "Cost"] {
|
|
s.add_field(SchemaField::new(name, FieldType::UInt32));
|
|
}
|
|
s
|
|
}
|
|
|
|
/// Read `TaxiPath.dbc` off the patch chain into a [`TaxiPaths`] catalog.
|
|
pub fn load_taxi_paths(chain: &mut Chain) -> Result<TaxiPaths> {
|
|
let bytes = chain.read_file(TAXI_PATH).context("reading TaxiPath.dbc")?;
|
|
let rs = parse(&bytes, schema(), "TaxiPath")?;
|
|
let mut rows = HashMap::with_capacity(rs.records().len());
|
|
let mut by_pair = HashMap::with_capacity(rs.records().len());
|
|
for r in rs.records() {
|
|
let Some(id) = u32_at(r, 0) else { continue };
|
|
let Some(from) = u32_at(r, 1) else { continue };
|
|
let Some(to) = u32_at(r, 2) else { continue };
|
|
let cost = u32_at(r, 3).unwrap_or(0);
|
|
rows.insert(id, TaxiPath { id, from, to, cost });
|
|
by_pair.insert((from, to), id);
|
|
}
|
|
Ok(TaxiPaths { rows, by_pair })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// The real 5875 table proves the layout and the pinned Stormwind→Sentinel Hill hop: 287
|
|
/// rows total, id 6 is the direct `2 → 4` hop at cost 110. Cross-checked with
|
|
/// [`crate::taxi_nodes::tests::real_taxi_nodes_layout_sanity`]'s node names. Skips without
|
|
/// client data.
|
|
#[test]
|
|
fn real_taxi_path_layout_sanity() {
|
|
let data = crate::wow_data_or_skip!();
|
|
let mut chain = crate::open_chain(&data).expect("open chain");
|
|
let cat = load_taxi_paths(&mut chain).expect("load TaxiPath");
|
|
assert_eq!(cat.len(), 287, "1.12 ships 287 taxi path hops");
|
|
|
|
let hop = cat.between(2, 4).expect("a direct 2 -> 4 hop exists");
|
|
assert_eq!(hop.id, 6);
|
|
assert_eq!(hop.cost, 110);
|
|
assert!(hop.cost > 0, "a real hop always carries a nonzero fare");
|
|
|
|
// paths_from surfaces the same row when walking node 2's outgoing hops.
|
|
assert!(cat.paths_from(2).any(|p| p.to == 4 && p.cost == 110));
|
|
}
|
|
}
|