use crate::events::{EventMetadata, GameEvent, MatchStateEvent};
use crate::log::entry::LogEntry;
use crate::parsers::api_common;
const MATCH_STATE_MARKER: &str = "matchGameRoomStateChangedEvent";
pub fn try_parse(
entry: &LogEntry,
timestamp: Option<chrono::DateTime<chrono::Utc>>,
) -> Option<GameEvent> {
let body = &entry.body;
if !body.contains(MATCH_STATE_MARKER) {
return None;
}
let parsed = api_common::parse_json_from_body(body, "matchGameRoomStateChangedEvent")?;
let state_event = parsed.get(MATCH_STATE_MARKER).or_else(|| {
if parsed.get("gameRoomInfo").is_some() {
Some(&parsed)
} else {
None
}
})?;
let payload = build_payload(state_event);
let metadata = EventMetadata::new(timestamp, body.as_bytes().to_vec());
Some(GameEvent::MatchState(MatchStateEvent::new(
metadata, payload,
)))
}
fn build_payload(state_event: &serde_json::Value) -> serde_json::Value {
let game_room_info = state_event.get("gameRoomInfo");
let state_type = game_room_info
.and_then(|info| info.get("stateType"))
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let match_lifecycle = match state_type {
"MatchGameRoomStateType_Playing" => "match_started",
"MatchGameRoomStateType_MatchCompleted" => "match_completed",
_ => "state_changed",
};
let game_room_config = game_room_info.and_then(|info| info.get("gameRoomConfig"));
let match_id = game_room_config
.and_then(|cfg| cfg.get("matchId"))
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let event_id = game_room_config
.and_then(|cfg| cfg.get("eventId"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
game_room_config
.and_then(|cfg| cfg.get("reservedPlayers"))
.and_then(serde_json::Value::as_array)
.and_then(|players| players.first())
.and_then(|p| p.get("eventId"))
.and_then(serde_json::Value::as_str)
})
.unwrap_or("");
let players = extract_players(game_room_config);
let final_result = game_room_info.and_then(|info| info.get("finalMatchResult"));
let result_list = final_result
.and_then(|r| r.get("resultList"))
.and_then(serde_json::Value::as_array);
let completed_reason = final_result
.and_then(|r| r.get("matchCompletedReason"))
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let game_results = build_game_results(result_list);
let mut payload = serde_json::json!({
"type": match_lifecycle,
"state_type": state_type,
"match_id": match_id,
"event_id": event_id,
"players": players,
});
if final_result.is_some() {
payload["match_completed_reason"] = serde_json::json!(completed_reason);
payload["game_results"] = game_results;
}
payload["raw_match_state"] = state_event.clone();
payload
}
fn extract_players(game_room_config: Option<&serde_json::Value>) -> serde_json::Value {
let reserved = game_room_config
.and_then(|cfg| cfg.get("reservedPlayers"))
.and_then(serde_json::Value::as_array);
let Some(players) = reserved else {
return serde_json::json!([]);
};
let extracted: Vec<serde_json::Value> = players
.iter()
.map(|p| {
serde_json::json!({
"user_id": p.get("userId").and_then(serde_json::Value::as_str).unwrap_or(""),
"player_name": p.get("playerName").and_then(serde_json::Value::as_str).unwrap_or(""),
"system_seat_id": p.get("systemSeatId").and_then(serde_json::Value::as_i64).unwrap_or(0),
"team_id": p.get("teamId").and_then(serde_json::Value::as_i64).unwrap_or(0),
})
})
.collect();
serde_json::json!(extracted)
}
fn build_game_results(result_list: Option<&Vec<serde_json::Value>>) -> serde_json::Value {
let Some(results) = result_list else {
return serde_json::json!([]);
};
let game_results: Vec<serde_json::Value> = results
.iter()
.map(|r| {
serde_json::json!({
"scope": r.get("scope").and_then(serde_json::Value::as_str).unwrap_or(""),
"result": r.get("result").and_then(serde_json::Value::as_str).unwrap_or(""),
"winning_team_id": r.get("winningTeamId").and_then(serde_json::Value::as_i64).unwrap_or(0),
})
})
.collect();
serde_json::json!(game_results)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parsers::test_helpers::{
match_state_payload, test_timestamp, unity_entry, EntryHeader,
};
fn match_start_body() -> String {
format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "abc123-match-id",
"eventId": "Ladder",
"reservedPlayers": [
{
"userId": "user-001",
"playerName": "Player1#12345",
"systemSeatId": 1,
"teamId": 1
},
{
"userId": "user-002",
"playerName": "Player2#67890",
"systemSeatId": 2,
"teamId": 2
}
]
}
}
}
})
)
}
fn match_completed_body() -> String {
format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_MatchCompleted",
"gameRoomConfig": {
"matchId": "abc123-match-id",
"eventId": "Ladder",
"reservedPlayers": [
{
"userId": "user-001",
"playerName": "Player1#12345",
"systemSeatId": 1,
"teamId": 1
},
{
"userId": "user-002",
"playerName": "Player2#67890",
"systemSeatId": 2,
"teamId": 2
}
]
},
"finalMatchResult": {
"matchId": "abc123-match-id",
"matchCompletedReason": "MatchCompletedReasonType_Success",
"resultList": [
{
"scope": "MatchScope_Game",
"result": "ResultType_WinLoss",
"winningTeamId": 1
},
{
"scope": "MatchScope_Match",
"result": "ResultType_WinLoss",
"winningTeamId": 1
}
]
}
}
}
})
)
}
fn bo3_match_completed_body() -> String {
format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_MatchCompleted",
"gameRoomConfig": {
"matchId": "bo3-match-id",
"eventId": "Traditional_Ladder",
"reservedPlayers": [
{
"userId": "user-001",
"playerName": "Player1#12345",
"systemSeatId": 1,
"teamId": 1
},
{
"userId": "user-002",
"playerName": "Player2#67890",
"systemSeatId": 2,
"teamId": 2
}
]
},
"finalMatchResult": {
"matchId": "bo3-match-id",
"matchCompletedReason": "MatchCompletedReasonType_Success",
"resultList": [
{
"scope": "MatchScope_Game",
"result": "ResultType_WinLoss",
"winningTeamId": 1
},
{
"scope": "MatchScope_Game",
"result": "ResultType_WinLoss",
"winningTeamId": 2
},
{
"scope": "MatchScope_Game",
"result": "ResultType_WinLoss",
"winningTeamId": 1
},
{
"scope": "MatchScope_Match",
"result": "ResultType_WinLoss",
"winningTeamId": 1
}
]
}
}
}
})
)
}
mod match_start {
use super::*;
#[test]
fn test_try_parse_match_start_detected() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
}
#[test]
fn test_try_parse_match_start_lifecycle_type() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["type"], "match_started");
}
#[test]
fn test_try_parse_match_start_state_type() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["state_type"], "MatchGameRoomStateType_Playing");
}
#[test]
fn test_try_parse_match_start_match_id() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["match_id"], "abc123-match-id");
}
#[test]
fn test_try_parse_match_start_event_id() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["event_id"], "Ladder");
}
#[test]
fn test_try_parse_match_start_player_count() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let players = payload["players"].as_array();
assert!(players.is_some());
let players = players.unwrap_or_else(|| unreachable!());
assert_eq!(players.len(), 2);
}
#[test]
fn test_try_parse_match_start_player_seat_assignments() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let players = payload["players"].as_array();
assert!(players.is_some());
let players = players.unwrap_or_else(|| unreachable!());
assert_eq!(players[0]["system_seat_id"], 1);
assert_eq!(players[0]["player_name"], "Player1#12345");
assert_eq!(players[0]["user_id"], "user-001");
assert_eq!(players[0]["team_id"], 1);
assert_eq!(players[1]["system_seat_id"], 2);
assert_eq!(players[1]["player_name"], "Player2#67890");
assert_eq!(players[1]["user_id"], "user-002");
assert_eq!(players[1]["team_id"], 2);
}
#[test]
fn test_try_parse_match_start_no_final_result() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert!(payload.get("match_completed_reason").is_none());
assert!(payload.get("game_results").is_none());
}
#[test]
fn test_try_parse_match_start_preserves_raw_bytes() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.metadata().raw_bytes(), body.as_bytes());
}
#[test]
fn test_try_parse_match_start_stores_timestamp() {
let body = match_start_body();
let entry = unity_entry(&body);
let ts = Some(test_timestamp());
let result = try_parse(&entry, ts);
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.metadata().timestamp(), ts);
}
#[test]
fn test_try_parse_match_start_includes_raw_match_state() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert!(payload.get("raw_match_state").is_some());
assert!(payload["raw_match_state"]["gameRoomInfo"].is_object());
}
}
mod match_completed {
use super::*;
#[test]
fn test_try_parse_match_completed_detected() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
}
#[test]
fn test_try_parse_match_completed_lifecycle_type() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["type"], "match_completed");
}
#[test]
fn test_try_parse_match_completed_state_type() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(
payload["state_type"],
"MatchGameRoomStateType_MatchCompleted"
);
}
#[test]
fn test_try_parse_match_completed_reason() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(
payload["match_completed_reason"],
"MatchCompletedReasonType_Success"
);
}
#[test]
fn test_try_parse_match_completed_game_results() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let results = payload["game_results"].as_array();
assert!(results.is_some());
let results = results.unwrap_or_else(|| unreachable!());
assert_eq!(results.len(), 2);
}
#[test]
fn test_try_parse_match_completed_game_result_fields() {
let body = match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let results = payload["game_results"].as_array();
assert!(results.is_some());
let results = results.unwrap_or_else(|| unreachable!());
assert_eq!(results[0]["scope"], "MatchScope_Game");
assert_eq!(results[0]["result"], "ResultType_WinLoss");
assert_eq!(results[0]["winning_team_id"], 1);
assert_eq!(results[1]["scope"], "MatchScope_Match");
assert_eq!(results[1]["winning_team_id"], 1);
}
#[test]
fn test_try_parse_match_completed_preserves_metadata() {
let body = match_completed_body();
let entry = unity_entry(&body);
let ts = Some(test_timestamp());
let result = try_parse(&entry, ts);
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(event.metadata().timestamp(), ts);
assert_eq!(event.metadata().raw_bytes(), body.as_bytes());
}
}
mod bo3_correlation {
use super::*;
#[test]
fn test_try_parse_bo3_match_id() {
let body = bo3_match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["match_id"], "bo3-match-id");
}
#[test]
fn test_try_parse_bo3_event_id() {
let body = bo3_match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["event_id"], "Traditional_Ladder");
}
#[test]
fn test_try_parse_bo3_game_results_count() {
let body = bo3_match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let results = payload["game_results"].as_array();
assert!(results.is_some());
let results = results.unwrap_or_else(|| unreachable!());
assert_eq!(results.len(), 4);
}
#[test]
fn test_try_parse_bo3_individual_game_results() {
let body = bo3_match_completed_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let results = payload["game_results"].as_array();
assert!(results.is_some());
let results = results.unwrap_or_else(|| unreachable!());
assert_eq!(results[0]["scope"], "MatchScope_Game");
assert_eq!(results[0]["winning_team_id"], 1);
assert_eq!(results[1]["scope"], "MatchScope_Game");
assert_eq!(results[1]["winning_team_id"], 2);
assert_eq!(results[2]["scope"], "MatchScope_Game");
assert_eq!(results[2]["winning_team_id"], 1);
assert_eq!(results[3]["scope"], "MatchScope_Match");
assert_eq!(results[3]["winning_team_id"], 1);
}
}
mod non_match_state {
use super::*;
#[test]
fn test_try_parse_unrelated_entry_returns_none() {
let body = "[UnityCrossThreadLogger]greToClientEvent\n{\"data\": 1}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_empty_body_returns_none() {
let body = "[UnityCrossThreadLogger]";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_session_event_returns_none() {
let body = "[UnityCrossThreadLogger]Updated account. \
DisplayName:Test, AccountID:abc123";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_no_json_body_returns_none() {
let body = "[UnityCrossThreadLogger]matchGameRoomStateChangedEvent with no json";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_malformed_json_returns_none() {
let body = "[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{invalid json}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_json_without_match_state_key_returns_none() {
let body = "[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n\
{\"someOtherEvent\": {\"data\": 1}}";
let entry = unity_entry(body);
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
#[test]
fn test_try_parse_client_gre_entry_returns_none() {
let entry = LogEntry {
header: EntryHeader::ClientGre,
body: "[Client GRE]matchGameRoomStateChangedEvent".to_owned(),
};
assert!(try_parse(&entry, Some(test_timestamp())).is_none());
}
}
mod edge_cases {
use super::*;
#[test]
fn test_try_parse_missing_game_room_config() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing"
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["match_id"], "");
assert_eq!(payload["event_id"], "");
let players = payload["players"].as_array();
assert!(players.is_some());
let players = players.unwrap_or_else(|| unreachable!());
assert!(players.is_empty());
}
#[test]
fn test_try_parse_empty_reserved_players() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "empty-players-match",
"eventId": "Ladder",
"reservedPlayers": []
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let players = payload["players"].as_array();
assert!(players.is_some());
let players = players.unwrap_or_else(|| unreachable!());
assert!(players.is_empty());
}
#[test]
fn test_try_parse_unknown_state_type() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_SomeNewState",
"gameRoomConfig": {
"matchId": "new-state-match",
"eventId": "Ladder"
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["type"], "state_changed");
assert_eq!(payload["state_type"], "MatchGameRoomStateType_SomeNewState");
}
#[test]
fn test_try_parse_with_timestamp_in_header() {
let body = format!(
"[UnityCrossThreadLogger]2/25/2026 12:00:00 PM matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "ts-match-id",
"eventId": "Ladder"
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["match_id"], "ts-match-id");
}
#[test]
fn test_try_parse_match_completed_disconnect_reason() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_MatchCompleted",
"gameRoomConfig": {
"matchId": "disconnect-match-id",
"eventId": "Ladder",
"reservedPlayers": []
},
"finalMatchResult": {
"matchId": "disconnect-match-id",
"matchCompletedReason": "MatchCompletedReasonType_PlayerDisconnectTimeout",
"resultList": [
{
"scope": "MatchScope_Match",
"result": "ResultType_WinLoss",
"winningTeamId": 2
}
]
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["type"], "match_completed");
assert_eq!(
payload["match_completed_reason"],
"MatchCompletedReasonType_PlayerDisconnectTimeout"
);
}
#[test]
fn test_try_parse_top_level_game_room_info() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "top-level-match",
"eventId": "Ladder",
"reservedPlayers": []
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["match_id"], "top-level-match");
}
#[test]
fn test_try_parse_event_id_from_reserved_players_extracted() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "reserved-event-match",
"reservedPlayers": [
{
"userId": "user-001",
"playerName": "Player1",
"systemSeatId": 1,
"teamId": 1,
"eventId": "Timeless_Ladder"
},
{
"userId": "user-002",
"playerName": "Player2",
"systemSeatId": 2,
"teamId": 2,
"eventId": "Timeless_Ladder"
}
]
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["event_id"], "Timeless_Ladder");
}
#[test]
fn test_try_parse_event_id_no_source_defaults_empty() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "no-event-id-match",
"reservedPlayers": []
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(payload["event_id"], "");
}
#[test]
fn test_try_parse_player_missing_optional_fields() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_Playing",
"gameRoomConfig": {
"matchId": "sparse-player-match",
"reservedPlayers": [
{
"systemSeatId": 1
}
]
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
let players = payload["players"].as_array();
assert!(players.is_some());
let players = players.unwrap_or_else(|| unreachable!());
assert_eq!(players.len(), 1);
assert_eq!(players[0]["system_seat_id"], 1);
assert_eq!(players[0]["user_id"], "");
assert_eq!(players[0]["player_name"], "");
assert_eq!(players[0]["team_id"], 0);
}
#[test]
fn test_try_parse_final_result_empty_result_list() {
let body = format!(
"[UnityCrossThreadLogger]matchGameRoomStateChangedEvent\n{}",
serde_json::json!({
"matchGameRoomStateChangedEvent": {
"gameRoomInfo": {
"stateType": "MatchGameRoomStateType_MatchCompleted",
"gameRoomConfig": {
"matchId": "empty-result-match",
"eventId": "Ladder"
},
"finalMatchResult": {
"matchId": "empty-result-match",
"matchCompletedReason": "MatchCompletedReasonType_Canceled",
"resultList": []
}
}
}
})
);
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
let payload = match_state_payload(event);
assert_eq!(
payload["match_completed_reason"],
"MatchCompletedReasonType_Canceled"
);
let results = payload["game_results"].as_array();
assert!(results.is_some());
let results = results.unwrap_or_else(|| unreachable!());
assert!(results.is_empty());
}
}
mod performance_class {
use super::*;
use crate::events::PerformanceClass;
#[test]
fn test_match_state_event_is_interactive_dispatch() {
let body = match_start_body();
let entry = unity_entry(&body);
let result = try_parse(&entry, Some(test_timestamp()));
assert!(result.is_some());
let event = result.as_ref().unwrap_or_else(|| unreachable!());
assert_eq!(
event.performance_class(),
PerformanceClass::InteractiveDispatch
);
}
}
mod helpers {
use super::*;
#[test]
fn test_extract_players_none_config() {
let result = extract_players(None);
assert_eq!(result, serde_json::json!([]));
}
#[test]
fn test_extract_players_no_reserved_players_key() {
let config = serde_json::json!({"matchId": "test"});
let result = extract_players(Some(&config));
assert_eq!(result, serde_json::json!([]));
}
#[test]
fn test_build_game_results_none() {
let result = build_game_results(None);
assert_eq!(result, serde_json::json!([]));
}
#[test]
fn test_build_game_results_empty() {
let empty_list: Vec<serde_json::Value> = vec![];
let result = build_game_results(Some(&empty_list));
assert_eq!(result, serde_json::json!([]));
}
#[test]
fn test_build_game_results_single_entry() {
let list = vec![serde_json::json!({
"scope": "MatchScope_Game",
"result": "ResultType_WinLoss",
"winningTeamId": 1
})];
let result = build_game_results(Some(&list));
let arr = result.as_array();
assert!(arr.is_some());
let arr = arr.unwrap_or_else(|| unreachable!());
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["scope"], "MatchScope_Game");
assert_eq!(arr[0]["winning_team_id"], 1);
}
}
}