use std::{borrow::Borrow, cell::RefCell, collections::HashMap, str::FromStr, time::Duration};
use nom::{multi::count, number::complete::le_u32, sequence::pair};
use serde::{Deserialize, Serialize};
use strum_macros::EnumString;
use tracing::{Level, debug, span, trace};
use variantly::Variantly;
use wowsunpack::{
data::{ResourceLoader, Version},
game_params::types::{CrewSkill, Param, ParamType, Species},
rpc::typedefs::ArgValue,
};
static TIME_UNTIL_GAME_START: Duration = Duration::from_secs(30);
use crate::{
IResult, Rc, ReplayMeta,
analyzer::{
analyzer::AnalyzerMut,
decoder::{ChatMessageExtra, DeathCause, DecodedPacket, OnArenaStateReceivedPlayer},
},
packet2::{EntityCreatePacket, Packet, PacketProcessorMut, PacketType},
};
#[derive(Debug, Default, Clone, Serialize)]
pub struct ShipConfig {
abilities: Vec<u32>,
hull: u32,
modernization: Vec<u32>,
units: Vec<u32>,
signals: Vec<u32>,
}
impl ShipConfig {
pub fn signals(&self) -> &[u32] {
self.signals.as_ref()
}
pub fn units(&self) -> &[u32] {
self.units.as_ref()
}
pub fn modernization(&self) -> &[u32] {
self.modernization.as_ref()
}
pub fn hull(&self) -> u32 {
self.hull
}
pub fn abilities(&self) -> &[u32] {
self.abilities.as_ref()
}
}
#[derive(Debug, Default, Clone, Serialize)]
pub struct Skills {
aircraft_carrier: Vec<u8>,
battleship: Vec<u8>,
cruiser: Vec<u8>,
destroyer: Vec<u8>,
auxiliary: Vec<u8>,
submarine: Vec<u8>,
}
impl Skills {
pub fn submarine(&self) -> &[u8] {
self.submarine.as_ref()
}
pub fn auxiliary(&self) -> &[u8] {
self.auxiliary.as_ref()
}
pub fn destroyer(&self) -> &[u8] {
self.destroyer.as_ref()
}
pub fn cruiser(&self) -> &[u8] {
self.cruiser.as_ref()
}
pub fn battleship(&self) -> &[u8] {
self.battleship.as_ref()
}
pub fn aircraft_carrier(&self) -> &[u8] {
self.aircraft_carrier.as_ref()
}
}
#[derive(Debug, Default, Serialize)]
pub struct ShipLoadout {
config: Option<ShipConfig>,
skills: Option<Skills>,
}
impl ShipLoadout {
pub fn skills(&self) -> Option<&Skills> {
self.skills.as_ref()
}
pub fn config(&self) -> Option<&ShipConfig> {
self.config.as_ref()
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Player {
name: String,
clan: String,
clan_id: i64,
clan_color: i64,
realm: String,
db_id: i64,
relation: u32,
avatar_id: u32,
ship_id: u32,
entity_id: u32,
division_id: u32,
team_id: u32,
max_health: u32,
is_abuser: bool,
is_hidden: bool,
is_client_loaded: bool,
did_disconnect: bool,
is_connected: bool,
vehicle: Rc<Param>,
raw_props: HashMap<i64, String>,
raw_props_with_name: HashMap<String, serde_json::Value>,
}
impl std::cmp::PartialEq for Player {
fn eq(&self, other: &Self) -> bool {
self.db_id == other.db_id && self.realm == other.realm
}
}
impl std::cmp::Eq for Player {}
impl std::hash::Hash for Player {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.db_id.hash(state);
self.realm.hash(state);
}
}
impl Player {
fn from_arena_player<G: ResourceLoader>(
player: &OnArenaStateReceivedPlayer,
metadata_player: &MetadataPlayer,
resources: &G,
) -> Player {
let OnArenaStateReceivedPlayer {
username,
clan,
realm,
db_id,
avatar_id: avatarid,
meta_ship_id: shipid,
entity_id,
prebattle_id,
team_id: teamid,
max_health: health,
raw,
raw_with_names,
is_abuser,
is_hidden,
is_client_loaded,
is_connected,
clan_id,
clan_color,
} = player;
Player {
name: username.clone(),
clan: clan.clone(),
clan_id: *clan_id,
clan_color: *clan_color,
realm: realm.clone(),
db_id: *db_id,
avatar_id: *avatarid as u32,
ship_id: *shipid as u32,
entity_id: *entity_id as u32,
team_id: *teamid as u32,
division_id: *prebattle_id as u32,
max_health: *health as u32,
vehicle: resources
.game_param_by_id(metadata_player.vehicle.id())
.expect("could not find vehicle"),
relation: metadata_player.relation,
is_abuser: *is_abuser,
is_hidden: *is_hidden,
is_client_loaded: *is_client_loaded,
is_connected: *is_connected,
did_disconnect: false,
raw_props: raw.clone(),
raw_props_with_name: raw_with_names
.iter()
.map(|(key, value)| (key.to_string(), value.clone()))
.collect(),
}
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn clan(&self) -> &str {
self.clan.as_ref()
}
pub fn relation(&self) -> u32 {
self.relation
}
pub fn avatar_id(&self) -> u32 {
self.avatar_id
}
pub fn ship_id(&self) -> u32 {
self.ship_id
}
pub fn entity_id(&self) -> u32 {
self.entity_id
}
pub fn team_id(&self) -> u32 {
self.team_id
}
pub fn max_health(&self) -> u32 {
self.max_health
}
pub fn vehicle(&self) -> &Param {
self.vehicle.as_ref()
}
pub fn realm(&self) -> &str {
self.realm.as_ref()
}
pub fn db_id(&self) -> i64 {
self.db_id
}
pub fn is_abuser(&self) -> bool {
self.is_abuser
}
pub fn is_hidden(&self) -> bool {
self.is_hidden
}
pub fn is_client_loaded(&self) -> bool {
self.is_client_loaded
}
pub fn did_disconnect(&self) -> bool {
self.did_disconnect
}
pub fn is_connected(&self) -> bool {
self.is_connected
}
pub fn division_id(&self) -> u32 {
self.division_id
}
pub fn clan_id(&self) -> i64 {
self.clan_id
}
pub fn clan_color(&self) -> i64 {
self.clan_color
}
pub fn raw_props_with_name(&self) -> &HashMap<String, serde_json::Value> {
&self.raw_props_with_name
}
}
#[derive(Debug)]
pub struct MetadataPlayer {
id: u32,
name: String,
relation: u32,
vehicle: Rc<Param>,
}
impl MetadataPlayer {
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn relation(&self) -> u32 {
self.relation
}
pub fn vehicle(&self) -> &Param {
self.vehicle.as_ref()
}
pub fn id(&self) -> u32 {
self.id
}
}
pub type SharedPlayer = Rc<MetadataPlayer>;
type MethodName = String;
pub trait EventHandler {
fn on_chat_message(&self, message: GameMessage) {}
fn on_aren_state_received(&self, entity_id: u32) {}
}
pub enum xEntityType {
Client = 1,
Cell = 2,
Base = 4,
}
#[derive(Debug, Clone, Copy, EnumString)]
pub enum EntityType {
Building,
BattleEntity,
BattleLogic,
Vehicle,
InteractiveZone,
SmokeScreen,
}
#[derive(Copy, Clone, Serialize)]
#[serde(tag = "type")]
pub enum BattleResult {
Win(i8),
Loss(i8),
Draw,
}
#[derive(Serialize)]
pub struct BattleReport {
arena_id: i64,
self_entity: Rc<VehicleEntity>,
version: Version,
map_name: String,
game_mode: String,
game_type: String,
match_group: String,
player_entities: Vec<Rc<VehicleEntity>>,
game_chat: Vec<GameMessage>,
battle_results: Option<String>,
players: Vec<Rc<Player>>,
frags: HashMap<Rc<Player>, Vec<DeathInfo>>,
match_result: Option<BattleResult>,
}
impl BattleReport {
pub fn self_entity(&self) -> Rc<VehicleEntity> {
self.self_entity.clone()
}
pub fn player_entities(&self) -> &[Rc<VehicleEntity>] {
self.player_entities.as_ref()
}
pub fn game_chat(&self) -> &[GameMessage] {
self.game_chat.as_ref()
}
pub fn match_group(&self) -> &str {
self.match_group.as_ref()
}
pub fn map_name(&self) -> &str {
self.map_name.as_ref()
}
pub fn version(&self) -> Version {
self.version
}
pub fn game_mode(&self) -> &str {
self.game_mode.as_ref()
}
pub fn game_type(&self) -> &str {
self.game_type.as_ref()
}
pub fn battle_results(&self) -> Option<&str> {
self.battle_results.as_deref()
}
pub fn players(&self) -> &[Rc<Player>] {
&self.players
}
pub fn arena_id(&self) -> i64 {
self.arena_id
}
pub fn frags(&self) -> &HashMap<Rc<Player>, Vec<DeathInfo>> {
&self.frags
}
pub fn battle_result(&self) -> Option<&BattleResult> {
self.match_result.as_ref()
}
}
type Id = u32;
struct DamageEvent {
amount: f32,
victim: Id,
}
pub struct BattleController<'res, 'replay, G> {
game_meta: &'replay ReplayMeta,
game_resources: &'res G,
metadata_players: Vec<SharedPlayer>,
player_entities: HashMap<Id, Rc<Player>>,
entities_by_id: HashMap<Id, Entity>,
method_callbacks: HashMap<(ParamType, String), fn(&PacketType<'_, '_>)>,
property_callbacks: HashMap<(ParamType, String), fn(&ArgValue<'_>)>,
damage_dealt: HashMap<u32, Vec<DamageEvent>>,
frags: HashMap<u32, Vec<Death>>,
event_handler: Option<Rc<dyn EventHandler>>,
game_chat: Vec<GameMessage>,
version: Version,
battle_results: Option<String>,
match_finished: bool,
winning_team: Option<i8>,
arena_id: i64,
}
impl<'res, 'replay, G> BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
pub fn new(game_meta: &'replay ReplayMeta, game_resources: &'res G) -> Self {
let players: Vec<SharedPlayer> = game_meta
.vehicles
.iter()
.map(|vehicle| {
Rc::new(MetadataPlayer {
id: vehicle.id as u32,
name: vehicle.name.clone(),
relation: vehicle.relation,
vehicle: game_resources
.game_param_by_id(vehicle.shipId as u32)
.expect("could not find vehicle"),
})
})
.collect();
Self {
game_meta,
game_resources,
metadata_players: players,
player_entities: HashMap::default(),
entities_by_id: Default::default(),
method_callbacks: Default::default(),
property_callbacks: Default::default(),
event_handler: None,
game_chat: Default::default(),
version: Version::from_client_exe(&game_meta.clientVersionFromExe),
damage_dealt: Default::default(),
frags: Default::default(),
battle_results: Default::default(),
match_finished: false,
winning_team: None,
arena_id: 0,
}
}
pub fn set_event_handler(&mut self, event_handler: Rc<dyn EventHandler>) {
self.event_handler = Some(event_handler);
}
pub fn players(&self) -> &[SharedPlayer] {
self.metadata_players.as_ref()
}
pub fn game_mode(&self) -> String {
let id = format!("IDS_SCENARIO_{}", self.game_meta.scenario.to_uppercase());
self.game_resources
.localized_name_from_id(&id)
.unwrap_or_else(|| self.game_meta.scenario.clone())
}
pub fn map_name(&self) -> String {
let id = format!("IDS_{}", self.game_meta.mapName.to_uppercase());
self.game_resources
.localized_name_from_id(&id)
.unwrap_or_else(|| self.game_meta.mapName.clone())
}
pub fn player_name(&self) -> &str {
self.game_meta.playerName.as_ref()
}
pub fn match_group(&self) -> &str {
self.game_meta.matchGroup.as_ref()
}
pub fn game_version(&self) -> &str {
self.game_meta.clientVersionFromExe.as_ref()
}
pub fn game_type(&self) -> String {
let id = format!("IDS_{}", self.game_meta.gameType.to_ascii_uppercase());
self.game_resources
.localized_name_from_id(&id)
.unwrap_or_else(|| self.game_meta.gameType.clone())
}
fn handle_chat_message<'packet>(
&mut self,
entity_id: u32,
sender_id: i32,
audience: &str,
message: &str,
extra_data: Option<ChatMessageExtra>,
) {
if sender_id == 0 {
return;
}
let sender_id = sender_id as u32;
let channel = match audience {
"battle_common" => ChatChannel::Global,
"battle_team" => ChatChannel::Team,
"battle_prebattle" => ChatChannel::Division,
other => panic!("unknown channel {}", other),
};
let mut sender_team = None;
let mut sender_name = "Unknown".to_owned();
let mut player = None;
for meta_vehicle in &self.game_meta.vehicles {
if meta_vehicle.id == (sender_id as i64) {
sender_name = meta_vehicle.name.clone();
sender_team = Some(meta_vehicle.relation);
player = self
.player_entities
.values()
.find(|player| player.ship_id == sender_id)
.cloned();
}
}
debug!("chat message from sender {sender_name} in channel {channel:?}: {message}");
let message = GameMessage {
sender_relation: sender_team,
sender_name,
channel,
message: message.to_string(),
entity_id,
player,
};
self.game_chat.push(message.clone());
debug!(
"{:p} game chat len: {}",
&self.game_chat,
self.game_chat.len()
);
if let Some(event_handler) = self.event_handler.as_ref() {
event_handler.on_chat_message(message);
}
}
fn handle_entity_create<'packet>(&mut self, packet: &EntityCreatePacket<'packet>) {
let entity_type = EntityType::from_str(packet.entity_type).unwrap_or_else(|_| {
panic!(
"failed to convert entity type {} to a string",
packet.entity_type
);
});
match entity_type {
EntityType::Vehicle => {
let mut props = VehicleProps::default();
props.update_from_args(&packet.props, self.version);
let player = self.player_entities.get(&packet.entity_id);
let captain_id = props.crew_modifiers_compact_params.params_id;
let captain = if captain_id != 0 {
Some(
self.game_resources
.game_param_by_id(captain_id)
.expect("failed to get captain"),
)
} else {
None
};
let vehicle = Rc::new(RefCell::new(VehicleEntity {
id: packet.entity_id,
player: player.cloned(),
props,
visibility_changed_at: 0.0,
captain,
damage: 0.0,
death_info: None,
results_info: None,
frags: Vec::default(),
}));
self.entities_by_id
.insert(packet.entity_id, Entity::Vehicle(vehicle.clone()));
}
EntityType::BattleLogic => debug!("BattleLogic create"),
EntityType::InteractiveZone => debug!("InteractiveZone create"),
EntityType::SmokeScreen => debug!("SmokeScreen create"),
EntityType::BattleEntity => debug!("BattleEntity create"),
EntityType::Building => debug!("Building create"),
}
}
pub fn game_chat(&self) -> &[GameMessage] {
self.game_chat.as_slice()
}
pub fn build_report(mut self) -> BattleReport {
for (aggressor, damage_events) in &self.damage_dealt {
if let Some(aggressor_player) = self.entities_by_id.get_mut(aggressor) {
let vehicle = aggressor_player
.vehicle_ref()
.expect("aggressor has no vehicle?");
let mut vehicle = vehicle.borrow_mut();
vehicle.damage += damage_events.iter().fold(0.0, |mut accum, event| {
accum += event.amount;
accum
});
} else {
}
}
self.entities_by_id.values().for_each(|entity| {
if let Some(vehicle) = entity.vehicle_ref() {
let mut vehicle = vehicle.borrow_mut();
if let Some(death) = self
.frags
.values()
.find_map(|deaths| deaths.iter().find(|death| death.victim == vehicle.id))
{
vehicle.death_info = Some(death.into());
}
}
});
let parsed_battle_results = self
.battle_results
.as_ref()
.and_then(|results| serde_json::Value::from_str(results.as_str()).ok());
let player_entity_ids: Vec<_> = self.player_entities.keys().cloned().collect();
let player_entities: Vec<Rc<VehicleEntity>> = self
.entities_by_id
.iter()
.filter_map(|(entity_id, entity)| {
if player_entity_ids.contains(entity_id) {
let mut vehicle: VehicleEntity = RefCell::borrow(entity.vehicle_ref()?).clone();
if let Some(battle_results) = parsed_battle_results
.as_ref()
.and_then(|results| results.as_object())
{
vehicle.results_info =
battle_results.get("playersPublicInfo").and_then(|infos| {
infos.as_object().and_then(|infos| {
if let Some(player_info) = vehicle.player.as_ref() {
infos.get(player_info.db_id.to_string().as_str()).cloned()
} else {
None
}
})
});
if let Some(player) = vehicle.player().as_ref()
&& let Some(frags) = self.frags.get(&player.entity_id)
{
vehicle.frags = frags.iter().map(DeathInfo::from).collect();
}
}
Some(Rc::new(vehicle))
} else {
None
}
})
.collect();
let frags: HashMap<Rc<Player>, Vec<DeathInfo>> =
HashMap::from_iter(self.frags.drain().filter_map(|(entity_id, kills)| {
let player = self.player_entities.get(&entity_id)?;
let kills: Vec<DeathInfo> = kills.iter().map(DeathInfo::from).collect();
Some((Rc::clone(player), kills))
}));
let self_entity = player_entities
.iter()
.find(|entity| entity.player.as_ref().unwrap().relation == 0)
.cloned()
.expect("could not find self_player");
BattleReport {
arena_id: self.arena_id,
match_result: if self.match_finished {
self.winning_team.map(|team| {
if team == self_entity.player.as_ref().unwrap().team_id as i8 {
BattleResult::Win(team)
} else if team >= 0 {
BattleResult::Loss(1)
} else {
BattleResult::Draw
}
})
} else {
None
},
self_entity,
version: Version::from_client_exe(self.game_version()),
match_group: self.match_group().to_owned(),
map_name: self.map_name(),
game_mode: self.game_mode(),
game_type: self.game_type(),
player_entities,
game_chat: self.game_chat,
battle_results: self.battle_results,
players: self.player_entities.values().cloned().collect(),
frags,
}
}
pub fn battle_results(&self) -> Option<&String> {
self.battle_results.as_ref()
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum ChatChannel {
Division,
Global,
Team,
}
fn parse_ship_config(blob: &[u8], version: Version) -> IResult<&[u8], ShipConfig> {
let i = blob;
let (i, _unk) = le_u32(i)?;
let (i, ship_params_id) = le_u32(i)?;
let (i, _unk2) = le_u32(i)?;
let (i, unit_count) = le_u32(i)?;
let (i, units) = count(le_u32, unit_count as usize)(i)?;
let i = if version.is_at_least(&Version {
major: 13,
minor: 2,
patch: 0,
build: 0,
}) {
let (i, _unk) = le_u32(i)?;
i
} else {
i
};
let (i, modernization_count) = le_u32(i)?;
let (i, modernization) = count(le_u32, modernization_count as usize)(i)?;
let (i, signal_count) = le_u32(i)?;
let (i, signals) = count(le_u32, signal_count as usize)(i)?;
let (i, _supply_state) = le_u32(i)?;
let (i, camo_info_count) = le_u32(i)?;
let (i, _camo) = count(pair(le_u32, le_u32), camo_info_count as usize)(i)?;
let (i, abilities_count) = le_u32(i)?;
let (i, abilities) = count(le_u32, abilities_count as usize)(i)?;
Ok((
i,
ShipConfig {
abilities,
hull: units[0],
modernization,
units,
signals,
},
))
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GameMessage {
pub sender_relation: Option<u32>,
pub sender_name: String,
pub channel: ChatChannel,
pub message: String,
pub entity_id: u32,
pub player: Option<Rc<Player>>,
}
#[derive(Debug, Default, Serialize, Clone)]
pub struct AAAura {
id: u32,
enabled: bool,
}
#[derive(Debug, Default, Serialize, Clone)]
pub struct VehicleState {
buffs: Option<()>,
vehicle_visual_state: u8,
battery: Option<()>,
}
#[derive(Debug, Default, Serialize, Clone)]
pub struct CrewModifiersCompactParams {
params_id: u32,
is_in_adaption: bool,
learned_skills: Skills,
}
trait UpdateFromReplayArgs {
fn update_by_name(&mut self, name: &str, value: &ArgValue<'_>, version: Version) {
let mut dict = HashMap::with_capacity(1);
dict.insert(name, value.clone());
self.update_from_args(&dict, version);
}
fn update_from_args(&mut self, args: &HashMap<&str, ArgValue<'_>>, version: Version);
}
macro_rules! set_arg_value {
($set_var:expr, $args:ident, $key:expr, String) => {
$set_var = (*value
.string_ref()
.unwrap_or_else(|| panic!("{} is not a string", $key)))
.clone()
};
($set_var:expr, $args:ident, $key:expr, i8) => {
set_arg_value!($set_var, $args, $key, int_8_ref, i8)
};
($set_var:expr, $args:ident, $key:expr, i16) => {
set_arg_value!($set_var, $args, $key, int_16_ref, i16)
};
($set_var:expr, $args:ident, $key:expr, i32) => {
set_arg_value!($set_var, $args, $key, int_32_ref, i32)
};
($set_var:expr, $args:ident, $key:expr, u8) => {
set_arg_value!($set_var, $args, $key, uint_8_ref, u8)
};
($set_var:expr, $args:ident, $key:expr, u16) => {
set_arg_value!($set_var, $args, $key, uint_16_ref, u16)
};
($set_var:expr, $args:ident, $key:expr, u32) => {
set_arg_value!($set_var, $args, $key, uint_32_ref, u32)
};
($set_var:expr, $args:ident, $key:expr, f32) => {
set_arg_value!($set_var, $args, $key, float_32_ref, f32)
};
($set_var:expr, $args:ident, $key:expr, bool) => {
if let Some(value) = $args.get($key) {
$set_var = (*value
.uint_8_ref()
.unwrap_or_else(|| panic!("{} is not a u8", $key)))
!= 0
}
};
($set_var:expr, $args:ident, $key:expr, Vec<u8>) => {
if let Some(value) = $args.get($key) {
$set_var = value
.blob_ref()
.unwrap_or_else(|| panic!("{} is not a u8", $key))
.clone()
}
};
($set_var:expr, $args:ident, $key:expr, &[()]) => {
set_arg_value!($set_var, $args, $key, array_ref, &[()])
};
($set_var:expr, $args:ident, $key:expr, $conversion_func:ident, $ty:ty) => {
if let Some(value) = $args.get($key) {
$set_var = value
.$conversion_func()
.unwrap_or_else(|| panic!("{} is not a {}", $key, stringify!($ty)))
.clone()
}
};
}
macro_rules! arg_value_to_type {
($args:ident, $key:expr, String) => {
arg_value_to_type!($args, $key, string_ref, String).clone()
};
($args:ident, $key:expr, i8) => {
*arg_value_to_type!($args, $key, int_8_ref, i8)
};
($args:ident, $key:expr, i16) => {
*arg_value_to_type!($args, $key, int_16_ref, i16)
};
($args:ident, $key:expr, i32) => {
*arg_value_to_type!($args, $key, int_32_ref, i32)
};
($args:ident, $key:expr, u8) => {
*arg_value_to_type!($args, $key, uint_8_ref, u8)
};
($args:ident, $key:expr, u16) => {
*arg_value_to_type!($args, $key, uint_16_ref, u16)
};
($args:ident, $key:expr, u32) => {
*arg_value_to_type!($args, $key, uint_32_ref, u32)
};
($args:ident, $key:expr, bool) => {
(*arg_value_to_type!($args, $key, uint_8_ref, u8)) != 0
};
($args:ident, $key:expr, &[()]) => {
arg_value_to_type!($args, $key, array_ref, &[()])
};
($args:ident, $key:expr, &[u8]) => {
arg_value_to_type!($args, $key, blob_ref, &[()]).as_ref()
};
($args:ident, $key:expr, HashMap<(), ()>) => {
arg_value_to_type!($args, $key, fixed_dict_ref, HashMap<(), ()>)
};
($args:ident, $key:expr, $conversion_func:ident, $ty:ty) => {
$args
.get($key)
.unwrap_or_else(|| panic!("could not get {}", $key))
.$conversion_func()
.unwrap_or_else(|| panic!("{} is not a {}", $key, stringify!($ty)))
};
}
impl UpdateFromReplayArgs for CrewModifiersCompactParams {
fn update_from_args(&mut self, args: &HashMap<&str, ArgValue<'_>>, version: Version) {
const PARAMS_ID_KEY: &str = "paramsId";
const IS_IN_ADAPTION_KEY: &str = "isInAdaption";
const LEARNED_SKILLS_KEY: &str = "learnedSkills";
if args.contains_key(PARAMS_ID_KEY) {
self.params_id = arg_value_to_type!(args, PARAMS_ID_KEY, u32);
}
if args.contains_key(IS_IN_ADAPTION_KEY) {
self.is_in_adaption = arg_value_to_type!(args, IS_IN_ADAPTION_KEY, bool);
}
if args.contains_key(LEARNED_SKILLS_KEY) {
let learned_skills = arg_value_to_type!(args, LEARNED_SKILLS_KEY, &[()]);
let skills_from_idx = |idx: usize| -> Vec<u8> {
learned_skills[idx]
.array_ref()
.unwrap()
.iter()
.map(|idx| *(*idx).uint_8_ref().unwrap())
.collect()
};
let skills = Skills {
aircraft_carrier: skills_from_idx(0),
battleship: skills_from_idx(1),
cruiser: skills_from_idx(2),
destroyer: skills_from_idx(3),
auxiliary: skills_from_idx(4),
submarine: skills_from_idx(5),
};
self.learned_skills = skills;
}
}
}
#[derive(Debug, Default, Serialize, Clone)]
pub struct VehicleProps {
ignore_map_borders: bool,
air_defense_dispersion_radius: f32,
death_settings: Vec<u8>,
owner: u32,
atba_targets: Vec<u32>,
effects: Vec<String>,
crew_modifiers_compact_params: CrewModifiersCompactParams,
laser_target_local_pos: u16,
anti_air_auras: Vec<AAAura>,
selected_weapon: u32,
regeneration_health: f32,
is_on_forsage: bool,
is_in_rage_mode: bool,
has_air_targets_in_range: bool,
torpedo_local_pos: u16,
air_defense_target_ids: Vec<()>,
buoyancy: f32,
max_health: f32,
rudders_angle: f32,
draught: f32,
target_local_pos: u16,
triggered_skills_data: Vec<u8>,
regenerated_health: f32,
blocked_controls: u8,
is_invisible: bool,
is_fog_horn_on: bool,
server_speed_raw: u16,
regen_crew_hp_limit: f32,
miscs_presets_status: Vec<()>,
buoyancy_current_waterline: f32,
is_alive: bool,
is_bot: bool,
visibility_flags: u32,
heat_infos: Vec<()>,
buoyancy_rudder_index: u8,
is_anti_air_mode: bool,
speed_sign_dir: i8,
oil_leak_state: u8,
sounds: Vec<()>,
ship_config: ShipConfig,
wave_local_pos: u16,
has_active_main_squadron: bool,
weapon_lock_flags: u16,
deep_rudders_angle: f32,
debug_text: Vec<()>,
health: f32,
engine_dir: i8,
state: VehicleState,
team_id: i8,
buoyancy_current_state: u8,
ui_enabled: bool,
respawn_time: u16,
engine_power: u8,
max_server_speed_raw: u32,
burning_flags: u16,
}
impl VehicleProps {
pub fn ignore_map_borders(&self) -> bool {
self.ignore_map_borders
}
pub fn air_defense_dispersion_radius(&self) -> f32 {
self.air_defense_dispersion_radius
}
pub fn death_settings(&self) -> &[u8] {
self.death_settings.as_ref()
}
pub fn owner(&self) -> u32 {
self.owner
}
pub fn atba_targets(&self) -> &[u32] {
self.atba_targets.as_ref()
}
pub fn effects(&self) -> &[String] {
self.effects.as_ref()
}
pub fn crew_modifiers_compact_params(&self) -> &CrewModifiersCompactParams {
&self.crew_modifiers_compact_params
}
pub fn laser_target_local_pos(&self) -> u16 {
self.laser_target_local_pos
}
pub fn anti_air_auras(&self) -> &[AAAura] {
self.anti_air_auras.as_ref()
}
pub fn selected_weapon(&self) -> u32 {
self.selected_weapon
}
pub fn regeneration_health(&self) -> f32 {
self.regeneration_health
}
pub fn is_on_forsage(&self) -> bool {
self.is_on_forsage
}
pub fn is_in_rage_mode(&self) -> bool {
self.is_in_rage_mode
}
pub fn has_air_targets_in_range(&self) -> bool {
self.has_air_targets_in_range
}
pub fn torpedo_local_pos(&self) -> u16 {
self.torpedo_local_pos
}
pub fn air_defense_target_ids(&self) -> &[()] {
self.air_defense_target_ids.as_ref()
}
pub fn buoyancy(&self) -> f32 {
self.buoyancy
}
pub fn max_health(&self) -> f32 {
self.max_health
}
pub fn rudders_angle(&self) -> f32 {
self.rudders_angle
}
pub fn draught(&self) -> f32 {
self.draught
}
pub fn target_local_pos(&self) -> u16 {
self.target_local_pos
}
pub fn triggered_skills_data(&self) -> &[u8] {
self.triggered_skills_data.as_ref()
}
pub fn regenerated_health(&self) -> f32 {
self.regenerated_health
}
pub fn blocked_controls(&self) -> u8 {
self.blocked_controls
}
pub fn is_invisible(&self) -> bool {
self.is_invisible
}
pub fn is_fog_horn_on(&self) -> bool {
self.is_fog_horn_on
}
pub fn server_speed_raw(&self) -> u16 {
self.server_speed_raw
}
pub fn regen_crew_hp_limit(&self) -> f32 {
self.regen_crew_hp_limit
}
pub fn miscs_presets_status(&self) -> &[()] {
self.miscs_presets_status.as_ref()
}
pub fn buoyancy_current_waterline(&self) -> f32 {
self.buoyancy_current_waterline
}
pub fn is_alive(&self) -> bool {
self.is_alive
}
pub fn is_bot(&self) -> bool {
self.is_bot
}
pub fn visibility_flags(&self) -> u32 {
self.visibility_flags
}
pub fn heat_infos(&self) -> &[()] {
self.heat_infos.as_ref()
}
pub fn buoyancy_rudder_index(&self) -> u8 {
self.buoyancy_rudder_index
}
pub fn is_anti_air_mode(&self) -> bool {
self.is_anti_air_mode
}
pub fn speed_sign_dir(&self) -> i8 {
self.speed_sign_dir
}
pub fn oil_leak_state(&self) -> u8 {
self.oil_leak_state
}
pub fn sounds(&self) -> &[()] {
self.sounds.as_ref()
}
pub fn ship_config(&self) -> &ShipConfig {
&self.ship_config
}
pub fn wave_local_pos(&self) -> u16 {
self.wave_local_pos
}
pub fn has_active_main_squadron(&self) -> bool {
self.has_active_main_squadron
}
pub fn weapon_lock_flags(&self) -> u16 {
self.weapon_lock_flags
}
pub fn deep_rudders_angle(&self) -> f32 {
self.deep_rudders_angle
}
pub fn debug_text(&self) -> &[()] {
self.debug_text.as_ref()
}
pub fn health(&self) -> f32 {
self.health
}
pub fn engine_dir(&self) -> i8 {
self.engine_dir
}
pub fn state(&self) -> &VehicleState {
&self.state
}
pub fn team_id(&self) -> i8 {
self.team_id
}
pub fn buoyancy_current_state(&self) -> u8 {
self.buoyancy_current_state
}
pub fn ui_enabled(&self) -> bool {
self.ui_enabled
}
pub fn respawn_time(&self) -> u16 {
self.respawn_time
}
pub fn engine_power(&self) -> u8 {
self.engine_power
}
pub fn max_server_speed_raw(&self) -> u32 {
self.max_server_speed_raw
}
pub fn burning_flags(&self) -> u16 {
self.burning_flags
}
}
impl UpdateFromReplayArgs for VehicleProps {
fn update_by_name(&mut self, name: &str, value: &ArgValue<'_>, version: Version) {
let mut dict = HashMap::with_capacity(1);
dict.insert(name, value.clone());
self.update_from_args(&dict, version);
}
fn update_from_args(&mut self, args: &HashMap<&str, ArgValue<'_>>, version: Version) {
const IGNORE_MAP_BORDERS_KEY: &str = "ignoreMapBorders";
const AIR_DEFENSE_DISPERSION_RADIUS_KEY: &str = "airDefenseDispRadius";
const DEATH_SETTINGS_KEY: &str = "deathSettings";
const OWNER_KEY: &str = "owner";
const ATBA_TARGETS_KEY: &str = "atbaTargets";
const EFFECTS_KEY: &str = "effects";
const CREW_MODIFIERS_COMPACT_PARAMS_KEY: &str = "crewModifiersCompactParams";
const LASER_TARGET_LOCAL_POS_KEY: &str = "laserTargetLocalPos";
const ANTI_AIR_AUROS_KEY: &str = "antiAirAuras";
const SELECTED_WEAPON_KEY: &str = "selectedWeapon";
const REGENERATION_HEALTH_KEY: &str = "regenerationHealth";
const IS_ON_FORSAGE_KEY: &str = "isOnForsage";
const IS_IN_RAGE_MODE_KEY: &str = "isInRageMode";
const HAS_AIR_TARGETS_IN_RANGE_KEY: &str = "hasAirTargetsInRange";
const TORPEDO_LOCAL_POS_KEY: &str = "torpedoLocalPos";
const AIR_DEFENSE_TARGET_IDS_KEY: &str = "airDefenseTargetIds";
const BUOYANCY_KEY: &str = "buoyancy";
const MAX_HEALTH_KEY: &str = "maxHealth";
const DRAUGHT_KEY: &str = "draught";
const RUDDERS_ANGLE_KEY: &str = "ruddersAngle";
const TARGET_LOCAL_POSITION_KEY: &str = "targetLocalPos";
const TRIGGERED_SKILLS_DATA_KEY: &str = "triggeredSkillsData";
const REGENERATED_HEALTH_KEY: &str = "regeneratedHealth";
const BLOCKED_CONTROLS_KEY: &str = "blockedControls";
const IS_INVISIBLE_KEY: &str = "isInvisible";
const IS_FOG_HORN_ON_KEY: &str = "isFogHornOn";
const SERVER_SPEED_RAW_KEY: &str = "serverSpeedRaw";
const REGEN_CREW_HP_LIMIT_KEY: &str = "regenCrewHpLimit";
const MISCS_PRESETS_STATUS_KEY: &str = "miscsPresetsStatus";
const BUOYANCY_CURRENT_WATERLINE_KEY: &str = "buoyancyCurrentWaterline";
const IS_ALIVE_KEY: &str = "isAlive";
const IS_BOT_KEY: &str = "isBot";
const VISIBILITY_FLAGS_KEY: &str = "visibilityFlags";
const HEAT_INFOS_KEY: &str = "heatInfos";
const BUOYANCY_RUDDER_INDEX_KEY: &str = "buoyancyRudderIndex";
const IS_ANTI_AIR_MODE_KEY: &str = "isAntiAirMode";
const SPEED_SIGN_DIR_KEY: &str = "speedSignDir";
const OIL_LEAK_STATE_KEY: &str = "oilLeakState";
const SOUNDS_KEY: &str = "sounds";
const SHIP_CONFIG_KEY: &str = "shipConfig";
const WAVE_LOCAL_POS_KEY: &str = "waveLocalPos";
const HAS_ACTIVE_MAIN_SQUADRON_KEY: &str = "hasActiveMainSquadron";
const WEAPON_LOCK_FLAGS_KEY: &str = "weaponLockFlags";
const DEEP_RUDDERS_ANGLE_KEY: &str = "deepRuddersAngle";
const DEBUG_TEXT_KEY: &str = "debugText";
const HEALTH_KEY: &str = "health";
const ENGINE_DIR_KEY: &str = "engineDir";
const STATE_KEY: &str = "state";
const TEAM_ID_KEY: &str = "teamId";
const BUOYANCY_CURRENT_STATE_KEY: &str = "buoyancyCurrentState";
const UI_ENABLED_KEY: &str = "uiEnabled";
const RESPAWN_TIME_KEY: &str = "respawnTime";
const ENGINE_POWER_KEY: &str = "enginePower";
const MAX_SERVER_SPEED_RAW_KEY: &str = "maxServerSpeedRaw";
const BURNING_FLAGS_KEY: &str = "burningFlags";
set_arg_value!(self.ignore_map_borders, args, IGNORE_MAP_BORDERS_KEY, bool);
set_arg_value!(
self.air_defense_dispersion_radius,
args,
AIR_DEFENSE_DISPERSION_RADIUS_KEY,
f32
);
set_arg_value!(self.death_settings, args, DEATH_SETTINGS_KEY, Vec<u8>);
if args.contains_key(OWNER_KEY) {
let value: u32 = arg_value_to_type!(args, OWNER_KEY, i32) as u32;
self.owner = value;
}
if args.contains_key(ATBA_TARGETS_KEY) {
let value: Vec<u32> = arg_value_to_type!(args, ATBA_TARGETS_KEY, &[()])
.iter()
.map(|elem| *elem.uint_32_ref().expect("atbaTargets elem is not a u32"))
.collect();
self.atba_targets = value;
}
if args.contains_key(EFFECTS_KEY) {
let value: Vec<String> = arg_value_to_type!(args, EFFECTS_KEY, &[()])
.iter()
.map(|elem| {
String::from_utf8(
elem.string_ref()
.expect("effects elem is not a string")
.clone(),
)
.expect("could not convert effects elem to string")
})
.collect();
self.effects = value;
}
if args.contains_key(CREW_MODIFIERS_COMPACT_PARAMS_KEY) {
self.crew_modifiers_compact_params.update_from_args(
arg_value_to_type!(args, CREW_MODIFIERS_COMPACT_PARAMS_KEY, HashMap<(), ()>),
version,
);
}
set_arg_value!(
self.laser_target_local_pos,
args,
LASER_TARGET_LOCAL_POS_KEY,
u16
);
set_arg_value!(self.selected_weapon, args, SELECTED_WEAPON_KEY, u32);
set_arg_value!(self.is_on_forsage, args, IS_ON_FORSAGE_KEY, bool);
set_arg_value!(self.is_in_rage_mode, args, IS_IN_RAGE_MODE_KEY, bool);
set_arg_value!(
self.has_air_targets_in_range,
args,
HAS_AIR_TARGETS_IN_RANGE_KEY,
bool
);
set_arg_value!(self.torpedo_local_pos, args, TORPEDO_LOCAL_POS_KEY, u16);
set_arg_value!(self.buoyancy, args, BUOYANCY_KEY, f32);
set_arg_value!(self.max_health, args, MAX_HEALTH_KEY, f32);
set_arg_value!(self.draught, args, DRAUGHT_KEY, f32);
set_arg_value!(self.rudders_angle, args, RUDDERS_ANGLE_KEY, f32);
set_arg_value!(self.target_local_pos, args, TARGET_LOCAL_POSITION_KEY, u16);
set_arg_value!(
self.triggered_skills_data,
args,
TRIGGERED_SKILLS_DATA_KEY,
Vec<u8>
);
set_arg_value!(self.regenerated_health, args, REGENERATED_HEALTH_KEY, f32);
set_arg_value!(self.blocked_controls, args, BLOCKED_CONTROLS_KEY, u8);
set_arg_value!(self.is_invisible, args, IS_INVISIBLE_KEY, bool);
set_arg_value!(self.is_fog_horn_on, args, IS_FOG_HORN_ON_KEY, bool);
set_arg_value!(self.server_speed_raw, args, SERVER_SPEED_RAW_KEY, u16);
set_arg_value!(self.regen_crew_hp_limit, args, REGEN_CREW_HP_LIMIT_KEY, f32);
set_arg_value!(
self.buoyancy_current_waterline,
args,
BUOYANCY_CURRENT_WATERLINE_KEY,
f32
);
set_arg_value!(self.is_alive, args, IS_ALIVE_KEY, bool);
set_arg_value!(self.is_bot, args, IS_BOT_KEY, bool);
set_arg_value!(self.visibility_flags, args, VISIBILITY_FLAGS_KEY, u32);
set_arg_value!(
self.buoyancy_rudder_index,
args,
BUOYANCY_RUDDER_INDEX_KEY,
u8
);
set_arg_value!(self.is_anti_air_mode, args, IS_ANTI_AIR_MODE_KEY, bool);
set_arg_value!(self.speed_sign_dir, args, SPEED_SIGN_DIR_KEY, i8);
set_arg_value!(self.oil_leak_state, args, OIL_LEAK_STATE_KEY, u8);
if args.contains_key(SHIP_CONFIG_KEY) {
let (_remainder, ship_config) =
parse_ship_config(arg_value_to_type!(args, SHIP_CONFIG_KEY, &[u8]), version)
.expect("failed to parse ship config");
self.ship_config = ship_config;
}
set_arg_value!(self.wave_local_pos, args, WAVE_LOCAL_POS_KEY, u16);
set_arg_value!(
self.has_active_main_squadron,
args,
HAS_ACTIVE_MAIN_SQUADRON_KEY,
bool
);
set_arg_value!(self.weapon_lock_flags, args, WEAPON_LOCK_FLAGS_KEY, u16);
set_arg_value!(self.deep_rudders_angle, args, DEEP_RUDDERS_ANGLE_KEY, f32);
set_arg_value!(self.health, args, HEALTH_KEY, f32);
set_arg_value!(self.engine_dir, args, ENGINE_DIR_KEY, i8);
set_arg_value!(self.team_id, args, TEAM_ID_KEY, i8);
set_arg_value!(
self.buoyancy_current_state,
args,
BUOYANCY_CURRENT_STATE_KEY,
u8
);
set_arg_value!(self.ui_enabled, args, UI_ENABLED_KEY, bool);
set_arg_value!(self.respawn_time, args, RESPAWN_TIME_KEY, u16);
set_arg_value!(self.engine_power, args, ENGINE_POWER_KEY, u8);
set_arg_value!(
self.max_server_speed_raw,
args,
MAX_SERVER_SPEED_RAW_KEY,
u32
);
set_arg_value!(self.burning_flags, args, BURNING_FLAGS_KEY, u16);
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DeathInfo {
time_lived: Duration,
killer: u32,
cause: DeathCause,
}
impl DeathInfo {
pub fn time_lived(&self) -> Duration {
self.time_lived
}
pub fn killer(&self) -> u32 {
self.killer
}
pub fn cause(&self) -> DeathCause {
self.cause
}
}
impl From<&Death> for DeathInfo {
fn from(death: &Death) -> Self {
let time_lived = if death.timestamp > TIME_UNTIL_GAME_START {
death.timestamp - TIME_UNTIL_GAME_START
} else {
Duration::from_secs(0)
};
DeathInfo {
time_lived,
killer: death.killer,
cause: death.cause,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct VehicleEntity {
id: u32,
player: Option<Rc<Player>>,
visibility_changed_at: f32,
props: VehicleProps,
captain: Option<Rc<Param>>,
damage: f32,
death_info: Option<DeathInfo>,
results_info: Option<serde_json::Value>,
frags: Vec<DeathInfo>,
}
impl VehicleEntity {
pub fn id(&self) -> u32 {
self.id
}
pub fn player(&self) -> Option<&Rc<Player>> {
self.player.as_ref()
}
pub fn props(&self) -> &VehicleProps {
&self.props
}
pub fn commander_id(&self) -> Id {
self.props.crew_modifiers_compact_params.params_id
}
pub fn commander_skills(&self) -> Option<Vec<&CrewSkill>> {
let vehicle_species = self
.player
.as_ref()
.expect("player has not yet loaded")
.vehicle
.species()
.expect("vehicle species not set");
let skills = &self.props.crew_modifiers_compact_params.learned_skills;
let skills_for_species = match vehicle_species {
Species::AirCarrier => skills.aircraft_carrier.as_slice(),
Species::Battleship => skills.battleship.as_slice(),
Species::Cruiser => skills.cruiser.as_slice(),
Species::Destroyer => skills.destroyer.as_slice(),
Species::Submarine => skills.submarine.as_slice(),
other => {
panic!("Unexpected vehicle species: {:?}", other);
}
};
let captain = self
.captain()?
.data()
.crew_ref()
.expect("captain is not a crew?");
let skills = skills_for_species
.iter()
.map(|skill_type| {
captain
.skill_by_type(*skill_type as u32)
.expect("could not get skill type")
})
.collect();
Some(skills)
}
pub fn commander_skills_raw(&self) -> &[u8] {
let vehicle_species = self
.player
.as_ref()
.expect("player has not yet loaded")
.vehicle
.species()
.expect("vehicle species not set");
let skills = &self.props.crew_modifiers_compact_params.learned_skills;
match vehicle_species {
Species::AirCarrier => skills.aircraft_carrier.as_slice(),
Species::Battleship => skills.battleship.as_slice(),
Species::Cruiser => skills.cruiser.as_slice(),
Species::Destroyer => skills.destroyer.as_slice(),
Species::Submarine => skills.submarine.as_slice(),
other => {
panic!("Unexpected vehicle species: {:?}", other);
}
}
}
pub fn captain(&self) -> Option<&Param> {
self.captain.as_ref().map(|rc| rc.as_ref())
}
pub fn damage(&self) -> f32 {
self.damage
}
pub fn death_info(&self) -> Option<&DeathInfo> {
self.death_info.as_ref()
}
pub fn results_info(&self) -> Option<&serde_json::Value> {
self.results_info.as_ref()
}
pub fn frags(&self) -> &[DeathInfo] {
&self.frags
}
}
#[derive(Debug, Variantly)]
pub enum Entity {
Vehicle(Rc<RefCell<VehicleEntity>>),
}
impl Entity {
fn update_arena_player(&self, arena_player: Rc<Player>) {
match self {
Entity::Vehicle(vehicle) => {
RefCell::borrow_mut(vehicle).player = Some(arena_player);
}
}
}
}
#[derive(Debug)]
struct Death {
timestamp: Duration,
killer: u32,
victim: u32,
cause: DeathCause,
}
impl<'res, 'replay, G> AnalyzerMut for BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
fn process_mut(&mut self, packet: &Packet<'_, '_>) {
let span = span!(Level::TRACE, "packet processing");
let _enter = span.enter();
let decoded = DecodedPacket::from(&self.version, false, packet);
let payload_kind = decoded.payload.kind();
match decoded.payload {
crate::analyzer::decoder::DecodedPacketPayload::Chat {
entity_id,
sender_id,
audience,
message,
extra_data,
} => {
self.handle_chat_message(entity_id, sender_id, audience, message, extra_data);
}
crate::analyzer::decoder::DecodedPacketPayload::VoiceLine {
sender_id,
is_global,
message,
} => {
trace!("HANDLE VOICE LINE");
}
crate::analyzer::decoder::DecodedPacketPayload::Ribbon(_ribbon) => {
trace!("HANDLE RIBBON")
}
crate::analyzer::decoder::DecodedPacketPayload::Position(_pos) => {
trace!("HANDLE POSITION")
}
crate::analyzer::decoder::DecodedPacketPayload::PlayerOrientation(_orientation) => {
trace!("PLAYER ORIENTATION")
}
crate::analyzer::decoder::DecodedPacketPayload::DamageStat(_damage) => {
trace!("DAMAGE STAT")
}
crate::analyzer::decoder::DecodedPacketPayload::ShipDestroyed {
killer,
victim,
cause,
} => {
self.frags.entry(killer as u32).or_default().push(Death {
timestamp: Duration::from_secs_f32(packet.clock),
killer: killer as u32,
victim: victim as u32,
cause,
});
}
crate::analyzer::decoder::DecodedPacketPayload::EntityMethod(method) => {
debug!("ENTITY METHOD, {:#?}", method)
}
crate::analyzer::decoder::DecodedPacketPayload::EntityProperty(prop) => {
if let Some(entity) = self.entities_by_id.get(&prop.entity_id)
&& let Some(vehicle) = entity.vehicle_ref()
{
let mut vehicle = RefCell::borrow_mut(vehicle);
vehicle
.props
.update_by_name(prop.property, &prop.value, self.version);
}
}
crate::analyzer::decoder::DecodedPacketPayload::BasePlayerCreate(base) => {
trace!("BASE PLAYER CREATE");
}
crate::analyzer::decoder::DecodedPacketPayload::CellPlayerCreate(cell) => {
trace!("CELL PLAYER CREATE");
}
crate::analyzer::decoder::DecodedPacketPayload::EntityEnter(e) => {
trace!("ENTITY ENTER")
}
crate::analyzer::decoder::DecodedPacketPayload::EntityLeave(_leave) => {
trace!("ENTITY LEAVE")
}
crate::analyzer::decoder::DecodedPacketPayload::EntityCreate(entity_create) => {
self.handle_entity_create(entity_create);
}
crate::analyzer::decoder::DecodedPacketPayload::OnArenaStateReceived {
arg0,
arg1,
arg2,
players,
} => {
debug!("OnArenaStateReceived");
self.arena_id = arg0;
for player in &players {
let metadata_player = self
.metadata_players
.iter()
.find(|meta_player| meta_player.id == player.meta_ship_id as u32)
.expect("could not map arena player to metadata player");
let mut battle_player = Player::from_arena_player(
player,
metadata_player.as_ref(),
self.game_resources,
);
if let Some(previous_state) = self.player_entities.get(&battle_player.entity_id)
&& previous_state.is_connected
&& !self.match_finished
{
battle_player.did_disconnect =
!battle_player.is_connected || previous_state.did_disconnect;
}
let battle_player = Rc::new(battle_player);
self.player_entities
.insert(battle_player.entity_id, battle_player.clone());
if let Some(entity) = self.entities_by_id.get(&battle_player.entity_id) {
entity.update_arena_player(battle_player);
}
}
}
crate::analyzer::decoder::DecodedPacketPayload::CheckPing(_) => trace!("CHECK PING"),
crate::analyzer::decoder::DecodedPacketPayload::DamageReceived {
victim,
aggressors,
} => {
for damage in aggressors {
self.damage_dealt
.entry(damage.aggressor as u32)
.or_default()
.push(DamageEvent {
amount: damage.damage,
victim,
});
}
}
crate::analyzer::decoder::DecodedPacketPayload::MinimapUpdate { updates, arg1 } => {
trace!("MINIMAP UPDATE")
}
crate::analyzer::decoder::DecodedPacketPayload::PropertyUpdate(update) => {
if let Some(entity) = self.entities_by_id.get(&(update.entity_id as u32)) {
debug!("PROPERTY UPDATE: {:#?}", update);
}
}
crate::analyzer::decoder::DecodedPacketPayload::BattleEnd {
winning_team,
state,
} => {
self.match_finished = true;
self.winning_team = winning_team;
}
crate::analyzer::decoder::DecodedPacketPayload::Consumable {
entity,
consumable,
duration,
} => {
trace!("CONSUMABLE");
}
crate::analyzer::decoder::DecodedPacketPayload::CruiseState { state, value } => {
trace!("CRUISE STATE")
}
crate::analyzer::decoder::DecodedPacketPayload::Map(_) => trace!("MAP"),
crate::analyzer::decoder::DecodedPacketPayload::Version(_) => trace!("VERSION"),
crate::analyzer::decoder::DecodedPacketPayload::Camera(_) => trace!("CAMERA"),
crate::analyzer::decoder::DecodedPacketPayload::CameraMode(_) => {
trace!("CAMERA MODE")
}
crate::analyzer::decoder::DecodedPacketPayload::CameraFreeLook(_) => {
trace!("CAMERA FREE LOOK")
}
crate::analyzer::decoder::DecodedPacketPayload::Unknown(_) => trace!("UNKNOWN"),
crate::analyzer::decoder::DecodedPacketPayload::Invalid(_) => trace!("INVALID"),
crate::analyzer::decoder::DecodedPacketPayload::Audit(_) => trace!("AUDIT"),
crate::analyzer::decoder::DecodedPacketPayload::BattleResults(json) => {
self.battle_results = Some(json.to_owned());
}
}
}
fn finish(&mut self) {}
}
impl<'res, 'replay, G> PacketProcessorMut for BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
fn process_mut(&mut self, packet: Packet<'_, '_>) {
AnalyzerMut::process_mut(self, &packet);
}
}