use std::{
cell::{RefCell, UnsafeCell},
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::{BigWorldDistance, CrewSkill, Param, Species},
game_types::{BattleStage, BattleType},
rpc::typedefs::ArgValue,
};
static TIME_UNTIL_GAME_START: Duration = Duration::from_secs(30);
use crate::game_constants::GameConstants;
use crate::{
IResult, Rc, ReplayMeta,
analyzer::{
analyzer::Analyzer,
decoder::{
ChatMessageExtra, DeathCause, DepthState, FinishType, PlayerStateData, Recognized,
WeaponType,
},
},
nested_property_path::{PropertyNestLevel, UpdateAction},
packet2::{EntityCreatePacket, Packet},
types::{AccountId, EntityId, GameClock, GameParamId, PlaneId, Relation, WorldPos},
};
use super::listener::BattleControllerState;
use super::state::{
ActiveConsumable, ActivePlane, ActiveShot, ActiveTorpedo, ActiveWard, BuffZoneState,
BuildingEntity, CapturePointState, CapturedBuff, DeadShip, KillRecord, MinimapPosition,
ScoringRules, ShipPosition, SmokeScreenEntity, TeamScore,
};
#[derive(Debug, Default, Clone, Serialize)]
pub struct ShipConfig {
abilities: Vec<GameParamId>,
hull: GameParamId,
modernization: Vec<GameParamId>,
units: Vec<GameParamId>,
signals: Vec<GameParamId>,
}
impl ShipConfig {
pub fn signals(&self) -> &[GameParamId] {
self.signals.as_ref()
}
pub fn units(&self) -> &[GameParamId] {
self.units.as_ref()
}
pub fn modernization(&self) -> &[GameParamId] {
self.modernization.as_ref()
}
pub fn hull(&self) -> GameParamId {
self.hull
}
pub fn abilities(&self) -> &[GameParamId] {
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()
}
pub fn for_species(&self, species: &Species) -> &[u8] {
match species {
Species::AirCarrier => &self.aircraft_carrier,
Species::Battleship => &self.battleship,
Species::Cruiser => &self.cruiser,
Species::Destroyer => &self.destroyer,
Species::Submarine => &self.submarine,
Species::Auxiliary => &self.auxiliary,
_ => &[],
}
}
}
#[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, Copy, Clone, PartialEq, Eq)]
pub enum ConnectionChangeKind {
Connected,
Disconnected,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ConnectionChangeInfo {
at_game_duration: Duration,
event_kind: ConnectionChangeKind,
had_death_event: bool,
}
impl ConnectionChangeInfo {
pub fn at_game_duration(&self) -> Duration {
self.at_game_duration
}
pub fn event_kind(&self) -> ConnectionChangeKind {
self.event_kind
}
pub fn had_death_event(&self) -> bool {
self.had_death_event
}
}
pub struct Player {
initial_state: PlayerStateData,
end_state: UnsafeCell<PlayerStateData>,
connection_change_info: UnsafeCell<Vec<ConnectionChangeInfo>>,
vehicle: Rc<Param>,
vehicle_entity: Option<VehicleEntity>,
relation: Relation,
}
#[cfg(feature = "arc")]
unsafe impl Sync for Player {}
impl std::fmt::Debug for Player {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Player")
.field("initial_state", &self.initial_state)
.field("end_state", self.end_state())
.field("connection_change_info", unsafe {
&*self.connection_change_info.get()
})
.field("vehicle", &self.vehicle)
.field("vehicle_entity", &self.vehicle_entity)
.field("relation", &self.relation)
.finish()
}
}
impl Clone for Player {
fn clone(&self) -> Self {
Self {
initial_state: self.initial_state.clone(),
end_state: UnsafeCell::new(unsafe { (*self.end_state.get()).clone() }),
connection_change_info: UnsafeCell::new(unsafe {
(*self.connection_change_info.get()).clone()
}),
vehicle: self.vehicle.clone(),
vehicle_entity: self.vehicle_entity.clone(),
relation: self.relation,
}
}
}
impl Serialize for Player {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("Player", 5)?;
state.serialize_field("initial_state", &self.initial_state)?;
state.serialize_field("end_state", self.end_state())?;
state.serialize_field("connection_change_info", self.connection_change_info())?;
state.serialize_field("vehicle", &self.vehicle)?;
state.serialize_field("relation", &self.relation)?;
state.end()
}
}
impl<'de> Deserialize<'de> for Player {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct PlayerHelper {
initial_state: PlayerStateData,
end_state: PlayerStateData,
connection_change_info: Vec<ConnectionChangeInfo>,
vehicle: Rc<Param>,
relation: Relation,
}
let helper = PlayerHelper::deserialize(deserializer)?;
Ok(Player {
initial_state: helper.initial_state,
end_state: UnsafeCell::new(helper.end_state),
connection_change_info: UnsafeCell::new(helper.connection_change_info),
vehicle: helper.vehicle,
vehicle_entity: None,
relation: helper.relation,
})
}
}
impl std::cmp::PartialEq for Player {
fn eq(&self, other: &Self) -> bool {
self.initial_state.db_id == other.initial_state.db_id
&& self.initial_state.realm == other.initial_state.realm
}
}
impl std::cmp::Eq for Player {}
impl std::hash::Hash for Player {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.initial_state.db_id.hash(state);
self.initial_state.realm.hash(state);
}
}
impl Player {
fn from_arena_player<G: ResourceLoader>(
player: &PlayerStateData,
metadata_player: &MetadataPlayer,
resources: &G,
) -> Player {
Player {
initial_state: player.clone(),
end_state: UnsafeCell::new(player.clone()),
vehicle_entity: None,
connection_change_info: UnsafeCell::new(Vec::new()),
vehicle: resources
.game_param_by_id(metadata_player.vehicle().id())
.expect("could not find player vehicle"),
relation: metadata_player.relation(),
}
}
pub fn connection_change_info(&self) -> &[ConnectionChangeInfo] {
unsafe { (&*self.connection_change_info.get()).as_slice() }
}
fn connection_change_info_mut(&self) -> &mut Vec<ConnectionChangeInfo> {
unsafe { &mut *self.connection_change_info.get() }
}
pub fn end_state(&self) -> &PlayerStateData {
unsafe { &*self.end_state.get() }
}
fn end_state_mut(&self) -> &mut PlayerStateData {
unsafe { &mut *self.end_state.get() }
}
pub fn initial_state(&self) -> &PlayerStateData {
&self.initial_state
}
pub fn relation(&self) -> Relation {
self.relation
}
pub fn vehicle_entity(&self) -> Option<&VehicleEntity> {
self.vehicle_entity.as_ref()
}
pub fn vehicle(&self) -> &Param {
&self.vehicle
}
pub fn is_bot(&self) -> bool {
self.initial_state.is_bot
}
}
#[derive(Debug)]
pub struct MetadataPlayer {
id: AccountId,
name: String,
relation: Relation,
vehicle: Rc<Param>,
}
impl MetadataPlayer {
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn relation(&self) -> Relation {
self.relation
}
pub fn vehicle(&self) -> &Param {
self.vehicle.as_ref()
}
pub fn id(&self) -> AccountId {
self.id
}
}
pub type SharedPlayer = Rc<MetadataPlayer>;
#[allow(dead_code)]
type MethodName = String;
#[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_player: Rc<Player>,
version: Version,
map_name: String,
game_mode: String,
game_type: Recognized<BattleType>,
match_group: String,
players: Vec<Rc<Player>>,
game_chat: Vec<GameMessage>,
battle_results: Option<String>,
frags: HashMap<Rc<Player>, Vec<DeathInfo>>,
match_result: Option<BattleResult>,
finish_type: Option<Recognized<FinishType>>,
capture_points: Vec<CapturePointState>,
buff_zones: HashMap<EntityId, BuffZoneState>,
captured_buffs: Vec<CapturedBuff>,
team_scores: Vec<TeamScore>,
buildings: Vec<BuildingEntity>,
battle_start_clock: Option<GameClock>,
}
impl BattleReport {
pub fn self_player(&self) -> &Rc<Player> {
&self.self_player
}
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) -> &Recognized<BattleType> {
&self.game_type
}
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()
}
pub fn capture_points(&self) -> &[CapturePointState] {
&self.capture_points
}
pub fn buff_zones(&self) -> &HashMap<EntityId, BuffZoneState> {
&self.buff_zones
}
pub fn captured_buffs(&self) -> &[CapturedBuff] {
&self.captured_buffs
}
pub fn team_scores(&self) -> &[TeamScore] {
&self.team_scores
}
pub fn buildings(&self) -> &[BuildingEntity] {
&self.buildings
}
pub fn finish_type(&self) -> Option<&Recognized<FinishType>> {
self.finish_type.as_ref()
}
pub fn game_clock_to_elapsed(&self, clock: f32) -> f32 {
let start = self.battle_start_clock.map(|c| c.seconds()).unwrap_or(0.0);
(clock - start).max(0.0)
}
pub fn elapsed_to_game_clock(&self, elapsed: f32) -> f32 {
let start = self.battle_start_clock.map(|c| c.seconds()).unwrap_or(0.0);
start + elapsed
}
}
#[allow(dead_code)]
struct DamageEvent {
amount: f32,
victim: EntityId,
}
pub struct BattleController<'res, 'replay, G> {
game_meta: &'replay ReplayMeta,
game_resources: &'res G,
metadata_players: Vec<SharedPlayer>,
player_entities: HashMap<EntityId, Rc<Player>>,
entities_by_id: HashMap<EntityId, Entity>,
damage_dealt: HashMap<EntityId, Vec<DamageEvent>>,
frags: HashMap<EntityId, Vec<Death>>,
game_chat: Vec<GameMessage>,
version: Version,
battle_results: Option<String>,
match_finished: bool,
battle_end_clock: Option<GameClock>,
winning_team: Option<i8>,
finish_type: Option<Recognized<FinishType>>,
arena_id: i64,
current_clock: GameClock,
ship_positions: HashMap<EntityId, ShipPosition>,
minimap_positions: HashMap<EntityId, MinimapPosition>,
capture_points: Vec<CapturePointState>,
interactive_zone_indices: HashMap<EntityId, usize>,
buff_zones: HashMap<EntityId, BuffZoneState>,
zone_drop_params: HashMap<EntityId, GameParamId>,
captured_buffs: Vec<CapturedBuff>,
team_scores: Vec<TeamScore>,
active_consumables: HashMap<EntityId, Vec<ActiveConsumable>>,
active_shots: Vec<ActiveShot>,
active_torpedoes: Vec<ActiveTorpedo>,
active_planes: HashMap<PlaneId, ActivePlane>,
active_wards: HashMap<PlaneId, ActiveWard>,
kills: Vec<KillRecord>,
dead_ships: HashMap<EntityId, DeadShip>,
turret_yaws: HashMap<EntityId, Vec<f32>>,
selected_ammo: HashMap<EntityId, GameParamId>,
target_yaws: HashMap<EntityId, f32>,
scoring_rules: Option<ScoringRules>,
time_left: Option<i64>,
battle_stage: Option<i64>,
battle_start_clock: Option<GameClock>,
game_constants: Option<&'res GameConstants>,
}
impl<'res, 'replay, G> BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
pub fn new(
game_meta: &'replay ReplayMeta,
game_resources: &'res G,
game_constants: Option<&'res GameConstants>,
) -> Self {
let players: Vec<SharedPlayer> = game_meta
.vehicles
.iter()
.map(|vehicle| {
Rc::new(MetadataPlayer {
id: vehicle.id,
name: vehicle.name.clone(),
relation: Relation::new(vehicle.relation),
vehicle: game_resources
.game_param_by_id(vehicle.shipId)
.expect("could not find vehicle"),
})
})
.collect();
Self {
game_meta,
game_resources,
metadata_players: players,
player_entities: HashMap::default(),
entities_by_id: Default::default(),
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,
battle_end_clock: None,
winning_team: None,
finish_type: None,
arena_id: 0,
current_clock: GameClock::default(),
ship_positions: HashMap::default(),
minimap_positions: HashMap::default(),
capture_points: Vec::new(),
interactive_zone_indices: HashMap::default(),
buff_zones: HashMap::default(),
zone_drop_params: HashMap::default(),
captured_buffs: Vec::new(),
team_scores: Vec::new(),
active_consumables: HashMap::default(),
active_shots: Vec::new(),
active_torpedoes: Vec::new(),
active_planes: HashMap::default(),
active_wards: HashMap::default(),
kills: Vec::new(),
dead_ships: HashMap::default(),
turret_yaws: HashMap::default(),
selected_ammo: HashMap::default(),
target_yaws: HashMap::default(),
scoring_rules: None,
time_left: None,
battle_stage: None,
battle_start_clock: None,
game_constants,
}
}
pub fn reset(&mut self) {
self.player_entities.clear();
self.entities_by_id.clear();
self.damage_dealt.clear();
self.frags.clear();
self.game_chat.clear();
self.battle_results = None;
self.match_finished = false;
self.battle_end_clock = None;
self.winning_team = None;
self.finish_type = None;
self.arena_id = 0;
self.current_clock = GameClock::default();
self.ship_positions.clear();
self.minimap_positions.clear();
self.capture_points.clear();
self.interactive_zone_indices.clear();
self.buff_zones.clear();
self.zone_drop_params.clear();
self.captured_buffs.clear();
self.team_scores.clear();
self.active_consumables.clear();
self.active_shots.clear();
self.active_torpedoes.clear();
self.active_planes.clear();
self.kills.clear();
self.dead_ships.clear();
self.turret_yaws.clear();
self.target_yaws.clear();
self.scoring_rules = None;
self.time_left = None;
self.battle_stage = None;
self.battle_start_clock = None;
}
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) -> Recognized<BattleType> {
BattleType::from_value(&self.game_meta.gameType, self.version.clone())
}
fn constants(&self) -> &GameConstants {
self.game_constants
.unwrap_or(&crate::game_constants::DEFAULT_GAME_CONSTANTS)
}
fn handle_chat_message<'packet>(
&mut self,
entity_id: EntityId,
sender_id: AccountId,
audience: &str,
message: &str,
_extra_data: Option<ChatMessageExtra>,
clock: GameClock,
) {
if sender_id.raw() == 0 {
return;
}
let channel = match audience {
"battle_common" => ChatChannel::Global,
"battle_team" => ChatChannel::Team,
"battle_prebattle" => ChatChannel::Division,
other => ChatChannel::Unknown(other.to_string()),
};
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 {
sender_name = meta_vehicle.name.clone();
sender_team = Some(Relation::new(meta_vehicle.relation));
player = self
.player_entities
.values()
.find(|player| player.initial_state.meta_ship_id() == sender_id)
.cloned();
}
}
debug!("chat message from sender {sender_name} in channel {channel:?}: {message}");
let message = GameMessage {
clock,
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()
);
}
fn handle_entity_create<'packet>(
&mut self,
clock: GameClock,
packet: &EntityCreatePacket<'packet>,
) {
self.handle_entity_create_with_clock(clock, packet);
}
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_entity) = self.entities_by_id.get_mut(aggressor) {
let vehicle = aggressor_entity
.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
});
}
}
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 players: Vec<Rc<Player>> = self
.player_entities
.values()
.map(|player| {
let vehicle = self
.entities_by_id
.get(&player.initial_state.entity_id())
.and_then(|entity| entity.vehicle_ref())
.map(|vehicle_rc| {
let mut vehicle: VehicleEntity = RefCell::borrow(vehicle_rc).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| {
infos
.get(player.initial_state.db_id.to_string().as_str())
.cloned()
})
});
if let Some(frags) = self.frags.get(&player.initial_state.entity_id()) {
vehicle.frags = frags.iter().map(DeathInfo::from).collect();
}
}
vehicle
});
let mut final_player = player.as_ref().clone();
final_player.vehicle_entity = vehicle;
Rc::new(final_player)
})
.collect();
let frags: HashMap<Rc<Player>, Vec<DeathInfo>> =
HashMap::from_iter(self.frags.drain().filter_map(|(entity_id, kills)| {
let player = players
.iter()
.find(|p| p.initial_state.entity_id() == entity_id)?;
let kills: Vec<DeathInfo> = kills.iter().map(DeathInfo::from).collect();
Some((Rc::clone(player), kills))
}));
let self_player = players
.iter()
.find(|player| player.relation.is_self())
.cloned()
.expect("could not find self_player");
let buildings: Vec<BuildingEntity> = self
.entities_by_id
.values()
.filter_map(|e| e.building_ref().map(|b| RefCell::borrow(b).clone()))
.collect();
BattleReport {
arena_id: self.arena_id,
match_result: if self.match_finished {
self.winning_team.map(|team| {
if team == self_player.initial_state.team_id as i8 {
BattleResult::Win(team)
} else if team >= 0 {
BattleResult::Loss(1)
} else {
BattleResult::Draw
}
})
} else {
None
},
self_player,
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(),
players,
game_chat: self.game_chat,
battle_results: self.battle_results,
finish_type: self.finish_type,
frags,
capture_points: self.capture_points,
buff_zones: self.buff_zones,
captured_buffs: self.captured_buffs,
team_scores: self.team_scores,
buildings,
battle_start_clock: self.battle_start_clock,
}
}
pub fn battle_results(&self) -> Option<&String> {
self.battle_results.as_ref()
}
fn handle_property_update(
&mut self,
_clock: GameClock,
update: &crate::packet2::PropertyUpdatePacket<'_>,
) {
if update.property != "state" {
return;
}
let levels = &update.update_cmd.levels;
let action = &update.update_cmd.action;
if levels.len() == 3
&& let PropertyNestLevel::DictKey("missions") = &levels[0]
&& let PropertyNestLevel::DictKey("teamsScore") = &levels[1]
&& let PropertyNestLevel::ArrayIndex(team_idx) = &levels[2]
&& let UpdateAction::SetKey {
key: "score",
value,
} = action
&& let Some(score) = Self::arg_to_i64(value)
{
while self.team_scores.len() <= *team_idx {
self.team_scores.push(TeamScore {
team_index: self.team_scores.len(),
..Default::default()
});
}
self.team_scores[*team_idx].score = score;
}
if levels.len() == 2
&& let PropertyNestLevel::DictKey("drop") = &levels[0]
&& let PropertyNestLevel::DictKey("data") = &levels[1]
&& let UpdateAction::SetRange { values, .. } = action
{
for value in values {
if let ArgValue::FixedDict(dict) = value {
let zone_id = dict
.get("zoneId")
.and_then(|v| Self::arg_to_i64(v))
.map(|v| EntityId::from(v as i32));
let params_id = dict
.get("paramsId")
.and_then(|v| Self::arg_to_i64(v))
.map(GameParamId::from);
if let (Some(zone_id), Some(params_id)) = (zone_id, params_id) {
self.zone_drop_params.insert(zone_id, params_id);
if let Some(bz) = self.buff_zones.get_mut(&zone_id) {
bz.drop_params_id = Some(params_id);
}
}
}
}
}
if levels.len() == 2
&& let PropertyNestLevel::DictKey("drop") = &levels[0]
&& let PropertyNestLevel::DictKey("picked") = &levels[1]
&& let UpdateAction::SetRange { values, .. } = action
{
for value in values {
if let ArgValue::FixedDict(dict) = value {
let params_id = dict
.get("paramsId")
.and_then(|v| Self::arg_to_i64(v))
.map(GameParamId::from);
let owners: Option<Vec<EntityId>> = dict.get("owners").and_then(|v| {
if let ArgValue::Array(arr) = v {
Some(
arr.iter()
.filter_map(|o| {
Self::arg_to_i64(o).map(|id| EntityId::from(id as i32))
})
.collect::<Vec<_>>(),
)
} else {
None
}
});
if let (Some(params_id), Some(owners)) = (params_id, owners) {
let team_id = owners.first().and_then(|owner_id| {
self.player_entities
.values()
.find(|p| p.initial_state.entity_id() == *owner_id)
.map(|p| p.initial_state.team_id as i64)
});
if let Some(team_id) = team_id {
self.captured_buffs.push(CapturedBuff {
params_id,
team_id,
clock: _clock,
});
}
}
}
}
}
}
fn handle_entity_create_with_clock(
&mut self,
_clock: GameClock,
packet: &EntityCreatePacket<'_>,
) {
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, self.constants());
let captain_id = props.crew_modifiers_compact_params.params_id;
let captain = if captain_id.raw() != 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,
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::Building => {
let mut is_alive = true;
let mut is_hidden = false;
let mut is_suppressed = false;
let mut team_id: i8 = 0;
let mut params_id: u32 = 0;
if let Some(v) = packet.props.get("isAlive") {
is_alive = v.uint_8_ref().map(|v| *v != 0).unwrap_or(true);
}
if let Some(v) = packet.props.get("isHidden") {
is_hidden = v.uint_8_ref().map(|v| *v != 0).unwrap_or(false);
}
if let Some(v) = packet.props.get("isSuppressed") {
is_suppressed = v.uint_8_ref().map(|v| *v != 0).unwrap_or(false);
}
if let Some(v) = packet.props.get("teamId") {
team_id = v.int_8_ref().copied().unwrap_or(0);
}
if let Some(v) = packet.props.get("paramsId") {
params_id = v.uint_32_ref().copied().unwrap_or(0);
}
let building = BuildingEntity {
id: packet.entity_id,
position: WorldPos {
x: packet.position.x,
y: packet.position.y,
z: packet.position.z,
},
is_alive,
is_hidden,
is_suppressed,
team_id,
params_id: GameParamId::from(params_id),
};
self.entities_by_id.insert(
packet.entity_id,
Entity::Building(Rc::new(RefCell::new(building.clone()))),
);
}
EntityType::SmokeScreen => {
let mut radius: f32 = 0.0;
if let Some(v) = packet.props.get("radius") {
radius = v.float_32_ref().copied().unwrap_or(0.0);
}
let position = WorldPos {
x: packet.position.x,
y: packet.position.y,
z: packet.position.z,
};
let smoke = SmokeScreenEntity {
id: packet.entity_id,
radius,
position,
points: vec![position],
};
self.entities_by_id.insert(
packet.entity_id,
Entity::SmokeScreen(Rc::new(RefCell::new(smoke))),
);
}
EntityType::BattleLogic => {
debug!("BattleLogic create");
if let Some(state) = packet.props.get("state")
&& let Some(state_dict) = Self::as_dict(state)
&& let Some(missions) = state_dict.get("missions")
&& let Some(missions_dict) = Self::as_dict(missions)
{
if let Some(ArgValue::Array(teams)) = missions_dict.get("teamsScore") {
for (idx, entry) in teams.iter().enumerate() {
if let Some(entry_dict) = Self::as_dict(entry) {
let score = entry_dict
.get("score")
.and_then(|v| Self::arg_to_i64(v))
.unwrap_or(0);
while self.team_scores.len() <= idx {
self.team_scores.push(TeamScore {
team_index: self.team_scores.len(),
..Default::default()
});
}
self.team_scores[idx].score = score;
}
}
}
let team_win_score = missions_dict
.get("teamWinScore")
.and_then(|v| Self::arg_to_i64(v))
.unwrap_or(1000);
let mut hold_reward: i64 = 3;
let mut hold_period: f32 = 5.0;
let mut hold_cp_indices: Vec<usize> = Vec::new();
if let Some(ArgValue::Array(holds)) = missions_dict.get("hold") {
if let Some(first_hold) = holds.first()
&& let Some(hold_dict) = Self::as_dict(first_hold)
{
if let Some(v) =
hold_dict.get("reward").and_then(|v| Self::arg_to_i64(v))
{
hold_reward = v;
}
if let Some(v) = hold_dict.get("period") {
hold_period = v.float_32_ref().copied().unwrap_or(5.0);
}
if let Some(ArgValue::Array(indices)) = hold_dict.get("cpIndices") {
for idx in indices {
if let Some(i) = Self::arg_to_i64(idx) {
hold_cp_indices.push(i as usize);
}
}
}
}
}
self.scoring_rules = Some(ScoringRules {
team_win_score,
hold_reward,
hold_period,
hold_cp_indices,
});
}
}
EntityType::InteractiveZone => {
let position = WorldPos {
x: packet.position.x,
y: packet.position.y,
z: packet.position.z,
};
let radius = packet
.props
.get("radius")
.and_then(|v| v.float_32_ref().copied())
.unwrap_or(0.0);
let team_id = packet
.props
.get("teamId")
.and_then(|v| Self::arg_to_i64(v))
.unwrap_or(-1);
let mut cp_index: Option<usize> = None;
let mut cp_type: i32 = 0;
let mut has_invaders = false;
let mut invader_team: i64 = -1;
let mut progress: f64 = 0.0;
let mut both_inside = false;
if let Some(cs) = packet.props.get("componentsState")
&& let Some(cs_dict) = Self::as_dict(cs)
{
if let Some(cp) = cs_dict.get("controlPoint")
&& let Some(cp_dict) = Self::as_dict(cp)
{
if let Some(idx) = cp_dict.get("index") {
cp_index = Self::arg_to_i64(idx).map(|v| v as usize);
}
if let Some(t) = cp_dict.get("type") {
cp_type = Self::arg_to_i64(t).unwrap_or(0) as i32;
}
}
if let Some(cl) = cs_dict.get("captureLogic")
&& let Some(cl_dict) = Self::as_dict(cl)
{
if let Some(v) = cl_dict.get("hasInvaders") {
has_invaders = Self::arg_to_i64(v).unwrap_or(0) != 0;
}
if let Some(v) = cl_dict.get("invaderTeam") {
invader_team = Self::arg_to_i64(v).unwrap_or(-1);
}
if let Some(v) = cl_dict.get("progress") {
progress = v.float_32_ref().map(|f| *f as f64).unwrap_or(0.0);
}
if let Some(v) = cl_dict.get("bothInside") {
both_inside = Self::arg_to_i64(v).unwrap_or(0) != 0;
}
}
}
let mut is_enabled = true;
if let Some(cs) = packet.props.get("componentsState")
&& let Some(cs_dict) = Self::as_dict(cs)
&& let Some(cl) = cs_dict.get("captureLogic")
&& let Some(cl_dict) = Self::as_dict(cl)
&& let Some(v) = cl_dict.get("isEnabled")
{
is_enabled = Self::arg_to_i64(v).unwrap_or(1) != 0;
}
if let Some(idx) = cp_index {
while self.capture_points.len() <= idx {
self.capture_points.push(CapturePointState::default());
}
self.capture_points[idx] = CapturePointState {
index: idx,
position: Some(position),
radius,
control_point_type: cp_type,
team_id,
invader_team,
progress: (progress, 0.0),
has_invaders,
both_inside,
is_enabled,
};
self.interactive_zone_indices.insert(packet.entity_id, idx);
} else {
let drop_params_id = self.zone_drop_params.get(&packet.entity_id).copied();
self.buff_zones.insert(
packet.entity_id,
BuffZoneState {
entity_id: packet.entity_id,
position,
radius,
team_id,
is_active: is_enabled,
drop_params_id,
},
);
self.interactive_zone_indices
.insert(packet.entity_id, usize::MAX);
}
}
EntityType::BattleEntity => debug!("BattleEntity create"),
}
}
fn as_dict<'a, 'b>(value: &'a ArgValue<'b>) -> Option<&'a HashMap<&'b str, ArgValue<'b>>> {
match value {
ArgValue::FixedDict(d) => Some(d),
ArgValue::NullableFixedDict(Some(d)) => Some(d),
_ => None,
}
}
fn arg_to_i64(value: &ArgValue<'_>) -> Option<i64> {
match value {
ArgValue::Int8(v) => Some(*v as i64),
ArgValue::Int16(v) => Some(*v as i64),
ArgValue::Int32(v) => Some(*v as i64),
ArgValue::Int64(v) => Some(*v),
ArgValue::Uint8(v) => Some(*v as i64),
ArgValue::Uint16(v) => Some(*v as i64),
ArgValue::Uint32(v) => Some(*v as i64),
ArgValue::Uint64(v) => Some(*v as i64),
_ => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ChatChannel {
Division,
Global,
Team,
System,
Unknown(String),
}
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)?;
let to_ids = |v: Vec<u32>| v.into_iter().map(GameParamId::from).collect();
Ok((
i,
ShipConfig {
abilities: to_ids(abilities),
hull: GameParamId::from(units[0]),
modernization: to_ids(modernization),
units: to_ids(units),
signals: to_ids(signals),
},
))
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GameMessage {
pub clock: GameClock,
pub sender_relation: Option<Relation>,
pub sender_name: String,
pub channel: ChatChannel,
pub message: String,
pub entity_id: EntityId,
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: GameParamId,
is_in_adaption: bool,
learned_skills: Skills,
}
impl CrewModifiersCompactParams {
pub fn params_id(&self) -> GameParamId {
self.params_id
}
pub fn learned_skills(&self) -> &Skills {
&self.learned_skills
}
}
trait UpdateFromReplayArgs {
fn update_by_name(
&mut self,
name: &str,
value: &ArgValue<'_>,
version: Version,
constants: &GameConstants,
) {
let mut dict = HashMap::with_capacity(1);
dict.insert(name, value.clone());
self.update_from_args(&dict, version, constants);
}
fn update_from_args(
&mut self,
args: &HashMap<&str, ArgValue<'_>>,
version: Version,
constants: &GameConstants,
);
}
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,
_constants: &GameConstants,
) {
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 = GameParamId::from(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, 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: Recognized<WeaponType>,
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: Recognized<DepthState>,
ui_enabled: bool,
respawn_time: u16,
engine_power: u8,
max_server_speed_raw: u32,
burning_flags: u16,
}
impl Default for VehicleProps {
fn default() -> Self {
Self {
ignore_map_borders: false,
air_defense_dispersion_radius: 0.0,
death_settings: Vec::new(),
owner: 0,
atba_targets: Vec::new(),
effects: Vec::new(),
crew_modifiers_compact_params: CrewModifiersCompactParams::default(),
laser_target_local_pos: 0,
anti_air_auras: Vec::new(),
selected_weapon: Recognized::Known(WeaponType::Artillery),
regeneration_health: 0.0,
is_on_forsage: false,
is_in_rage_mode: false,
has_air_targets_in_range: false,
torpedo_local_pos: 0,
air_defense_target_ids: Vec::new(),
buoyancy: 0.0,
max_health: 0.0,
rudders_angle: 0.0,
draught: 0.0,
target_local_pos: 0,
triggered_skills_data: Vec::new(),
regenerated_health: 0.0,
blocked_controls: 0,
is_invisible: false,
is_fog_horn_on: false,
server_speed_raw: 0,
regen_crew_hp_limit: 0.0,
miscs_presets_status: Vec::new(),
buoyancy_current_waterline: 0.0,
is_alive: false,
is_bot: false,
visibility_flags: 0,
heat_infos: Vec::new(),
buoyancy_rudder_index: 0,
is_anti_air_mode: false,
speed_sign_dir: 0,
oil_leak_state: 0,
sounds: Vec::new(),
ship_config: ShipConfig::default(),
wave_local_pos: 0,
has_active_main_squadron: false,
weapon_lock_flags: 0,
deep_rudders_angle: 0.0,
debug_text: Vec::new(),
health: 0.0,
engine_dir: 0,
state: VehicleState::default(),
team_id: 0,
buoyancy_current_state: Recognized::Known(DepthState::Surface),
ui_enabled: false,
respawn_time: 0,
engine_power: 0,
max_server_speed_raw: 0,
burning_flags: 0,
}
}
}
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) -> &Recognized<WeaponType> {
&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) -> &Recognized<DepthState> {
&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_from_args(
&mut self,
args: &HashMap<&str, ArgValue<'_>>,
version: Version,
constants: &GameConstants,
) {
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 SELECTED_WEAPON_KEY: &str = "selectedWeapon";
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 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 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 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 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 HEALTH_KEY: &str = "health";
const ENGINE_DIR_KEY: &str = "engineDir";
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,
constants,
);
}
set_arg_value!(
self.laser_target_local_pos,
args,
LASER_TARGET_LOCAL_POS_KEY,
u16
);
if args.contains_key(SELECTED_WEAPON_KEY) {
if let Some(wt) = WeaponType::from_id(
arg_value_to_type!(args, SELECTED_WEAPON_KEY, u32) as i32,
constants.ships(),
version,
) {
self.selected_weapon = wt;
}
}
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);
if args.contains_key(BUOYANCY_CURRENT_STATE_KEY) {
if let Some(ds) = DepthState::from_id(
arg_value_to_type!(args, BUOYANCY_CURRENT_STATE_KEY, u8) as i32,
constants.battle(),
version,
) {
self.buoyancy_current_state = ds;
}
}
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: EntityId,
cause: Recognized<DeathCause>,
}
impl DeathInfo {
pub fn time_lived(&self) -> Duration {
self.time_lived
}
pub fn killer(&self) -> EntityId {
self.killer
}
pub fn cause(&self) -> &Recognized<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.clone(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct VehicleEntity {
id: EntityId,
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) -> EntityId {
self.id
}
pub fn props(&self) -> &VehicleProps {
&self.props
}
pub fn commander_id(&self) -> GameParamId {
self.props.crew_modifiers_compact_params.params_id
}
pub fn commander_skills(&self, vehicle_species: Species) -> Option<Vec<&CrewSkill>> {
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, vehicle_species: Species) -> &[u8] {
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>>),
Building(Rc<RefCell<BuildingEntity>>),
SmokeScreen(Rc<RefCell<SmokeScreenEntity>>),
}
#[derive(Debug)]
struct Death {
timestamp: Duration,
killer: EntityId,
victim: EntityId,
cause: Recognized<DeathCause>,
}
impl<'res, 'replay, G> BattleControllerState for BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
fn clock(&self) -> GameClock {
self.current_clock
}
fn ship_positions(&self) -> &HashMap<EntityId, ShipPosition> {
&self.ship_positions
}
fn minimap_positions(&self) -> &HashMap<EntityId, MinimapPosition> {
&self.minimap_positions
}
fn player_entities(&self) -> &HashMap<EntityId, Rc<Player>> {
&self.player_entities
}
fn metadata_players(&self) -> &[SharedPlayer] {
&self.metadata_players
}
fn entities_by_id(&self) -> &HashMap<EntityId, Entity> {
&self.entities_by_id
}
fn capture_points(&self) -> &[CapturePointState] {
&self.capture_points
}
fn buff_zones(&self) -> &HashMap<EntityId, BuffZoneState> {
&self.buff_zones
}
fn captured_buffs(&self) -> &[CapturedBuff] {
&self.captured_buffs
}
fn team_scores(&self) -> &[TeamScore] {
&self.team_scores
}
fn game_chat(&self) -> &[GameMessage] {
&self.game_chat
}
fn active_consumables(&self) -> &HashMap<EntityId, Vec<ActiveConsumable>> {
&self.active_consumables
}
fn active_shots(&self) -> &[ActiveShot] {
&self.active_shots
}
fn active_torpedoes(&self) -> &[ActiveTorpedo] {
&self.active_torpedoes
}
fn active_planes(&self) -> &HashMap<PlaneId, ActivePlane> {
&self.active_planes
}
fn active_wards(&self) -> &HashMap<PlaneId, ActiveWard> {
&self.active_wards
}
fn kills(&self) -> &[KillRecord] {
&self.kills
}
fn dead_ships(&self) -> &HashMap<EntityId, DeadShip> {
&self.dead_ships
}
fn battle_end_clock(&self) -> Option<GameClock> {
self.battle_end_clock
}
fn winning_team(&self) -> Option<i8> {
self.winning_team
}
fn finish_type(&self) -> Option<&Recognized<FinishType>> {
self.finish_type.as_ref()
}
fn turret_yaws(&self) -> &HashMap<EntityId, Vec<f32>> {
&self.turret_yaws
}
fn target_yaws(&self) -> &HashMap<EntityId, f32> {
&self.target_yaws
}
fn selected_ammo(&self) -> &HashMap<EntityId, GameParamId> {
&self.selected_ammo
}
fn battle_type(&self) -> Recognized<BattleType> {
BattleType::from_value(&self.game_meta.gameType, self.version.clone())
}
fn scoring_rules(&self) -> Option<&ScoringRules> {
self.scoring_rules.as_ref()
}
fn time_left(&self) -> Option<i64> {
self.time_left
}
fn battle_stage(&self) -> Option<BattleStage> {
let raw = self.battle_stage?;
self.constants().common().battle_stage(raw as i32).cloned()
}
fn battle_start_clock(&self) -> Option<GameClock> {
self.battle_start_clock
}
fn game_clock_to_elapsed(&self, clock: f32) -> f32 {
let start = self.battle_start_clock.map(|c| c.seconds()).unwrap_or(0.0);
(clock - start).max(0.0)
}
fn elapsed_to_game_clock(&self, elapsed: f32) -> f32 {
let start = self.battle_start_clock.map(|c| c.seconds()).unwrap_or(0.0);
start + elapsed
}
}
impl<'res, 'replay, G> Analyzer for BattleController<'res, 'replay, G>
where
G: ResourceLoader,
{
fn process(&mut self, packet: &Packet<'_, '_>) {
let span = span!(Level::TRACE, "packet processing");
let _enter = span.enter();
self.current_clock = packet.clock;
let default_constants = &*crate::game_constants::DEFAULT_GAME_CONSTANTS;
let constants = self.game_constants.unwrap_or(default_constants);
let packet_decoder = crate::analyzer::decoder::PacketDecoder::builder()
.version(self.version.clone())
.battle_constants(constants.battle())
.common_constants(constants.common())
.build();
let decoded = packet_decoder.decode(packet);
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,
packet.clock,
);
}
crate::analyzer::decoder::DecodedPacketPayload::VoiceLine { .. } => {
trace!("HANDLE VOICE LINE");
}
crate::analyzer::decoder::DecodedPacketPayload::Ribbon(_ribbon) => {}
crate::analyzer::decoder::DecodedPacketPayload::Position(pos) => {
let world_pos = WorldPos {
x: pos.position.x,
y: pos.position.y,
z: pos.position.z,
};
let ship_pos = ShipPosition {
entity_id: pos.pid,
position: world_pos,
yaw: pos.rotation.yaw,
pitch: pos.rotation.pitch,
roll: pos.rotation.roll,
last_updated: packet.clock,
};
self.ship_positions.insert(pos.pid, ship_pos);
}
crate::analyzer::decoder::DecodedPacketPayload::PlayerOrientation(ref orientation) => {
if orientation.parent_id == EntityId::from(0u32) {
let ship_pos = ShipPosition {
entity_id: orientation.pid,
position: WorldPos {
x: orientation.position.x,
y: orientation.position.y,
z: orientation.position.z,
},
yaw: orientation.rotation.yaw,
pitch: orientation.rotation.pitch,
roll: orientation.rotation.roll,
last_updated: packet.clock,
};
self.ship_positions.insert(orientation.pid, ship_pos);
}
}
crate::analyzer::decoder::DecodedPacketPayload::DamageStat(_damage) => {
trace!("DAMAGE STAT")
}
crate::analyzer::decoder::DecodedPacketPayload::ShipDestroyed {
killer,
victim,
cause,
} => {
self.frags.entry(killer).or_default().push(Death {
timestamp: packet.clock.to_duration(),
killer,
victim,
cause: cause.clone(),
});
self.kills.push(KillRecord {
clock: packet.clock,
killer,
victim,
cause,
});
if let Some(ship_pos) = self.ship_positions.get(&victim) {
self.dead_ships.insert(
victim,
DeadShip {
clock: packet.clock,
position: ship_pos.position,
},
);
}
}
crate::analyzer::decoder::DecodedPacketPayload::EntityMethod(method) => {
debug!("ENTITY METHOD, {:#?}", method)
}
crate::analyzer::decoder::DecodedPacketPayload::EntityProperty(prop) => {
let entity_id = prop.entity_id;
if let Some(entity) = self.entities_by_id.get(&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,
self.constants(),
);
}
if prop.property == "targetLocalPos"
&& let Some(val) = Self::arg_to_i64(&prop.value)
{
let lo = (val & 0xFF) as f32;
let yaw = (lo / 256.0) * std::f32::consts::TAU - std::f32::consts::PI;
self.target_yaws.insert(entity_id, yaw);
}
if prop.property == "teamId"
&& let Some(&cp_idx) = self.interactive_zone_indices.get(&entity_id)
&& let Some(v) = Self::arg_to_i64(&prop.value)
{
self.capture_points[cp_idx].team_id = v;
}
if prop.property == "timeLeft" {
if let Some(v) = Self::arg_to_i64(&prop.value) {
self.time_left = Some(v);
}
}
if prop.property == "battleStage" {
if let Some(v) = Self::arg_to_i64(&prop.value) {
if self.battle_start_clock.is_none() {
let resolved = constants.common().battle_stage(v as i32).copied();
if matches!(resolved, Some(BattleStage::Waiting)) {
self.battle_start_clock = Some(packet.clock);
}
}
self.battle_stage = Some(v);
}
}
if prop.property == "battleResult"
&& let Some(dict) = Self::as_dict(&prop.value)
{
if let Some(winner) = dict.get("winnerTeamId").and_then(|v| Self::arg_to_i64(v))
&& winner >= -1
{
self.winning_team = Some(winner as i8);
}
if let Some(reason) = dict.get("finishReason").and_then(|v| Self::arg_to_i64(v))
&& reason > 0
{
self.finish_type = FinishType::from_id(
reason as i32,
constants.battle(),
self.version.clone(),
);
}
}
}
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) => {
let entity_id = leave.entity_id;
if self
.entities_by_id
.get(&entity_id)
.and_then(|e| e.smoke_screen_ref())
.is_some()
{
self.entities_by_id.remove(&entity_id);
}
self.buff_zones.remove(&entity_id);
}
crate::analyzer::decoder::DecodedPacketPayload::EntityCreate(entity_create) => {
self.handle_entity_create(packet.clock, entity_create);
}
crate::analyzer::decoder::DecodedPacketPayload::OnArenaStateReceived {
arena_id: arg0,
team_build_type_id: _,
pre_battles_info: _,
player_states: players,
bot_states: bots,
} => {
debug!("OnArenaStateReceived");
self.arena_id = arg0;
for player in players.iter().chain(bots.iter()) {
let metadata_player = self
.metadata_players
.iter()
.find(|meta_player| meta_player.id == player.meta_ship_id())
.expect("could not map arena player to metadata player");
let battle_player = Player::from_arena_player(
player,
metadata_player.as_ref(),
self.game_resources,
);
let player_has_died = self
.entities_by_id
.get(&player.entity_id())
.map(|vehicle| {
let Some(vehicle) = vehicle.vehicle_ref() else {
return false;
};
let vehicle = RefCell::borrow(vehicle);
self.frags.values().any(|deaths| {
deaths.iter().any(|death| death.victim == vehicle.id())
})
})
.unwrap_or_default();
if player.is_connected() {
battle_player
.connection_change_info_mut()
.push(ConnectionChangeInfo {
at_game_duration: packet.clock.to_duration(),
event_kind: ConnectionChangeKind::Connected,
had_death_event: player_has_died,
});
}
let battle_player = Rc::new(battle_player);
self.player_entities
.insert(battle_player.initial_state.entity_id(), battle_player);
}
}
crate::analyzer::decoder::DecodedPacketPayload::CheckPing(_) => trace!("CHECK PING"),
crate::analyzer::decoder::DecodedPacketPayload::DamageReceived {
victim,
ref aggressors,
} => {
for damage in aggressors {
self.damage_dealt
.entry(damage.aggressor)
.or_default()
.push(DamageEvent {
amount: damage.damage,
victim,
});
}
}
crate::analyzer::decoder::DecodedPacketPayload::MinimapUpdate {
ref updates,
arg1: _,
} => {
for update in updates {
let is_sentinel = update.position.x == -1.5 && update.position.y == -1.5;
let visible = !is_sentinel;
let (position, heading) = if is_sentinel {
let prev = self.minimap_positions.get(&update.entity_id);
(
prev.map(|p| p.position).unwrap_or(update.position),
prev.map(|p| p.heading).unwrap_or(update.heading),
)
} else {
(update.position, update.heading)
};
self.minimap_positions.insert(
update.entity_id,
MinimapPosition {
entity_id: update.entity_id,
position,
heading,
visible,
last_updated: packet.clock,
},
);
}
}
crate::analyzer::decoder::DecodedPacketPayload::PropertyUpdate(update) => {
if self.entities_by_id.contains_key(&update.entity_id) {
debug!("PROPERTY UPDATE: {:#?}", update);
}
if update.property == "points"
&& let Some(entity) = self.entities_by_id.get(&update.entity_id)
&& let Some(smoke_ref) = entity.smoke_screen_ref()
{
let mut smoke = RefCell::borrow_mut(smoke_ref);
match &update.update_cmd.action {
UpdateAction::SetRange { start, values, .. } => {
while smoke.points.len() < start + values.len() {
smoke.points.push(WorldPos::default());
}
for (i, v) in values.iter().enumerate() {
if let ArgValue::Vector3((x, y, z)) = v {
smoke.points[start + i] = WorldPos {
x: *x,
y: *y,
z: *z,
};
}
}
}
UpdateAction::RemoveRange { start, stop } => {
let end = (*stop).min(smoke.points.len());
smoke.points.drain(*start..end);
}
_ => {}
}
drop(smoke);
}
if update.property == "componentsState"
&& let Some(&cp_idx) = self.interactive_zone_indices.get(&update.entity_id)
&& matches!(
update.update_cmd.levels.first(),
Some(PropertyNestLevel::DictKey("captureLogic"))
)
&& let UpdateAction::SetKey { key, value } = &update.update_cmd.action
{
if cp_idx != usize::MAX {
match *key {
"hasInvaders" => {
if let Some(v) = Self::arg_to_i64(value) {
self.capture_points[cp_idx].has_invaders = v != 0;
}
}
"invaderTeam" => {
if let Some(v) = Self::arg_to_i64(value) {
self.capture_points[cp_idx].invader_team = v;
}
}
"progress" => {
if let Some(f) = value.float_32_ref() {
self.capture_points[cp_idx].progress = (*f as f64, 0.0);
}
}
"bothInside" => {
if let Some(v) = Self::arg_to_i64(value) {
self.capture_points[cp_idx].both_inside = v != 0;
}
}
"teamId" | "invaderTeamId" => {
if let Some(v) = Self::arg_to_i64(value) {
self.capture_points[cp_idx].invader_team = v;
}
}
"isEnabled" => {
if let Some(v) = Self::arg_to_i64(value) {
self.capture_points[cp_idx].is_enabled = v != 0;
}
}
_ => {}
}
}
}
self.handle_property_update(packet.clock, update);
}
crate::analyzer::decoder::DecodedPacketPayload::BattleEnd {
winning_team,
finish_type,
} => {
self.match_finished = true;
self.battle_end_clock = Some(packet.clock);
if winning_team.is_some() {
self.winning_team = winning_team;
}
if finish_type.is_some() {
self.finish_type = finish_type;
}
}
crate::analyzer::decoder::DecodedPacketPayload::Consumable {
entity,
consumable,
duration,
} => {
self.active_consumables
.entry(entity)
.or_default()
.push(ActiveConsumable {
consumable,
activated_at: packet.clock,
duration,
});
}
crate::analyzer::decoder::DecodedPacketPayload::ArtilleryShots {
entity_id,
salvos,
} => {
for salvo in salvos {
self.active_shots.push(ActiveShot {
entity_id,
salvo,
fired_at: packet.clock,
});
}
}
crate::analyzer::decoder::DecodedPacketPayload::TorpedoesReceived {
entity_id,
torpedoes,
} => {
for torpedo in torpedoes {
self.active_torpedoes.push(ActiveTorpedo {
entity_id,
torpedo,
launched_at: packet.clock,
});
}
}
crate::analyzer::decoder::DecodedPacketPayload::PlanePosition {
entity_id: _,
plane_id,
x,
y,
} => {
if let Some(plane) = self.active_planes.get_mut(&plane_id) {
plane.position = WorldPos { x, y: 0.0, z: y };
plane.last_updated = packet.clock;
}
}
crate::analyzer::decoder::DecodedPacketPayload::PlaneAdded {
entity_id,
plane_id,
team_id,
params_id,
x,
y,
} => {
let pos = WorldPos { x, y: 0.0, z: y };
self.active_planes.insert(
plane_id,
ActivePlane {
plane_id,
owner_id: entity_id,
team_id,
params_id,
position: pos,
last_updated: packet.clock,
},
);
}
crate::analyzer::decoder::DecodedPacketPayload::PlaneRemoved {
entity_id: _,
plane_id,
} => {
self.active_planes.remove(&plane_id);
}
crate::analyzer::decoder::DecodedPacketPayload::WardAdded {
plane_id,
position,
radius,
owner_id,
..
} => {
self.active_wards.insert(
plane_id,
ActiveWard {
plane_id,
position,
radius: BigWorldDistance::from(radius),
owner_id,
},
);
}
crate::analyzer::decoder::DecodedPacketPayload::WardRemoved { plane_id, .. } => {
self.active_wards.remove(&plane_id);
}
crate::analyzer::decoder::DecodedPacketPayload::GunSync {
entity_id,
group,
turret,
yaw,
..
} => {
if group == 0 {
let turrets = self.turret_yaws.entry(entity_id).or_default();
let idx = turret as usize;
if turrets.len() <= idx {
turrets.resize(idx + 1, 0.0);
}
turrets[idx] = yaw;
}
}
crate::analyzer::decoder::DecodedPacketPayload::SetAmmoForWeapon {
entity_id,
weapon_type,
ammo_param_id,
is_reload: _,
} => {
if weapon_type == 0 {
self.selected_ammo.insert(entity_id, ammo_param_id);
}
}
crate::analyzer::decoder::DecodedPacketPayload::CruiseState { .. } => {
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::ShotKills { entity_id: _, hits } => {
for hit in &hits {
let composite_key = format!("{}{}", hit.owner_id, hit.shot_id);
if let Ok(key) = composite_key.parse::<u64>() {
self.active_torpedoes.retain(|t| {
let torp_key = format!("{}{}", t.torpedo.owner_id, t.torpedo.shot_id);
torp_key.parse::<u64>().map(|k| k != key).unwrap_or(true)
});
}
}
}
crate::analyzer::decoder::DecodedPacketPayload::EntityControl(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::SmokeScreenDrift(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::ViewDirection(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::ServerTimestamp(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::OwnShip(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::VehicleRef(_) => {}
crate::analyzer::decoder::DecodedPacketPayload::ServerTick(_) => {}
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());
}
crate::analyzer::decoder::DecodedPacketPayload::OnGameRoomStateChanged {
player_states,
} => {
for player_state in &player_states {
let Some(meta_ship_id) = player_state.get(PlayerStateData::KEY_ID) else {
continue;
};
let meta_ship_id = *meta_ship_id.i64_ref().expect("player_id is not an i64");
let Some(player) = self.player_entities.values().find(|player| {
player.initial_state().meta_ship_id() == AccountId::from(meta_ship_id)
}) else {
debug!("Failed to find player with meta ship ID {meta_ship_id:?}");
continue;
};
{
player.end_state_mut().update_from_dict(player_state);
}
let player_has_died = self
.entities_by_id
.get(&player.initial_state().entity_id())
.map(|vehicle| {
let Some(vehicle) = vehicle.vehicle_ref() else {
return false;
};
let vehicle = RefCell::borrow(vehicle);
self.frags.values().any(|deaths| {
deaths.iter().any(|death| death.victim == vehicle.id())
})
})
.unwrap_or_default();
let connection_event_kind = if player.end_state().is_connected() {
ConnectionChangeKind::Connected
} else {
ConnectionChangeKind::Disconnected
};
if (player.connection_change_info().is_empty()
&& connection_event_kind != ConnectionChangeKind::Disconnected)
|| player
.connection_change_info()
.last()
.map(|info| info.event_kind != connection_event_kind)
.unwrap_or_default()
{
player
.connection_change_info_mut()
.push(ConnectionChangeInfo {
at_game_duration: packet.clock.to_duration(),
event_kind: connection_event_kind,
had_death_event: player_has_died,
});
}
}
}
}
}
fn finish(&mut self) {}
}