use crate::Verb;
use std::collections::{BTreeMap, BTreeSet};
use crate::QuestEffect;
use crate::diagnostic::{Diagnostic, codes};
use crate::envelope::Campaign;
use crate::metrics::Metrics;
use crate::registry::{
AnchorRegistry, BlockRegistry, EntityRegistry, ItemBackedBlockRegistry, ItemRegistry,
VendoredAnchorRegistry, VendoredEffectRegistry, VendoredEntityRegistry, VendoredItemRegistry,
};
pub fn validate_campaign(c: &Campaign) -> Vec<Diagnostic> {
let items = VendoredItemRegistry::v1_21_11();
let entities = VendoredEntityRegistry::v1_21_11();
let anchors = VendoredAnchorRegistry::hello_world();
validate_campaign_with(c, &items, &anchors, &entities)
}
pub fn validate_campaign_with(
c: &Campaign,
items: &dyn ItemRegistry,
anchors: &dyn AnchorRegistry,
entities: &dyn EntityRegistry,
) -> Vec<Diagnostic> {
let mut d: Vec<Diagnostic> = Vec::new();
crate::envelope::envelope_checks(c, &mut d);
crate::world::world_id_syntax(c, &mut d);
crate::npc::npc_id_syntax(c, &mut d);
crate::class::class_id_syntax(c, &mut d);
crate::quest_plan::plan_id_syntax(c, &mut d);
crate::quest::check::quest_id_syntax(c, &mut d);
crate::actor::actor_id_syntax(c, &mut d);
crate::assembly::assembly_id_syntax(c, &mut d);
crate::dialogue::dialogue_id_syntax(c, &mut d);
crate::world::world_id_uniqueness(c, &mut d);
crate::npc::npc_id_uniqueness(c, &mut d);
crate::class::class_id_uniqueness(c, &mut d);
crate::quest_plan::plan_id_uniqueness(c, &mut d);
crate::quest::check::quest_id_uniqueness(c, &mut d);
crate::actor::actor_id_uniqueness(c, &mut d);
crate::assembly::assembly_id_uniqueness(c, &mut d);
crate::dialogue::dialogue_id_uniqueness(c, &mut d);
crate::npc::npc_dangling_refs(c, &mut d);
crate::world::atmosphere_dangling_refs(c, &mut d);
crate::quest_plan::plan_dangling_refs(c, &mut d);
crate::quest::check::quest_dangling_refs(c, &mut d);
crate::dialogue::dialogue_graph_checks(c, &mut d);
crate::quest_plan::plan_checks(c, &mut d);
crate::quest::check::after_ordering(c, &mut d);
crate::trigger::press_answer_checks(c, &mut d);
crate::trigger::press_obligation_checks(c, &mut d);
crate::world::horizon_param_checks(c, &mut d);
crate::world::world_checks(c, &mut d);
crate::world::lighting_range_checks(c, &mut d);
crate::state::state_checks(c, &mut d);
crate::state::gate_contradiction_checks(c, &mut d);
crate::state::read_after_write_checks(c, &mut d);
crate::lethal::lethal_stage_checks(c, &mut d);
crate::quest::check::status_effect_checks(c, &VendoredEffectRegistry::v1_21_11(), &mut d);
crate::economy::economy_checks(c, &mut d);
crate::body::body_traversal_checks(c, &mut d);
crate::world::prefab_binding(c, anchors, &mut d);
let providers = AnchorProviders::build(c, anchors);
crate::npc::npc_anchor_checks(c, &providers, &mut d);
crate::quest::check::quest_anchor_checks(c, &providers, &mut d);
crate::trigger::trigger_anchor_checks(c, &providers, &mut d);
crate::class::kit_item_checks(c, items, &mut d);
crate::quest::check::cross_stage(c, &mut d);
crate::dialogue::npc_tree_checks(c, &mut d);
crate::class::item_gate_class_checks(c, &mut d);
crate::wave::wave_decl_checks(c, entities, &mut d);
crate::quest::check::quest_reference_checks(c, items, anchors, &mut d);
crate::trigger::trigger_effect_flag_checks(c, &mut d);
let blocks = ItemBackedBlockRegistry::new(items);
let effects_reg = VendoredEffectRegistry::v1_21_11();
crate::npc::npc_skin_checks(c, &mut d);
crate::wave::mob_effect_checks(c, &effects_reg, &mut d);
crate::trigger::trigger_prop_checks(c, &blocks, &mut d);
crate::quest::check::quest_prop_checks(c, &blocks, &mut d);
let flags = collect_declared_flags(c);
crate::trigger::trigger_decl_checks(c, anchors, &blocks, &flags, &mut d);
crate::dialogue::dialogue_flag_checks(c, &flags, &mut d);
let npc_ids: BTreeSet<&str> = c.npcs.content.npcs.iter().map(|n| n.id.as_str()).collect();
crate::npc::despawned_ref_check(c, &npc_ids, &mut d);
crate::npc::deferred_npc_checks(c, &npc_ids, &mut d);
crate::cutscene::cutscene_style_checks(c, &mut d);
crate::actor::actor_checks(c, anchors, entities, &mut d);
crate::quest::check::carrier_one_checks(c, &mut d);
crate::wave::wave_equipment_checks(c, items, &mut d);
crate::actor::actor_equipment_checks(c, items, &mut d);
crate::equipment::fit_checks(c, items, &mut d);
crate::wave::check_drops(c, &c.quests.content, items, &mut d);
crate::wave::rest_reseat_checks(c, &mut d);
crate::class::bonfire_flask_checks(c, declares_bonfire(c), &mut d);
crate::assembly::assembly_checks(c, anchors, &mut d);
crate::trap::trap_checks(c, items, entities, anchors, &mut d);
crate::shortcut::shortcut_checks(c, anchors, &mut d);
crate::ambush::ambush_checks(c, &mut d);
crate::timed_gate::timed_gate_checks(c, anchors, &mut d);
crate::loot::loot_checks(c, items, anchors, &mut d);
crate::quest::check::give_item_enchantment_checks(c, &mut d);
crate::wave::lane_checks(c, anchors, &mut d);
crate::wave::difficulty_checks(c, &mut d);
crate::firework::firework_checks(c, &mut d);
crate::perception::perception_checks(c, &mut d);
crate::healthbar::health_bar_checks(c, &mut d);
if c.world_edits.is_some() {
let blocks = ItemBackedBlockRegistry::new(items);
crate::world_edits::world_edits_checks(c, &blocks, &mut d);
}
if c.geometry_brief.is_some() || c.layout_graph.is_some() || c.site_plan.is_some() {
let mut reads = crate::metrics::Reads::new();
crate::layout::check(c, &mut reads, &mut d);
crate::siteplan::check(c, &mut reads, &mut d);
let stage = if c.site_plan.is_some() {
"site-plan"
} else if c.layout_graph.is_some() {
"layout-graph"
} else {
"geometry-brief"
};
if let Some(notice) = Metrics::table().notice(&reads, stage) {
d.push(notice);
}
}
crate::quest_plan::branch_point_checks(c, &mut d);
crate::loot::collect_container_claim_checks(c, &mut d);
crate::quest_plan::happening_subject_checks(c, &mut d);
let effects_reg = VendoredEffectRegistry::v1_21_11();
crate::class::kit_potion_checks(c, &effects_reg, &mut d);
crate::lethal::lethal_volume_checks(c, anchors, &mut d);
crate::r#loop::loop_checks(c, anchors, &mut d);
crate::economy::shop_anchor_checks(c, anchors, &mut d);
crate::economy::purchase_checks(c, &mut d);
crate::onkill::on_kill_checks(c, &mut d);
crate::design::check(c, &mut d);
crate::celestial::check(c, &mut d);
d
}
pub(crate) struct AnchorProviders {
per_area: BTreeMap<String, BTreeSet<String>>,
kinds: BTreeMap<String, crate::layout::StationKind>,
union: BTreeSet<String>,
deferred: bool,
all_areas_known: bool,
placement: crate::placement::Placement,
}
impl AnchorProviders {
pub(crate) fn build(c: &Campaign, anchors: &dyn AnchorRegistry) -> Self {
let mut per_area: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut deferred = false;
let mut declared_areas = c.world.content.areas.len();
let mut kinds: BTreeMap<String, crate::layout::StationKind> = BTreeMap::new();
if c.site_plan.is_some() {
declared_areas += 1;
if crate::placement::anchor_vocabulary_unknowable(c) {
deferred = true;
} else {
kinds = crate::siteplan::synthesized_anchor_kinds(c);
per_area.insert(
crate::siteplan::SITE_AREA.to_string(),
kinds.keys().cloned().collect(),
);
}
}
for a in &c.world.content.areas {
if let Some(prefab) = &a.prefab {
if let Some(set) = anchors.anchors_for(prefab) {
per_area.insert(a.id.as_str().to_string(), set.clone());
}
} else if a.prefab_pool.is_some() {
deferred = true;
}
}
let union: BTreeSet<String> = per_area.values().flatten().cloned().collect();
let all_areas_known = declared_areas == per_area.len();
Self {
per_area,
kinds,
union,
deferred,
all_areas_known,
placement: crate::placement::Placement::of(c),
}
}
pub(crate) fn wrong_kind(
&self,
name: &str,
demands: crate::layout::StationKind,
) -> Option<crate::layout::StationKind> {
let declared = *self.kinds.get(name)?;
(declared != demands).then_some(declared)
}
pub(crate) fn for_area(&self, area: &str) -> Option<&BTreeSet<String>> {
self.per_area.get(area)
}
pub(crate) fn resolvable(&self, name: &str) -> bool {
self.deferred || self.union.contains(name)
}
pub(crate) fn all_areas_known(&self) -> bool {
self.all_areas_known
}
pub(crate) fn union(&self) -> &BTreeSet<String> {
&self.union
}
pub(crate) fn anchor_remedy<'a>(&self, prefab: &'a str) -> &'a str {
self.placement.anchor_remedy(prefab)
}
}
pub(crate) fn station_kind_diag(
providers: &AnchorProviders,
name: &str,
demands: impl Into<Option<crate::layout::StationKind>>,
what: &str,
stage: &'static str,
path: String,
) -> Option<Diagnostic> {
let demands = demands.into()?;
let declared = providers.wrong_kind(name, demands)?;
Some(Diagnostic::error(
crate::layout::DW_STATION_KIND,
stage,
path,
format!(
"`{name}` is declared as a {got} station, and {what} demands a {want}. A station's \
`kind` is its SHAPE: a `point` is a cell a body is put at — a seat, a subject, an \
affordance, the centre of an anchor-centred volume — and a `gate` is a region that \
seals and clears, which is what `open-gate`, `close-gate`, a `shortcut` and a \
`timed-gate` address. This is read from the layout graph's declaration, so it is \
answered before any piece is bound and the same shape is demanded of the piece \
anchor when one is. Either change this station's `kind` to `{want}` in the layout \
graph, or name a station that is already one.",
got = declared.word(),
want = demands.word(),
),
))
}
pub fn declares_bonfire(c: &Campaign) -> bool {
collected_effect_any(c, |eff| eff.bonfire().is_some())
}
pub fn declares_checkpoint(c: &Campaign) -> bool {
collected_effect_any(c, |eff| {
eff.bonfire().is_some() || eff.set_checkpoint().is_some()
}) || c.dialogue.content.dialogues.iter().any(|tree| {
tree.nodes.iter().any(|node| {
node.options
.iter()
.any(|opt| opt.effects.iter().any(|e| e.set_checkpoint().is_some()))
})
})
}
fn collected_effect_any(c: &Campaign, pred: impl Fn(&QuestEffect) -> bool) -> bool {
use crate::EffectSite;
let mut found = false;
crate::for_each_campaign_effect(c, &mut |_, site, eff| {
let collected = match site {
EffectSite::Objective { .. }
| EffectSite::QuestComplete { .. }
| EffectSite::Trigger { .. } => true,
EffectSite::Trap { .. }
| EffectSite::DialogueRespawn { .. }
| EffectSite::ShortcutUnlock { .. }
| EffectSite::ShopOffer { .. }
| EffectSite::OnDeath
| EffectSite::OnKill { .. }
| EffectSite::AssemblyLand { .. }
| EffectSite::LoopCross { .. } => false,
};
found |= collected && pred(eff);
});
found
}
pub(crate) fn for_each_effect_deep(q: &crate::Quest, mut f: impl FnMut(String, &QuestEffect)) {
fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
f(path.clone(), eff);
for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
for (j, inner) in list.iter().enumerate() {
descend(format!("{path}/{pseg}/{j}"), inner, f);
}
}
}
for (key, effs) in &q.on_objective_complete {
for (m, eff) in effs.iter().enumerate() {
descend(format!("on_objective_complete/{key}/{m}"), eff, &mut f);
}
}
for (m, eff) in q.on_complete.iter().enumerate() {
descend(format!("on_complete/{m}"), eff, &mut f);
}
}
pub(crate) fn for_each_trigger_effect_deep(
t: &crate::EnvTrigger,
mut f: impl FnMut(String, &QuestEffect),
) {
fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
f(path.clone(), eff);
for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
for (j, inner) in list.iter().enumerate() {
descend(format!("{path}/{pseg}/{j}"), inner, f);
}
}
}
for (m, eff) in t.effects.iter().enumerate() {
descend(format!("effects/{m}"), eff, &mut f);
}
}
pub(crate) fn for_each_trap_payload_deep(t: &crate::Trap, mut f: impl FnMut(String, &QuestEffect)) {
fn descend(path: String, eff: &QuestEffect, f: &mut dyn FnMut(String, &QuestEffect)) {
f(path.clone(), eff);
for (pseg, _kseg, list) in eff.nested_effect_lists_labeled() {
for (j, inner) in list.iter().enumerate() {
descend(format!("{path}/{pseg}/{j}"), inner, f);
}
}
}
for (m, eff) in t.payload.iter().enumerate() {
descend(format!("payload/{m}"), eff, &mut f);
}
}
pub(crate) fn split_blockstate(block: &str) -> Result<&str, String> {
let Some(open) = block.find('[') else {
return Ok(block);
};
let rest = &block[open..];
if !rest.ends_with(']') {
return Err(format!(
"malformed blockstate in `{block}` — the `[...]` suffix must close with `]` \
(e.g. `minecraft:grindstone[face=floor]`)"
));
}
let inner = &rest[1..rest.len() - 1];
if inner.trim().is_empty() {
return Err(format!(
"malformed blockstate in `{block}` — the `[...]` suffix is empty; drop the brackets or \
add a `key=value` property"
));
}
let is_token = |s: &str| {
!s.is_empty()
&& s.chars()
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
};
for prop in inner.split(',') {
let mut kv = prop.splitn(2, '=');
let key = kv.next().unwrap_or("").trim();
match kv.next().map(str::trim) {
Some(val) if is_token(key) && is_token(val) => {}
_ => {
return Err(format!(
"malformed blockstate in `{block}` — each property must be `key=value` with \
lowercase `[a-z0-9_]` tokens (e.g. `face=floor`)"
));
}
}
}
Ok(&block[..open])
}
pub(crate) fn check_block_field(
blocks: &dyn BlockRegistry,
block: &str,
path: String,
kind: &str,
example: &str,
d: &mut Vec<Diagnostic>,
) {
match split_blockstate(block) {
Ok(base) => {
if !blocks.contains(base) {
d.push(Diagnostic::error(
codes::BLOCK_UNKNOWN,
"quests",
path,
format!(
"`{kind}` block `{block}` is not a known 1.21.11 block id — use a valid \
namespaced block id (e.g. `{example}`)"
),
));
}
}
Err(reason) => {
d.push(Diagnostic::error(
codes::BLOCK_UNKNOWN,
"quests",
path,
reason,
));
}
}
}
pub(crate) fn collect_declared_flags(c: &Campaign) -> BTreeSet<&str> {
let mut flags: BTreeSet<&str> = BTreeSet::new();
crate::for_each_campaign_effect(c, &mut |_path, _site, e| {
if let Some(f) = e.set_flag() {
flags.insert(f.as_str());
}
});
for t in &c.quests.content.traps {
if let Some(dis) = &t.disarm {
flags.insert(dis.sets_flag.as_str());
}
}
for g in &c.quests.content.timed_gates {
if let Some(dis) = &g.disarm {
flags.insert(dis.sets_flag.as_str());
}
}
for tree in &c.dialogue.content.dialogues {
for node in &tree.nodes {
for opt in &node.options {
for e in &opt.effects {
if let Some(f) = e.set_flag() {
flags.insert(f.as_str());
}
}
}
}
}
flags
}
pub(crate) fn quest_ancestors(c: &Campaign) -> BTreeMap<&str, BTreeSet<&str>> {
let deps: BTreeMap<&str, &Vec<crate::ids::QuestId>> = c
.quest_plan
.content
.quests
.iter()
.map(|q| (q.id.as_str(), &q.depends_on))
.collect();
let mut out = BTreeMap::new();
for q in c.quest_plan.content.quests.iter() {
let mut anc: BTreeSet<&str> = BTreeSet::new();
let mut stack = vec![q.id.as_str()];
while let Some(cur) = stack.pop() {
if let Some(ds) = deps.get(cur) {
for dep in ds.iter() {
if anc.insert(dep.as_str()) {
stack.push(dep.as_str());
}
}
}
}
out.insert(q.id.as_str(), anc);
}
out
}
pub(crate) fn graph_has_cycle<'a>(
nodes: &[&'a str],
edges: &BTreeMap<&'a str, Vec<&'a str>>,
) -> bool {
let mut color: BTreeMap<&'a str, u8> = BTreeMap::new();
for &n in nodes {
if color.get(n).copied().unwrap_or(0) == 0 && dfs_cycle(n, edges, &mut color) {
return true;
}
}
false
}
fn dfs_cycle<'a>(
node: &'a str,
edges: &BTreeMap<&'a str, Vec<&'a str>>,
color: &mut BTreeMap<&'a str, u8>,
) -> bool {
color.insert(node, 1);
if let Some(neis) = edges.get(node) {
for &n in neis {
match color.get(n).copied().unwrap_or(0) {
0 => {
if dfs_cycle(n, edges, color) {
return true;
}
}
1 => return true,
_ => {}
}
}
}
color.insert(node, 2);
false
}
pub fn produced_flags(c: &Campaign) -> BTreeSet<String> {
let mut out = BTreeSet::new();
crate::for_each_campaign_effect(c, &mut |_p, _site, eff| {
if let Verb::SetFlag { flag, .. } = &eff.verb {
out.insert(flag.as_str().to_string());
}
});
for t in &c.dialogue.content.dialogues {
for n in &t.nodes {
for o in &n.options {
for e in &o.effects {
if let crate::DialogueEffect::SetFlag { flag } = e {
out.insert(flag.as_str().to_string());
}
}
}
}
}
for trap in &c.quests.content.traps {
if let Some(dis) = &trap.disarm {
out.insert(dis.sets_flag.as_str().to_string());
}
}
out
}