|
|
|
|
@@ -43,6 +43,7 @@ impl From<crate::nats_types::CheckSubmissionRequest> for CheckRequest{
|
|
|
|
|
enum Zone{
|
|
|
|
|
Start(ModeID),
|
|
|
|
|
Finish(ModeID),
|
|
|
|
|
Anticheat(ModeID),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
|
|
|
|
@@ -50,20 +51,6 @@ 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{
|
|
|
|
|
@@ -76,8 +63,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 +76,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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -102,23 +95,14 @@ impl SpawnID{
|
|
|
|
|
#[derive(Debug,Hash,Eq,PartialEq)]
|
|
|
|
|
struct WormholeOutID(u64);
|
|
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
|
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>,
|
|
|
|
|
}
|
|
|
|
|
impl Counts{
|
|
|
|
|
fn new()->Self{
|
|
|
|
|
Self{
|
|
|
|
|
mode_start_counts:HashMap::new(),
|
|
|
|
|
mode_finish_counts:HashMap::new(),
|
|
|
|
|
spawn_counts:HashMap::new(),
|
|
|
|
|
wormhole_out_counts:HashMap::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub struct ModelInfo<'a>{
|
|
|
|
|
model_class:&'a str,
|
|
|
|
|
@@ -127,22 +111,20 @@ pub struct ModelInfo<'a>{
|
|
|
|
|
counts:Counts,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_model_info(dom:&rbx_dom_weak::WeakDom)->Result<ModelInfo,GetRootInstanceError>{
|
|
|
|
|
// extract the root instance, otherwise immediately return
|
|
|
|
|
let model_instance=get_root_instance(&dom)?;
|
|
|
|
|
|
|
|
|
|
pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{
|
|
|
|
|
// extract model info
|
|
|
|
|
let map_info=get_mapinfo(&dom,model_instance);
|
|
|
|
|
|
|
|
|
|
// count objects
|
|
|
|
|
let mut counts=Counts::new();
|
|
|
|
|
// count objects (default count is 0)
|
|
|
|
|
let mut counts=Counts::default();
|
|
|
|
|
for instance in dom.descendants_of(model_instance.referent()){
|
|
|
|
|
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,
|
|
|
|
|
Err(_)=>(),
|
|
|
|
|
}
|
|
|
|
|
// Spawns
|
|
|
|
|
let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
|
|
|
|
|
@@ -161,12 +143,12 @@ pub fn get_model_info(dom:&rbx_dom_weak::WeakDom)->Result<ModelInfo,GetRootInsta
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(ModelInfo{
|
|
|
|
|
ModelInfo{
|
|
|
|
|
model_class:model_instance.class.as_str(),
|
|
|
|
|
model_name:model_instance.name.as_str(),
|
|
|
|
|
map_info,
|
|
|
|
|
counts,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check if an observed string matches an expected string
|
|
|
|
|
@@ -195,12 +177,14 @@ impl<'a,Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'a,Str>{
|
|
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fn check_empty(value:&str)->Result<&str,StringEmpty>{
|
|
|
|
|
value.is_empty().then_some(value).ok_or(StringEmpty)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check for duplicate objects
|
|
|
|
|
pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>);
|
|
|
|
|
@@ -220,12 +204,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,
|
|
|
|
|
@@ -233,21 +241,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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -267,10 +274,10 @@ pub struct MapCheck<'a>{
|
|
|
|
|
model_name:StringCheck<'a,(),String>,
|
|
|
|
|
// Map must have a StringValue named DisplayName.
|
|
|
|
|
// Value must not be empty, must be in title case.
|
|
|
|
|
display_name:Result<StringEmptyCheck<StringCheck<'a,&'a str,String>>,StringValueError>,
|
|
|
|
|
display_name:Result<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
|
|
|
|
|
// Map must have a StringValue named Creator.
|
|
|
|
|
// Value must not be empty.
|
|
|
|
|
creator:Result<StringEmptyCheck<&'a str>,StringValueError>,
|
|
|
|
|
creator:Result<Result<&'a str,StringEmpty>,StringValueError>,
|
|
|
|
|
// The prefix of the model's name must match the game it was submitted for.
|
|
|
|
|
// bhop_ for bhop, and surf_ for surf
|
|
|
|
|
game_id:Result<GameID,ParseGameIDError>,
|
|
|
|
|
@@ -281,8 +288,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#
|
|
|
|
|
@@ -293,38 +301,30 @@ pub struct MapCheck<'a>{
|
|
|
|
|
|
|
|
|
|
impl<'a> ModelInfo<'a>{
|
|
|
|
|
fn check(self)->MapCheck<'a>{
|
|
|
|
|
// Check class is exactly "Model"
|
|
|
|
|
let model_class=StringCheckContext{
|
|
|
|
|
observed:self.model_class,
|
|
|
|
|
expected:"Model",
|
|
|
|
|
}.check(());
|
|
|
|
|
|
|
|
|
|
// Check model name is snake case
|
|
|
|
|
let model_name=StringCheckContext{
|
|
|
|
|
observed:self.model_name,
|
|
|
|
|
expected:self.model_name.to_snake_case(),
|
|
|
|
|
}.check(());
|
|
|
|
|
|
|
|
|
|
// check display name
|
|
|
|
|
// Check display name is not empty and has title case
|
|
|
|
|
let display_name=self.map_info.display_name.map(|display_name|{
|
|
|
|
|
if display_name.is_empty(){
|
|
|
|
|
StringEmptyCheck(Err(StringEmpty))
|
|
|
|
|
}else{
|
|
|
|
|
StringEmptyCheck(Ok(StringCheckContext{
|
|
|
|
|
observed:display_name,
|
|
|
|
|
expected:display_name.to_title_case(),
|
|
|
|
|
}.check(display_name)))
|
|
|
|
|
}
|
|
|
|
|
check_empty(display_name).map(|display_name|StringCheckContext{
|
|
|
|
|
observed:display_name,
|
|
|
|
|
expected:display_name.to_title_case(),
|
|
|
|
|
}.check(display_name))
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// check Creator
|
|
|
|
|
let creator=self.map_info.creator.map(|creator|{
|
|
|
|
|
if creator.is_empty(){
|
|
|
|
|
StringEmptyCheck(Err(StringEmpty))
|
|
|
|
|
}else{
|
|
|
|
|
StringEmptyCheck(Ok(creator))
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// Check Creator is not empty
|
|
|
|
|
let creator=self.map_info.creator.map(check_empty);
|
|
|
|
|
|
|
|
|
|
// check GameID
|
|
|
|
|
// Check GameID (model name was prefixed with bhop_ surf_ etc)
|
|
|
|
|
let game_id=self.map_info.game_id;
|
|
|
|
|
|
|
|
|
|
// MapStart must exist
|
|
|
|
|
@@ -341,17 +341,23 @@ impl<'a> ModelInfo<'a>{
|
|
|
|
|
Err(())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// check that at least one end zone exists for each start zone.
|
|
|
|
|
let mode_finish_counts=SetDifferenceCheckContext::new(self.counts.mode_finish_counts)
|
|
|
|
|
// Check that at least one finish zone exists for each start zone.
|
|
|
|
|
// This also checks that there are no finish zones without a corresponding start zone.
|
|
|
|
|
let mode_finish_counts=SetDifferenceCheckContextAtLeastOne::new(self.counts.mode_finish_counts)
|
|
|
|
|
.check(&self.counts.mode_start_counts);
|
|
|
|
|
|
|
|
|
|
// there must be exactly one start zone for every mode in the map.
|
|
|
|
|
// Check that there are no anticheat zones without a 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.
|
|
|
|
|
let mode_start_counts=DuplicateCheckContext(self.counts.mode_start_counts).check();
|
|
|
|
|
|
|
|
|
|
// there must be exactly one of any perticular spawn id in the map.
|
|
|
|
|
// There must be exactly one of any perticular spawn id in the map.
|
|
|
|
|
let spawn_counts=DuplicateCheckContext(self.counts.spawn_counts).check();
|
|
|
|
|
|
|
|
|
|
// there must be exactly one of any perticular wormhole out id in the map.
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
MapCheck{
|
|
|
|
|
@@ -363,6 +369,7 @@ impl<'a> ModelInfo<'a>{
|
|
|
|
|
mapstart,
|
|
|
|
|
mode_start_counts,
|
|
|
|
|
mode_finish_counts,
|
|
|
|
|
mode_anticheat_counts,
|
|
|
|
|
spawn1,
|
|
|
|
|
spawn_counts,
|
|
|
|
|
wormhole_out_counts,
|
|
|
|
|
@@ -376,12 +383,13 @@ impl<'a> MapCheck<'a>{
|
|
|
|
|
MapCheck{
|
|
|
|
|
model_class:StringCheck(Ok(())),
|
|
|
|
|
model_name:StringCheck(Ok(())),
|
|
|
|
|
display_name:Ok(StringEmptyCheck(Ok(StringCheck(Ok(display_name))))),
|
|
|
|
|
creator:Ok(StringEmptyCheck(Ok(creator))),
|
|
|
|
|
display_name:Ok(Ok(StringCheck(Ok(display_name)))),
|
|
|
|
|
creator:Ok(Ok(creator)),
|
|
|
|
|
game_id:Ok(game_id),
|
|
|
|
|
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(())),
|
|
|
|
|
@@ -397,10 +405,11 @@ 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,
|
|
|
|
|
{
|
|
|
|
|
fn write_comma_separated<T>(
|
|
|
|
|
f:&mut std::fmt::Formatter<'_>,
|
|
|
|
|
mut it:impl Iterator<Item=T>,
|
|
|
|
|
custom_write:impl Fn(&mut std::fmt::Formatter<'_>,T)->std::fmt::Result
|
|
|
|
|
)->std::fmt::Result{
|
|
|
|
|
if let Some(t)=it.next(){
|
|
|
|
|
custom_write(f,t)?;
|
|
|
|
|
for t in it{
|
|
|
|
|
@@ -411,6 +420,15 @@ where
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! write_zone{
|
|
|
|
|
($f:expr,$mode:expr,$zone:expr)=>{
|
|
|
|
|
match $mode{
|
|
|
|
|
ModeID(0)=>write!($f,concat!("Map",$zone)),
|
|
|
|
|
ModeID(1)=>write!($f,concat!("Bonus",$zone)),
|
|
|
|
|
ModeID(other)=>write!($f,concat!("Bonus{}",$zone),other),
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
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{
|
|
|
|
|
@@ -420,16 +438,16 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|
|
|
|
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}")?,
|
|
|
|
|
Ok(Ok(StringCheck(Ok(_))))=>(),
|
|
|
|
|
Ok(Ok(StringCheck(Err(context))))=>writeln!(f,"Invalid DisplayName: {context}")?,
|
|
|
|
|
Ok(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}")?,
|
|
|
|
|
Ok(Ok(_))=>(),
|
|
|
|
|
Ok(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")?,
|
|
|
|
|
@@ -442,8 +460,8 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|
|
|
|
}
|
|
|
|
|
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_comma_separated(f,context.iter(),|f,(mode_id,count)|{
|
|
|
|
|
write_zone!(f,mode_id,"Start")?;
|
|
|
|
|
write!(f,"({count} duplicates)")?;
|
|
|
|
|
Ok(())
|
|
|
|
|
})?;
|
|
|
|
|
@@ -453,15 +471,26 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|
|
|
|
// 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))?;
|
|
|
|
|
write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)|
|
|
|
|
|
write_zone!(f,mode_id,"Finish")
|
|
|
|
|
)?;
|
|
|
|
|
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)
|
|
|
|
|
write_comma_separated(f,context.missing.iter(),|f,mode_id|
|
|
|
|
|
write_zone!(f,mode_id,"Finish")
|
|
|
|
|
)?;
|
|
|
|
|
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: ")?;
|
|
|
|
|
write_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)|
|
|
|
|
|
write_zone!(f,mode_id,"Anticheat")
|
|
|
|
|
)?;
|
|
|
|
|
writeln!(f,"")?;
|
|
|
|
|
}
|
|
|
|
|
@@ -471,14 +500,14 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|
|
|
|
}
|
|
|
|
|
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_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: ")?;
|
|
|
|
|
comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)|
|
|
|
|
|
write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)|
|
|
|
|
|
write!(f,"WormholeOut{wormhole_out_id}({count} duplicates)")
|
|
|
|
|
)?;
|
|
|
|
|
writeln!(f,"")?;
|
|
|
|
|
@@ -515,8 +544,11 @@ impl crate::message_handler::MessageHandler{
|
|
|
|
|
// decode dom (slow!)
|
|
|
|
|
let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;
|
|
|
|
|
|
|
|
|
|
// extract the root instance
|
|
|
|
|
let model_instance=get_root_instance(&dom).map_err(Error::GetRootInstance)?;
|
|
|
|
|
|
|
|
|
|
// extract information from the model
|
|
|
|
|
let model_info=get_model_info(&dom).map_err(Error::GetRootInstance)?;
|
|
|
|
|
let model_info=get_model_info(&dom,model_instance);
|
|
|
|
|
|
|
|
|
|
// convert the model information into a structured report
|
|
|
|
|
let map_check=model_info.check();
|
|
|
|
|
|