diff --git a/validation/src/check.rs b/validation/src/check.rs index ff195d2..0b428fa 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -50,6 +50,20 @@ struct ModeID(u64); 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"), + } + } } #[allow(dead_code)] pub enum ZoneParseError{ @@ -173,10 +187,20 @@ impl<'a,Str> StringCheckContext<'a,Str> } } } +impl<'a,Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'a,Str>{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"expected: {}, observed: {}",self.expected,self.observed) + } +} // check if a string is empty pub struct StringEmpty; pub struct StringEmptyCheck<Context>(Result<Context,StringEmpty>); +impl std::fmt::Display for StringEmpty{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"Empty string") + } +} // check for duplicate objects pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>); @@ -355,6 +379,98 @@ impl<'a> MapCheck<'a>{ } } +fn comma_separated<T,F>(f:&mut std::fmt::Formatter<'_>,mut it:impl Iterator<Item=T>,custom_write:F)->std::fmt::Result +where + F:Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result, +{ + if let Some(t)=it.next(){ + custom_write(f,t)?; + for t in it{ + write!(f,", ")?; + custom_write(f,t)?; + } + } + Ok(()) +} + +impl<'a> std::fmt::Display for MapCheck<'a>{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + if let StringCheck(Err(context))=&self.model_class{ + writeln!(f,"Invalid Model Class: {context}")?; + } + if let StringCheck(Err(context))=&self.model_name{ + writeln!(f,"Invalid Model Name: {context}")?; + } + match &self.display_name{ + Ok(StringEmptyCheck(Ok(StringCheck(Ok(_)))))=>(), + Ok(StringEmptyCheck(Ok(StringCheck(Err(context)))))=>writeln!(f,"Invalid DisplayName: {context}")?, + Ok(StringEmptyCheck(Err(context)))=>writeln!(f,"Invalid DisplayName: {context}")?, + Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing DisplayName StringValue")?, + Err(StringValueError::ValueNotSet)=>writeln!(f,"DisplayName Value not set")?, + Err(StringValueError::NonStringValue)=>writeln!(f,"DisplayName Value is not a String")?, + } + match &self.creator{ + Ok(StringEmptyCheck(Ok(_)))=>(), + Ok(StringEmptyCheck(Err(context)))=>writeln!(f,"Invalid Creator: {context}")?, + Err(StringValueError::ObjectNotFound)=>writeln!(f,"Missing Creator StringValue")?, + Err(StringValueError::ValueNotSet)=>writeln!(f,"Creator Value not set")?, + Err(StringValueError::NonStringValue)=>writeln!(f,"Creator Value is not a String")?, + } + if let Err(_parse_game_id_error)=&self.game_id{ + writeln!(f,"Model name must be prefixed with bhop_ surf_ or flytrials_")?; + } + if let Err(())=&self.mapstart{ + writeln!(f,"Model has no MapStart")?; + } + if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{ + write!(f,"Duplicate start zones: ")?; + comma_separated(f,context.iter(),|f,(mode_id,count)|{ + mode_id.write_start_zone(f)?; + write!(f,"({count} duplicates)")?; + Ok(()) + })?; + writeln!(f,"")?; + } + if let AtLeastOneMatchingAndNoExtraCheck(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: ")?; + comma_separated(f,context.extra.iter(),|f,(mode_id,_count)| + mode_id.write_finish_zone(f))?; + writeln!(f,"")?; + } + // perhaps there are missing end zones (context.missing) + if !context.missing.is_empty(){ + write!(f,"Missing finish zones: ")?; + comma_separated(f,context.missing.iter(),|f,mode_id| + mode_id.write_finish_zone(f) + )?; + writeln!(f,"")?; + } + } + if let Err(())=&self.spawn1{ + writeln!(f,"Model has no Spawn1")?; + } + if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{ + write!(f,"Duplicate spawn zones: ")?; + comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)|{ + write!(f,"Spawn{spawn_id}({count} duplicates)")?; + Ok(()) + })?; + writeln!(f,"")?; + } + if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{ + write!(f,"Duplicate wormhole out: ")?; + comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)|{ + write!(f,"WormholeOut{wormhole_out_id}({count} duplicates)")?; + Ok(()) + })?; + writeln!(f,"")?; + } + Ok(()) + } +} + pub struct CheckReportAndVersion{ pub status:Result<MapInfoOwned,String>, pub version:u64,