validator: add teleport and wormhole set difference checks #131
@ -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)]
|
#[derive(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||||
struct ModeID(u64);
|
struct ModeID(u64);
|
||||||
impl ModeID{
|
impl ModeID{
|
||||||
const MAIN:Self=Self(0);
|
const MAIN:Self=Self(0);
|
||||||
const BONUS:Self=Self(1);
|
const BONUS:Self=Self(1);
|
||||||
}
|
}
|
||||||
|
enum Zone{
|
||||||
|
Start(ModeID),
|
||||||
|
Finish(ModeID),
|
||||||
|
Anticheat(ModeID),
|
||||||
|
}
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum ZoneParseError{
|
pub enum IDParseError{
|
||||||
NoCaptures,
|
NoCaptures,
|
||||||
ParseInt(core::num::ParseIntError)
|
ParseInt(core::num::ParseIntError)
|
||||||
}
|
}
|
||||||
impl std::str::FromStr for Zone{
|
impl std::str::FromStr for Zone{
|
||||||
type Err=ZoneParseError;
|
type Err=IDParseError;
|
||||||
fn from_str(s:&str)->Result<Self,Self::Err>{
|
fn from_str(s:&str)->Result<Self,Self::Err>{
|
||||||
match s{
|
match s{
|
||||||
"MapStart"=>Ok(Self::Start(ModeID::MAIN)),
|
"MapStart"=>Ok(Self::Start(ModeID::MAIN)),
|
||||||
@ -70,45 +69,85 @@ impl std::str::FromStr for Zone{
|
|||||||
other=>{
|
other=>{
|
||||||
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
|
let bonus_start_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Start$|^BonusStart(\d+)$");
|
||||||
if let Some(captures)=bonus_start_pattern.captures(other){
|
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+)$");
|
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Finish$|^BonusFinish(\d+)$");
|
||||||
if let Some(captures)=bonus_finish_pattern.captures(other){
|
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+)$");
|
let bonus_finish_pattern=lazy_regex::lazy_regex!(r"^Bonus(\d+)Anticheat$|^BonusAnticheat(\d+)$");
|
||||||
if let Some(captures)=bonus_finish_pattern.captures(other){
|
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(Clone,Copy,Debug,Hash,Eq,PartialEq)]
|
||||||
#[derive(Debug,Hash,Eq,PartialEq)]
|
|
||||||
struct SpawnID(u64);
|
struct SpawnID(u64);
|
||||||
impl SpawnID{
|
impl SpawnID{
|
||||||
const FIRST:Self=Self(1);
|
const FIRST:Self=Self(1);
|
||||||
}
|
}
|
||||||
#[derive(Debug,Hash,Eq,PartialEq)]
|
enum SpawnTeleport{
|
||||||
struct WormholeOutID(u64);
|
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)]
|
#[derive(Default)]
|
||||||
struct Counts{
|
struct Counts<'a>{
|
||||||
mode_start_counts:HashMap<ModeID,u64>,
|
mode_start_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||||
mode_finish_counts:HashMap<ModeID,u64>,
|
mode_finish_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||||
mode_anticheat_counts:HashMap<ModeID,u64>,
|
mode_anticheat_counts:HashMap<ModeID,Vec<&'a str>>,
|
||||||
|
teleport_counts:HashMap<SpawnID,Vec<&'a str>>,
|
||||||
spawn_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>{
|
pub struct ModelInfo<'a>{
|
||||||
model_class:&'a str,
|
model_class:&'a str,
|
||||||
model_name:&'a str,
|
model_name:&'a str,
|
||||||
map_info:MapInfo<'a>,
|
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>{
|
pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_dom_weak::Instance)->ModelInfo<'a>{
|
||||||
@ -121,24 +160,22 @@ 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"){
|
if class_is_a(instance.class.as_str(),"BasePart"){
|
||||||
// Zones
|
// Zones
|
||||||
match instance.name.parse(){
|
match instance.name.parse(){
|
||||||
Ok(Zone::Start(mode_id))=>*counts.mode_start_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_insert(0)+=1,
|
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_insert(0)+=1,
|
Ok(Zone::Anticheat(mode_id))=>counts.mode_anticheat_counts.entry(mode_id).or_default().push(instance.name.as_str()),
|
||||||
Err(_)=>(),
|
Err(_)=>(),
|
||||||
}
|
}
|
||||||
// Spawns
|
// Spawns & Teleports
|
||||||
let spawn_pattern=lazy_regex::lazy_regex!(r"^Spawn(\d+)$");
|
match instance.name.parse(){
|
||||||
if let Some(captures)=spawn_pattern.captures(instance.name.as_str()){
|
Ok(SpawnTeleport::Teleport(spawn_id))=>counts.teleport_counts.entry(spawn_id).or_default().push(instance.name.as_str()),
|
||||||
if let Ok(spawn_id)=captures[1].parse(){
|
Ok(SpawnTeleport::Spawn(spawn_id))=>*counts.spawn_counts.entry(spawn_id).or_insert(0)+=1,
|
||||||
*counts.spawn_counts.entry(SpawnID(spawn_id)).or_insert(0)+=1;
|
Err(_)=>(),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// WormholeOuts
|
// Wormholes
|
||||||
let wormhole_out_pattern=lazy_regex::lazy_regex!(r"^WormholeOut(\d+)$");
|
match instance.name.parse(){
|
||||||
if let Some(captures)=wormhole_out_pattern.captures(instance.name.as_str()){
|
Ok(Wormhole::In(wormhole_id))=>*counts.wormhole_in_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||||
if let Ok(wormhole_out_id)=captures[1].parse(){
|
Ok(Wormhole::Out(wormhole_id))=>*counts.wormhole_out_counts.entry(wormhole_id).or_insert(0)+=1,
|
||||||
*counts.wormhole_out_counts.entry(WormholeOutID(wormhole_out_id)).or_insert(0)+=1;
|
Err(_)=>(),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,13 +224,13 @@ fn check_empty(value:&str)->Result<&str,StringEmpty>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for duplicate objects
|
// check for duplicate objects
|
||||||
pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>);
|
pub struct DuplicateCheckContext<ID,T>(HashMap<ID,T>);
|
||||||
pub struct DuplicateCheck<ID>(Result<(),DuplicateCheckContext<ID>>);
|
pub struct DuplicateCheck<ID,T>(Result<(),DuplicateCheckContext<ID,T>>);
|
||||||
impl<ID> DuplicateCheckContext<ID>{
|
impl<ID,T> DuplicateCheckContext<ID,T>{
|
||||||
fn check(self)->DuplicateCheck<ID>{
|
fn check(self,f:impl Fn(&T)->bool)->DuplicateCheck<ID,T>{
|
||||||
let Self(mut set)=self;
|
let Self(mut set)=self;
|
||||||
// remove correct entries
|
// remove correct entries
|
||||||
set.retain(|_,&mut c|c!=1);
|
set.retain(|_,c|f(c));
|
||||||
// if any entries remain, they are incorrect
|
// if any entries remain, they are incorrect
|
||||||
if set.is_empty(){
|
if set.is_empty(){
|
||||||
DuplicateCheck(Ok(()))
|
DuplicateCheck(Ok(()))
|
||||||
@ -204,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
|
// check that there is at least one matching item for each item in a reference set, and no extra items
|
||||||
pub struct SetDifferenceCheckContextAllowNone<ID>{
|
pub struct SetDifferenceCheckContextAllowNone<ID,T>{
|
||||||
extra:HashMap<ID,u64>,
|
extra:HashMap<ID,T>,
|
||||||
}
|
}
|
||||||
pub struct SetDifferenceCheckContextAtLeastOne<ID>{
|
pub struct SetDifferenceCheckContextAtLeastOne<ID,T>{
|
||||||
extra:HashMap<ID,u64>,
|
extra:HashMap<ID,T>,
|
||||||
missing:HashSet<ID>,
|
missing:HashSet<ID>,
|
||||||
}
|
}
|
||||||
pub struct SetDifferenceCheck<Context>(Result<(),Context>);
|
pub struct SetDifferenceCheck<Context>(Result<(),Context>);
|
||||||
impl<ID> SetDifferenceCheckContextAllowNone<ID>{
|
impl<ID,T> SetDifferenceCheckContextAllowNone<ID,T>{
|
||||||
fn new(initial_set:HashMap<ID,u64>)->Self{
|
fn new(initial_set:HashMap<ID,T>)->Self{
|
||||||
Self{
|
Self{
|
||||||
extra:initial_set,
|
extra:initial_set,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<ID:Eq+std::hash::Hash> SetDifferenceCheckContextAllowNone<ID>{
|
impl<ID:Eq+std::hash::Hash,T> SetDifferenceCheckContextAllowNone<ID,T>{
|
||||||
fn check<T>(mut self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<Self>{
|
fn check<U>(mut self,reference_set:&HashMap<ID,U>)->SetDifferenceCheck<Self>{
|
||||||
// remove correct entries
|
// remove correct entries
|
||||||
for (id,_) in reference_set{
|
for (id,_) in reference_set{
|
||||||
self.extra.remove(id);
|
self.extra.remove(id);
|
||||||
@ -233,16 +270,16 @@ impl<ID:Eq+std::hash::Hash> SetDifferenceCheckContextAllowNone<ID>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<ID> SetDifferenceCheckContextAtLeastOne<ID>{
|
impl<ID,T> SetDifferenceCheckContextAtLeastOne<ID,T>{
|
||||||
fn new(initial_set:HashMap<ID,u64>)->Self{
|
fn new(initial_set:HashMap<ID,T>)->Self{
|
||||||
Self{
|
Self{
|
||||||
extra:initial_set,
|
extra:initial_set,
|
||||||
missing:HashSet::new(),
|
missing:HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<ID:Copy+Eq+std::hash::Hash> SetDifferenceCheckContextAtLeastOne<ID>{
|
impl<ID:Copy+Eq+std::hash::Hash,T> SetDifferenceCheckContextAtLeastOne<ID,T>{
|
||||||
fn check<T>(mut self,reference_set:&HashMap<ID,T>)->SetDifferenceCheck<Self>{
|
fn check<U>(mut self,reference_set:&HashMap<ID,U>)->SetDifferenceCheck<Self>{
|
||||||
// remove correct entries
|
// remove correct entries
|
||||||
for (id,_) in reference_set{
|
for (id,_) in reference_set{
|
||||||
if self.extra.remove(id).is_none(){
|
if self.extra.remove(id).is_none(){
|
||||||
@ -286,17 +323,22 @@ pub struct MapCheck<'a>{
|
|||||||
// MapStart must exist
|
// MapStart must exist
|
||||||
mapstart:Result<(),()>,
|
mapstart:Result<(),()>,
|
||||||
// No duplicate map starts (including bonuses)
|
// 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
|
// 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)
|
// 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 must exist
|
||||||
spawn1:Result<(),()>,
|
spawn1:Result<(),()>,
|
||||||
|
// Check for dangling Teleport# (no associated Spawn#)
|
||||||
|
teleport_counts:SetDifferenceCheck<SetDifferenceCheckContextAllowNone<SpawnID,Vec<&'a str>>>,
|
||||||
// No duplicate Spawn#
|
// 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,u64>>,
|
||||||
// No duplicate WormholeOut# (duplicate WormholeIn# ok)
|
// No duplicate WormholeOut# (duplicate WormholeIn# ok)
|
||||||
wormhole_out_counts:DuplicateCheck<WormholeOutID>,
|
// No dangling WormholeOut#
|
||||||
|
wormhole_out_counts:DuplicateCheck<WormholeID,u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ModelInfo<'a>{
|
impl<'a> ModelInfo<'a>{
|
||||||
@ -352,13 +394,23 @@ impl<'a> ModelInfo<'a>{
|
|||||||
.check(&self.counts.mode_start_counts);
|
.check(&self.counts.mode_start_counts);
|
||||||
|
|
||||||
// There must be exactly one start zone for every mode in the map.
|
// 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.
|
||||||
|
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.
|
// 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.
|
||||||
|
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.
|
// 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{
|
MapCheck{
|
||||||
model_class,
|
model_class,
|
||||||
@ -371,7 +423,9 @@ impl<'a> ModelInfo<'a>{
|
|||||||
mode_finish_counts,
|
mode_finish_counts,
|
||||||
mode_anticheat_counts,
|
mode_anticheat_counts,
|
||||||
spawn1,
|
spawn1,
|
||||||
|
teleport_counts,
|
||||||
spawn_counts,
|
spawn_counts,
|
||||||
|
wormhole_in_counts,
|
||||||
wormhole_out_counts,
|
wormhole_out_counts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -391,7 +445,9 @@ impl<'a> MapCheck<'a>{
|
|||||||
mode_finish_counts:SetDifferenceCheck(Ok(())),
|
mode_finish_counts:SetDifferenceCheck(Ok(())),
|
||||||
mode_anticheat_counts:SetDifferenceCheck(Ok(())),
|
mode_anticheat_counts:SetDifferenceCheck(Ok(())),
|
||||||
spawn1:Ok(()),
|
spawn1:Ok(()),
|
||||||
|
teleport_counts:SetDifferenceCheck(Ok(())),
|
||||||
spawn_counts:DuplicateCheck(Ok(())),
|
spawn_counts:DuplicateCheck(Ok(())),
|
||||||
|
wormhole_in_counts:SetDifferenceCheck(Ok(())),
|
||||||
wormhole_out_counts:DuplicateCheck(Ok(())),
|
wormhole_out_counts:DuplicateCheck(Ok(())),
|
||||||
}=>{
|
}=>{
|
||||||
Ok(MapInfoOwned{
|
Ok(MapInfoOwned{
|
||||||
@ -460,25 +516,25 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|||||||
}
|
}
|
||||||
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{
|
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.mode_start_counts{
|
||||||
write!(f,"Duplicate start zones: ")?;
|
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_zone!(f,mode_id,"Start")?;
|
||||||
write!(f,"({count} duplicates)")?;
|
write!(f,"({} duplicates)",names.len())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
writeln!(f,"")?;
|
writeln!(f,"")?;
|
||||||
}
|
}
|
||||||
if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{
|
if let SetDifferenceCheck(Err(context))=&self.mode_finish_counts{
|
||||||
// perhaps there are extra end zones (context.extra)
|
|
||||||
if !context.extra.is_empty(){
|
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_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)|
|
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")
|
write_zone!(f,mode_id,"Finish")
|
||||||
)?;
|
)?;
|
||||||
writeln!(f,"")?;
|
writeln!(f,"")?;
|
||||||
}
|
}
|
||||||
// perhaps there are missing end zones (context.missing)
|
|
||||||
if !context.missing.is_empty(){
|
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_comma_separated(f,context.missing.iter(),|f,mode_id|
|
||||||
write_zone!(f,mode_id,"Finish")
|
write_zone!(f,mode_id,"Finish")
|
||||||
)?;
|
)?;
|
||||||
@ -486,10 +542,10 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{
|
if let SetDifferenceCheck(Err(context))=&self.mode_anticheat_counts{
|
||||||
// perhaps there are extra end zones (context.extra)
|
|
||||||
if !context.extra.is_empty(){
|
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_comma_separated(f,context.extra.iter(),|f,(mode_id,_count)|
|
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")
|
write_zone!(f,mode_id,"Anticheat")
|
||||||
)?;
|
)?;
|
||||||
writeln!(f,"")?;
|
writeln!(f,"")?;
|
||||||
@ -498,17 +554,45 @@ impl<'a> std::fmt::Display for MapCheck<'a>{
|
|||||||
if let Err(())=&self.spawn1{
|
if let Err(())=&self.spawn1{
|
||||||
writeln!(f,"Model has no Spawn1")?;
|
writeln!(f,"Model has no Spawn1")?;
|
||||||
}
|
}
|
||||||
|
if let SetDifferenceCheck(Err(context))=&self.teleport_counts{
|
||||||
|
for (_,names) in &context.extra{
|
||||||
|
let plural=if names.len()==1{"object"}else{"objects"};
|
||||||
|
write!(f,"No matching Spawn for {plural}: ")?;
|
||||||
|
write_comma_separated(f,names.iter(),|f,&name|{
|
||||||
|
write!(f,"{name}")
|
||||||
|
})?;
|
||||||
|
writeln!(f,"")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.spawn_counts{
|
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_comma_separated(f,context.iter(),|f,(SpawnID(spawn_id),count)|
|
||||||
write!(f,"Spawn{spawn_id}({count} duplicates)")
|
write!(f,"Spawn{spawn_id}({count} duplicates)")
|
||||||
)?;
|
)?;
|
||||||
writeln!(f,"")?;
|
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{
|
if let DuplicateCheck(Err(DuplicateCheckContext(context)))=&self.wormhole_out_counts{
|
||||||
write!(f,"Duplicate wormhole out: ")?;
|
write!(f,"Duplicate WormholeOut: ")?;
|
||||||
write_comma_separated(f,context.iter(),|f,(WormholeOutID(wormhole_out_id),count)|
|
write_comma_separated(f,context.iter(),|f,(WormholeID(wormhole_id),count)|
|
||||||
write!(f,"WormholeOut{wormhole_out_id}({count} duplicates)")
|
write!(f,"WormholeOut{wormhole_id}({count} duplicates)")
|
||||||
)?;
|
)?;
|
||||||
writeln!(f,"")?;
|
writeln!(f,"")?;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user