1use strum::{EnumCount, IntoEnumIterator};
6
7const ID_KEY: &[&str] = &["id"];
8const SPEC_KEY: &[&str] = &["spec_id"];
9const EXPANSION_SYSTEM_KEY: &[&str] = &["expansion_id", "system"];
10const ITEM_DROP_KEY: &[&str] = &["item_id", "source_kind", "difficulty_key"];
11const LEVEL_KEY: &[&str] = &["level"];
12const ITEM_LEVEL_KEY: &[&str] = &["item_level"];
13const CHALLENGE_LEVEL_KEY: &[&str] = &["challenge_level"];
14const EXPANSION_KEY: &[&str] = &["expansion"];
15const PROFESSION_KEY: &[&str] = &["profession"];
16const RACIAL_SPELL_KEY: &[&str] = &["race_id", "spell_id", "class_mask"];
17
18#[derive(
20 Clone,
21 Copy,
22 Debug,
23 Eq,
24 Hash,
25 Ord,
26 PartialEq,
27 PartialOrd,
28 strum::EnumCount,
29 strum::EnumIter,
30 strum::EnumString,
31 strum::IntoStaticStr,
32)]
33#[strum(serialize_all = "kebab-case")]
34#[repr(u8)]
35#[non_exhaustive]
36pub enum GameDataTable {
37 Spells,
38 Traits,
39 ExpansionTraits,
40 Items,
41 Specs,
42 Classes,
43 GlobalColors,
44 GlobalStrings,
45 ItemBonuses,
46 Curves,
47 CurvePoints,
48 RandPropPoints,
49 ItemScalingConfigs,
50 ItemOffsetCurves,
51 ItemSquishEras,
52 Enchantments,
53 GemProperties,
54 ItemDamageScaling,
55 CooldownSets,
56 ExpectedStats,
57 ExpectedStatMods,
58 SpecializationSpells,
59 RacialSpells,
60 PowerTypes,
61 ItemArmorQuality,
62 ItemArmorShield,
63 ItemArmorTotal,
64 ArmorLocation,
65 JournalTiers,
66 JournalInstances,
67 CraftedProducts,
68 Professions,
69 PvpSeasons,
70 PvpTiers,
71 MythicPlusSeasons,
72 Difficulties,
73 ItemDropScaling,
74 Currencies,
75 CurrencyCategories,
76 ItemBonusListGroups,
77 ItemGroupIlvlScalingEntries,
78 ItemLevelSelectors,
79 ItemLevelSelectorQualitySets,
80 ItemLevelSelectorQualities,
81 ItemConversions,
82 ChallengeModeItemBonusOverrides,
83 ItemBonusSequenceSpells,
84 DelvesSeasons,
85 HpPerSta,
86 CombatRatings,
87 CombatRatingsMultByIlvl,
88 StaminaMultByIlvl,
89 BaseMp,
90 SpellScaling,
91 ArmorMitigationByLvl,
92 NpcTotalHp,
93 NpcDamageByClass,
94 ChallengeModeHealth,
95 ChallengeModeDamage,
96 ItemSocketCostPerLevel,
97 ProfessionRatings,
98 BaseProfessionRatings,
99 Creatures,
100 CreatureDifficulties,
101 ContentTunings,
102 ContentTuningXDifficulty,
103 ContentTuningXExpected,
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct GameDataTableMetadata {
109 pub database_name: &'static str,
110 pub snapshot_key: &'static [&'static str],
112 pub snapshot_exposed: bool,
113 pub mcp_exposed: bool,
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117enum TableExposure {
118 Internal,
119 Snapshot,
120 Mcp,
121 SnapshotAndMcp,
122}
123
124const fn metadata(
125 database_name: &'static str,
126 snapshot_key: &'static [&'static str],
127 exposure: TableExposure,
128) -> GameDataTableMetadata {
129 GameDataTableMetadata {
130 database_name,
131 snapshot_key,
132 snapshot_exposed: matches!(
133 exposure,
134 TableExposure::Snapshot | TableExposure::SnapshotAndMcp
135 ),
136 mcp_exposed: matches!(exposure, TableExposure::Mcp | TableExposure::SnapshotAndMcp),
137 }
138}
139
140pub const GAME_DATA_TABLE_REGISTRY: [GameDataTableMetadata; GameDataTable::COUNT] = [
142 metadata("game.spells", ID_KEY, TableExposure::Mcp),
143 metadata("game.specs_traits", SPEC_KEY, TableExposure::SnapshotAndMcp),
144 metadata(
145 "game.expansion_traits",
146 EXPANSION_SYSTEM_KEY,
147 TableExposure::Snapshot,
148 ),
149 metadata("game.items", ID_KEY, TableExposure::Mcp),
150 metadata("game.specs", ID_KEY, TableExposure::SnapshotAndMcp),
151 metadata("game.classes", ID_KEY, TableExposure::SnapshotAndMcp),
152 metadata("game.global_colors", ID_KEY, TableExposure::SnapshotAndMcp),
153 metadata("game.global_strings", ID_KEY, TableExposure::Mcp),
154 metadata("game.item_bonuses", ID_KEY, TableExposure::SnapshotAndMcp),
155 metadata("game.curves", ID_KEY, TableExposure::SnapshotAndMcp),
156 metadata("game.curve_points", ID_KEY, TableExposure::SnapshotAndMcp),
157 metadata(
158 "game.rand_prop_points",
159 ID_KEY,
160 TableExposure::SnapshotAndMcp,
161 ),
162 metadata("game.item_scaling_configs", ID_KEY, TableExposure::Snapshot),
163 metadata("game.item_offset_curves", ID_KEY, TableExposure::Snapshot),
164 metadata("game.item_squish_eras", ID_KEY, TableExposure::Snapshot),
165 metadata("game.enchantments", ID_KEY, TableExposure::Mcp),
166 metadata("game.gem_properties", ID_KEY, TableExposure::Mcp),
167 metadata("game.item_damage_scaling", ID_KEY, TableExposure::Mcp),
168 metadata("game.cooldown_sets", ID_KEY, TableExposure::Mcp),
169 metadata("game.expected_stats", ID_KEY, TableExposure::Mcp),
170 metadata("game.expected_stat_mods", ID_KEY, TableExposure::Mcp),
171 metadata("game.specialization_spells", ID_KEY, TableExposure::Mcp),
172 metadata(
173 "game.racial_spells",
174 RACIAL_SPELL_KEY,
175 TableExposure::Internal,
176 ),
177 metadata("game.power_types", ID_KEY, TableExposure::SnapshotAndMcp),
178 metadata("game.item_armor_quality", ID_KEY, TableExposure::Mcp),
179 metadata("game.item_armor_shield", ID_KEY, TableExposure::Mcp),
180 metadata("game.item_armor_total", ID_KEY, TableExposure::Mcp),
181 metadata("game.armor_location", ID_KEY, TableExposure::Mcp),
182 metadata("game.journal_tiers", ID_KEY, TableExposure::Mcp),
183 metadata(
184 "game.journal_instances",
185 ID_KEY,
186 TableExposure::SnapshotAndMcp,
187 ),
188 metadata("game.crafted_products", ID_KEY, TableExposure::Mcp),
189 metadata("game.professions", ID_KEY, TableExposure::Mcp),
190 metadata("game.pvp_seasons", ID_KEY, TableExposure::Mcp),
191 metadata("game.pvp_tiers", ID_KEY, TableExposure::Mcp),
192 metadata("game.mythic_plus_seasons", ID_KEY, TableExposure::Mcp),
193 metadata("game.difficulties", ID_KEY, TableExposure::Mcp),
194 metadata(
195 "game.item_drop_scaling",
196 ITEM_DROP_KEY,
197 TableExposure::SnapshotAndMcp,
198 ),
199 metadata("game.currencies", ID_KEY, TableExposure::Mcp),
200 metadata("game.currency_categories", ID_KEY, TableExposure::Mcp),
201 metadata("game.item_bonus_list_groups", ID_KEY, TableExposure::Mcp),
202 metadata(
203 "game.item_group_ilvl_scaling_entries",
204 ID_KEY,
205 TableExposure::Mcp,
206 ),
207 metadata("game.item_level_selectors", ID_KEY, TableExposure::Mcp),
208 metadata(
209 "game.item_level_selector_quality_sets",
210 ID_KEY,
211 TableExposure::Mcp,
212 ),
213 metadata(
214 "game.item_level_selector_qualities",
215 ID_KEY,
216 TableExposure::Mcp,
217 ),
218 metadata("game.item_conversions", ID_KEY, TableExposure::Mcp),
219 metadata(
220 "game.challenge_mode_item_bonus_overrides",
222 ID_KEY,
223 TableExposure::Mcp,
224 ),
225 metadata(
226 "game.item_bonus_sequence_spells",
227 ID_KEY,
228 TableExposure::Mcp,
229 ),
230 metadata("game.delves_seasons", ID_KEY, TableExposure::Mcp),
231 metadata("game.hp_per_sta", LEVEL_KEY, TableExposure::SnapshotAndMcp),
232 metadata(
233 "game.combat_ratings",
234 LEVEL_KEY,
235 TableExposure::SnapshotAndMcp,
236 ),
237 metadata(
238 "game.combat_ratings_mult_by_ilvl",
239 ITEM_LEVEL_KEY,
240 TableExposure::SnapshotAndMcp,
241 ),
242 metadata(
243 "game.stamina_mult_by_ilvl",
244 ITEM_LEVEL_KEY,
245 TableExposure::Mcp,
246 ),
247 metadata("game.base_mp", LEVEL_KEY, TableExposure::Mcp),
248 metadata(
249 "game.spell_scaling",
250 LEVEL_KEY,
251 TableExposure::SnapshotAndMcp,
252 ),
253 metadata(
254 "game.armor_mitigation_by_lvl",
255 LEVEL_KEY,
256 TableExposure::Mcp,
257 ),
258 metadata("game.npc_total_hp", LEVEL_KEY, TableExposure::Mcp),
259 metadata("game.npc_damage_by_class", LEVEL_KEY, TableExposure::Mcp),
260 metadata(
261 "game.challenge_mode_health",
262 CHALLENGE_LEVEL_KEY,
263 TableExposure::Mcp,
264 ),
265 metadata(
266 "game.challenge_mode_damage",
267 CHALLENGE_LEVEL_KEY,
268 TableExposure::Mcp,
269 ),
270 metadata(
271 "game.item_socket_cost_per_level",
272 ITEM_LEVEL_KEY,
273 TableExposure::Mcp,
274 ),
275 metadata("game.profession_ratings", EXPANSION_KEY, TableExposure::Mcp),
276 metadata(
277 "game.base_profession_ratings",
278 PROFESSION_KEY,
279 TableExposure::Mcp,
280 ),
281 metadata("game.creatures", ID_KEY, TableExposure::Internal),
282 metadata(
283 "game.creature_difficulties",
284 ID_KEY,
285 TableExposure::Internal,
286 ),
287 metadata("game.content_tunings", ID_KEY, TableExposure::Internal),
288 metadata(
289 "game.content_tuning_x_difficulty",
290 ID_KEY,
291 TableExposure::Internal,
292 ),
293 metadata(
294 "game.content_tuning_x_expected",
295 ID_KEY,
296 TableExposure::Internal,
297 ),
298];
299
300pub const PUBLISHED_SNAPSHOT_TABLES: &[GameDataTable] = &[
302 GameDataTable::CurvePoints,
303 GameDataTable::Curves,
304 GameDataTable::ItemBonuses,
305 GameDataTable::RandPropPoints,
306 GameDataTable::ItemScalingConfigs,
307 GameDataTable::ItemOffsetCurves,
308 GameDataTable::ItemSquishEras,
309 GameDataTable::CombatRatings,
310 GameDataTable::CombatRatingsMultByIlvl,
311 GameDataTable::HpPerSta,
312 GameDataTable::SpellScaling,
313 GameDataTable::ItemDropScaling,
314 GameDataTable::Traits,
315 GameDataTable::ExpansionTraits,
316 GameDataTable::JournalInstances,
317 GameDataTable::GlobalColors,
318 GameDataTable::Specs,
319 GameDataTable::PowerTypes,
320 GameDataTable::Classes,
321];
322
323impl GameDataTable {
324 pub const COUNT: usize = <Self as EnumCount>::COUNT;
326
327 #[must_use]
329 pub fn iter() -> impl ExactSizeIterator<Item = Self> {
330 <Self as IntoEnumIterator>::iter()
331 }
332
333 #[must_use]
335 pub fn name(self) -> &'static str {
336 if matches!(self, Self::Traits) {
337 return "traits";
338 }
339
340 match self.database_name().strip_prefix("game.") {
341 Some(name) => name,
342 None => self.database_name(),
343 }
344 }
345
346 #[must_use]
348 pub fn command_name(self) -> &'static str {
349 self.into()
350 }
351
352 #[must_use]
354 pub const fn metadata(self) -> &'static GameDataTableMetadata {
355 &GAME_DATA_TABLE_REGISTRY[self as usize]
357 }
358
359 #[must_use]
361 pub const fn database_name(self) -> &'static str {
362 self.metadata().database_name
363 }
364
365 #[must_use]
367 pub const fn snapshot_key(self) -> &'static [&'static str] {
368 self.metadata().snapshot_key
369 }
370
371 #[must_use]
373 pub const fn is_snapshot_exposed(self) -> bool {
374 self.metadata().snapshot_exposed
375 }
376
377 #[must_use]
379 pub const fn is_mcp_exposed(self) -> bool {
380 self.metadata().mcp_exposed
381 }
382}
383
384impl std::fmt::Display for GameDataTable {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 f.write_str(self.name())
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use std::collections::BTreeSet;
393
394 use googletest::prelude::*;
395
396 use super::*;
397
398 #[gtest]
399 fn registry_has_exactly_sixty_seven_unique_stable_tables() -> Result<()> {
401 let tables = GameDataTable::iter().collect::<Vec<_>>();
402
403 verify_that!(tables.len(), eq(67))?;
404 verify_that!(GAME_DATA_TABLE_REGISTRY.len(), eq(67))?;
405
406 let database_names = tables
407 .iter()
408 .map(|table| table.database_name())
409 .collect::<Vec<_>>();
410
411 verify_that!(
412 database_names,
413 container_eq([
414 "game.spells",
415 "game.specs_traits",
416 "game.expansion_traits",
417 "game.items",
418 "game.specs",
419 "game.classes",
420 "game.global_colors",
421 "game.global_strings",
422 "game.item_bonuses",
423 "game.curves",
424 "game.curve_points",
425 "game.rand_prop_points",
426 "game.item_scaling_configs",
427 "game.item_offset_curves",
428 "game.item_squish_eras",
429 "game.enchantments",
430 "game.gem_properties",
431 "game.item_damage_scaling",
432 "game.cooldown_sets",
433 "game.expected_stats",
434 "game.expected_stat_mods",
435 "game.specialization_spells",
436 "game.racial_spells",
437 "game.power_types",
438 "game.item_armor_quality",
439 "game.item_armor_shield",
440 "game.item_armor_total",
441 "game.armor_location",
442 "game.journal_tiers",
443 "game.journal_instances",
444 "game.crafted_products",
445 "game.professions",
446 "game.pvp_seasons",
447 "game.pvp_tiers",
448 "game.mythic_plus_seasons",
449 "game.difficulties",
450 "game.item_drop_scaling",
451 "game.currencies",
452 "game.currency_categories",
453 "game.item_bonus_list_groups",
454 "game.item_group_ilvl_scaling_entries",
455 "game.item_level_selectors",
456 "game.item_level_selector_quality_sets",
457 "game.item_level_selector_qualities",
458 "game.item_conversions",
459 "game.challenge_mode_item_bonus_overrides",
461 "game.item_bonus_sequence_spells",
462 "game.delves_seasons",
463 "game.hp_per_sta",
464 "game.combat_ratings",
465 "game.combat_ratings_mult_by_ilvl",
466 "game.stamina_mult_by_ilvl",
467 "game.base_mp",
468 "game.spell_scaling",
469 "game.armor_mitigation_by_lvl",
470 "game.npc_total_hp",
471 "game.npc_damage_by_class",
472 "game.challenge_mode_health",
473 "game.challenge_mode_damage",
474 "game.item_socket_cost_per_level",
475 "game.profession_ratings",
476 "game.base_profession_ratings",
477 "game.creatures",
478 "game.creature_difficulties",
479 "game.content_tunings",
480 "game.content_tuning_x_difficulty",
481 "game.content_tuning_x_expected",
482 ])
483 )?;
484
485 verify_that!(
486 database_names
487 .iter()
488 .copied()
489 .collect::<BTreeSet<_>>()
490 .len(),
491 eq(67)
492 )?;
493 verify_true!(tables.iter().all(|table| !table.snapshot_key().is_empty()))?;
494 verify_true!(tables.iter().all(|table| table.to_string() == table.name()))?;
495
496 verify_true!(
497 tables
498 .iter()
499 .all(|table| table.command_name().parse::<GameDataTable>() == Ok(*table))
500 )
501 }
502
503 #[gtest]
504 fn snapshot_exposure_and_publication_order_are_exact() -> Result<()> {
505 let exposed = GameDataTable::iter()
506 .filter(|table| table.is_snapshot_exposed())
507 .collect::<BTreeSet<_>>();
508 let published = PUBLISHED_SNAPSHOT_TABLES
509 .iter()
510 .copied()
511 .collect::<BTreeSet<_>>();
512
513 verify_that!(exposed, eq(&published))?;
514 verify_that!(exposed.len(), eq(19))?;
515
516 let contracts = PUBLISHED_SNAPSHOT_TABLES
517 .iter()
518 .map(|table| (table.database_name(), table.snapshot_key()))
519 .collect::<Vec<_>>();
520
521 verify_that!(
522 contracts,
523 container_eq([
524 ("game.curve_points", ID_KEY),
525 ("game.curves", ID_KEY),
526 ("game.item_bonuses", ID_KEY),
527 ("game.rand_prop_points", ID_KEY),
528 ("game.item_scaling_configs", ID_KEY),
529 ("game.item_offset_curves", ID_KEY),
530 ("game.item_squish_eras", ID_KEY),
531 ("game.combat_ratings", LEVEL_KEY),
532 ("game.combat_ratings_mult_by_ilvl", ITEM_LEVEL_KEY),
533 ("game.hp_per_sta", LEVEL_KEY),
534 ("game.spell_scaling", LEVEL_KEY),
535 ("game.item_drop_scaling", ITEM_DROP_KEY),
536 ("game.specs_traits", SPEC_KEY),
537 ("game.expansion_traits", EXPANSION_SYSTEM_KEY),
538 ("game.journal_instances", ID_KEY),
539 ("game.global_colors", ID_KEY),
540 ("game.specs", ID_KEY),
541 ("game.power_types", ID_KEY),
542 ("game.classes", ID_KEY),
543 ])
544 )
545 }
546
547 #[gtest]
548 fn mcp_exposure_boundary_is_exact() -> Result<()> {
549 let not_exposed = GameDataTable::iter()
550 .filter(|table| !table.is_mcp_exposed())
551 .map(GameDataTable::database_name)
552 .collect::<Vec<_>>();
553
554 verify_that!(
555 not_exposed,
556 container_eq([
557 "game.expansion_traits",
558 "game.item_scaling_configs",
559 "game.item_offset_curves",
560 "game.item_squish_eras",
561 "game.racial_spells",
562 "game.creatures",
563 "game.creature_difficulties",
564 "game.content_tunings",
565 "game.content_tuning_x_difficulty",
566 "game.content_tuning_x_expected",
567 ])
568 )?;
569
570 verify_that!(
571 GameDataTable::iter()
572 .filter(|table| table.is_mcp_exposed())
573 .count(),
574 eq(57)
575 )
576 }
577}