Skip to main content

warpack_formats/
metadata.rs

1use std::collections::BTreeMap;
2use std::collections::HashMap;
3use std::fs;
4use std::path::Path;
5
6use serde::{Deserialize, Serialize};
7use slotmap::{DenseSlotMap, new_key_type};
8
9use crate::object::Object;
10use crate::ObjectId;
11use crate::ObjectKind;
12use crate::parser::slk;
13use crate::parser::slk::read_row_num;
14use crate::parser::slk::read_row_str;
15use crate::ValueType;
16
17new_key_type! {
18    struct FieldKey;
19}
20
21struct BasicInfo {
22    field_id:   ObjectId,
23    field_name: String,
24    value_ty:   String,
25    index:      i8,
26    is_profile: bool,
27}
28
29fn read_basic_info<'src>(row: &slk::Row<'src>, legend: &slk::Legend<'src>) -> BasicInfo {
30    let field_id = read_row_str(&row, legend, "ID").unwrap();
31    let field_name: String = read_row_str(&row, legend, "field").unwrap().into();
32    let value_ty: String = read_row_str(&row, legend, "type").unwrap().into();
33    let index: i8 = read_row_num(&row, legend, "index").unwrap_or(-1);
34    let slk = read_row_str(&row, legend, "slk").unwrap();
35
36    let field_id = ObjectId::from_bytes(field_id.as_bytes()).unwrap();
37
38    BasicInfo {
39        field_id,
40        field_name,
41        value_ty,
42        index,
43        is_profile: slk == "Profile",
44    }
45}
46
47#[derive(Debug, Serialize, Deserialize)]
48pub struct FieldDesc {
49    pub id:           ObjectId,
50    pub index:        i8,
51    pub variant:      FieldVariant,
52    pub value_ty:     ValueType,
53    pub value_ty_raw: String,
54    pub exclusive:    Option<Vec<ObjectId>>,
55    pub kind:         ObjectKind,
56    pub is_profile:   bool,
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60pub enum FieldVariant {
61    Normal { name: String },
62    Leveled { name: String },
63    Data { id: u8 },
64}
65
66impl FieldVariant {
67    pub fn name(&self) -> &str {
68        match self {
69            FieldVariant::Normal { name } => name,
70            FieldVariant::Leveled { name } => name,
71            FieldVariant::Data { .. } => "data",
72        }
73    }
74
75    pub fn is_normal(&self) -> bool {
76        match self {
77            FieldVariant::Normal { .. } => true,
78            _ => false,
79        }
80    }
81
82    pub fn is_leveled(&self) -> bool {
83        match self {
84            FieldVariant::Leveled { .. } => true,
85            FieldVariant::Data { .. } => true,
86            _ => false,
87        }
88    }
89
90    pub fn is_data(&self) -> bool {
91        match self {
92            FieldVariant::Data { .. } => true,
93            _ => false,
94        }
95    }
96
97    pub fn data_id(&self) -> Option<u8> {
98        match self {
99            FieldVariant::Data { id } => Some(*id),
100            _ => None,
101        }
102    }
103}
104
105fn split_by_digits(input: &str) -> Option<(&str, &str)> {
106    input
107        .find(|c: char| c.is_digit(10))
108        .map(|i| (&input[0..i], &input[i..input.len()]))
109}
110
111fn data_char_to_id(input: u8) -> u8 {
112    match input {
113        b'a' | b'A' => 1,
114        b'b' | b'B' => 2,
115        b'c' | b'C' => 3,
116        b'd' | b'D' => 4,
117        b'e' | b'E' => 5,
118        b'f' | b'F' => 6,
119        b'g' | b'G' => 7,
120        b'h' | b'H' => 8,
121        b'i' | b'I' => 9,
122        b'j' | b'J' => 10,
123        _ => panic!("unknown data field id"),
124    }
125}
126
127#[derive(Default, Debug, Serialize, Deserialize)]
128pub struct MetadataStore {
129    // primary store for the fields
130    // other collections in this struct hold references to FieldKeys returned by this
131    fields:            DenseSlotMap<FieldKey, FieldDesc>,
132    // for fields that are only present on certain objects (namely, ability Data fields),
133    // this holds the association between objects and fields that are available only on them
134    objects_with_data: HashMap<ObjectId, Vec<ObjectId>>,
135    // mapping between field ids and field keys
136    ids_to_keys:       HashMap<ObjectId, FieldKey>,
137    // mapping between field names and field keys
138    // multiple fields may be mapped to the same name,
139    // namely if they belong to different objects or have different indices
140    // so additional filtering must be performed
141    names_to_keys:     BTreeMap<String, Vec<FieldKey>>,
142}
143
144impl MetadataStore {
145    pub fn fields(&self) -> impl Iterator<Item = &FieldDesc> {
146        self.fields.values()
147    }
148
149    fn add_field(&mut self, field: FieldDesc) {
150        let id = field.id;
151        let name = field.variant.name().to_ascii_lowercase();
152        let key = self.fields.insert(field);
153        self.ids_to_keys.insert(id, key);
154        self.names_to_keys.entry(name).or_default().push(key);
155    }
156
157    fn insert_basic_field<'src>(
158        &mut self,
159        row: slk::Row<'src>,
160        legend: &slk::Legend<'src>,
161        kind: ObjectKind,
162    ) {
163        let basic_info = read_basic_info(&row, &legend);
164
165        let repeat = read_row_num::<u8>(&row, legend, "repeat");
166        let mut leveled = false;
167        if let Some(repeat) = repeat {
168            leveled = repeat != 0;
169        }
170
171        let variant = if leveled {
172            FieldVariant::Leveled {
173                name: basic_info.field_name.clone(),
174            }
175        } else {
176            FieldVariant::Normal {
177                name: basic_info.field_name.clone(),
178            }
179        };
180
181        self.add_field(FieldDesc {
182            id: basic_info.field_id,
183            index: basic_info.index,
184            value_ty: ValueType::new(&basic_info.value_ty),
185            value_ty_raw: basic_info.value_ty,
186            exclusive: None,
187            variant,
188            kind,
189            is_profile: basic_info.is_profile,
190        });
191    }
192
193    fn insert_unit_row<'src>(&mut self, row: slk::Row<'src>, legend: &slk::Legend<'src>) {
194        let basic_info = read_basic_info(&row, &legend);
195
196        let use_unit: u8 = read_row_num(&row, legend, "useUnit").unwrap_or(0);
197        let use_bld: u8 = read_row_num(&row, legend, "useBuilding").unwrap_or(0);
198        let use_hero: u8 = read_row_num(&row, legend, "useHero").unwrap_or(0);
199        let use_item: u8 = read_row_num(&row, legend, "useItem").unwrap_or(0);
200
201        let mut kind = ObjectKind::empty();
202        if use_item != 0 {
203            kind |= ObjectKind::ITEM
204        }
205
206        if (use_unit != 0) || (use_bld != 0) || (use_hero != 0) {
207            kind |= ObjectKind::UNIT
208        }
209
210        let variant = FieldVariant::Normal {
211            name: basic_info.field_name.clone(),
212        };
213
214        self.add_field(FieldDesc {
215            id: basic_info.field_id,
216            index: basic_info.index,
217            value_ty: ValueType::new(&basic_info.value_ty),
218            value_ty_raw: basic_info.value_ty,
219            exclusive: None,
220            variant,
221            kind,
222            is_profile: basic_info.is_profile,
223        });
224    }
225
226    fn insert_ability_row<'src>(&mut self, row: slk::Row<'src>, legend: &slk::Legend<'src>) {
227        let basic_info = read_basic_info(&row, &legend);
228
229        let repeat = read_row_num::<u8>(&row, legend, "repeat");
230        let data_id = read_row_num::<u8>(&row, legend, "data");
231        let exclusive = read_row_str(&row, legend, "useSpecific");
232
233        let mut leveled = false;
234        if let Some(repeat) = repeat {
235            leveled = repeat != 0;
236        }
237
238        let variant = if basic_info.field_name == "Data" {
239            if data_id.is_none() {
240                return;
241            }
242
243            let data_id = data_id.unwrap();
244
245            FieldVariant::Data { id: data_id }
246        } else if leveled {
247            FieldVariant::Leveled {
248                name: basic_info.field_name.clone(),
249            }
250        } else {
251            FieldVariant::Normal {
252                name: basic_info.field_name.clone(),
253            }
254        };
255
256        let exclusive = exclusive.map(|e| {
257            let list = e
258                .split(',')
259                .filter_map(|s| ObjectId::from_bytes(s.as_bytes()))
260                .collect::<Vec<_>>();
261
262            for object_id in &list {
263                self.objects_with_data
264                    .entry(*object_id)
265                    .or_default()
266                    .push(basic_info.field_id);
267            }
268
269            list
270        });
271
272        self.add_field(FieldDesc {
273            id: basic_info.field_id,
274            value_ty: ValueType::new(&basic_info.value_ty),
275            value_ty_raw: basic_info.value_ty,
276            variant,
277            exclusive,
278            index: basic_info.index,
279            kind: ObjectKind::ABILITY,
280            is_profile: basic_info.is_profile,
281        });
282    }
283
284    fn find_data_field(&self, object_id: ObjectId, data_id: u8) -> Option<&FieldDesc> {
285        self.objects_with_data.get(&object_id).and_then(|v| {
286            self.find_field(
287                v.iter().filter_map(|id| self.ids_to_keys.get(id)).copied(),
288                |f| match f.variant {
289                    FieldVariant::Data { id } => id == data_id,
290                    _ => false,
291                },
292            )
293        })
294    }
295
296    fn find_named_field<F>(&self, name: &str, closure: F) -> Option<&FieldDesc>
297    where
298        F: FnMut(&FieldDesc) -> bool,
299    {
300        self.names_to_keys
301            .get(&name.to_ascii_lowercase())
302            .and_then(|v| self.find_field(v.iter().copied(), closure))
303    }
304
305    fn find_field<I, F>(&self, iter: I, mut closure: F) -> Option<&FieldDesc>
306    where
307        I: Iterator<Item = FieldKey>,
308        F: FnMut(&FieldDesc) -> bool,
309    {
310        iter.filter_map(|k| self.fields.get(k)).find(|f| closure(f))
311    }
312
313    /// Queries an SLK field by it's name and target object.
314    /// The object is necessary because the same field name can
315    /// resolve to different fields depending on the object.
316    pub fn query_slk_field(
317        &self,
318        field_name: &str,
319        object: &Object,
320    ) -> Option<(&FieldDesc, Option<u32>)> {
321        let object_kind = object.kind();
322        let object_id = object.id();
323        self.find_named_field(&field_name, |f| {
324            !f.is_profile && f.kind.contains(object_kind)
325        })
326        .map(|f| (f, None))
327        .or_else(|| {
328            split_by_digits(&field_name).and_then(|(name, raw_level)| {
329                let level: u32 = raw_level.parse().unwrap();
330
331                let field = if name.starts_with("Data") {
332                    let data_id = data_char_to_id(name.as_bytes()[4]);
333                    self.find_data_field(object_id, data_id)
334                } else {
335                    self.find_named_field(name, |f| !f.is_profile && f.kind.contains(object_kind))
336                };
337
338                field.map(|f| (f, Some(level)))
339            })
340        })
341    }
342
343    /// Queries a Profile field by it's name and target object.
344    /// The object is necessary because the same field name can
345    /// resolve to different fields depending on the object.
346    pub fn query_profile_field(
347        &self,
348        field_name: &str,
349        object: &Object,
350        index: i8,
351    ) -> Option<(&FieldDesc, Option<u32>)> {
352        let object_kind = object.kind();
353
354        self.find_named_field(&field_name, |f| {
355            f.is_profile
356                && f.kind.contains(object_kind)
357                && ((f.variant.is_normal() && (f.index == index || f.index == -1))
358                    || (f.variant.is_leveled() && (f.index == 0)))
359        })
360        .map(|f| {
361            if f.variant.is_leveled() {
362                (f, Some(index as u32))
363            } else {
364                (f, None)
365            }
366        })
367        .or_else(|| None)
368    }
369
370    pub fn query_object_field(&self, field_id: ObjectId, object: &Object) -> Option<&FieldDesc> {
371        let desc = self
372            .ids_to_keys
373            .get(&field_id)
374            .and_then(|k| self.fields.get(*k))?;
375
376        if !desc.kind.contains(object.kind()) {
377            return None;
378        }
379
380        if let Some(exclusive) = &desc.exclusive {
381            let held_by_aliased = object.aliased_id().map(|id| exclusive.contains(&id)).unwrap_or(false);
382            let held_by_parent = object.parent_id().map(|id| exclusive.contains(&id)).unwrap_or(false);
383            if !exclusive.contains(&object.id()) && !held_by_aliased && !held_by_parent {
384                return None;
385            }
386        }
387
388        Some(desc)
389    }
390
391    /// Will return an iterator of all fields available for this object,
392    /// irrespective of which fields exist on the object itself.
393    pub fn query_all_object_fields(&self, object: &Object) -> impl Iterator<Item = &FieldDesc> {
394        let object_kind = object.kind();
395        let object_id = if let Some(id) = object.aliased_id() {
396            id
397        } else if let Some(id) = object.parent_id() {
398            id
399        } else {
400            object.id()
401        };
402
403        self.fields
404            .values()
405            .filter(move |desc| desc.kind.contains(object_kind))
406            .filter(move |desc| {
407                if let Some(exclusive) = &desc.exclusive {
408                    exclusive.contains(&object_id)
409                } else {
410                    true
411                }
412            })
413    }
414
415    /// First tries to fetch a Profile/Func field in the format of
416    /// <field_name><index id>, e.g. Buttonpos1 resolving to abpx for units, and Buttonpos2 resolving to abpy,
417    /// if that fails then will try to grab an slk field using the regular approach
418    pub fn query_lua_field(
419        &self,
420        object: &Object,
421        full_name: &str,
422    ) -> Option<(&FieldDesc, Option<u32>)> {
423        split_by_digits(full_name)
424            .map(|(name, index)| {
425                let err_msg = format!("invalid field name '{}': field must not have leading non-numeric characters after digits", full_name);
426
427                (
428                    name,
429                    index
430                        .parse()
431                        .expect(err_msg.as_str()),
432                )
433            })
434            .or_else(|| Some((full_name, 0)))
435            .and_then(|(name, index)| self.query_profile_field(name, object, index))
436            .or_else(|| self.query_slk_field(full_name, object))
437    }
438
439    pub fn field_by_id(&self, id: ObjectId) -> Option<&FieldDesc> {
440        let field_key = self.ids_to_keys.get(&id)?;
441        self.fields.get(*field_key)
442    }
443}
444
445pub fn read_metadata_dir<P: AsRef<Path>>(path: P) -> MetadataStore {
446    let path = path.as_ref();
447    let mut metadata = MetadataStore::default();
448
449    read_metadata_file(path.join("units/unitmetadata.slk"), |row, legend| {
450        metadata.insert_unit_row(row, legend);
451    });
452
453    read_metadata_file(path.join("units/abilitymetadata.slk"), |row, legend| {
454        metadata.insert_ability_row(row, legend);
455    });
456
457    read_metadata_file(path.join("units/abilitybuffmetadata.slk"), |row, legend| {
458        metadata.insert_basic_field(row, legend, ObjectKind::BUFF);
459    });
460
461    read_metadata_file(path.join("units/upgrademetadata.slk"), |row, legend| {
462        metadata.insert_basic_field(row, legend, ObjectKind::UPGRADE);
463    });
464
465    read_metadata_file(
466        path.join("units/destructablemetadata.slk"),
467        |row, legend| {
468            metadata.insert_basic_field(row, legend, ObjectKind::DESTRUCTABLE);
469        },
470    );
471
472    read_metadata_file(path.join("units/miscmetadata.slk"), |row, legend| {
473        metadata.insert_basic_field(row, legend, ObjectKind::MISC);
474    });
475
476    read_metadata_file(path.join("doodads/doodadmetadata.slk"), |row, legend| {
477        metadata.insert_basic_field(row, legend, ObjectKind::DOODAD);
478    });
479
480    metadata.add_field(
481        FieldDesc {
482            id:           ObjectId::from_bytes(b"liid").unwrap(),
483            index:        0,
484            variant:      FieldVariant::Normal {
485                name: "Name".to_string(),
486            },
487            value_ty:     ValueType::String,
488            value_ty_raw: "string".to_string(),
489            exclusive:    None,
490            kind:         ObjectKind::LIGHTNING,
491            is_profile:   false,
492        }
493    );
494
495    metadata.add_field(
496        FieldDesc {
497            id:           ObjectId::from_bytes(b"lnam").unwrap(),
498            index:        0,
499            variant:      FieldVariant::Normal {
500                name: "comment".to_string(),
501            },
502            value_ty:     ValueType::String,
503            value_ty_raw: "string".to_string(),
504            exclusive:    None,
505            kind:         ObjectKind::LIGHTNING,
506            is_profile:   false,
507        }
508    );
509
510    metadata.add_field(
511        FieldDesc {
512            id:           ObjectId::from_bytes(b"ldir").unwrap(),
513            index:        0,
514            variant:      FieldVariant::Normal {
515                name: "Dir".to_string(),
516            },
517            value_ty:     ValueType::String,
518            value_ty_raw: "string".to_string(),
519            exclusive:    None,
520            kind:         ObjectKind::LIGHTNING,
521            is_profile:   false,
522        }
523    );
524
525    metadata.add_field(
526        FieldDesc {
527            id:           ObjectId::from_bytes(b"lfil").unwrap(),
528            index:        0,
529            variant:      FieldVariant::Normal {
530                name: "file".to_string(),
531            },
532            value_ty:     ValueType::String,
533            value_ty_raw: "string".to_string(),
534            exclusive:    None,
535            kind:         ObjectKind::LIGHTNING,
536            is_profile:   false,
537        }
538    );
539
540    metadata.add_field(
541        FieldDesc {
542            id:           ObjectId::from_bytes(b"lasl").unwrap(),
543            index:        0,
544            variant:      FieldVariant::Normal {
545                name: "AvgSegLen".to_string(),
546            },
547            value_ty:     ValueType::Int,
548            value_ty_raw: "int".to_string(),
549            exclusive:    None,
550            kind:         ObjectKind::LIGHTNING,
551            is_profile:   false,
552        }
553    );
554
555    metadata.add_field(
556        FieldDesc {
557            id:           ObjectId::from_bytes(b"lwid").unwrap(),
558            index:        0,
559            variant:      FieldVariant::Normal {
560                name: "Width".to_string(),
561            },
562            value_ty:     ValueType::Int,
563            value_ty_raw: "int".to_string(),
564            exclusive:    None,
565            kind:         ObjectKind::LIGHTNING,
566            is_profile:   false,
567        }
568    );
569
570    metadata.add_field(
571        FieldDesc {
572            id:           ObjectId::from_bytes(b"lred").unwrap(),
573            index:        0,
574            variant:      FieldVariant::Normal {
575                name: "R".to_string(),
576            },
577            value_ty:     ValueType::Int,
578            value_ty_raw: "int".to_string(),
579            exclusive:    None,
580            kind:         ObjectKind::LIGHTNING,
581            is_profile:   false,
582        }
583    );
584
585    metadata.add_field(
586        FieldDesc {
587            id:           ObjectId::from_bytes(b"lgrn").unwrap(),
588            index:        0,
589            variant:      FieldVariant::Normal {
590                name: "G".to_string(),
591            },
592            value_ty:     ValueType::Int,
593            value_ty_raw: "int".to_string(),
594            exclusive:    None,
595            kind:         ObjectKind::LIGHTNING,
596            is_profile:   false,
597        }
598    );
599
600    metadata.add_field(
601        FieldDesc {
602            id:           ObjectId::from_bytes(b"lblu").unwrap(),
603            index:        0,
604            variant:      FieldVariant::Normal {
605                name: "B".to_string(),
606            },
607            value_ty:     ValueType::Int,
608            value_ty_raw: "int".to_string(),
609            exclusive:    None,
610            kind:         ObjectKind::LIGHTNING,
611            is_profile:   false,
612        }
613    );
614
615    metadata.add_field(
616        FieldDesc {
617            id:           ObjectId::from_bytes(b"lalp").unwrap(),
618            index:        0,
619            variant:      FieldVariant::Normal {
620                name: "A".to_string(),
621            },
622            value_ty:     ValueType::Int,
623            value_ty_raw: "int".to_string(),
624            exclusive:    None,
625            kind:         ObjectKind::LIGHTNING,
626            is_profile:   false,
627        }
628    );
629
630    metadata.add_field(
631        FieldDesc {
632            id:           ObjectId::from_bytes(b"lnsc").unwrap(),
633            index:        0,
634            variant:      FieldVariant::Normal {
635                name: "NoiseScale".to_string(),
636            },
637            value_ty:     ValueType::Real,
638            value_ty_raw: "real".to_string(),
639            exclusive:    None,
640            kind:         ObjectKind::LIGHTNING,
641            is_profile:   false,
642        }
643    );
644
645    metadata.add_field(
646        FieldDesc {
647            id:           ObjectId::from_bytes(b"ltcs").unwrap(),
648            index:        0,
649            variant:      FieldVariant::Normal {
650                name: "TexCoordScale".to_string(),
651            },
652            value_ty:     ValueType::Real,
653            value_ty_raw: "real".to_string(),
654            exclusive:    None,
655            kind:         ObjectKind::LIGHTNING,
656            is_profile:   false,
657        }
658    );
659
660    metadata.add_field(
661        FieldDesc {
662            id:           ObjectId::from_bytes(b"ldur").unwrap(),
663            index:        0,
664            variant:      FieldVariant::Normal {
665                name: "Duration".to_string(),
666            },
667            value_ty:     ValueType::Int,
668            value_ty_raw: "int".to_string(),
669            exclusive:    None,
670            kind:         ObjectKind::LIGHTNING,
671            is_profile:   false,
672        }
673    );
674
675    metadata.add_field(
676        FieldDesc {
677            id:           ObjectId::from_bytes(b"lver").unwrap(),
678            index:        0,
679            variant:      FieldVariant::Normal {
680                name: "version".to_string(),
681            },
682            value_ty:     ValueType::Int,
683            value_ty_raw: "int".to_string(),
684            exclusive:    None,
685            kind:         ObjectKind::LIGHTNING,
686            is_profile:   false,
687        }
688    );
689
690    metadata.add_field(
691        FieldDesc {
692            id:           ObjectId::from_bytes(b"lfxf").unwrap(),
693            index:        0,
694            variant:      FieldVariant::Normal {
695                name: "FxFile".to_string(),
696            },
697            value_ty:     ValueType::String,
698            value_ty_raw: "string".to_string(),
699            exclusive:    None,
700            kind:         ObjectKind::LIGHTNING,
701            is_profile:   false,
702        }
703    );
704
705    metadata.add_field(
706        FieldDesc {
707            id:           ObjectId::from_bytes(b"uvox").unwrap(),
708            index:        0,
709            variant:      FieldVariant::Normal {
710                name: "projectileVisOffsetX".to_string(),
711            },
712            value_ty:     ValueType::Real,
713            value_ty_raw: "real".to_string(),
714            exclusive:    None,
715            kind:         ObjectKind::UNIT,
716            is_profile:   true,
717        }
718    );
719
720    metadata.add_field(
721        FieldDesc {
722            id:           ObjectId::from_bytes(b"uvoy").unwrap(),
723            index:        0,
724            variant:      FieldVariant::Normal {
725                name: "projectileVisOffsetY".to_string(),
726            },
727            value_ty:     ValueType::Real,
728            value_ty_raw: "real".to_string(),
729            exclusive:    None,
730            kind:         ObjectKind::UNIT,
731            is_profile:   true,
732        }
733    );
734
735    // string id field for unit names
736    metadata.add_field(FieldDesc {
737        id:           ObjectId::from_bytes(b"siid").unwrap(),
738        index:        0,
739        variant:      FieldVariant::Normal {
740            name: "name".to_string(),
741        },
742        value_ty:     ValueType::String,
743        value_ty_raw: "string".to_string(),
744        exclusive:    None,
745        kind:         ObjectKind::UNIT,
746        is_profile:   false,
747    });
748
749    metadata
750}
751
752fn read_metadata_file<C, P>(path: P, mut callback: C)
753where
754    C: FnMut(slk::Row, &slk::Legend) -> (),
755    P: AsRef<Path>,
756{
757    let src = fs::read(path).unwrap();
758    let table = slk::Table::new(&src);
759
760    if table.is_none() {
761        return;
762    }
763
764    let mut table = table.unwrap();
765    let legend = table.legend();
766
767    while table.has_next() {
768        if let Some(row) = table.next_row() {
769            callback(row, &legend)
770        }
771    }
772}