Skip to main content

wowlab_types/
table_registry.rs

1// #t(file: rust_duplicate_strings) The canonical registry intentionally repeats protocol identifiers across declaration forms.
2
3//! Canonical game-data table identities and exposure metadata.
4
5use 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/// Stable identity for a synchronized game-data table.
19#[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/// Stable storage and external-exposure policy for one game-data table.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct GameDataTableMetadata {
109    pub database_name: &'static str,
110    /// Ordered columns that uniquely identify a row in published snapshots.
111    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
140/// Metadata for every synchronized game-data table, indexed by [`GameDataTable`].
141pub 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        // #t(rust_duplicate_strings) Canonical registry and serialization declarations intentionally repeat this protocol literal.
221        "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
300/// Snapshot-exposed tables in their stable publication and generated-code order.
301pub 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    /// Number of canonical synchronized tables.
325    pub const COUNT: usize = <Self as EnumCount>::COUNT;
326
327    /// Iterates over every canonical table in registry order.
328    #[must_use]
329    pub fn iter() -> impl ExactSizeIterator<Item = Self> {
330        <Self as IntoEnumIterator>::iter()
331    }
332
333    /// Returns the stable snake-case registry and reporting name.
334    #[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    /// Returns the stable kebab-case command-line argument name.
347    #[must_use]
348    pub fn command_name(self) -> &'static str {
349        self.into()
350    }
351
352    /// Returns this table's canonical storage and exposure metadata.
353    #[must_use]
354    pub const fn metadata(self) -> &'static GameDataTableMetadata {
355        // BOUNDS: every repr value is contiguous from zero and the registry length is COUNT.
356        &GAME_DATA_TABLE_REGISTRY[self as usize]
357    }
358
359    /// Returns the fully qualified `PostgreSQL` table name.
360    #[must_use]
361    pub const fn database_name(self) -> &'static str {
362        self.metadata().database_name
363    }
364
365    /// Returns the ordered columns that uniquely identify a snapshot row.
366    #[must_use]
367    pub const fn snapshot_key(self) -> &'static [&'static str] {
368        self.metadata().snapshot_key
369    }
370
371    /// Reports whether the table is included in published Studio snapshots.
372    #[must_use]
373    pub const fn is_snapshot_exposed(self) -> bool {
374        self.metadata().snapshot_exposed
375    }
376
377    /// Reports whether the table must have an MCP query descriptor.
378    #[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    // #t(fn: rust_duplicate_strings) This canonical registry function intentionally repeats a protocol table identifier.
400    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                // #t(rust_duplicate_strings) Canonical registry and serialization declarations intentionally repeat this protocol literal.
460                "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}