use std::collections::HashMap;
use crate::content::Guid;
use crate::content::changes::{Change, Entry, Field, Value};
use super::entries::{ItemsFile, LootFile, RecipesFile, ValueEntry};
use super::files::PackError;
use super::pack::Reader;
use super::{ITEMS_FILE, LOOT_FILE, RECIPES_FILE};
type RecipeField = (Field, Option<(Guid, String)>, Value);
type ChangeEntry<'a> = (usize, &'a [String], Vec<(Field, Option<ValueEntry>)>);
fn source(file: &str, line: usize, name: &str) -> String {
if Guid::try_parse(name).is_some() {
format!("{file}:{line}")
} else {
format!("{file}:{line} ({name})")
}
}
fn container(reader: &Reader<'_>, item: Guid, name: &str, slots: Value) -> Result<(), String> {
let Some(vanilla) = reader.vanilla else {
return Ok(());
};
match vanilla.container_slots(item) {
Ok(None) => Err(format!("{name} places no container")),
Ok(Some(has)) => match slots {
Value::Set(v) if v < f64::from(has) => Err(format!(
"slots {v} are fewer than the {has} {name} holds, and a container only grows"
)),
_ => Ok(()),
},
Err(_) => Ok(()),
}
}
fn value(entry: ValueEntry) -> Value {
match entry {
ValueEntry::Set(v) => Value::Set(v),
ValueEntry::Scale(scale) => Value::Times(scale.times),
}
}
pub(super) fn item_changes(
reader: &mut Reader<'_>,
items_file: &ItemsFile,
text: &str,
) -> Vec<(Change, String)> {
let entries: Vec<ChangeEntry<'_>> = items_file
.change
.iter()
.map(|entry| {
let e = entry.get_ref();
let fields = vec![
(Field::MaxStackSize, e.max_stack),
(Field::Slots, e.slots),
(Field::DamageMod, e.damage_mod),
(Field::SpeedMod, e.speed_mod),
(Field::Armor, e.armor),
(Field::BurnTime, e.burn_time),
(Field::EnergyOutput, e.energy_output),
(Field::ComfortAmount, e.comfort_amount),
];
(entry.span().start, e.items.as_slice(), fields)
})
.collect();
changes_of(reader, ITEMS_FILE, Entry::Item, &entries, text)
}
pub(super) fn loot_changes(
reader: &mut Reader<'_>,
loot_file: &LootFile,
text: &str,
) -> Vec<(Change, String)> {
let entries: Vec<ChangeEntry<'_>> = loot_file
.change
.iter()
.map(|entry| {
let e = entry.get_ref();
let fields = vec![
(Field::StackSize, e.stack_size),
(Field::Count, e.count),
(Field::SpawnProbability, e.spawn_probability),
(Field::Weight, e.weight),
];
(entry.span().start, e.items.as_slice(), fields)
})
.collect();
changes_of(reader, LOOT_FILE, Entry::Loot, &entries, text)
}
fn changes_of(
reader: &mut Reader<'_>,
file: &str,
kind: Entry,
entries: &[ChangeEntry<'_>],
text: &str,
) -> Vec<(Change, String)> {
let mut out = Vec::new();
let mut seen: HashMap<(Guid, Field), usize> = HashMap::new();
for (index, (at, items, given)) in entries.iter().enumerate() {
let at = *at;
let line = PackError::at(file, text, at, "").line;
let number = index + 1;
let fields: Vec<(Field, Value)> = given
.iter()
.filter_map(|(field, v)| v.map(|v| (*field, value(v))))
.collect();
if items.is_empty() {
reader.error(
file,
text,
at,
format!("change {number}: items names no item"),
);
}
if fields.is_empty() {
let names: Vec<&str> = Field::ALL
.iter()
.filter(|f| f.entry() == kind)
.map(|f| f.name())
.collect();
reader.error(
file,
text,
at,
format!(
"change {number}: changes nothing; give {}",
names.join(", ")
),
);
}
let mut valid = Vec::new();
for &(field, value) in &fields {
match field.check(value) {
Ok(()) => valid.push((field, value)),
Err(why) => reader.error(file, text, at, format!("change {number}: {why}")),
}
}
for name in *items {
let Some((item, _)) = reader.reference(file, text, at, name, "item", "") else {
continue;
};
for &(field, value) in &valid {
if field == Field::Slots
&& let Err(why) = container(reader, item, name, value)
{
reader.error(file, text, at, format!("change {number}: {why}"));
continue;
}
if let Some(&first) = seen.get(&(item, field)) {
reader.error(
file,
text,
at,
format!(
"change {number}: {field} of {name} is changed at line {first} already"
),
);
continue;
}
seen.insert((item, field), line);
out.push((Change::new(item, field, value), source(file, line, name)));
}
}
}
out
}
fn recipe(reader: &mut Reader<'_>, text: &str, at: usize, name: &str) -> Option<Guid> {
let file = RECIPES_FILE;
if let Some(guid) = Guid::try_parse(name) {
return Some(guid);
}
let own = reader.own(name);
if name.contains(':') || reader.defined.get(&own) == Some(&"recipe") {
reader.error(
file,
text,
at,
format!(
"{name:?} is a recipe a mod or this pack adds: set its values where it is defined"
),
);
return None;
}
let found = match reader.vanilla {
None => Err(format!(
"no vanilla recipe {name:?} at hand; name it by its GUID, or check with the game's data"
)),
Some(vanilla) => vanilla.recipe(name).map_err(|why| why.to_string()),
};
found.map_err(|why| reader.error(file, text, at, why)).ok()
}
fn recipes_making(reader: &mut Reader<'_>, text: &str, at: usize, name: &str) -> Vec<Guid> {
let file = RECIPES_FILE;
let Some((item, _)) = reader.reference(file, text, at, name, "item", "") else {
return Vec::new();
};
let found = match reader.vanilla {
None => Err(format!(
"output {name:?} needs the game's data at hand to find its recipes"
)),
Some(vanilla) => vanilla.recipes_making(item).map_err(|why| why.to_string()),
};
match found {
Ok(recipes) if recipes.is_empty() => {
reader.error(file, text, at, format!("no vanilla recipe makes {name}"));
Vec::new()
}
Ok(recipes) => recipes,
Err(why) => {
reader.error(file, text, at, why);
Vec::new()
}
}
}
pub(super) fn recipe_changes(
reader: &mut Reader<'_>,
recipes_file: &RecipesFile,
text: &str,
) -> Vec<(Change, String)> {
let file = RECIPES_FILE;
let mut out = Vec::new();
let mut seen: HashMap<(Guid, Field, Option<Guid>), usize> = HashMap::new();
for (index, entry) in recipes_file.change.iter().enumerate() {
let at = entry.span().start;
let line = PackError::at(file, text, at, "").line;
let e = entry.get_ref();
let number = index + 1;
let mut fields: Vec<RecipeField> = [
(Field::Output, e.output_count),
(Field::Cost, e.cost),
(Field::CraftingDuration, e.time),
]
.into_iter()
.filter_map(|(field, v)| v.map(|v| (field, None, value(v))))
.collect();
for (name, v) in &e.ingredients {
if let Some((item, _)) = reader.reference(file, text, at, name, "item", "") {
fields.push((Field::Ingredient, Some((item, name.clone())), value(*v)));
}
}
if e.recipes.is_empty() && e.output.is_empty() {
reader.error(
file,
text,
at,
format!("change {number}: names no recipe; give recipes or output"),
);
}
if fields.is_empty() && e.ingredients.is_empty() {
let names: Vec<&str> = Field::ALL
.iter()
.filter(|f| f.entry() == Entry::Recipe && **f != Field::Ingredient)
.map(|f| f.name())
.chain(["ingredients"])
.collect();
reader.error(
file,
text,
at,
format!(
"change {number}: changes nothing; give {}",
names.join(", ")
),
);
}
let mut valid = Vec::new();
for (field, item, value) in fields {
match field.check(value) {
Ok(()) => valid.push((field, item, value)),
Err(why) => reader.error(file, text, at, format!("change {number}: {why}")),
}
}
let mut picked: Vec<(Guid, &str)> = Vec::new();
for name in &e.recipes {
if let Some(guid) = recipe(reader, text, at, name) {
picked.push((guid, name));
}
}
for name in &e.output {
for guid in recipes_making(reader, text, at, name) {
picked.push((guid, name));
}
}
for (recipe, name) in picked {
for (field, item, value) in &valid {
let key = (recipe, *field, item.as_ref().map(|(guid, _)| *guid));
if let Some(&first) = seen.get(&key) {
let what = match item {
Some((_, item)) => format!("{field} {item}"),
None => field.to_string(),
};
reader.error(
file,
text,
at,
format!(
"change {number}: {what} of {name} is changed at line {first} already"
),
);
continue;
}
seen.insert(key, line);
let change = match item {
Some((item, _)) => Change::ingredient(recipe, *item, *value),
None => Change::new(recipe, *field, *value),
};
out.push((change, source(file, line, name)));
}
}
}
out
}