diff --git a/validation/src/check.rs b/validation/src/check.rs index f96e5c1..d001dd3 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -43,27 +43,28 @@ 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); +macro_rules! write_zone{ + ($fname:ident,$zone:expr)=>{ + fn $fname(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + match self{ + ModeID(0)=>write!(f,concat!("Map",$zone)), + ModeID(1)=>write!(f,concat!("Bonus",$zone)), + ModeID(other)=>write!(f,concat!("Bonus{}",$zone),other), + } + } + }; +} impl ModeID{ const MAIN:Self=Self(0); const BONUS:Self=Self(1); - fn write_start_zone(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ - match self{ - ModeID(0)=>write!(f,"MapStart"), - ModeID(1)=>write!(f,"BonusStart"), - ModeID(other)=>write!(f,"Bonus{other}Start"), - } - } - fn write_finish_zone(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ - match self{ - ModeID(0)=>write!(f,"MapFinish"), - ModeID(1)=>write!(f,"BonusFinish"), - ModeID(other)=>write!(f,"Bonus{other}Finish"), - } - } + write_zone!(write_start_zone,"Start"); + write_zone!(write_finish_zone,"Finish"); + write_zone!(write_anticheat_zone,"Anticheat"); } #[allow(dead_code)] pub enum ZoneParseError{ @@ -76,8 +77,10 @@ impl std::str::FromStr for Zone{ match s{ "MapStart"=>Ok(Self::Start(ModeID::MAIN)), "MapFinish"=>Ok(Self::Finish(ModeID::MAIN)), + "MapAnticheat"=>Ok(Self::Anticheat(ModeID::MAIN)), "BonusStart"=>Ok(Self::Start(ModeID::BONUS)), "BonusFinish"=>Ok(Self::Finish(ModeID::BONUS)), + "BonusAnticheat"=>Ok(Self::Anticheat(ModeID::BONUS)), other=>{ let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$"); if let Some(captures)=bonus_start_pattern.captures(other){ @@ -87,6 +90,10 @@ impl std::str::FromStr for Zone{ if let Some(captures)=bonus_finish_pattern.captures(other){ return Ok(Self::Finish(ModeID(captures[1].parse().map_err(ZoneParseError::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)?))); + } Err(ZoneParseError::NoCaptures) } } @@ -106,6 +113,7 @@ struct WormholeOutID(u64); struct Counts{ mode_start_counts:HashMap<ModeID,u64>, mode_finish_counts:HashMap<ModeID,u64>, + mode_anticheat_counts:HashMap<ModeID,u64>, spawn_counts:HashMap<SpawnID,u64>, wormhole_out_counts:HashMap<WormholeOutID,u64>, } @@ -129,7 +137,8 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d 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, + Err(_)=>(), } // Spawns let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$"); @@ -217,12 +226,36 @@ 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 SetDifferenceCheckContext<ID>{ +pub struct SetDifferenceCheckContextAllowNone<ID>{ + extra:HashMap<ID,u64>, +} +pub struct SetDifferenceCheckContextAtLeastOne<ID>{ extra:HashMap<ID,u64>, missing:HashSet<ID>, } -pub struct SetDifferenceCheck<ID>(Result<(),SetDifferenceCheckContext<ID>>); -impl<ID> SetDifferenceCheckContext<ID>{ +pub struct SetDifferenceCheck<Context>(Result<(),Context>); +impl<ID> SetDifferenceCheckContextAllowNone<ID>{ + fn new(initial_set:HashMap<ID,u64>)->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>{ + // remove correct entries + for (id,_) in reference_set{ + self.extra.remove(id); + } + // if any entries remain, they are incorrect + if self.extra.is_empty(){ + SetDifferenceCheck(Ok(())) + }else{ + SetDifferenceCheck(Err(self)) + } + } +} +impl<ID> SetDifferenceCheckContextAtLeastOne<ID>{ fn new(initial_set:HashMap<ID,u64>)->Self{ Self{ extra:initial_set, @@ -230,21 +263,20 @@ impl<ID> SetDifferenceCheckContext<ID>{ } } } -impl<ID:Copy+Eq+std::hash::Hash> SetDifferenceCheckContext<ID>{ - fn check<T>(self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<ID>{ - let Self{mut extra,mut missing}=self; +impl<ID:Copy+Eq+std::hash::Hash> SetDifferenceCheckContextAtLeastOne<ID>{ + fn check<T>(mut self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<Self>{ // remove correct entries for (id,_) in reference_set{ - if extra.remove(id).is_none(){ + if self.extra.remove(id).is_none(){ // the set did not contain a required item. This is a fail - missing.insert(*id); + self.missing.insert(*id); } } // if any entries remain, they are incorrect - if extra.is_empty()&&missing.is_empty(){ + if self.extra.is_empty()&&self.missing.is_empty(){ SetDifferenceCheck(Ok(())) }else{ - SetDifferenceCheck(Err(Self{extra,missing})) + SetDifferenceCheck(Err(self)) } } } @@ -278,8 +310,9 @@ pub struct MapCheck<'a>{ // No duplicate map starts (including bonuses) mode_start_counts:DuplicateCheck<ModeID>, // At least one finish zone for each start zone, and no finishes with no start - mode_finish_counts:SetDifferenceCheck<ModeID>, - // TODO: check for dangling MapAnticheat zones (no associated MapStart) + mode_finish_counts:SetDifferenceCheck<SetDifferenceCheckContextAtLeastOne<ModeID>>, + // check for dangling MapAnticheat zones (no associated MapStart) + mode_anticheat_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<ModeID>>, // Spawn1 must exist spawn1:Result<(),()>, // No duplicate Spawn# @@ -331,7 +364,12 @@ impl<'a> ModelInfo<'a>{ }; // check that at least one end zone exists for each start zone. - let mode_finish_counts=SetDifferenceCheckContext::new(self.counts.mode_finish_counts) + let mode_finish_counts=SetDifferenceCheckContextAtLeastOne::new(self.counts.mode_finish_counts) + .check(&self.counts.mode_start_counts); + + // check that there are no anticheat zones that have no corresponding start zone. + // modes are allowed to have 0 anticheat zones. + let mode_anticheat_counts=SetDifferenceCheckContextAllowNone::new(self.counts.mode_anticheat_counts) .check(&self.counts.mode_start_counts); // there must be exactly one start zone for every mode in the map. @@ -352,6 +390,7 @@ impl<'a> ModelInfo<'a>{ mapstart, mode_start_counts, mode_finish_counts, + mode_anticheat_counts, spawn1, spawn_counts, wormhole_out_counts, @@ -371,6 +410,7 @@ impl<'a> MapCheck<'a>{ mapstart:Ok(()), mode_start_counts:DuplicateCheck(Ok(())), mode_finish_counts:SetDifferenceCheck(Ok(())), + mode_anticheat_counts:SetDifferenceCheck(Ok(())), spawn1:Ok(()), spawn_counts:DuplicateCheck(Ok(())), wormhole_out_counts:DuplicateCheck(Ok(())), @@ -443,7 +483,8 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ if !context.extra.is_empty(){ write!(f,"Extra finish zones with no matching start zone: ")?; comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| - mode_id.write_finish_zone(f))?; + mode_id.write_finish_zone(f) + )?; writeln!(f,"")?; } // perhaps there are missing end zones (context.missing) @@ -455,6 +496,16 @@ impl<'a> std::fmt::Display for MapCheck<'a>{ writeln!(f,"")?; } } + 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: ")?; + comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| + mode_id.write_anticheat_zone(f) + )?; + writeln!(f,"")?; + } + } if let Err(())=&self.spawn1{ writeln!(f,"Model has no Spawn1")?; }