diff --git a/validation/src/check.rs b/validation/src/check.rs index 24927c9..cc4098a 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -51,6 +51,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{ @@ -171,10 +185,20 @@ impl<'a> StringCheckContext<'a>{ } } } +impl<'a> std::fmt::Display for StringCheckContext<'a>{ + 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>); @@ -353,6 +377,102 @@ 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.display_name{ + Ok(StringEmptyCheck(Ok(StringCheck(Ok(_)))))=>(), + Ok(StringEmptyCheck(Ok(StringCheck(Err(context)))))=>writeln!(f,"Invalid Creator: {context}")?, + 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)?; + Ok(()) + })?; + 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)?; + Ok(()) + })?; + 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,