cdragon_prop/lib.rs
1//! Support of Riot PROP files
2//!
3//! # Overview
4//!
5//! PROP files, more commonly called *bin* files, contain nested data structures.
6//! All structures are typed and usually abide to the same type, but type is provided in the data
7//! itself, so there is no need to have a schema to decode it.
8//!
9//! Each PROP File contains a list of [entries](BinEntry) which itself contains a nested list of
10//! [fields](BinField) of various [data types](BinType).
11//!
12//! # Dynamic types
13//!
14//! Container types store data whose type is defined dynamically.
15//! Since Rust types are defined statically, they cannot be used directly.
16//! Moreover, even if the embedded bin type is known by the container at run-time, the Rust type
17//! system requires the user to explicitely request a given type, one way or the other.
18//!
19//! The [`binget!()`] macro makes it easier to chain casts and should be enough when the names and
20//! types to get are known in advance.
21//!
22//! Data can also be casted explicitely, using the `downcast()` method provided by
23//! all types wrapping dynamically typed data:
24//! ```no_run
25//! # use cdragon_prop::data::*;
26//! # fn test(field: BinField) {
27//! field.downcast::<BinString>();
28//! # }
29//! // => `Some(BinString)` if field contains a string, `None` otherwise
30//! ```
31//! Containers with fields provide a `getv()` helper to follow a `get()` with a `downcast()`.
32//!
33//! If all possible types have to be handled, the [`binvalue_map_type!()`] can be used to provide a
34//! single generic expression to handle all possible types.
35//!
36//! Map keys support only a subset or types. [`binvalue_map_keytype!()`] can be used to map only
37//! key types. It can be combined with another [`binvalue_map_type!()`] to handle both keys and
38//! values.
39//!
40//! **Note:** those macros are expanded to a `match` handling all possible cases; resulting code can be
41//! large.
42//!
43//! ## Examples
44//! ```
45//! # use cdragon_prop::{binvalue_map_keytype, binvalue_map_type, data::*};
46//! # fn test(field: BinField, map: BinMap) {
47//! binvalue_map_type!(field.vtype, T, {
48//! let value: &T = field.downcast::<T>().unwrap();
49//! });
50//!
51//! binvalue_map_keytype!(map.ktype, K,
52//! binvalue_map_type!(map.vtype, V, {
53//! let entries: &Vec<(K, V)> = map.downcast::<K, V>().unwrap();
54//! })
55//! );
56//! # }
57//! ```
58//!
59//! # Bin hashes
60//!
61//! Bin files use 32-bit FNV-1a hashes for several identifier names:
62//!
63//! - [entry paths](BinEntryPath)
64//! - [class names](BinClassName)
65//! - [field names](BinFieldName)
66//! - ["bin hash" values](BinHashValue).
67//!
68//! Hash values can be computed at compile-time with [`cdragon_hashes::binh!()`], or
69//! using other methods from [`cdragon_hashes::bin`].
70//!
71//! A [`BinHashMappers`] gather all hash-to-string conversion needed by bin data.
72
73mod macros;
74mod parser;
75mod serializer;
76mod text_tree;
77mod json;
78pub mod visitor;
79pub mod data;
80
81use std::io;
82use std::fs;
83use std::path::Path;
84use thiserror::Error;
85use cdragon_hashes::{HashKind, HashMapper, HashError};
86use cdragon_utils::parsing::ParseError;
87pub use cdragon_hashes::bin::{BinHashKind, BinHashMapper};
88
89pub use serializer::{BinSerializer, BinEntriesSerializer};
90pub use data::*;
91pub use parser::{BinEntryScanner, BinEntryScannerItem};
92pub use text_tree::TextTreeSerializer;
93pub use json::JsonSerializer;
94pub use visitor::{BinVisitor, BinTraversal};
95
96
97/// Result type for PROP file errors
98type Result<T, E = PropError> = std::result::Result<T, E>;
99
100
101/// Generic type to associate each kind of hash from bin files to a value
102#[allow(missing_docs)]
103#[derive(Debug)]
104pub struct BinHashKindMapping<T, U> {
105 pub entry_path: T,
106 pub class_name: T,
107 pub field_name: T,
108 pub hash_value: T,
109 pub path_value: U,
110}
111
112impl<T, U> BinHashKindMapping<T, U> {
113 /// Give access to a specific field from its kind
114 #[inline]
115 pub fn get(&self, kind: BinHashKind) -> &T {
116 match kind {
117 BinHashKind::EntryPath => &self.entry_path,
118 BinHashKind::ClassName => &self.class_name,
119 BinHashKind::FieldName => &self.field_name,
120 BinHashKind::HashValue => &self.hash_value,
121 }
122 }
123
124 /// Give mutable access to a specific mapper from its kind
125 #[inline]
126 pub fn get_mut(&mut self, kind: BinHashKind) -> &mut T {
127 match kind {
128 BinHashKind::EntryPath => &mut self.entry_path,
129 BinHashKind::ClassName => &mut self.class_name,
130 BinHashKind::FieldName => &mut self.field_name,
131 BinHashKind::HashValue => &mut self.hash_value,
132 }
133 }
134}
135
136impl<T: Default, U: Default> Default for BinHashKindMapping<T, U> {
137 fn default() -> Self {
138 Self {
139 entry_path: T::default(),
140 class_name: T::default(),
141 field_name: T::default(),
142 hash_value: T::default(),
143 path_value: U::default(),
144 }
145 }
146}
147
148
149/// Hash mappers for all kinds of bin hashes
150///
151/// Each individual mapper can be accessed either directly through its field, or from a
152/// `BinHashKind` value.
153pub type BinHashMappers = BinHashKindMapping<BinHashMapper, HashMapper<u64, 64>>;
154
155impl BinHashMappers {
156 /// Create mapper, load all sub-mappers from a directory path
157 pub fn from_dirpath(path: &Path) -> Result<Self, HashError> {
158 let mut this = Self::default();
159 this.load_dirpath(path)?;
160 Ok(this)
161 }
162
163 /// Load all sub-mappers from a directory path
164 pub fn load_dirpath(&mut self, path: &Path) -> Result<(), HashError> {
165 self.entry_path.load_path(path.join(HashKind::BinEntryPath.mapping_path()))?;
166 self.class_name.load_path(path.join(HashKind::BinClassName.mapping_path()))?;
167 self.field_name.load_path(path.join(HashKind::BinFieldName.mapping_path()))?;
168 self.hash_value.load_path(path.join(HashKind::BinHashValue.mapping_path()))?;
169 self.path_value.load_path(path.join(HashKind::WadGame.mapping_path()))?;
170 Ok(())
171 }
172
173 /// Write all sub-mappers to a directory path
174 pub fn write_dirpath(&self, path: &Path) -> Result<(), HashError> {
175 self.entry_path.write_path(path.join(HashKind::BinEntryPath.mapping_path()))?;
176 self.class_name.write_path(path.join(HashKind::BinClassName.mapping_path()))?;
177 self.field_name.write_path(path.join(HashKind::BinFieldName.mapping_path()))?;
178 self.hash_value.write_path(path.join(HashKind::BinHashValue.mapping_path()))?;
179 self.path_value.write_path(path.join(HashKind::WadGame.mapping_path()))?;
180 Ok(())
181 }
182}
183
184/// PROP file, with entries
185///
186/// This structure contains all the data of a PROP file, completely parsed.
187/// It also provides methods to simply scan an file, without storing all the data, and possibly
188/// skipping unneeded data.
189#[derive(Debug)]
190pub struct PropFile {
191 /// PROP version
192 pub version: u32,
193 /// `true` for patch file
194 ///
195 /// Patch files are used to hot-patch data from other, regular files.
196 /// They are usually much slower and a notably used by Riot to update the game without a new
197 /// release (patches are then provided directly by the server when the game starts).
198 pub is_patch: bool,
199 /// List of paths to other PROP files
200 pub linked_files: Vec<String>,
201 /// List of bin entries
202 pub entries: Vec<BinEntry>,
203}
204
205impl PropFile {
206 /// Parse a whole `PropFile` from data
207 pub fn from_slice(data: &[u8]) -> Result<PropFile> {
208 Ok(parser::binparse(data)?)
209 }
210
211 /// Parse a whole `PropFile` from data
212 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<PropFile> {
213 Self::from_slice(&fs::read(path.as_ref())?)
214 }
215
216 /// Iterate on entry headers (path and type) from a PROP reader
217 pub fn scan_entries_from_reader<R: io::Read>(reader: R) -> Result<BinEntryScanner<R>> {
218 let scanner = BinEntryScanner::new(reader)?;
219 Ok(scanner)
220 }
221
222 /// Iterate on entry headers (path and type) from a PROP file path
223 pub fn scan_entries_from_path<P: AsRef<Path>>(path: P) -> Result<BinEntryScanner<io::BufReader<fs::File>>> {
224 let file = fs::File::open(path)?;
225 let reader = io::BufReader::new(file);
226 let scanner = BinEntryScanner::new(reader)?;
227 Ok(scanner)
228 }
229}
230
231/// Entry header, used by parsers that iterate on entries
232pub type BinEntryHeader = (BinEntryPath, BinClassName);
233
234/// Entry in a PROP file
235#[derive(Debug)]
236pub struct BinEntry {
237 /// Entry path (hashed)
238 pub path: BinEntryPath,
239 /// Class type of the entry
240 pub ctype: BinClassName,
241 /// Struct fields
242 pub fields: Vec<BinField>,
243}
244
245impl BinEntry {
246 /// Get a field by its name
247 pub fn get(&self, name: BinFieldName) -> Option<&BinField> {
248 self.fields.iter().find(|f| f.name == name)
249 }
250
251 /// Get a field by its name and downcast it
252 pub fn getv<T: BinValue + 'static>(&self, name: BinFieldName) -> Option<&T> {
253 self.get(name).and_then(|field| field.downcast::<T>())
254 }
255}
256
257/// Files known to not be PROP files, despite their extension
258pub const NON_PROP_BASENAMES: &[&str] = &[
259 "atlas_info.bin",
260 "tftoutofgamecharacterdata.bin",
261 "tftmapcharacterlists.bin",
262 "tftactivesets.bin",
263 "tftitemlist.bin",
264];
265
266/// Return `true` if a path is a bin file path
267///
268/// This helper is intended to be used with files extracted by CDragon.
269/// It has several limitations.
270///
271/// - File content is not checked, only path is checkde
272/// - Some PROP files don't have the `.bin` extension, they will not be detected.
273/// (CDragon add the missing extension and thus does not have this problem.)
274/// - Some files have a `.bin` extension but are not actually PROP files.
275/// This helper return `false` for known occurrences.
276pub fn is_binfile_path(path: &Path) -> bool {
277 if let Some(true) = path.extension().map(|s| s == "bin") {
278 if let Some(name) = path.file_name() {
279 // Some files are not actual 'PROP' files
280 return name.to_str()
281 .map(|s| !NON_PROP_BASENAMES.contains(&s))
282 .unwrap_or(false)
283 }
284 }
285 false
286}
287
288
289/// Error in a PROP file
290#[allow(missing_docs)]
291#[derive(Error, Debug)]
292pub enum PropError {
293 #[error(transparent)]
294 Io(#[from] std::io::Error),
295 #[error("parsing error")]
296 Parsing(#[from] ParseError),
297}
298