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 fields: DenseSlotMap<FieldKey, FieldDesc>,
132 objects_with_data: HashMap<ObjectId, Vec<ObjectId>>,
135 ids_to_keys: HashMap<ObjectId, FieldKey>,
137 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 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 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 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 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 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}