Merge branch 'tygyh/default-loadout' into 'master'

LoadoutBuilder refactoring

See merge request veloren/veloren!2361
This commit is contained in:
Samuel Keiffer 2021-06-02 23:42:39 +00:00
commit 9a31d77f0a
2 changed files with 231 additions and 199 deletions

View File

@ -0,0 +1,9 @@
// Keep in mind that this should be safe defaults for new character
({
Armor(Chest): Item("common.items.armor.misc.chest.worker_purple_brown"),
Armor(Legs): Item("common.items.armor.misc.pants.worker_brown"),
Armor(Feet): Item("common.items.armor.misc.foot.sandals"),
Lantern: Item("common.items.lantern.black_0"),
Glider: Item("common.items.glider.glider_cloverleaf"),
})

View File

@ -1,3 +1,5 @@
#![warn(clippy::pedantic)]
//#![warn(clippy::nursery)]
use crate::{
assets::{self, AssetExt},
comp::{
@ -13,14 +15,14 @@ use crate::{
trade::{Good, SiteInformation},
};
use hashbrown::HashMap;
use rand::{seq::SliceRandom, Rng};
use rand::{self, distributions::WeightedError, seq::SliceRandom, Rng};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tracing::warn;
/// Builder for character Loadouts, containing weapon and armour items belonging
/// to a character, along with some helper methods for loading Items and
/// ItemConfig
/// to a character, along with some helper methods for loading `Item`-s and
/// `ItemConfig`
///
/// ```
/// use veloren_common::{
@ -70,6 +72,71 @@ enum ItemSpec {
Choice(Vec<(f32, Option<ItemSpec>)>),
}
impl ItemSpec {
fn try_to_item(&self, asset_specifier: &str) -> Option<Item> {
match self {
ItemSpec::Item(specifier) => Some(Item::new_from_asset_expect(&specifier)),
ItemSpec::Choice(items) => {
choose(&items, asset_specifier)
.as_ref()
.and_then(|e| match e {
entry @ ItemSpec::Item { .. } => entry.try_to_item(asset_specifier),
choice @ ItemSpec::Choice { .. } => choice.try_to_item(asset_specifier),
})
},
}
}
#[cfg(test)]
// Read everything and checks if it's loading
fn validate(&self, key: EquipSlot) {
match self {
ItemSpec::Item(specifier) => std::mem::drop(Item::new_from_asset_expect(&specifier)),
ItemSpec::Choice(items) => {
for (p, entry) in items {
if p <= &0.0 {
let err =
format!("Weight is less or equal to 0.0.\n ({:?}: {:?})", key, self,);
panic!("\n\n{}\n\n", err);
} else {
entry.as_ref().map(|e| e.validate(key));
}
}
},
}
}
}
fn choose<'a>(items: &'a [(f32, Option<ItemSpec>)], asset_specifier: &str) -> &'a Option<ItemSpec> {
let mut rng = rand::thread_rng();
items.choose_weighted(&mut rng, |item| item.0).map_or_else(
|err| match err {
WeightedError::NoItem | WeightedError::AllWeightsZero => &None,
WeightedError::InvalidWeight => {
let err = format!("Negative values of probability in {}.", asset_specifier);
if cfg!(tests) {
panic!("{}", err);
} else {
warn!("{}", err);
&None
}
},
WeightedError::TooMany => {
let err = format!("More than u32::MAX values in {}.", asset_specifier);
if cfg!(tests) {
panic!("{}", err);
} else {
warn!("{}", err);
&None
}
},
},
|(_p, itemspec)| itemspec,
)
}
#[derive(Debug, Deserialize, Clone)]
pub struct LoadoutSpec(HashMap<EquipSlot, ItemSpec>);
impl assets::Asset for LoadoutSpec {
@ -78,6 +145,7 @@ impl assets::Asset for LoadoutSpec {
const EXTENSION: &'static str = "ron";
}
#[must_use]
pub fn make_potion_bag(quantity: u32) -> Item {
let mut bag = Item::new_from_asset_expect("common.items.armor.misc.bag.tiny_leather_pouch");
if let Some(i) = bag.slots_mut().iter_mut().next() {
@ -90,6 +158,11 @@ pub fn make_potion_bag(quantity: u32) -> Item {
bag
}
#[must_use]
// We have many species so this function is long
// Also we are using default tools for un-specified species so
// it's fine to have wildcards
#[allow(clippy::too_many_lines, clippy::match_wildcard_for_single_variants)]
pub fn default_main_tool(body: &Body) -> Option<Item> {
match body {
Body::Golem(golem) => match golem.species {
@ -260,12 +333,17 @@ pub fn default_main_tool(body: &Body) -> Option<Item> {
}
}
impl Default for LoadoutBuilder {
fn default() -> Self { Self::new() }
}
impl LoadoutBuilder {
#[allow(clippy::new_without_default)] // TODO: Pending review in #587
#[must_use]
pub fn new() -> Self { Self(Loadout::new_empty()) }
#[must_use]
fn with_default_equipment(body: &Body, active_item: Option<Item>) -> Self {
let mut builder = LoadoutBuilder::new();
let mut builder = Self::new();
builder = match body {
Body::BipedLarge(biped_large::Body {
species: biped_large::Species::Mindflayer,
@ -309,130 +387,111 @@ impl LoadoutBuilder {
builder.active_mainhand(active_item)
}
#[must_use]
pub fn from_asset_expect(asset_specifier: &str) -> Self {
let loadout = Self::new();
loadout.apply_asset_expect(asset_specifier)
}
/// # Usage
/// Creates new `LoadoutBuilder` with all field replaced from
/// `asset_specifier` which corresponds to loadout config
///
/// # Panics
/// 1) Will panic if there is no asset with such `asset_specifier`
/// 2) Will panic if path to item specified in loadout file doesn't exist
/// 3) Will panic while runs in tests and asset doesn't have "correct" form
#[must_use]
pub fn apply_asset_expect(mut self, asset_specifier: &str) -> Self {
let spec = LoadoutSpec::load_expect(asset_specifier).read().0.clone();
let mut loadout = LoadoutBuilder::new();
for (key, specifier) in spec {
let item = match specifier {
ItemSpec::Item(specifier) => Item::new_from_asset_expect(&specifier),
ItemSpec::Choice(items) => {
let mut rng = rand::thread_rng();
match items
.choose_weighted(&mut rng, |item| item.0)
.unwrap_or_else(|_| {
panic!(
"failed to choose item from loadout asset ({})",
asset_specifier
)
}) {
(_, Some(ItemSpec::Item(item_specifier))) => {
Item::new_from_asset_expect(&item_specifier)
},
(_, Some(ItemSpec::Choice(_))) => {
let err = format!(
"Using choice of choices in ({}): {:?}. Unimplemented.",
asset_specifier, key,
);
if cfg!(tests) {
panic!("{}", err);
} else {
warn!("{}", err);
}
continue;
},
(_, None) => continue,
}
},
for (key, entry) in spec {
let item = match entry.try_to_item(asset_specifier) {
Some(item) => item,
None => continue,
};
match key {
EquipSlot::ActiveMainhand => {
loadout = loadout.active_mainhand(Some(item));
self = self.active_mainhand(Some(item));
},
EquipSlot::ActiveOffhand => {
loadout = loadout.active_offhand(Some(item));
self = self.active_offhand(Some(item));
},
EquipSlot::InactiveMainhand => {
loadout = loadout.inactive_mainhand(Some(item));
self = self.inactive_mainhand(Some(item));
},
EquipSlot::InactiveOffhand => {
loadout = loadout.inactive_offhand(Some(item));
self = self.inactive_offhand(Some(item));
},
EquipSlot::Armor(ArmorSlot::Head) => {
loadout = loadout.head(Some(item));
self = self.head(Some(item));
},
EquipSlot::Armor(ArmorSlot::Shoulders) => {
loadout = loadout.shoulder(Some(item));
self = self.shoulder(Some(item));
},
EquipSlot::Armor(ArmorSlot::Chest) => {
loadout = loadout.chest(Some(item));
self = self.chest(Some(item));
},
EquipSlot::Armor(ArmorSlot::Hands) => {
loadout = loadout.hands(Some(item));
self = self.hands(Some(item));
},
EquipSlot::Armor(ArmorSlot::Legs) => {
loadout = loadout.pants(Some(item));
self = self.pants(Some(item));
},
EquipSlot::Armor(ArmorSlot::Feet) => {
loadout = loadout.feet(Some(item));
self = self.feet(Some(item));
},
EquipSlot::Armor(ArmorSlot::Belt) => {
loadout = loadout.belt(Some(item));
self = self.belt(Some(item));
},
EquipSlot::Armor(ArmorSlot::Back) => {
loadout = loadout.back(Some(item));
self = self.back(Some(item));
},
EquipSlot::Armor(ArmorSlot::Neck) => {
loadout = loadout.neck(Some(item));
self = self.neck(Some(item));
},
EquipSlot::Armor(ArmorSlot::Ring1) => {
loadout = loadout.ring1(Some(item));
self = self.ring1(Some(item));
},
EquipSlot::Armor(ArmorSlot::Ring2) => {
loadout = loadout.ring2(Some(item));
self = self.ring2(Some(item));
},
EquipSlot::Lantern => {
loadout = loadout.lantern(Some(item));
self = self.lantern(Some(item));
},
EquipSlot::Armor(ArmorSlot::Tabard) => {
loadout = loadout.tabard(Some(item));
self = self.tabard(Some(item));
},
EquipSlot::Glider => {
loadout = loadout.glider(Some(item));
self = self.glider(Some(item));
},
EquipSlot::Armor(slot @ ArmorSlot::Bag1)
| EquipSlot::Armor(slot @ ArmorSlot::Bag2)
| EquipSlot::Armor(slot @ ArmorSlot::Bag3)
| EquipSlot::Armor(slot @ ArmorSlot::Bag4) => {
loadout = loadout.bag(slot, Some(item));
EquipSlot::Armor(
slot @ (ArmorSlot::Bag1 | ArmorSlot::Bag2 | ArmorSlot::Bag3 | ArmorSlot::Bag4),
) => {
self = self.bag(slot, Some(item));
},
};
}
loadout
self
}
/// Set default armor items for the loadout. This may vary with game
/// updates, but should be safe defaults for a new character.
pub fn defaults(self) -> Self {
self.chest(Some(Item::new_from_asset_expect(
"common.items.armor.misc.chest.worker_purple_brown",
)))
.pants(Some(Item::new_from_asset_expect(
"common.items.armor.misc.pants.worker_brown",
)))
.feet(Some(Item::new_from_asset_expect(
"common.items.armor.misc.foot.sandals",
)))
.lantern(Some(Item::new_from_asset_expect(
"common.items.lantern.black_0",
)))
.glider(Some(Item::new_from_asset_expect(
"common.items.glider.glider_cloverleaf",
)))
}
#[must_use]
pub fn defaults(self) -> Self { self.apply_asset_expect("common.loadouts.default") }
/// Builds loadout of creature when spawned
#[allow(clippy::single_match)]
#[must_use]
// The reason why this function is so long is creating merchant inventory
// with all items to sell.
// Maybe we should do it on the caller side?
#[allow(
clippy::too_many_lines,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
pub fn build_loadout(
body: Body,
mut main_tool: Option<Item>,
@ -445,7 +504,7 @@ impl LoadoutBuilder {
}
// Constructs ItemConfig from Item
let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(|i| i.kind()) {
let active_item = if let Some(ItemKind::Tool(_)) = main_tool.as_ref().map(Item::kind) {
main_tool
} else {
Some(Item::empty())
@ -458,87 +517,64 @@ impl LoadoutBuilder {
}
});
// Creates rest of loadout
let loadout = if let Some(config) = config {
use LoadoutConfig::*;
let loadout_builder = if let Some(config) = config {
let builder = Self::new().active_mainhand(active_item);
// NOTE: we apply asset after active mainhand so asset has ability override it
match config {
Gnarling => match active_tool_kind {
Some(ToolKind::Bow) | Some(ToolKind::Staff) | Some(ToolKind::Spear) => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-0.gnarling")
.active_mainhand(active_item)
.build()
LoadoutConfig::Gnarling => match active_tool_kind {
Some(ToolKind::Bow | ToolKind::Staff | ToolKind::Spear) => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-0.gnarling")
},
_ => LoadoutBuilder::new().active_mainhand(active_item).build(),
_ => builder,
},
Adlet => match active_tool_kind {
Some(ToolKind::Bow) => LoadoutBuilder::from_asset_expect(
"common.loadouts.dungeon.tier-1.adlet_bow",
)
.active_mainhand(active_item)
.build(),
Some(ToolKind::Spear) | Some(ToolKind::Staff) => {
LoadoutBuilder::from_asset_expect(
"common.loadouts.dungeon.tier-1.adlet_spear",
)
.active_mainhand(active_item)
.build()
LoadoutConfig::Adlet => match active_tool_kind {
Some(ToolKind::Bow) => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-1.adlet_bow")
},
_ => LoadoutBuilder::new().active_mainhand(active_item).build(),
Some(ToolKind::Spear | ToolKind::Staff) => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-1.adlet_spear")
},
_ => builder,
},
Sahagin => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-2.sahagin")
.active_mainhand(active_item)
.build()
LoadoutConfig::Sahagin => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-2.sahagin")
},
Haniwa => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-3.haniwa")
.active_mainhand(active_item)
.build()
LoadoutConfig::Haniwa => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-3.haniwa")
},
Myrmidon => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-4.myrmidon")
.active_mainhand(active_item)
.build()
LoadoutConfig::Myrmidon => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-4.myrmidon")
},
Husk => LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-5.husk")
.active_mainhand(active_item)
.build(),
Beastmaster => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-5.beastmaster")
.active_mainhand(active_item)
.build()
LoadoutConfig::Husk => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-5.husk")
},
Warlord => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-5.warlord")
.active_mainhand(active_item)
.build()
LoadoutConfig::Beastmaster => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-5.beastmaster")
},
Warlock => {
LoadoutBuilder::from_asset_expect("common.loadouts.dungeon.tier-5.warlock")
.active_mainhand(active_item)
.build()
LoadoutConfig::Warlord => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-5.warlord")
},
Villager => LoadoutBuilder::from_asset_expect("common.loadouts.village.villager")
.active_mainhand(active_item)
.bag(ArmorSlot::Bag1, Some(make_potion_bag(10)))
.build(),
Guard => LoadoutBuilder::from_asset_expect("common.loadouts.village.guard")
.active_mainhand(active_item)
.bag(ArmorSlot::Bag1, Some(make_potion_bag(25)))
.build(),
Merchant => {
LoadoutConfig::Warlock => {
builder.apply_asset_expect("common.loadouts.dungeon.tier-5.warlock")
},
LoadoutConfig::Villager => builder
.apply_asset_expect("common.loadouts.village.villager")
.bag(ArmorSlot::Bag1, Some(make_potion_bag(10))),
LoadoutConfig::Guard => builder
.apply_asset_expect("common.loadouts.village.guard")
.bag(ArmorSlot::Bag1, Some(make_potion_bag(25))),
LoadoutConfig::Merchant => {
let mut backpack =
Item::new_from_asset_expect("common.items.armor.misc.back.backpack");
let mut coins = economy
.map(|e| e.unconsumed_stock.get(&Good::Coin))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Coin))
.copied()
.unwrap_or_default()
.round()
.min(rand::thread_rng().gen_range(1000.0..3000.0))
as u32;
let armor = economy
.map(|e| e.unconsumed_stock.get(&Good::Armor))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Armor))
.copied()
.unwrap_or_default()
/ 10.0;
@ -563,8 +599,7 @@ impl LoadoutBuilder {
"common.items.armor.misc.bag.reliable_backpack",
);
let weapon = economy
.map(|e| e.unconsumed_stock.get(&Good::Tools))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Tools))
.copied()
.unwrap_or_default()
/ 10.0;
@ -580,7 +615,7 @@ impl LoadoutBuilder {
let mut rng = rand::thread_rng();
let mut item_with_amount = |item_id: &str, amount: &mut f32| {
if *amount > 0.0 {
let mut item = Item::new_from_asset_expect(&item_id);
let mut item = Item::new_from_asset_expect(item_id);
// NOTE: Conversion to and from f32 works fine because we make sure the
// number we're converting is ≤ 100.
let max = amount.min(16.min(item.max_amount()) as f32) as u32;
@ -599,8 +634,7 @@ impl LoadoutBuilder {
"common.items.armor.misc.bag.reliable_backpack",
);
let mut ingredients = economy
.map(|e| e.unconsumed_stock.get(&Good::Ingredients))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Ingredients))
.copied()
.unwrap_or_default()
/ 10.0;
@ -619,8 +653,7 @@ impl LoadoutBuilder {
// food for sale at every site, to be used until we have some solution like NPC
// houses as a limit on econsim population growth
let mut food = economy
.map(|e| e.unconsumed_stock.get(&Good::Food))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Food))
.copied()
.unwrap_or_default()
.max(10000.0)
@ -634,8 +667,7 @@ impl LoadoutBuilder {
"common.items.armor.misc.bag.reliable_backpack",
);
let mut potions = economy
.map(|e| e.unconsumed_stock.get(&Good::Potions))
.flatten()
.and_then(|e| e.unconsumed_stock.get(&Good::Potions))
.copied()
.unwrap_or_default()
/ 10.0;
@ -646,118 +678,137 @@ impl LoadoutBuilder {
*i = item_with_amount(&item_id, &mut potions);
}
}
LoadoutBuilder::from_asset_expect("common.loadouts.village.merchant")
.active_mainhand(active_item)
builder
.apply_asset_expect("common.loadouts.village.merchant")
.back(Some(backpack))
.bag(ArmorSlot::Bag1, Some(bag1))
.bag(ArmorSlot::Bag2, Some(bag2))
.bag(ArmorSlot::Bag3, Some(bag3))
.bag(ArmorSlot::Bag4, Some(bag4))
.build()
},
}
} else {
LoadoutBuilder::with_default_equipment(&body, active_item).build()
Self::with_default_equipment(&body, active_item)
};
Self(loadout)
Self(loadout_builder.build())
}
#[must_use]
pub fn active_mainhand(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::ActiveMainhand, item);
self
}
#[must_use]
pub fn active_offhand(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::ActiveOffhand, item);
self
}
#[must_use]
pub fn inactive_mainhand(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::InactiveMainhand, item);
self
}
#[must_use]
pub fn inactive_offhand(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::InactiveOffhand, item);
self
}
#[must_use]
pub fn shoulder(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Shoulders), item);
self
}
#[must_use]
pub fn chest(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Chest), item);
self
}
#[must_use]
pub fn belt(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Belt), item);
self
}
#[must_use]
pub fn hands(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Hands), item);
self
}
#[must_use]
pub fn pants(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Legs), item);
self
}
#[must_use]
pub fn feet(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Feet), item);
self
}
#[must_use]
pub fn back(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Back), item);
self
}
#[must_use]
pub fn ring1(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Ring1), item);
self
}
#[must_use]
pub fn ring2(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Ring2), item);
self
}
#[must_use]
pub fn neck(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Neck), item);
self
}
#[must_use]
pub fn lantern(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Lantern, item);
self
}
#[must_use]
pub fn glider(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Glider, item);
self
}
#[must_use]
pub fn head(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Head), item);
self
}
#[must_use]
pub fn tabard(mut self, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(ArmorSlot::Tabard), item);
self
}
#[must_use]
pub fn bag(mut self, which: ArmorSlot, item: Option<Item>) -> Self {
self.0.swap(EquipSlot::Armor(which), item);
self
}
#[must_use]
pub fn build(self) -> Loadout { self.0 }
}
@ -797,14 +848,14 @@ mod tests {
];
for config in LoadoutConfig::iter() {
test_weapons.iter().for_each(|test_weapon| {
LoadoutBuilder::build_loadout(
for test_weapon in &test_weapons {
std::mem::drop(LoadoutBuilder::build_loadout(
Body::Humanoid(comp::humanoid::Body::random()),
Some(Item::new_from_asset_expect(test_weapon)),
Some(config),
None,
);
});
));
}
}
}
@ -828,18 +879,18 @@ mod tests {
body_type: comp::$species::BodyType::Male,
..body
};
LoadoutBuilder::build_loadout(
std::mem::drop(LoadoutBuilder::build_loadout(
Body::$body(female_body),
None,
None,
None,
);
LoadoutBuilder::build_loadout(
));
std::mem::drop(LoadoutBuilder::build_loadout(
Body::$body(male_body),
None,
None,
None,
);
));
}
};
// recursive call
@ -867,14 +918,11 @@ mod tests {
);
}
#[test]
fn test_loadout_asset() { LoadoutBuilder::from_asset_expect("common.loadouts.test"); }
#[test]
fn test_all_loadout_assets() {
#[derive(Clone)]
struct LoadoutsList(Vec<LoadoutSpec>);
impl assets::Compound for LoadoutsList {
struct LoadoutList(Vec<LoadoutSpec>);
impl assets::Compound for LoadoutList {
fn load<S: assets::source::Source>(
cache: &assets::AssetCache<S>,
specifier: &str,
@ -886,44 +934,19 @@ mod tests {
.map(|spec| LoadoutSpec::load_cloned(spec))
.collect::<Result<_, Error>>()?;
Ok(LoadoutsList(list))
Ok(Self(list))
}
}
// It just load everything that could
// TODO: add some checks, e.g. that Armor(Head) key correspond
// to Item with ItemKind Head(_)
fn validate_asset(loadout: LoadoutSpec) {
let spec = loadout.0;
for (key, specifier) in spec {
match specifier {
ItemSpec::Item(specifier) => {
Item::new_from_asset_expect(&specifier);
},
ItemSpec::Choice(ref items) => {
for item in items {
match item {
(_, Some(ItemSpec::Item(specifier))) => {
Item::new_from_asset_expect(&specifier);
},
(_, None) => {},
(_, _) => {
panic!(
"\n\nChoice of Choice is unimplemented. (Search for \
\n{:?}: {:#?})\n\n",
key, specifier,
);
},
};
}
},
};
}
}
let loadouts = LoadoutsList::load_expect_cloned("common.loadouts.*").0;
let loadouts = LoadoutList::load_expect_cloned("common.loadouts.*").0;
for loadout in loadouts {
validate_asset(loadout);
let spec = loadout.0;
for (key, entry) in spec {
entry.validate(key);
}
}
}
}