use std::collections::HashMap;
use std::sync::OnceLock;
use crate::generated::{
Ability, DeploymentPattern, Detachment, Enhancement, Faction, ForceDisposition, GameVersion,
InteractionFlag, LeaderAttachment, Mission, MissionMatchup, Phase, PhaseMapping, ResourcePool,
SecondaryCard, Stratagem, TerrainLayout, TerrainTemplate, TimingFlag, Unit, UnitComposition,
Wargear, WargearOption, Weapon, WeaponKeyword,
};
use super::collection::Collection;
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct RawData {
#[serde(default)]
pub units: Vec<Unit>,
#[serde(default)]
pub weapons: Vec<Weapon>,
#[serde(default)]
pub weapon_keywords: Vec<WeaponKeyword>,
#[serde(default)]
pub factions: Vec<Faction>,
#[serde(default)]
pub abilities: Vec<Ability>,
#[serde(default)]
pub phase_mappings: Vec<PhaseMapping>,
#[serde(default)]
pub detachments: Vec<Detachment>,
#[serde(default)]
pub stratagems: Vec<Stratagem>,
#[serde(default)]
pub enhancements: Vec<Enhancement>,
#[serde(default)]
pub leader_attachments: Vec<LeaderAttachment>,
#[serde(default)]
pub unit_compositions: Vec<UnitComposition>,
#[serde(default)]
pub wargear_options: Vec<WargearOption>,
#[serde(default)]
pub wargear: Vec<Wargear>,
#[serde(default)]
pub game_versions: Vec<GameVersion>,
#[serde(default)]
pub missions: Vec<Mission>,
#[serde(default)]
pub mission_matchups: Vec<MissionMatchup>,
#[serde(default)]
pub mission_cards: Vec<SecondaryCard>,
#[serde(default)]
pub deployment_patterns: Vec<DeploymentPattern>,
#[serde(default)]
pub force_dispositions: Vec<ForceDisposition>,
#[serde(default)]
pub terrain_templates: Vec<TerrainTemplate>,
#[serde(default)]
pub terrain_layouts: Vec<TerrainLayout>,
#[serde(default)]
pub resource_pools: Vec<ResourcePool>,
#[serde(default)]
pub timing_flags: Vec<TimingFlag>,
#[serde(default)]
pub interaction_flags: Vec<InteractionFlag>,
}
pub struct Dataset {
pub units: Collection<Unit>,
pub weapons: Collection<Weapon>,
pub weapon_keywords: Collection<WeaponKeyword>,
pub factions: Collection<Faction>,
pub abilities: Collection<Ability>,
pub detachments: Collection<Detachment>,
pub enhancements: Collection<Enhancement>,
pub stratagems: Collection<Stratagem>,
pub wargear_options: Collection<WargearOption>,
pub wargear: Collection<Wargear>,
pub missions: Collection<Mission>,
pub mission_matchups: Collection<MissionMatchup>,
pub mission_cards: Collection<SecondaryCard>,
pub deployment_patterns: Collection<DeploymentPattern>,
pub force_dispositions: Collection<ForceDisposition>,
pub terrain_templates: Collection<TerrainTemplate>,
pub terrain_layouts: Collection<TerrainLayout>,
pub resource_pools: Collection<ResourcePool>,
pub leader_attachments: Vec<LeaderAttachment>,
pub unit_compositions: Vec<UnitComposition>,
pub game_versions: Vec<GameVersion>,
pub timing_flags: Vec<TimingFlag>,
pub interaction_flags: Vec<InteractionFlag>,
pub phase_mappings: Vec<PhaseMapping>,
phase_index: HashMap<String, Vec<Phase>>,
units_by_ability: HashMap<String, Vec<usize>>,
units_by_weapon: HashMap<String, Vec<usize>>,
wargear_options_by_unit: HashMap<String, Vec<usize>>,
}
#[cfg(feature = "bundled-data")]
const BUNDLE_JSON: &str = include_str!("bundle.generated.json");
impl Dataset {
#[cfg(feature = "bundled-data")]
pub fn embedded() -> &'static Dataset {
static EMBEDDED: OnceLock<Dataset> = OnceLock::new();
EMBEDDED.get_or_init(|| {
let raw: RawData =
serde_json::from_str(BUNDLE_JSON).expect("embedded data bundle is valid JSON");
Dataset::from_raw(raw)
})
}
pub fn from_raw(raw: RawData) -> Dataset {
let units = Collection::build(
raw.units,
|u| u.id.to_string(),
|u| Some(u.name.as_str()),
|u| Some(u.faction_id.as_str()),
|u| format!("{}::{}", u.faction_id.as_str(), u.id.as_str()),
);
let weapons = Collection::build(
raw.weapons,
|w| w.id.to_string(),
|w| Some(w.name.as_str()),
|_| None,
|w| w.id.to_string(),
);
let weapon_keywords = id_name_collection(
raw.weapon_keywords,
|k| k.id.to_string(),
|k| Some(k.name.as_str()),
);
let factions = Collection::build(
raw.factions,
|f| f.id.to_string(),
|f| Some(f.name.as_str()),
|_| None,
|f| f.id.to_string(),
);
let abilities = Collection::build(
raw.abilities,
|a| a.ability_id.to_string(),
|a| Some(a.name.as_str()),
|a| a.faction_id.as_ref().map(|e| e.as_str()),
|a| a.ability_id.to_string(),
);
let detachments = Collection::build(
raw.detachments,
|d| d.id.to_string(),
|d| Some(d.name.as_str()),
|d| Some(d.faction_id.as_str()),
|d| d.id.to_string(),
);
let enhancements = id_name_collection(
raw.enhancements,
|e| e.id.to_string(),
|e| Some(e.name.as_str()),
);
let stratagems = id_name_collection(
raw.stratagems,
|s| s.id.to_string(),
|s| Some(s.name.as_str()),
);
let wargear_options =
id_name_collection(raw.wargear_options, |w| w.id.to_string(), |_| None);
let wargear =
id_name_collection(raw.wargear, |w| w.id.to_string(), |w| Some(w.name.as_str()));
let missions = id_name_collection(
raw.missions,
|m| m.id.to_string(),
|m| Some(m.name.as_str()),
);
let mission_matchups =
id_name_collection(raw.mission_matchups, |m| m.id.to_string(), |_| None);
let mission_cards = id_name_collection(
raw.mission_cards,
|s| s.id.to_string(),
|s| Some(s.name.as_str()),
);
let deployment_patterns = id_name_collection(
raw.deployment_patterns,
|d| d.id.to_string(),
|d| Some(d.name.as_str()),
);
let force_dispositions = id_name_collection(
raw.force_dispositions,
|f| f.id.to_string(),
|f| Some(f.name.as_str()),
);
let terrain_templates = id_name_collection(
raw.terrain_templates,
|t| t.id.to_string(),
|t| Some(t.name.as_str()),
);
let terrain_layouts = id_name_collection(
raw.terrain_layouts,
|l| l.id.to_string(),
|l| Some(l.name.as_str()),
);
let resource_pools = Collection::build(
raw.resource_pools,
|r| r.id.to_string(),
|r| Some(r.name.as_str()),
|r| Some(r.faction_id.as_str()),
|r| r.id.to_string(),
);
let phase_index = build_phase_index(&raw.phase_mappings);
let (units_by_ability, units_by_weapon) = build_reverse_indexes(&units);
let mut wargear_options_by_unit: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, option) in wargear_options.all().iter().enumerate() {
wargear_options_by_unit
.entry(option.unit_id.to_string())
.or_default()
.push(idx);
}
Dataset {
units,
weapons,
weapon_keywords,
factions,
abilities,
detachments,
enhancements,
stratagems,
wargear_options,
wargear,
missions,
mission_matchups,
mission_cards,
deployment_patterns,
force_dispositions,
terrain_templates,
terrain_layouts,
resource_pools,
leader_attachments: raw.leader_attachments,
unit_compositions: raw.unit_compositions,
game_versions: raw.game_versions,
timing_flags: raw.timing_flags,
interaction_flags: raw.interaction_flags,
phase_mappings: raw.phase_mappings,
phase_index,
units_by_ability,
units_by_weapon,
wargear_options_by_unit,
}
}
pub fn find_unit(&self, query: &str) -> Option<&Unit> {
self.units.find(query)
}
pub fn find_weapon(&self, query: &str) -> Option<&Weapon> {
self.weapons.find(query)
}
pub fn find_faction(&self, query: &str) -> Option<&Faction> {
self.factions.find(query)
}
pub fn find_ability(&self, query: &str) -> Option<&Ability> {
self.abilities.find(query)
}
pub fn faction_of(&self, unit: &Unit) -> Option<&Faction> {
self.factions.get(unit.faction_id.as_str())
}
pub fn resolve_terrain(
&self,
layout: &TerrainLayout,
) -> Result<Vec<crate::terrain::ResolvedPiece>, crate::terrain::TerrainResolveError> {
fn convert<T: serde::de::DeserializeOwned, S: serde::Serialize>(value: &S) -> T {
serde_json::from_value(
serde_json::to_value(value).expect("generated terrain serializes"),
)
.expect("generated terrain type matches resolver shape")
}
let r_layout: crate::terrain::TerrainLayout = convert(layout);
let templates: Vec<crate::terrain::TerrainTemplate> =
self.terrain_templates.all().iter().map(convert).collect();
crate::terrain::resolve_layout(&r_layout, &templates)
}
pub fn recommended_terrain_layouts(&self, pattern: &DeploymentPattern) -> Vec<&TerrainLayout> {
pattern
.recommended_terrain_layout_ids
.iter()
.flatten()
.filter_map(|id| self.terrain_layouts.get(id.as_str()))
.collect()
}
pub fn weapons_of(&self, unit: &Unit) -> Vec<&Weapon> {
unit.weapon_ids
.iter()
.filter_map(|id| self.weapons.get(id.as_str()))
.collect()
}
pub fn wargear_options_of(&self, unit: &Unit) -> Vec<&WargearOption> {
self.wargear_options_by_unit
.get(unit.id.as_str())
.map(|idxs| idxs.iter().map(|&i| self.wargear_options.at(i)).collect())
.unwrap_or_default()
}
pub fn abilities_of(&self, unit: &Unit) -> Vec<&Ability> {
unit.ability_ids
.iter()
.filter_map(|id| self.abilities.get(id.as_str()))
.collect()
}
pub fn phases_of(&self, ability: &Ability) -> &[Phase] {
self.phases_for("ability", ability.ability_id.as_str())
}
pub fn phases_for(&self, source_type: &str, source_id: &str) -> &[Phase] {
self.phase_index
.get(&format!("{source_type}:{source_id}"))
.map_or(&[], Vec::as_slice)
}
pub fn units_with_ability(&self, ability_id: &str) -> Vec<&Unit> {
self.units_by_ability
.get(ability_id)
.map(|idxs| idxs.iter().map(|&i| self.units.at(i)).collect())
.unwrap_or_default()
}
pub fn units_with_weapon(&self, weapon_id: &str) -> Vec<&Unit> {
self.units_by_weapon
.get(weapon_id)
.map(|idxs| idxs.iter().map(|&i| self.units.at(i)).collect())
.unwrap_or_default()
}
pub fn leaders_attachable_to(&self, bodyguard_unit_id: &str) -> Vec<&Unit> {
let mut out: Vec<&Unit> = self
.leader_attachments
.iter()
.filter(|la| {
la.eligible_bodyguard_ids
.iter()
.any(|id| id.as_str() == bodyguard_unit_id)
})
.filter_map(|la| self.units.get(la.leader_id.as_str()))
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn bodyguards_attachable_from(&self, leader_unit_id: &str) -> Vec<&Unit> {
let mut seen = std::collections::HashSet::new();
let mut out: Vec<&Unit> = Vec::new();
for la in &self.leader_attachments {
if la.leader_id.as_str() != leader_unit_id {
continue;
}
for bodyguard_id in &la.eligible_bodyguard_ids {
if !seen.insert(bodyguard_id.as_str()) {
continue;
}
if let Some(unit) = self.units.get(bodyguard_id.as_str()) {
out.push(unit);
}
}
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
pub fn abilities_of_faction(&self, faction_id: &str) -> Vec<&Ability> {
self.abilities.by_faction(faction_id)
}
pub fn weapons_of_faction(&self, faction_id: &str) -> Vec<&Weapon> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::new();
for unit in self.units.by_faction(faction_id) {
for weapon in self.weapons_of(unit) {
if seen.insert(weapon.id.as_str().to_string()) {
out.push(weapon);
}
}
}
out
}
}
fn id_name_collection<T>(
items: Vec<T>,
id_of: impl Fn(&T) -> String,
name_of: impl Fn(&T) -> Option<&str>,
) -> Collection<T> {
Collection::build(items, &id_of, name_of, |_| None, |i| id_of(i))
}
fn build_phase_index(phase_mappings: &[PhaseMapping]) -> HashMap<String, Vec<Phase>> {
let mut index: HashMap<String, Vec<Phase>> = HashMap::new();
for pm in phase_mappings {
let key = format!("{}:{}", pm.source_type, pm.source_id.as_str());
let entry = index.entry(key).or_default();
for &phase in pm.phases.iter() {
if !entry.contains(&phase) {
entry.push(phase);
}
}
}
index
}
fn build_reverse_indexes(
units: &Collection<Unit>,
) -> (HashMap<String, Vec<usize>>, HashMap<String, Vec<usize>>) {
let mut by_ability: HashMap<String, Vec<usize>> = HashMap::new();
let mut by_weapon: HashMap<String, Vec<usize>> = HashMap::new();
for (idx, unit) in units.all().iter().enumerate() {
for ability_id in &unit.ability_ids {
by_ability
.entry(ability_id.to_string())
.or_default()
.push(idx);
}
for weapon_id in &unit.weapon_ids {
by_weapon
.entry(weapon_id.to_string())
.or_default()
.push(idx);
}
}
(by_ability, by_weapon)
}