validator: implement dangling anticheat zone check

This commit is contained in:
Quaternions 2025-04-11 22:38:21 -07:00
parent ea58fcedc9
commit c63997d161
Signed by: Quaternions
GPG Key ID: D0DF5964F79AC131

@ -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")?;
}