From ddef30984f9b197b97b0bb76e2a2233d99c0c45c Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Fri, 11 Apr 2025 23:42:43 -0700 Subject: [PATCH 1/5] validator: remove placeholder comments --- validation/src/check.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 1f957b4..7f15db5 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -468,7 +468,6 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ writeln!(f,"")?; } if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{ - // perhaps there are extra end zones (context.extra) if !context.extra.is_empty(){ write!(f,"Extra finish zones with no matching start zone: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| @@ -476,7 +475,6 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ )?; writeln!(f,"")?; } - // perhaps there are missing end zones (context.missing) if !context.missing.is_empty(){ write!(f,"Missing finish zones: ")?; write_comma_separated(f,context.missing.iter(),|f,mode_id| @@ -486,7 +484,6 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ } } if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{ - // perhaps there are extra end zones (context.extra) if !context.extra.is_empty(){ write!(f,"Extra anticheat zones with no matching start zone: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| -- 2.47.1 From 109b24061ad99879e063856a9208be7a8b273596 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Sat, 12 Apr 2025 11:32:10 -0700 Subject: [PATCH 2/5] validator: pluralize some error messages --- validation/src/check.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 7f15db5..56cf35b 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -469,14 +469,16 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ } if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{ if !context.extra.is_empty(){ - write!(f,"Extra finish zones with no matching start zone: ")?; + let plural=if context.extra.len()==1{"zone"}else{"zones"}; + write!(f,"Extra finish {plural} with no matching start zone: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| write_zone!(f,mode_id,"Finish") )?; writeln!(f,"")?; } if !context.missing.is_empty(){ - write!(f,"Missing finish zones: ")?; + let plural=if context.missing.len()==1{"zone"}else{"zones"}; + write!(f,"Missing finish {plural}: ")?; write_comma_separated(f,context.missing.iter(),|f,mode_id| write_zone!(f,mode_id,"Finish") )?; @@ -485,7 +487,8 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ } if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{ if !context.extra.is_empty(){ - write!(f,"Extra anticheat zones with no matching start zone: ")?; + let plural=if context.extra.len()==1{"zone"}else{"zones"}; + write!(f,"Extra anticheat {plural} with no matching start zone: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| write_zone!(f,mode_id,"Anticheat") )?; @@ -496,16 +499,16 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ writeln!(f,"Model has no Spawn1")?; } if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{ - write!(f,"Duplicate spawn zones: ")?; + write!(f,"Duplicate Spawn: ")?; write_comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)| write!(f,"Spawn{spawn_id}({count} duplicates)") )?; writeln!(f,"")?; } if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{ - write!(f,"Duplicate wormhole out: ")?; - write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)| - write!(f,"WormholeOut{wormhole_out_id}({count} duplicates)") + write!(f,"Duplicate WormholeOut: ")?; + write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_id),count)| + write!(f,"WormholeOut{wormhole_id}({count} duplicates)") )?; writeln!(f,"")?; } -- 2.47.1 From a942c81ea8a3831b3c067842f81064b7f8ff3279 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Sat, 12 Apr 2025 11:13:30 -0700 Subject: [PATCH 3/5] validator: add teleport and wormhole set difference checks --- validation/src/check.rs | 149 +++++++++++++++++++++++++++++++--------- 1 file changed, 117 insertions(+), 32 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 56cf35b..5821148 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -40,25 +40,24 @@ impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{ } } -enum Zone{ - Start(ModeID), - Finish(ModeID), - Anticheat(ModeID), -} - #[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] struct ModeID(u64); impl ModeID{ const MAIN:Self=Self(0); const BONUS:Self=Self(1); } +enum Zone{ + Start(ModeID), + Finish(ModeID), + Anticheat(ModeID), +} #[allow(dead_code)] -pub enum ZoneParseError{ +pub enum IDParseError{ NoCaptures, ParseInt(core::num::ParseIntError) } impl std::str::FromStr for Zone{ - type Err=ZoneParseError; + type Err=IDParseError; fn from_str(s:&str)->Result<Self,Self::Err>{ match s{ "MapStart"=>Ok(Self::Start(ModeID::MAIN)), @@ -70,38 +69,78 @@ impl std::str::FromStr for Zone{ other=>{ let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$"); if let Some(captures)=bonus_start_pattern.captures(other){ - return Ok(Self::Start(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?))); + return Ok(Self::Start(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?))); } let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$"); if let Some(captures)=bonus_finish_pattern.captures(other){ - return Ok(Self::Finish(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?))); + return Ok(Self::Finish(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?))); } let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$"); if let Some(captures)=bonus_finish_pattern.captures(other){ - return Ok(Self::Anticheat(ModeID(captures[1].parse().map_err(ZoneParseError::ParseInt)?))); + return Ok(Self::Anticheat(ModeID(captures[1].parse().map_err(IDParseError::ParseInt)?))); } - Err(ZoneParseError::NoCaptures) + Err(IDParseError::NoCaptures) } } } } - -#[derive(Debug,Hash,Eq,PartialEq)] +#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] struct SpawnID(u64); impl SpawnID{ const FIRST:Self=Self(1); } -#[derive(Debug,Hash,Eq,PartialEq)] -struct WormholeOutID(u64); +enum SpawnTeleport{ + Teleport(SpawnID), + Spawn(SpawnID), +} +impl std::str::FromStr for SpawnTeleport{ + type Err=IDParseError; + fn from_str(s:&str)->Result<Self,Self::Err>{ + // Trigger ForceTrigger Teleport ForceTeleport SpawnAt ForceSpawnAt + let bonus_start_pattern=lazy_regex::lazy_regex!(r"^(?:Force)?(Teleport|SpawnAt|Trigger)(\d+)$"); + if let Some(captures)=bonus_start_pattern.captures(s){ + return Ok(Self::Teleport(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?))); + } + // Spawn + let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$"); + if let Some(captures)=bonus_finish_pattern.captures(s){ + return Ok(Self::Spawn(SpawnID(captures[1].parse().map_err(IDParseError::ParseInt)?))); + } + Err(IDParseError::NoCaptures) + } +} + +#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)] +struct WormholeID(u64); +enum Wormhole{ + In(WormholeID), + Out(WormholeID), +} +impl std::str::FromStr for Wormhole{ + type Err=IDParseError; + fn from_str(s:&str)->Result<Self,Self::Err>{ + let bonus_start_pattern=lazy_regex::lazy_regex!(r"^WormholeIn(\d+)$"); + if let Some(captures)=bonus_start_pattern.captures(s){ + return Ok(Self::In(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?))); + } + let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$"); + if let Some(captures)=bonus_finish_pattern.captures(s){ + return Ok(Self::Out(WormholeID(captures[1].parse().map_err(IDParseError::ParseInt)?))); + } + Err(IDParseError::NoCaptures) + } +} #[derive(Default)] struct Counts{ mode_start_counts:HashMap<ModeID,u64>, mode_finish_counts:HashMap<ModeID,u64>, mode_anticheat_counts:HashMap<ModeID,u64>, + teleport_counts:HashMap<SpawnID,u64>, spawn_counts:HashMap<SpawnID,u64>, - wormhole_out_counts:HashMap<WormholeOutID,u64>, + wormhole_in_counts:HashMap<WormholeID,u64>, + wormhole_out_counts:HashMap<WormholeID,u64>, } pub struct ModelInfo<'a>{ @@ -126,19 +165,17 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d Ok(Zone::Anticheat(mode_id))=>*counts.mode_anticheat_counts.entry(mode_id).or_insert(0)+=1, Err(_)=>(), } - // Spawns - let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$"); - if let Some(captures)=spawn_pattern.captures(instance.name.as_str()){ - if let Ok(spawn_id)=captures[1].parse(){ - *counts.spawn_counts.entry(SpawnID(spawn_id)).or_insert(0)+=1; - } + // Spawns & Teleports + match instance.name.parse(){ + Ok(SpawnTeleport::Teleport(spawn_id))=>*counts.teleport_counts.entry(spawn_id).or_insert(0)+=1, + Ok(SpawnTeleport::Spawn(spawn_id))=>*counts.spawn_counts.entry(spawn_id).or_insert(0)+=1, + Err(_)=>(), } - // WormholeOuts - let wormhole_out_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$"); - if let Some(captures)=wormhole_out_pattern.captures(instance.name.as_str()){ - if let Ok(wormhole_out_id)=captures[1].parse(){ - *counts.wormhole_out_counts.entry(WormholeOutID(wormhole_out_id)).or_insert(0)+=1; - } + // Wormholes + match instance.name.parse(){ + Ok(Wormhole::In(wormhole_id))=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1, + Ok(Wormhole::Out(wormhole_id))=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1, + Err(_)=>(), } } } @@ -289,14 +326,19 @@ pub struct MapCheck<'a>{ mode_start_counts:DuplicateCheck<ModeID>, // At least one finish zone for each start zone, and no finishes with no start mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID>>, - // check for dangling MapAnticheat zones (no associated MapStart) + // Check for dangling MapAnticheat zones (no associated MapStart) mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID>>, // Spawn1 must exist spawn1:Result<(),()>, + // Check for dangling Teleport# (no associated Spawn#) + teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID>>, // No duplicate Spawn# spawn_counts:DuplicateCheck<SpawnID>, + // Check for dangling WormholeIn# (no associated WormholeOut#) + wormhole_in_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<WormholeID>>, // No duplicate WormholeOut# (duplicate WormholeIn# ok) - wormhole_out_counts:DuplicateCheck<WormholeOutID>, + // No dangling WormholeOut# + wormhole_out_counts:DuplicateCheck<WormholeID>, } impl<'a> ModelInfo<'a>{ @@ -354,9 +396,19 @@ impl<'a> ModelInfo<'a>{ // There must be exactly one start zone for every mode in the map. let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check(); + // Check that there are no Teleports without a corresponding Spawn. + // Spawns are allowed to have 0 Teleports. + let teleport_counts=SetDifferenceCheckContextAllowNone::new(self.counts.teleport_counts) + .check(&self.counts.spawn_counts); + // There must be exactly one of any perticular spawn id in the map. let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check(); + // Check that at least one WormholeIn exists for each WormholeOut. + // This also checks that there are no WormholeIn without a corresponding WormholeOut. + let wormhole_in_counts=SetDifferenceCheckContextAtLeastOne::new(self.counts.wormhole_in_counts) + .check(&self.counts.wormhole_out_counts); + // There must be exactly one of any perticular wormhole out id in the map. let wormhole_out_counts=DuplicateCheckContext(self.counts.wormhole_out_counts).check(); @@ -371,7 +423,9 @@ impl<'a> ModelInfo<'a>{ mode_finish_counts, mode_anticheat_counts, spawn1, + teleport_counts, spawn_counts, + wormhole_in_counts, wormhole_out_counts, } } @@ -391,7 +445,9 @@ impl<'a> MapCheck<'a>{ mode_finish_counts:SetDifferenceCheck(Ok(())), mode_anticheat_counts:SetDifferenceCheck(Ok(())), spawn1:Ok(()), + teleport_counts:SetDifferenceCheck(Ok(())), spawn_counts:DuplicateCheck(Ok(())), + wormhole_in_counts:SetDifferenceCheck(Ok(())), wormhole_out_counts:DuplicateCheck(Ok(())), }=>{ Ok(MapInfoOwned{ @@ -498,6 +554,17 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if let Err(())=&self.spawn1{ writeln!(f,"Model has no Spawn1")?; } + if let SetDifferenceCheck(Err(context))=&self.teleport_counts{ + if !context.extra.is_empty(){ + // TODO: include original names of objects in hashmap value as Vec<&str> + let plural=if context.extra.len()==1{"object"}else{"objects"}; + write!(f,"Extra Spawn-type {plural} with no matching Spawn: ")?; + write_comma_separated(f,context.extra.iter(),|f,(SpawnID(spawn_id),_count)| + write!(f,"Teleport or Trigger or SpawnAt #{spawn_id}") + )?; + writeln!(f,"")?; + } + } if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{ write!(f,"Duplicate Spawn: ")?; write_comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)| @@ -505,9 +572,27 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ )?; writeln!(f,"")?; } + if let SetDifferenceCheck(Err(context))=&self.wormhole_in_counts{ + if !context.extra.is_empty(){ + write!(f,"WormholeIn with no matching WormholeOut: ")?; + write_comma_separated(f,context.extra.iter(),|f,(WormholeID(wormhole_id),_count)| + write!(f,"WormholeIn{wormhole_id}") + )?; + writeln!(f,"")?; + } + if !context.missing.is_empty(){ + // This counts WormholeIn objects, but + // flipped logic is easier to understand + write!(f,"WormholeOut with no matching WormholeIn: ")?; + write_comma_separated(f,context.missing.iter(),|f,WormholeID(wormhole_id)| + write!(f,"WormholeOut{wormhole_id}") + )?; + writeln!(f,"")?; + } + } if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{ write!(f,"Duplicate WormholeOut: ")?; - write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_id),count)| + write_comma_separated(f,context.iter(),|f,(WormholeID(wormhole_id),count)| write!(f,"WormholeOut{wormhole_id}({count} duplicates)") )?; writeln!(f,"")?; -- 2.47.1 From e4f710c83ffda6a283fa3ceefb6e3eda14d07e6d Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Sat, 12 Apr 2025 11:58:27 -0700 Subject: [PATCH 4/5] validator: include original names of some objects in error message --- validation/src/check.rs | 93 ++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index 5821148..e17540c 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -133,11 +133,11 @@ impl std::str::FromStr for Wormhole{ } #[derive(Default)] -struct Counts{ - mode_start_counts:HashMap<ModeID,u64>, - mode_finish_counts:HashMap<ModeID,u64>, - mode_anticheat_counts:HashMap<ModeID,u64>, - teleport_counts:HashMap<SpawnID,u64>, +struct Counts<'a>{ + mode_start_counts:HashMap<ModeID,Vec<&'a str>>, + mode_finish_counts:HashMap<ModeID,Vec<&'a str>>, + mode_anticheat_counts:HashMap<ModeID,Vec<&'a str>>, + teleport_counts:HashMap<SpawnID,Vec<&'a str>>, spawn_counts:HashMap<SpawnID,u64>, wormhole_in_counts:HashMap<WormholeID,u64>, wormhole_out_counts:HashMap<WormholeID,u64>, @@ -147,7 +147,7 @@ pub struct ModelInfo<'a>{ model_class:&'a str, model_name:&'a str, map_info:MapInfo<'a>, - counts:Counts, + counts:Counts<'a>, } pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{ @@ -160,14 +160,14 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d if class_is_a(instance.class.as_str(),"BasePart"){ // Zones match instance.name.parse(){ - Ok(Zone::Start(mode_id))=>*counts.mode_start_counts.entry(mode_id).or_insert(0)+=1, - Ok(Zone::Finish(mode_id))=>*counts.mode_finish_counts.entry(mode_id).or_insert(0)+=1, - Ok(Zone::Anticheat(mode_id))=>*counts.mode_anticheat_counts.entry(mode_id).or_insert(0)+=1, + Ok(Zone::Start(mode_id))=>counts.mode_start_counts.entry(mode_id).or_default().push(instance.name.as_str()), + Ok(Zone::Finish(mode_id))=>counts.mode_finish_counts.entry(mode_id).or_default().push(instance.name.as_str()), + Ok(Zone::Anticheat(mode_id))=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()), Err(_)=>(), } // Spawns & Teleports match instance.name.parse(){ - Ok(SpawnTeleport::Teleport(spawn_id))=>*counts.teleport_counts.entry(spawn_id).or_insert(0)+=1, + Ok(SpawnTeleport::Teleport(spawn_id))=>counts.teleport_counts.entry(spawn_id).or_default().push(instance.name.as_str()), Ok(SpawnTeleport::Spawn(spawn_id))=>*counts.spawn_counts.entry(spawn_id).or_insert(0)+=1, Err(_)=>(), } @@ -224,13 +224,13 @@ fn check_empty(value:&str)->Result<&str,StringEmpty>{ } // check for duplicate objects -pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>); -pub struct DuplicateCheck<ID>(Result<(),DuplicateCheckContext<ID>>); -impl<ID> DuplicateCheckContext<ID>{ - fn check(self)->DuplicateCheck<ID>{ +pub struct DuplicateCheckContext<ID,T>(HashMap<ID,T>); +pub struct DuplicateCheck<ID,T>(Result<(),DuplicateCheckContext<ID,T>>); +impl<ID,T> DuplicateCheckContext<ID,T>{ + fn check(self,f:impl Fn(&T)->bool)->DuplicateCheck<ID,T>{ let Self(mut set)=self; // remove correct entries - set.retain(|_,&mut c|c!=1); + set.retain(|_,c|f(c)); // if any entries remain, they are incorrect if set.is_empty(){ DuplicateCheck(Ok(())) @@ -241,23 +241,23 @@ impl<ID> DuplicateCheckContext<ID>{ } // check that there is at least one matching item for each item in a reference set, and no extra items -pub struct SetDifferenceCheckContextAllowNone<ID>{ - extra:HashMap<ID,u64>, +pub struct SetDifferenceCheckContextAllowNone<ID,T>{ + extra:HashMap<ID,T>, } -pub struct SetDifferenceCheckContextAtLeastOne<ID>{ - extra:HashMap<ID,u64>, +pub struct SetDifferenceCheckContextAtLeastOne<ID,T>{ + extra:HashMap<ID,T>, missing:HashSet<ID>, } pub struct SetDifferenceCheck<Context>(Result<(),Context>); -impl<ID> SetDifferenceCheckContextAllowNone<ID>{ - fn new(initial_set:HashMap<ID,u64>)->Self{ +impl<ID,T> SetDifferenceCheckContextAllowNone<ID,T>{ + fn new(initial_set:HashMap<ID,T>)->Self{ Self{ extra:initial_set, } } } -impl<ID:Eq+std::hash::Hash> SetDifferenceCheckContextAllowNone<ID>{ - fn check<T>(mut self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<Self>{ +impl<ID:Eq+std::hash::Hash,T> SetDifferenceCheckContextAllowNone<ID,T>{ + fn check<U>(mut self,reference_set:&HashMap<ID,U>)->SetDifferenceCheck<Self>{ // remove correct entries for (id,_) in reference_set{ self.extra.remove(id); @@ -270,16 +270,16 @@ impl<ID:Eq+std::hash::Hash> SetDifferenceCheckContextAllowNone<ID>{ } } } -impl<ID> SetDifferenceCheckContextAtLeastOne<ID>{ - fn new(initial_set:HashMap<ID,u64>)->Self{ +impl<ID,T> SetDifferenceCheckContextAtLeastOne<ID,T>{ + fn new(initial_set:HashMap<ID,T>)->Self{ Self{ extra:initial_set, missing:HashSet::new(), } } } -impl<ID:Copy+Eq+std::hash::Hash> SetDifferenceCheckContextAtLeastOne<ID>{ - fn check<T>(mut self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<Self>{ +impl<ID:Copy+Eq+std::hash::Hash,T> SetDifferenceCheckContextAtLeastOne<ID,T>{ + fn check<U>(mut self,reference_set:&HashMap<ID,U>)->SetDifferenceCheck<Self>{ // remove correct entries for (id,_) in reference_set{ if self.extra.remove(id).is_none(){ @@ -323,22 +323,22 @@ pub struct MapCheck<'a>{ // MapStart must exist mapstart:Result<(),()>, // No duplicate map starts (including bonuses) - mode_start_counts:DuplicateCheck<ModeID>, + mode_start_counts:DuplicateCheck<ModeID,Vec<&'a str>>, // At least one finish zone for each start zone, and no finishes with no start - mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID>>, + mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID,Vec<&'a str>>>, // Check for dangling MapAnticheat zones (no associated MapStart) - mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID>>, + mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID,Vec<&'a str>>>, // Spawn1 must exist spawn1:Result<(),()>, // Check for dangling Teleport# (no associated Spawn#) - teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID>>, + teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID,Vec<&'a str>>>, // No duplicate Spawn# - spawn_counts:DuplicateCheck<SpawnID>, + spawn_counts:DuplicateCheck<SpawnID,u64>, // Check for dangling WormholeIn# (no associated WormholeOut#) - wormhole_in_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<WormholeID>>, + wormhole_in_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<WormholeID,u64>>, // No duplicate WormholeOut# (duplicate WormholeIn# ok) // No dangling WormholeOut# - wormhole_out_counts:DuplicateCheck<WormholeID>, + wormhole_out_counts:DuplicateCheck<WormholeID,u64>, } impl<'a> ModelInfo<'a>{ @@ -394,7 +394,7 @@ impl<'a> ModelInfo<'a>{ .check(&self.counts.mode_start_counts); // There must be exactly one start zone for every mode in the map. - let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check(); + let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check(|c|c.len()<=1); // Check that there are no Teleports without a corresponding Spawn. // Spawns are allowed to have 0 Teleports. @@ -402,7 +402,7 @@ impl<'a> ModelInfo<'a>{ .check(&self.counts.spawn_counts); // There must be exactly one of any perticular spawn id in the map. - let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check(); + let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check(|&c|c<=1); // Check that at least one WormholeIn exists for each WormholeOut. // This also checks that there are no WormholeIn without a corresponding WormholeOut. @@ -410,7 +410,7 @@ impl<'a> ModelInfo<'a>{ .check(&self.counts.wormhole_out_counts); // There must be exactly one of any perticular wormhole out id in the map. - let wormhole_out_counts=DuplicateCheckContext(self.counts.wormhole_out_counts).check(); + let wormhole_out_counts=DuplicateCheckContext(self.counts.wormhole_out_counts).check(|&c|c<=1); MapCheck{ model_class, @@ -516,9 +516,9 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ } if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{ write!(f,"Duplicate start zones: ")?; - write_comma_separated(f,context.iter(),|f,(mode_id,count)|{ + write_comma_separated(f,context.iter(),|f,(mode_id,names)|{ write_zone!(f,mode_id,"Start")?; - write!(f,"({count} duplicates)")?; + write!(f,"({} duplicates)",names.len())?; Ok(()) })?; writeln!(f,"")?; @@ -527,7 +527,7 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if !context.extra.is_empty(){ let plural=if context.extra.len()==1{"zone"}else{"zones"}; write!(f,"Extra finish {plural} with no matching start zone: ")?; - write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| + write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)| write_zone!(f,mode_id,"Finish") )?; writeln!(f,"")?; @@ -545,7 +545,7 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if !context.extra.is_empty(){ let plural=if context.extra.len()==1{"zone"}else{"zones"}; write!(f,"Extra anticheat {plural} with no matching start zone: ")?; - write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| + write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)| write_zone!(f,mode_id,"Anticheat") )?; writeln!(f,"")?; @@ -555,13 +555,12 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ writeln!(f,"Model has no Spawn1")?; } if let SetDifferenceCheck(Err(context))=&self.teleport_counts{ - if !context.extra.is_empty(){ - // TODO: include original names of objects in hashmap value as Vec<&str> - let plural=if context.extra.len()==1{"object"}else{"objects"}; + for (_,names) in &context.extra{ + let plural=if names.len()==1{"object"}else{"objects"}; write!(f,"Extra Spawn-type {plural} with no matching Spawn: ")?; - write_comma_separated(f,context.extra.iter(),|f,(SpawnID(spawn_id),_count)| - write!(f,"Teleport or Trigger or SpawnAt #{spawn_id}") - )?; + write_comma_separated(f,names.iter(),|f,&name|{ + write!(f,"{name}") + })?; writeln!(f,"")?; } } -- 2.47.1 From 404e1281fffc38fde069423ec7a36c7c08a4b8b3 Mon Sep 17 00:00:00 2001 From: Quaternions <krakow20@gmail.com> Date: Sat, 12 Apr 2025 12:02:34 -0700 Subject: [PATCH 5/5] validator: improve "extra" error messages --- validation/src/check.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/validation/src/check.rs b/validation/src/check.rs index e17540c..e414e08 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -526,7 +526,7 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{ if !context.extra.is_empty(){ let plural=if context.extra.len()==1{"zone"}else{"zones"}; - write!(f,"Extra finish {plural} with no matching start zone: ")?; + write!(f,"No matching start zone for finish {plural}: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)| write_zone!(f,mode_id,"Finish") )?; @@ -544,7 +544,7 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{ if !context.extra.is_empty(){ let plural=if context.extra.len()==1{"zone"}else{"zones"}; - write!(f,"Extra anticheat {plural} with no matching start zone: ")?; + write!(f,"No matching start zone for anticheat {plural}: ")?; write_comma_separated(f,context.extra.iter(),|f,(mode_id,_names)| write_zone!(f,mode_id,"Anticheat") )?; @@ -557,7 +557,7 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if let SetDifferenceCheck(Err(context))=&self.teleport_counts{ for (_,names) in &context.extra{ let plural=if names.len()==1{"object"}else{"objects"}; - write!(f,"Extra Spawn-type {plural} with no matching Spawn: ")?; + write!(f,"No matching Spawn for {plural}: ")?; write_comma_separated(f,names.iter(),|f,&name|{ write!(f,"{name}") })?; -- 2.47.1