mod seed;
mod spoiler;
mod placement;
pub use seed::{Seed, SeedWorld};
pub use spoiler::{SeedSpoiler, SpoilerGroup, SpoilerWorldReachable, SpoilerPlacement};
pub use placement::Placement;
use std::{fmt::Write, cmp::Ordering};
use rand::{
Rng, prelude::StdRng,
};
use rand_seeder::Seeder;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{
item::{Item, Message, UberStateOperator},
settings::{UniverseSettings, InlineHeader, HeaderConfig, Goal}, uber_state::UberStateTrigger,
world::{
World,
graph::Graph,
Pool,
}, header::{self, HeaderBuild}, Header, files::FileAccess
};
use placement::generate_placements;
pub fn generate_seed<'graph, 'settings>(graph: &'graph Graph, file_access: &impl FileAccess, settings: &'settings UniverseSettings) -> Result<Seed<'graph, 'settings>, String> {
let mut rng: StdRng = Seeder::from(&settings.seed).make_rng();
log::trace!("Seeded RNG with {}", settings.seed);
let (worlds, (flags, headers)): (Vec<_>, (Vec<_>, Vec<_>)) = settings.world_settings.iter().map(|world_settings| {
let mut world = World::new_spawn(graph, world_settings);
world.pool = Pool::preset();
let (goals, flags, headers) = parse_headers(&mut world, file_access, &mut rng)?;
world.goals = goals;
Ok((world, (flags, headers)))
}).collect::<Result<Vec<_>, String>>()?.into_iter().unzip();
let (mut worlds, spoiler) = generate_placements(graph, &worlds, &mut rng)?;
for ((world, flags), headers) in worlds.iter_mut().zip(flags).zip(headers) {
world.flags = flags;
world.headers = headers;
}
Ok(Seed { worlds, graph, settings, spoiler })
}
fn parse_headers(world: &mut World, file_access: &impl FileAccess, rng: &mut impl Rng) -> Result<(Vec<Goal>, Vec<String>, String), String> {
validate_header_names(&world.player.settings.headers, &world.player.settings.inline_headers)?;
let mut config_map = build_config_map(&world.player.settings.header_config)?;
let mut headers = vec![];
let mut includes = FxHashSet::default();
includes.extend(world.player.settings.headers.iter().cloned());
for header_name in &world.player.settings.headers {
let header = file_access.read_header(header_name)?;
parse_header(header_name.clone(), header, &mut headers, &mut includes, &mut config_map, file_access, rng)?;
}
for inline_header in &world.player.settings.inline_headers {
let header_name = inline_header.name.as_ref().cloned().unwrap_or_else(|| "Anonymous Header".to_string());
let header = inline_header.content.clone();
parse_header(header_name, header, &mut headers, &mut includes, &mut config_map, file_access, rng)?;
}
let mut excludes = FxHashMap::default();
let mut seed_contents = String::new();
let mut flags = vec![];
let mut goals = vec![];
let mut state_sets = vec![];
flags.push(world.player.settings.difficulty.to_string());
if !world.player.settings.tricks.is_empty() { flags.push("Glitches".to_string()); }
if world.player.settings.is_random_spawn() { flags.push("Random Spawn".to_string()); }
if world.player.settings.hard { flags.push("Hard".to_string()); }
let header_names = headers.into_iter().map(|(header_name, mut header)| {
for exclude in header.excludes {
excludes.insert(exclude, header_name.clone());
}
if !header.seed_content.is_empty() {
seed_contents.push_str(&header.seed_content);
seed_contents.push('\n');
}
flags.append(&mut header.flags);
goals.append(&mut header.goals);
for preplacement in header.preplacements {
block_spawn_sets(&preplacement, world);
header.item_pool_changes.entry(preplacement.item.clone()).and_modify(|prior| *prior -= 1).or_insert(-1);
world.preplace(preplacement.trigger, preplacement.item);
}
for (item, amount) in header.item_pool_changes {
#[allow(clippy::cast_sign_loss)]
match amount.cmp(&0) {
Ordering::Less => world.pool.remove(&item, (-amount) as u32),
Ordering::Equal => {},
Ordering::Greater => world.pool.grant(item, amount as u32),
}
}
for (item, details) in header.item_details {
let display = item.to_string();
if world.custom_items.insert(item, details).is_some() {
return Err(format!("multiple headers tried to customize the item {display}"));
}
}
state_sets.append(&mut header.state_sets);
Ok(header_name)
}).collect::<Result<Vec<_>, _>>()?;
for header_name in &header_names {
if let Some(other) = excludes.get(header_name) {
return Err(format!("headers {other} and {header_name} are incompatible"));
}
}
for header_with_parameters in config_map.keys() {
if !header_names.iter().any(|header| header == header_with_parameters) {
log::warn!("The header {header_with_parameters} referenced in a header argument isn't active");
}
}
goals.extend(world.player.settings.goals.iter().cloned());
for flag in goals.iter().map(Goal::flag_name) {
flags.push(flag.to_string());
}
let mut header_block = String::new();
write!(header_block, "{seed_contents}").unwrap();
if !state_sets.is_empty() { writeln!(header_block, "// Sets: {}", state_sets.join(", ")).unwrap(); }
Ok((goals, flags, header_block))
}
fn parse_header(
header_name: String,
header: String,
headers: &mut Vec<(String, HeaderBuild)>,
includes: &mut FxHashSet<String>,
config_map: &mut FxHashMap::<String, FxHashMap<String, String>>,
file_access: &impl FileAccess,
rng: &mut impl Rng
) -> Result<(), String> {
log::trace!("Parsing header {header_name}");
let header_config = config_map.remove(&header_name).unwrap_or_default();
let header = Header::parse(header, rng)
.map_err(|err| format!("Error in header {}:\n{}", header_name, err.verbose_display()))?
.build(header_config)?;
for include in &header.includes {
if includes.insert(include.clone()) {
let header = file_access.read_header(include)?;
parse_header(include.clone(), header, headers, includes, config_map, file_access, rng)?;
}
}
headers.push((header_name, header));
Ok(())
}
fn validate_header_names(headers: &FxHashSet<String>, inline_headers: &[InlineHeader]) -> Result<(), String> {
for inline_header in inline_headers {
if let Some(name) = &inline_header.name {
if headers.contains(name) {
return Err(format!("Ambiguous name: {name} used both as a file header and an inline header"));
}
}
}
Ok(())
}
fn build_config_map(header_config: &[HeaderConfig]) -> Result<FxHashMap::<String, FxHashMap<String, String>>, String> {
let mut config_map = FxHashMap::<String, FxHashMap<_, _>>::default();
for config in header_config {
if let Some(prior) = config_map.entry(config.header_name.clone())
.or_default()
.insert(config.config_name.clone(), config.config_value.clone())
{
if prior != config.config_value {
return Err(format!("provided multiple values for configuration parameter {} for header {} ({} and {})", config.config_name, config.header_name, prior, config.config_value));
}
}
}
Ok(config_map)
}
fn block_spawn_sets(preplacement: &header::Pickup, world: &mut World) {
if let Item::UberState(uber_state_item) = &preplacement.item {
if preplacement.trigger != UberStateTrigger::spawn() { return }
if let UberStateOperator::Value(value) = &uber_state_item.operator {
for trigger in world.graph.nodes.iter()
.filter(|node| node.can_place())
.filter_map(|node| node.trigger())
.filter(|trigger| trigger.check(uber_state_item.identifier, value.to_f32()))
{
log::trace!("adding an empty pickup at {} to prevent placements", trigger.code());
let mut message = Message::new(String::new());
message.frames = Some(0);
message.quiet = true;
message.noclear = true;
let null_item = Item::Message(message);
world.preplace(trigger.clone(), null_item);
}
}
}
}