-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new matchmaking handling and rules
Matchmaking now has additional rules and handling for dlc on both ends. Added tests for matchmaking rules
- Loading branch information
1 parent
f36cd47
commit 0776633
Showing
1 changed file
with
286 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,98 +1,314 @@ | ||
use crate::services::game::AttrMap; | ||
use super::AttrMap; | ||
|
||
/// Rule sets are fairly cheap to clone. Rule values are not usually | ||
/// very long. | ||
#[derive(Debug)] | ||
pub struct Rule { | ||
// The rule key | ||
key: &'static str, | ||
// Game attribute represented by the rule | ||
attr: &'static str, | ||
} | ||
|
||
impl Rule { | ||
const fn new(key: &'static str, attr: &'static str) -> Self { | ||
Self { key, attr } | ||
} | ||
} | ||
|
||
/// Known rules and the attribute they operate over | ||
pub static RULES: &[Rule] = &[ | ||
// Map type | ||
Rule::new("ME3_gameMapMatchRule", "ME3map"), | ||
// Enemy type | ||
Rule::new("ME3_gameEnemyTypeRule", "ME3gameEnemyType"), | ||
// Difficulty type | ||
Rule::new("ME3_gameDifficultyRule", "ME3gameDifficulty"), | ||
]; | ||
|
||
/// Rules for DLC that are present | ||
pub static DLC_RULES: &[Rule] = &[ | ||
// DLC Rules | ||
Rule::new("ME3_rule_dlc2300", "ME3_dlc2300"), | ||
Rule::new("ME3_rule_dlc2500", "ME3_dlc2500"), | ||
Rule::new("ME3_rule_dlc2700", "ME3_dlc2700"), | ||
Rule::new("ME3_rule_dlc3050", "ME3_dlc3050"), | ||
Rule::new("ME3_rule_dlc3225", "ME3_dlc3225"), | ||
]; | ||
|
||
/// Attribute determining the game privacy for public | ||
/// match checking | ||
const PRIVACY_ATTR: &str = "ME3privacy"; | ||
|
||
/// Value for rules that have been abstained from matching | ||
/// when a rule is abstained it is ignored | ||
const ABSTAIN: &str = "abstain"; | ||
|
||
/// Defines a rule to be matched and the value to match | ||
#[derive(Debug)] | ||
pub struct MatchRule { | ||
/// Rule being matched for | ||
rule: &'static Rule, | ||
/// Value to match using | ||
value: String, | ||
} | ||
|
||
/// Set of rules to match | ||
#[derive(Debug)] | ||
pub struct RuleSet { | ||
/// Map rule provided in the matchmaking request | ||
map_rule: Option<String>, | ||
/// Enemy rule provided in the matchmaking request | ||
enemy_rule: Option<String>, | ||
/// Difficulty rule provided in the matchmaking request | ||
difficulty_rule: Option<String>, | ||
/// The rules to match | ||
rules: Vec<MatchRule>, | ||
} | ||
|
||
impl RuleSet { | ||
/// Attribute determining the game privacy for public | ||
/// match checking | ||
const PRIVACY_ATTR: &'static str = "ME3privacy"; | ||
|
||
/// Map attribute and rule keys | ||
const MAP_ATTR: &'static str = "ME3map"; | ||
const MAP_RULE: &'static str = "ME3_gameMapMatchRule"; | ||
|
||
/// Enemy attribute and rule keys | ||
const ENEMY_ATTR: &'static str = "ME3gameEnemyType"; | ||
const ENEMY_RULE: &'static str = "ME3_gameEnemyTypeRule"; | ||
|
||
/// Difficulty attribute and rule keys | ||
const DIFFICULTY_ATTR: &'static str = "ME3gameDifficulty"; | ||
const DIFFICULTY_RULE: &'static str = "ME3_gameDifficultyRule"; | ||
|
||
/// Value for rules that have been abstained from matching | ||
/// when a rule is abstained it is ignored | ||
const ABSTAIN: &'static str = "abstain"; | ||
|
||
/// Creates a new rule set from the provided list | ||
/// of rule key values | ||
/// | ||
/// `rules` The rules to create from | ||
pub fn new(rules: Vec<(String, String)>) -> Self { | ||
let mut map_rule: Option<String> = None; | ||
let mut enemy_rule: Option<String> = None; | ||
let mut difficulty_rule: Option<String> = None; | ||
|
||
for (rule, value) in rules { | ||
if value == Self::ABSTAIN { | ||
/// Creates a new set of rule matches from the provided rule value pairs | ||
pub fn new(pairs: Vec<(String, String)>) -> Self { | ||
let mut rules = Vec::new(); | ||
|
||
for (rule_key, value) in pairs { | ||
if value == ABSTAIN { | ||
continue; | ||
} | ||
match &rule as &str { | ||
Self::MAP_RULE => map_rule = Some(value), | ||
Self::ENEMY_RULE => enemy_rule = Some(value), | ||
Self::DIFFICULTY_RULE => difficulty_rule = Some(value), | ||
_ => {} | ||
|
||
let rule = RULES | ||
.iter() | ||
.chain(DLC_RULES.iter()) | ||
.find(|rule| rule.key.eq(&rule_key)); | ||
|
||
if let Some(rule) = rule { | ||
rules.push(MatchRule { rule, value }) | ||
} | ||
} | ||
Self { | ||
map_rule, | ||
enemy_rule, | ||
difficulty_rule, | ||
} | ||
|
||
Self { rules } | ||
} | ||
|
||
/// Checks if the rules provided in this rule set match the values in | ||
/// the attributes map. | ||
/// | ||
/// `attributes` The attributes map to check for matches | ||
pub fn matches(&self, attributes: &AttrMap) -> bool { | ||
// Non public matches are unable to be matched | ||
if let Some(privacy) = attributes.get(Self::PRIVACY_ATTR) { | ||
if let Some(privacy) = attributes.get(PRIVACY_ATTR) { | ||
if privacy != "PUBLIC" { | ||
return false; | ||
} | ||
} | ||
|
||
fn compare_rule(rule: Option<&String>, value: Option<&String>) -> bool { | ||
rule.zip(value) | ||
.map(|(a, b)| a.eq(b)) | ||
// Missing rules / attributes count as match and continue | ||
.unwrap_or(true) | ||
// Handle matching requested rules | ||
for rule in &self.rules { | ||
// Ensure the attribute is present and matching | ||
if !attributes | ||
.get(rule.rule.attr) | ||
.is_some_and(|value| value.eq(&rule.value)) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
if !compare_rule(self.map_rule.as_ref(), attributes.get(Self::MAP_ATTR)) { | ||
return false; | ||
// Handle the game requiring a DLC rule but the client not specifying it | ||
for rule in DLC_RULES { | ||
let local_rule = self | ||
.rules | ||
.iter() | ||
.find(|match_rule| match_rule.rule.key == rule.key); | ||
let dlc_attribute = attributes.get(rule.attr); | ||
if dlc_attribute.is_some() && local_rule.is_none() { | ||
return false; | ||
} | ||
} | ||
|
||
if !compare_rule(self.enemy_rule.as_ref(), attributes.get(Self::ENEMY_ATTR)) { | ||
return false; | ||
} | ||
true | ||
} | ||
} | ||
|
||
if !compare_rule( | ||
self.difficulty_rule.as_ref(), | ||
attributes.get(Self::DIFFICULTY_ATTR), | ||
) { | ||
return false; | ||
} | ||
#[cfg(test)] | ||
mod test { | ||
use crate::services::game::AttrMap; | ||
|
||
true | ||
use super::RuleSet; | ||
|
||
/// Public match should succeed if the attributes meet the specified criteria | ||
#[test] | ||
fn test_public_match() { | ||
let attributes = [ | ||
("ME3_dlc2300", "required"), | ||
("ME3_dlc2500", "required"), | ||
("ME3_dlc2700", "required"), | ||
("ME3_dlc3050", "required"), | ||
("ME3_dlc3225", "required"), | ||
("ME3gameDifficulty", "difficulty0"), | ||
("ME3gameEnemyType", "enemy1"), | ||
("ME3map", "map2"), | ||
("ME3privacy", "PUBLIC"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<AttrMap>(); | ||
|
||
let rules = [ | ||
("ME3_gameMapMatchRule", "abstain"), | ||
("ME3_gameEnemyTypeRule", "abstain"), | ||
("ME3_gameDifficultyRule", "abstain"), | ||
("ME3_rule_dlc2500", "required"), | ||
("ME3_rule_dlc2300", "required"), | ||
("ME3_rule_dlc2700", "required"), | ||
("ME3_rule_dlc3050", "required"), | ||
("ME3_rule_dlc3225", "required"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<Vec<(String, String)>>(); | ||
|
||
let rule_set = RuleSet::new(rules); | ||
|
||
let matches = rule_set.matches(&attributes); | ||
|
||
assert!(matches, "Rule set didn't match the provided attributes"); | ||
} | ||
|
||
/// When attributes aren't abstain they should match exactly | ||
#[test] | ||
fn test_specific_attributes() { | ||
let attributes = [ | ||
("ME3_dlc2300", "required"), | ||
("ME3_dlc2500", "required"), | ||
("ME3_dlc2700", "required"), | ||
("ME3_dlc3050", "required"), | ||
("ME3_dlc3225", "required"), | ||
("ME3gameDifficulty", "difficulty0"), | ||
("ME3gameEnemyType", "enemy1"), | ||
("ME3map", "map2"), | ||
("ME3privacy", "PUBLIC"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<AttrMap>(); | ||
|
||
let rules = [ | ||
("ME3_gameMapMatchRule", "map2"), | ||
("ME3_gameEnemyTypeRule", "enemy1"), | ||
("ME3_gameDifficultyRule", "difficulty0"), | ||
("ME3_rule_dlc2500", "required"), | ||
("ME3_rule_dlc2300", "required"), | ||
("ME3_rule_dlc2700", "required"), | ||
("ME3_rule_dlc3050", "required"), | ||
("ME3_rule_dlc3225", "required"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<Vec<(String, String)>>(); | ||
|
||
let rule_set = RuleSet::new(rules); | ||
|
||
let matches = rule_set.matches(&attributes); | ||
|
||
assert!(matches, "Rule set didn't match the provided attributes"); | ||
} | ||
|
||
/// Private match should always fail a matchmaking rule set regardless | ||
/// of the other attributes | ||
#[test] | ||
fn test_private_match() { | ||
let attributes = [ | ||
("ME3_dlc2300", "required"), | ||
("ME3_dlc2500", "required"), | ||
("ME3_dlc2700", "required"), | ||
("ME3_dlc3050", "required"), | ||
("ME3_dlc3225", "required"), | ||
("ME3gameDifficulty", "difficulty0"), | ||
("ME3gameEnemyType", "enemy1"), | ||
("ME3map", "map2"), | ||
("ME3privacy", "PRIVATE"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<AttrMap>(); | ||
|
||
let rules = [ | ||
("ME3_gameMapMatchRule", "abstain"), | ||
("ME3_gameEnemyTypeRule", "abstain"), | ||
("ME3_gameDifficultyRule", "abstain"), | ||
("ME3_rule_dlc2500", "required"), | ||
("ME3_rule_dlc2300", "required"), | ||
("ME3_rule_dlc2700", "required"), | ||
("ME3_rule_dlc3050", "required"), | ||
("ME3_rule_dlc3225", "required"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<Vec<(String, String)>>(); | ||
|
||
let rule_set = RuleSet::new(rules); | ||
|
||
let matches = rule_set.matches(&attributes); | ||
|
||
assert!(!matches, "Rule set matched a private match"); | ||
} | ||
|
||
/// If the player has a DLC requirement that the host doesn't have | ||
/// the matching should fail | ||
#[test] | ||
fn test_dlc_mismatch() { | ||
let attributes = [ | ||
("ME3_dlc2300", "required"), | ||
("ME3_dlc2500", "required"), | ||
("ME3_dlc2700", "required"), | ||
("ME3_dlc3050", "required"), | ||
("ME3_dlc3225", "required"), | ||
("ME3gameDifficulty", "difficulty0"), | ||
("ME3gameEnemyType", "enemy1"), | ||
("ME3map", "map2"), | ||
("ME3privacy", "PUBLIC"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<AttrMap>(); | ||
|
||
let rules = [ | ||
("ME3_gameMapMatchRule", "abstain"), | ||
("ME3_gameEnemyTypeRule", "abstain"), | ||
("ME3_gameDifficultyRule", "abstain"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<Vec<(String, String)>>(); | ||
|
||
let rule_set = RuleSet::new(rules); | ||
|
||
let matches = rule_set.matches(&attributes); | ||
|
||
assert!(!matches, "Matched host with missing DLC"); | ||
} | ||
|
||
/// If the host has required DLC but the player is missing it | ||
/// the matching should fail | ||
#[test] | ||
fn test_player_dlc_mismatch() { | ||
let attributes = [ | ||
("ME3gameDifficulty", "difficulty0"), | ||
("ME3gameEnemyType", "enemy1"), | ||
("ME3map", "map2"), | ||
("ME3privacy", "PRIVATE"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<AttrMap>(); | ||
|
||
let rules = [ | ||
("ME3_gameMapMatchRule", "abstain"), | ||
("ME3_gameEnemyTypeRule", "abstain"), | ||
("ME3_gameDifficultyRule", "abstain"), | ||
("ME3_rule_dlc2500", "required"), | ||
("ME3_rule_dlc2300", "required"), | ||
("ME3_rule_dlc2700", "required"), | ||
("ME3_rule_dlc3050", "required"), | ||
("ME3_rule_dlc3225", "required"), | ||
] | ||
.into_iter() | ||
.map(|(key, value)| (key.to_string(), value.to_string())) | ||
.collect::<Vec<(String, String)>>(); | ||
|
||
let rule_set = RuleSet::new(rules); | ||
|
||
let matches = rule_set.matches(&attributes); | ||
|
||
assert!(!matches, "Matched player with missing DLC"); | ||
} | ||
} |