Skip to content

Commit

Permalink
feat: new matchmaking handling and rules
Browse files Browse the repository at this point in the history
Matchmaking now has additional rules and handling for dlc on both ends.

Added tests for matchmaking rules
  • Loading branch information
jacobtread committed Jul 2, 2024
1 parent f36cd47 commit 0776633
Showing 1 changed file with 286 additions and 70 deletions.
356 changes: 286 additions & 70 deletions src/services/game/rules.rs
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");
}
}

0 comments on commit 0776633

Please sign in to comment.