use crate::IResult;
use crate::packet2::{EntityMethodPacket, Packet, PacketType};
use crate::types::{AccountId, EntityId, GameParamId, NormalizedPos, PlaneId};
use kinded::Kinded;
use nom::number::complete::{le_f32, le_u8, le_u16, le_u64};
use pickled::Value;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap};
use std::convert::TryInto;
use std::iter::FromIterator;
use wowsunpack::data::Version;
use wowsunpack::game_params::convert::pickle_to_json;
use wowsunpack::rpc::typedefs::ArgValue;
use wowsunpack::unpack_rpc_args;
use super::analyzer::Analyzer;
pub struct DecoderBuilder {
silent: bool,
no_meta: bool,
path: Option<String>,
}
impl DecoderBuilder {
pub fn new(silent: bool, no_meta: bool, output: Option<&str>) -> Self {
Self {
silent,
no_meta,
path: output.map(|s| s.to_string()),
}
}
pub fn build(self, meta: &crate::ReplayMeta) -> Box<dyn Analyzer> {
let version = Version::from_client_exe(&meta.clientVersionFromExe);
let mut decoder = Decoder {
silent: self.silent,
output: self.path.as_ref().map(|path| {
Box::new(std::fs::File::create(path).unwrap()) as Box<dyn std::io::Write>
}),
version,
};
if !self.no_meta {
decoder.write(&serde_json::to_string(&meta).unwrap());
}
Box::new(decoder)
}
}
pub use wowsunpack::game_types::{
BatteryState, CameraMode, Consumable, DeathCause, DepthState, FinishType, Ribbon, VoiceLine,
WeaponType,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HumanPlayerProperties {
pub(crate) avatar_id: AccountId,
pub(crate) prebattle_id: i64,
pub(crate) is_client_loaded: bool,
pub(crate) is_connected: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerStateData {
pub(crate) username: String,
pub(crate) clan: String,
pub(crate) clan_id: i64,
pub(crate) clan_color: i64,
pub(crate) db_id: AccountId,
pub(crate) realm: String,
pub(crate) meta_ship_id: AccountId,
pub(crate) entity_id: EntityId,
pub(crate) team_id: i64,
pub(crate) max_health: i64,
pub(crate) is_abuser: bool,
pub(crate) is_hidden: bool,
pub(crate) is_bot: bool,
pub(crate) human_properties: Option<HumanPlayerProperties>,
#[serde(skip_deserializing)]
pub(crate) raw: HashMap<i64, String>,
#[serde(skip_deserializing)]
pub(crate) raw_with_names: HashMap<&'static str, serde_json::Value>,
}
impl PlayerStateData {
pub(crate) const KEY_ACCOUNT_DBID: &'static str = "accountDBID";
pub(crate) const KEY_ANTI_ABUSE_ENABLED: &'static str = "antiAbuseEnabled";
pub(crate) const KEY_AVATAR_ID: &'static str = "avatarId";
pub(crate) const KEY_CAMOUFLAGE_INFO: &'static str = "camouflageInfo";
pub(crate) const KEY_CLAN_COLOR: &'static str = "clanColor";
pub(crate) const KEY_CLAN_ID: &'static str = "clanID";
pub(crate) const KEY_CLAN_TAG: &'static str = "clanTag";
pub(crate) const KEY_CREW_PARAMS: &'static str = "crewParams";
pub(crate) const KEY_DOG_TAG: &'static str = "dogTag";
pub(crate) const KEY_FRAGS_COUNT: &'static str = "fragsCount";
pub(crate) const KEY_FRIENDLY_FIRE_ENABLED: &'static str = "friendlyFireEnabled";
pub(crate) const KEY_ID: &'static str = "id";
pub(crate) const KEY_INVITATIONS_ENABLED: &'static str = "invitationsEnabled";
pub(crate) const KEY_IS_ABUSER: &'static str = "isAbuser";
pub(crate) const KEY_IS_ALIVE: &'static str = "isAlive";
pub(crate) const KEY_IS_BOT: &'static str = "isBot";
pub(crate) const KEY_IS_CLIENT_LOADED: &'static str = "isClientLoaded";
pub(crate) const KEY_IS_CONNECTED: &'static str = "isConnected";
pub(crate) const KEY_IS_HIDDEN: &'static str = "isHidden";
pub(crate) const KEY_IS_LEAVER: &'static str = "isLeaver";
pub(crate) const KEY_IS_PRE_BATTLE_OWNER: &'static str = "isPreBattleOwner";
pub(crate) const KEY_IS_T_SHOOTER: &'static str = "isTShooter";
pub(crate) const KEY_KEY_TARGET_MARKERS: &'static str = "keyTargetMarkers";
pub(crate) const KEY_KILLED_BUILDINGS_COUNT: &'static str = "killedBuildingsCount";
pub(crate) const KEY_MAX_HEALTH: &'static str = "maxHealth";
pub(crate) const KEY_NAME: &'static str = "name";
pub(crate) const KEY_PLAYER_MODE: &'static str = "playerMode";
pub(crate) const KEY_PRE_BATTLE_ID_ON_START: &'static str = "preBattleIdOnStart";
pub(crate) const KEY_PRE_BATTLE_SIGN: &'static str = "preBattleSign";
pub(crate) const KEY_PREBATTLE_ID: &'static str = "prebattleId";
pub(crate) const KEY_REALM: &'static str = "realm";
pub(crate) const KEY_SHIP_COMPONENTS: &'static str = "shipComponents";
pub(crate) const KEY_SHIP_CONFIG_DUMP: &'static str = "shipConfigDump";
pub(crate) const KEY_SHIP_ID: &'static str = "shipId";
pub(crate) const KEY_SHIP_PARAMS_ID: &'static str = "shipParamsId";
pub(crate) const KEY_SKIN_ID: &'static str = "skinId";
pub(crate) const KEY_TEAM_ID: &'static str = "teamId";
pub(crate) const KEY_TTK_STATUS: &'static str = "ttkStatus";
fn convert_raw_dict(
values: &HashMap<i64, Value>,
version: &Version,
is_bot: bool,
) -> HashMap<&'static str, Value> {
let keys: HashMap<&'static str, i64> = if is_bot {
Self::bot_key_map(version)
} else {
Self::player_key_map(version)
};
let mut raw_with_names = HashMap::new();
for (k, v) in values.iter() {
if let Some(name) = keys
.iter()
.find_map(|(name, idx)| if *idx == *k { Some(*name) } else { None })
{
raw_with_names.insert(name, v.clone());
}
}
raw_with_names
}
fn player_key_map(version: &Version) -> HashMap<&'static str, i64> {
if version.is_at_least(&Version::from_client_exe("0,12,8,0")) {
let mut h = HashMap::new();
h.insert(Self::KEY_ACCOUNT_DBID, 0);
h.insert(Self::KEY_ANTI_ABUSE_ENABLED, 1);
h.insert(Self::KEY_AVATAR_ID, 2);
h.insert(Self::KEY_CAMOUFLAGE_INFO, 3);
h.insert(Self::KEY_CLAN_COLOR, 4);
h.insert(Self::KEY_CLAN_ID, 5);
h.insert(Self::KEY_CLAN_TAG, 6);
h.insert(Self::KEY_CREW_PARAMS, 7);
h.insert(Self::KEY_DOG_TAG, 8);
h.insert(Self::KEY_FRAGS_COUNT, 9);
h.insert(Self::KEY_FRIENDLY_FIRE_ENABLED, 10);
h.insert(Self::KEY_ID, 11);
h.insert(Self::KEY_INVITATIONS_ENABLED, 12);
h.insert(Self::KEY_IS_ABUSER, 13);
h.insert(Self::KEY_IS_ALIVE, 14);
h.insert(Self::KEY_IS_BOT, 15);
h.insert(Self::KEY_IS_CLIENT_LOADED, 16);
h.insert(Self::KEY_IS_CONNECTED, 17);
h.insert(Self::KEY_IS_HIDDEN, 18);
h.insert(Self::KEY_IS_LEAVER, 19);
h.insert(Self::KEY_IS_PRE_BATTLE_OWNER, 20);
h.insert(Self::KEY_IS_T_SHOOTER, 21);
h.insert(Self::KEY_KEY_TARGET_MARKERS, 22);
h.insert(Self::KEY_KILLED_BUILDINGS_COUNT, 23);
h.insert(Self::KEY_MAX_HEALTH, 24);
h.insert(Self::KEY_NAME, 25);
h.insert(Self::KEY_PLAYER_MODE, 26);
h.insert(Self::KEY_PRE_BATTLE_ID_ON_START, 27);
h.insert(Self::KEY_PRE_BATTLE_SIGN, 28);
h.insert(Self::KEY_PREBATTLE_ID, 29);
h.insert(Self::KEY_REALM, 30);
h.insert(Self::KEY_SHIP_COMPONENTS, 31);
h.insert(Self::KEY_SHIP_CONFIG_DUMP, 32);
h.insert(Self::KEY_SHIP_ID, 33);
h.insert(Self::KEY_SHIP_PARAMS_ID, 34);
h.insert(Self::KEY_SKIN_ID, 35);
h.insert(Self::KEY_TEAM_ID, 36);
h.insert(Self::KEY_TTK_STATUS, 37);
h
} else if version.is_at_least(&Version::from_client_exe("0,10,9,0")) {
let mut h = HashMap::new();
h.insert(Self::KEY_AVATAR_ID, 0x2);
h.insert(Self::KEY_CLAN_TAG, 0x6);
h.insert(Self::KEY_MAX_HEALTH, 0x17);
h.insert(Self::KEY_NAME, 0x18);
h.insert(Self::KEY_SHIP_ID, 0x20);
h.insert(Self::KEY_SHIP_PARAMS_ID, 0x21);
h.insert(Self::KEY_SKIN_ID, 0x22);
h.insert(Self::KEY_TEAM_ID, 0x23);
h
} else if version.is_at_least(&Version::from_client_exe("0,10,7,0")) {
let mut h = HashMap::new();
h.insert(Self::KEY_AVATAR_ID, 0x1);
h.insert(Self::KEY_CLAN_TAG, 0x5);
h.insert(Self::KEY_MAX_HEALTH, 0x16);
h.insert(Self::KEY_NAME, 0x17);
h.insert(Self::KEY_SHIP_ID, 0x1e);
h.insert(Self::KEY_SHIP_PARAMS_ID, 0x1f);
h.insert(Self::KEY_SKIN_ID, 0x20);
h.insert(Self::KEY_TEAM_ID, 0x21);
h
} else {
let mut h = HashMap::new();
h.insert(Self::KEY_AVATAR_ID, 0x1);
h.insert(Self::KEY_CLAN_TAG, 0x5);
h.insert(Self::KEY_MAX_HEALTH, 0x15);
h.insert(Self::KEY_NAME, 0x16);
h.insert(Self::KEY_SHIP_ID, 0x1d);
h.insert(Self::KEY_SHIP_PARAMS_ID, 0x1e);
h.insert(Self::KEY_SKIN_ID, 0x1f);
h.insert(Self::KEY_TEAM_ID, 0x20);
h
}
}
fn bot_key_map(version: &Version) -> HashMap<&'static str, i64> {
if version.is_at_least(&Version::from_client_exe("0,12,8,0")) {
let mut h = HashMap::new();
h.insert(Self::KEY_ACCOUNT_DBID, 0);
h.insert(Self::KEY_ANTI_ABUSE_ENABLED, 1);
h.insert(Self::KEY_CAMOUFLAGE_INFO, 2);
h.insert(Self::KEY_CLAN_COLOR, 3);
h.insert(Self::KEY_CLAN_ID, 4);
h.insert(Self::KEY_CLAN_TAG, 5);
h.insert(Self::KEY_CREW_PARAMS, 6);
h.insert(Self::KEY_DOG_TAG, 7);
h.insert(Self::KEY_FRAGS_COUNT, 8);
h.insert(Self::KEY_FRIENDLY_FIRE_ENABLED, 9);
h.insert(Self::KEY_ID, 10);
h.insert(Self::KEY_IS_ABUSER, 11);
h.insert(Self::KEY_IS_ALIVE, 12);
h.insert(Self::KEY_IS_BOT, 13);
h.insert(Self::KEY_IS_HIDDEN, 14);
h.insert(Self::KEY_IS_T_SHOOTER, 15);
h.insert(Self::KEY_KILLED_BUILDINGS_COUNT, 16);
h.insert(Self::KEY_KEY_TARGET_MARKERS, 17);
h.insert(Self::KEY_MAX_HEALTH, 18);
h.insert(Self::KEY_NAME, 19);
h.insert(Self::KEY_REALM, 20);
h.insert(Self::KEY_SHIP_COMPONENTS, 21);
h.insert(Self::KEY_SHIP_CONFIG_DUMP, 22);
h.insert(Self::KEY_SHIP_ID, 23);
h.insert(Self::KEY_SHIP_PARAMS_ID, 24);
h.insert(Self::KEY_SKIN_ID, 25);
h.insert(Self::KEY_TEAM_ID, 26);
h.insert(Self::KEY_TTK_STATUS, 27);
h
} else {
Self::player_key_map(version)
}
}
fn from_pickle(value: &pickled::Value, version: &Version, is_bot: bool) -> Self {
let raw_values = convert_flat_dict_to_real_dict(value);
let mapped_values = Self::convert_raw_dict(&raw_values, version, is_bot);
Self::from_values(raw_values, mapped_values, version)
}
fn from_values(
raw_values: HashMap<i64, pickled::Value>,
mut mapped_values: HashMap<&'static str, pickled::Value>,
_version: &Version,
) -> Self {
let username = mapped_values
.get(Self::KEY_NAME)
.unwrap()
.string_ref()
.expect("name is not a string")
.inner()
.clone();
let clan = mapped_values
.get(Self::KEY_CLAN_TAG)
.unwrap()
.string_ref()
.expect("clanTag is not a string")
.inner()
.clone();
let clan_id = *mapped_values
.get(Self::KEY_CLAN_ID)
.unwrap()
.i64_ref()
.expect("clanID is not an i64");
let shipid = *mapped_values
.get(Self::KEY_SHIP_ID)
.unwrap()
.i64_ref()
.expect("shipId is not an i64");
let meta_ship_id = *mapped_values
.get(Self::KEY_ID)
.unwrap()
.i64_ref()
.expect("id is not an i64");
let team = *mapped_values
.get(Self::KEY_TEAM_ID)
.unwrap()
.i64_ref()
.expect("teamId is not an i64");
let health = *mapped_values
.get(Self::KEY_MAX_HEALTH)
.unwrap()
.i64_ref()
.expect("maxHealth is not an i64");
let realm = mapped_values
.get(Self::KEY_REALM)
.unwrap()
.string_ref()
.expect("realm is not a string")
.inner()
.clone();
let db_id = mapped_values
.get(Self::KEY_ACCOUNT_DBID)
.unwrap()
.i64_ref()
.cloned()
.expect("accountDBID is not an i64");
let is_abuser = mapped_values
.get(Self::KEY_IS_ABUSER)
.unwrap()
.bool_ref()
.cloned()
.expect("isAbuser is not a bool");
let is_hidden = mapped_values
.get(Self::KEY_IS_HIDDEN)
.unwrap()
.bool_ref()
.cloned()
.expect("isHidden is not a bool");
let is_bot = mapped_values
.get(Self::KEY_IS_BOT)
.and_then(|v| v.bool_ref().cloned())
.unwrap_or(false);
let clan_color = mapped_values
.get(Self::KEY_CLAN_COLOR)
.unwrap()
.i64_ref()
.cloned()
.expect("clanColor is not an integer");
let human_properties = mapped_values
.get(Self::KEY_AVATAR_ID)
.and_then(|v| v.i64_ref().copied())
.map(|avatar_id| {
let prebattle_id = mapped_values
.get(Self::KEY_PREBATTLE_ID)
.and_then(|v| v.i64_ref().copied())
.unwrap_or(0);
let is_connected = mapped_values
.get(Self::KEY_IS_CONNECTED)
.and_then(|v| v.bool_ref().copied())
.unwrap_or(false);
let is_client_loaded = mapped_values
.get(Self::KEY_IS_CLIENT_LOADED)
.and_then(|v| v.bool_ref().copied())
.unwrap_or(false);
HumanPlayerProperties {
avatar_id: AccountId::from(avatar_id),
prebattle_id,
is_connected,
is_client_loaded,
}
});
let mut raw = HashMap::new();
for (k, v) in raw_values.iter() {
raw.insert(*k, format!("{:?}", v));
}
PlayerStateData {
username,
clan,
clan_id,
clan_color,
realm,
db_id: AccountId::from(db_id),
meta_ship_id: AccountId::from(meta_ship_id),
entity_id: EntityId::from(shipid),
team_id: team,
max_health: health,
is_abuser,
is_hidden,
is_bot,
human_properties,
raw,
raw_with_names: HashMap::from_iter(
mapped_values.drain().map(|(k, v)| (k, pickle_to_json(v))),
),
}
}
pub fn update_from_dict(&mut self, values: &HashMap<&'static str, pickled::Value>) {
if let Some(v) = values.get(Self::KEY_AVATAR_ID)
&& let Some(id) = v.i64_ref()
&& let Some(ref mut hp) = self.human_properties
{
hp.avatar_id = AccountId::from(*id);
}
if let Some(v) = values.get(Self::KEY_NAME)
&& let Some(s) = v.string_ref()
{
self.username = s.inner().clone();
}
if let Some(v) = values.get(Self::KEY_CLAN_TAG)
&& let Some(s) = v.string_ref()
{
self.clan = s.inner().clone();
}
if let Some(v) = values.get(Self::KEY_CLAN_ID)
&& let Some(id) = v.i64_ref()
{
self.clan_id = *id;
}
if let Some(v) = values.get(Self::KEY_CLAN_COLOR)
&& let Some(id) = v.i64_ref()
{
self.clan_color = *id;
}
if let Some(v) = values.get(Self::KEY_SHIP_ID)
&& let Some(id) = v.i64_ref()
{
self.entity_id = EntityId::from(*id);
}
if let Some(v) = values.get(Self::KEY_ID)
&& let Some(id) = v.i64_ref()
{
self.meta_ship_id = AccountId::from(*id);
}
if let Some(v) = values.get(Self::KEY_TEAM_ID)
&& let Some(id) = v.i64_ref()
{
self.team_id = *id;
}
if let Some(v) = values.get(Self::KEY_MAX_HEALTH)
&& let Some(id) = v.i64_ref()
{
self.max_health = *id;
}
if let Some(v) = values.get(Self::KEY_REALM)
&& let Some(s) = v.string_ref()
{
self.realm = s.inner().clone();
}
if let Some(v) = values.get(Self::KEY_ACCOUNT_DBID)
&& let Some(id) = v.i64_ref()
{
self.db_id = AccountId::from(*id);
}
if let Some(v) = values.get(Self::KEY_PREBATTLE_ID)
&& let Some(id) = v.i64_ref()
&& let Some(ref mut hp) = self.human_properties
{
hp.prebattle_id = *id;
}
if let Some(v) = values.get(Self::KEY_IS_ABUSER)
&& let Some(b) = v.bool_ref()
{
self.is_abuser = *b;
}
if let Some(v) = values.get(Self::KEY_IS_HIDDEN)
&& let Some(b) = v.bool_ref()
{
self.is_hidden = *b;
}
if let Some(v) = values.get(Self::KEY_IS_CONNECTED)
&& let Some(b) = v.bool_ref()
&& let Some(ref mut hp) = self.human_properties
{
hp.is_connected = *b;
}
if let Some(v) = values.get(Self::KEY_IS_CLIENT_LOADED)
&& let Some(b) = v.bool_ref()
&& let Some(ref mut hp) = self.human_properties
{
hp.is_client_loaded = *b;
}
if let Some(v) = values.get(Self::KEY_IS_BOT)
&& let Some(b) = v.bool_ref()
{
self.is_bot = *b;
}
for (k, v) in values.iter() {
self.raw_with_names.insert(k, pickle_to_json(v.clone()));
}
}
pub fn username(&self) -> &str {
&self.username
}
pub fn clan(&self) -> &str {
&self.clan
}
pub fn clan_id(&self) -> i64 {
self.clan_id
}
pub fn clan_color(&self) -> i64 {
self.clan_color
}
pub fn db_id(&self) -> AccountId {
self.db_id
}
pub fn realm(&self) -> &str {
&self.realm
}
pub fn avatar_id(&self) -> Option<AccountId> {
self.human_properties.as_ref().map(|hp| hp.avatar_id)
}
pub fn meta_ship_id(&self) -> AccountId {
self.meta_ship_id
}
pub fn entity_id(&self) -> EntityId {
self.entity_id
}
pub fn team_id(&self) -> i64 {
self.team_id
}
pub fn division_id(&self) -> i64 {
self.human_properties
.as_ref()
.map(|hp| hp.prebattle_id)
.unwrap_or(0)
}
pub fn max_health(&self) -> i64 {
self.max_health
}
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.human_properties
.as_ref()
.map(|hp| hp.is_client_loaded)
.unwrap_or_else(|| self.is_bot())
}
pub fn is_connected(&self) -> bool {
self.human_properties
.as_ref()
.map(|hp| hp.is_connected)
.unwrap_or_else(|| self.is_bot())
}
pub fn human_properties(&self) -> Option<&HumanPlayerProperties> {
self.human_properties.as_ref()
}
pub fn is_bot(&self) -> bool {
self.is_bot
}
pub fn raw(&self) -> &HashMap<i64, String> {
&self.raw
}
pub fn raw_with_names(&self) -> &HashMap<&'static str, serde_json::Value> {
&self.raw_with_names
}
}
fn convert_flat_dict_to_real_dict(value: &Value) -> HashMap<i64, Value> {
let mut raw_values = HashMap::new();
if let pickled::value::Value::List(elements) = value {
for elem in elements.inner().iter() {
if let pickled::value::Value::Tuple(kv) = elem {
let key = kv.inner()[0]
.i64_ref()
.expect("tuple first value was not an integer");
raw_values.insert(*key, kv.inner()[1].clone());
}
}
}
raw_values
}
#[derive(Debug, Clone, Serialize)]
pub struct DamageReceived {
pub aggressor: EntityId,
pub damage: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct MinimapUpdate {
pub entity_id: EntityId,
pub disappearing: bool,
pub heading: f32,
pub position: NormalizedPos,
pub unknown: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArtilleryShotData {
pub origin: (f32, f32, f32),
pub target: (f32, f32, f32),
pub shot_id: u32,
pub speed: f32,
}
#[derive(Debug, Clone, Serialize)]
pub struct ArtillerySalvo {
pub owner_id: EntityId,
pub params_id: GameParamId,
pub salvo_id: u32,
pub shots: Vec<ArtilleryShotData>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TorpedoData {
pub owner_id: EntityId,
pub params_id: GameParamId,
pub salvo_id: u32,
pub shot_id: u32,
pub origin: (f32, f32, f32),
pub direction: (f32, f32, f32),
}
#[derive(Debug, Clone, Serialize)]
pub struct ShotHit {
pub owner_id: EntityId,
pub shot_id: u32,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum CruiseState {
Throttle,
Rudder,
DiveDepth,
Unknown(u32),
}
#[derive(Debug, Serialize)]
pub struct ChatMessageExtra {
pre_battle_sign: i64,
pre_battle_id: i64,
player_clan_tag: String,
typ: i64,
player_avatar_id: AccountId,
player_name: String,
}
#[derive(Debug, Serialize, Kinded)]
#[kinded(derive(Serialize))]
pub enum DecodedPacketPayload<'replay, 'argtype, 'rawpacket> {
Chat {
entity_id: EntityId,
sender_id: AccountId,
audience: &'replay str,
message: &'replay str,
extra_data: Option<ChatMessageExtra>,
},
VoiceLine {
sender_id: AccountId,
is_global: bool,
message: VoiceLine,
},
Ribbon(Ribbon),
Position(crate::packet2::PositionPacket),
PlayerOrientation(crate::packet2::PlayerOrientationPacket),
DamageStat(Vec<((i64, i64), (i64, f64))>),
ShipDestroyed {
killer: EntityId,
victim: EntityId,
cause: DeathCause,
},
EntityMethod(&'rawpacket EntityMethodPacket<'argtype>),
EntityProperty(&'rawpacket crate::packet2::EntityPropertyPacket<'argtype>),
BasePlayerCreate(&'rawpacket crate::packet2::BasePlayerCreatePacket<'argtype>),
CellPlayerCreate(&'rawpacket crate::packet2::CellPlayerCreatePacket<'argtype>),
EntityEnter(&'rawpacket crate::packet2::EntityEnterPacket),
EntityLeave(&'rawpacket crate::packet2::EntityLeavePacket),
EntityCreate(&'rawpacket crate::packet2::EntityCreatePacket<'argtype>),
OnArenaStateReceived {
arena_id: i64,
team_build_type_id: i8,
pre_battles_info: HashMap<i64, Vec<Option<HashMap<String, String>>>>,
player_states: Vec<PlayerStateData>,
bot_states: Vec<PlayerStateData>,
},
OnGameRoomStateChanged {
player_states: Vec<HashMap<&'static str, pickled::Value>>,
},
CheckPing(u64),
DamageReceived {
victim: EntityId,
aggressors: Vec<DamageReceived>,
},
MinimapUpdate {
updates: Vec<MinimapUpdate>,
arg1: &'rawpacket Vec<ArgValue<'argtype>>,
},
PropertyUpdate(&'rawpacket crate::packet2::PropertyUpdatePacket<'argtype>),
BattleEnd {
winning_team: Option<i8>,
finish_type: Option<FinishType>,
},
Consumable {
entity: EntityId,
consumable: Consumable,
duration: f32,
},
CruiseState {
state: CruiseState,
value: i32,
},
Map(&'rawpacket crate::packet2::MapPacket<'replay>),
Version(String),
Camera(&'rawpacket crate::packet2::CameraPacket),
CameraMode(CameraMode),
CameraFreeLook(bool),
ArtilleryShots {
entity_id: EntityId,
salvos: Vec<ArtillerySalvo>,
},
TorpedoesReceived {
entity_id: EntityId,
torpedoes: Vec<TorpedoData>,
},
ShotKills {
entity_id: EntityId,
hits: Vec<ShotHit>,
},
GunSync {
entity_id: EntityId,
group: u32,
turret: u32,
yaw: f32,
pitch: f32,
},
PlaneAdded {
entity_id: EntityId,
plane_id: PlaneId,
team_id: u32,
params_id: GameParamId,
x: f32,
y: f32,
},
PlaneRemoved {
entity_id: EntityId,
plane_id: PlaneId,
},
PlanePosition {
entity_id: EntityId,
plane_id: PlaneId,
x: f32,
y: f32,
},
SetAmmoForWeapon {
entity_id: EntityId,
weapon_type: u32,
ammo_param_id: GameParamId,
is_reload: bool,
},
EntityControl(&'rawpacket crate::packet2::EntityControlPacket),
SmokeScreenDrift(&'rawpacket crate::packet2::SmokeScreenDriftPacket),
ViewDirection(&'rawpacket crate::packet2::ViewDirectionPacket),
ServerTimestamp(f64),
OwnShip(&'rawpacket crate::packet2::OwnShipPacket),
VehicleRef(&'rawpacket crate::packet2::VehicleRefPacket),
ServerTick(f64),
Unknown(&'replay [u8]),
Invalid(&'rawpacket crate::packet2::InvalidPacket<'replay>),
Audit(String),
BattleResults(&'replay str),
}
fn try_convert_hashable_pickle_to_string(
value: pickled::value::HashableValue,
) -> pickled::value::HashableValue {
match value {
pickled::value::HashableValue::Bytes(b) => {
if let Ok(s) = std::str::from_utf8(b.inner()) {
pickled::value::HashableValue::String(s.to_owned().into())
} else {
pickled::value::HashableValue::Bytes(b)
}
}
pickled::value::HashableValue::Tuple(t) => pickled::value::HashableValue::Tuple(
t.inner()
.iter()
.cloned()
.map(try_convert_hashable_pickle_to_string)
.collect::<Vec<_>>()
.into(),
),
pickled::value::HashableValue::FrozenSet(s) => pickled::value::HashableValue::FrozenSet(
s.inner()
.iter()
.cloned()
.map(try_convert_hashable_pickle_to_string)
.collect::<BTreeSet<_>>()
.into(),
),
value => value,
}
}
fn try_convert_pickle_to_string(value: pickled::value::Value) -> pickled::value::Value {
match value {
pickled::value::Value::Bytes(b) => {
if let Ok(s) = std::str::from_utf8(b.inner()) {
pickled::value::Value::String(s.to_owned().into())
} else {
pickled::value::Value::Bytes(b)
}
}
pickled::value::Value::List(l) => pickled::value::Value::List(
l.inner()
.iter()
.cloned()
.map(try_convert_pickle_to_string)
.collect::<Vec<_>>()
.into(),
),
pickled::value::Value::Tuple(t) => pickled::value::Value::Tuple(
t.inner()
.iter()
.cloned()
.map(try_convert_pickle_to_string)
.collect::<Vec<_>>()
.into(),
),
pickled::value::Value::Set(s) => pickled::value::Value::Set(
s.inner()
.iter()
.cloned()
.map(try_convert_hashable_pickle_to_string)
.collect::<BTreeSet<_>>()
.into(),
),
pickled::value::Value::FrozenSet(s) => pickled::value::Value::FrozenSet(
s.inner()
.iter()
.cloned()
.map(try_convert_hashable_pickle_to_string)
.collect::<BTreeSet<_>>()
.into(),
),
pickled::value::Value::Dict(d) => pickled::value::Value::Dict(
d.inner()
.iter()
.map(|(k, v)| {
(
try_convert_hashable_pickle_to_string(k.clone()),
try_convert_pickle_to_string(v.clone()),
)
})
.collect::<std::collections::BTreeMap<_, _>>()
.into(),
),
value => value,
}
}
fn parse_receive_common_cmd_blob(blob: &[u8]) -> IResult<&[u8], (VoiceLine, bool)> {
let i = blob;
let (i, line) = le_u16(i)?;
let (i, audience) = le_u8(i)?;
let is_global = match audience {
0 => false,
1 => true,
_ => {
panic!("Got unknown audience {}", audience);
}
};
let (i, message) = match line {
1 => {
let (i, x) = le_u16(i)?;
let (i, y) = le_u16(i)?;
(i, VoiceLine::AttentionToSquare(x as u32, y as u32))
}
2 => {
let (i, target_type) = le_u16(i)?;
let (i, target_id) = le_u64(i)?;
(i, VoiceLine::QuickTactic(target_type, target_id))
}
3 => (i, VoiceLine::RequestingSupport(None)),
5 => (i, VoiceLine::Wilco),
6 => (i, VoiceLine::Negative),
7 => (i, VoiceLine::WellDone), 8 => (i, VoiceLine::FairWinds),
9 => (i, VoiceLine::Curses),
10 => (i, VoiceLine::DefendTheBase),
11 => (i, VoiceLine::ProvideAntiAircraft),
12 => {
let (i, _target_type) = le_u16(i)?;
let (i, target_id) = le_u64(i)?;
(
i,
VoiceLine::Retreat(if target_id != 0 {
Some(target_id as i32)
} else {
None
}),
)
}
13 => (i, VoiceLine::IntelRequired),
14 => (i, VoiceLine::SetSmokeScreen),
15 => (i, VoiceLine::UsingRadar),
16 => (i, VoiceLine::UsingHydroSearch),
17 => (i, VoiceLine::FollowMe),
18 => {
let (i, x) = le_f32(i)?;
let (i, y) = le_f32(i)?;
(i, VoiceLine::MapPointAttention(x, y))
}
19 => (i, VoiceLine::UsingSubmarineLocator),
line => {
eprintln!("Warning: Unknown voice line {}, {:#X?}", line, i);
(i, VoiceLine::Unknown(line as i64))
}
};
Ok((i, (message, is_global)))
}
impl<'replay, 'argtype, 'rawpacket> DecodedPacketPayload<'replay, 'argtype, 'rawpacket>
where
'rawpacket: 'replay,
'rawpacket: 'argtype,
{
fn from(
version: &Version,
audit: bool,
payload: &'rawpacket crate::packet2::PacketType<'replay, 'argtype>,
packet_type: u32,
battle_constants: Option<&wowsunpack::game_constants::BattleConstants>,
) -> Self {
match payload {
PacketType::EntityMethod(em) => {
DecodedPacketPayload::from_entity_method(version, audit, em, battle_constants)
}
PacketType::Camera(camera) => DecodedPacketPayload::Camera(camera),
PacketType::CameraMode(mode) => {
if let Some(cm) = battle_constants
.and_then(|bc| bc.camera_mode(*mode as i32))
.map(CameraMode::from_name)
{
DecodedPacketPayload::CameraMode(cm)
} else {
match mode {
1 => DecodedPacketPayload::CameraMode(CameraMode::Airplanes),
2 => DecodedPacketPayload::CameraMode(CameraMode::Dock),
3 => DecodedPacketPayload::CameraMode(CameraMode::OverheadMap),
4 => DecodedPacketPayload::CameraMode(CameraMode::DevFree),
5 => DecodedPacketPayload::CameraMode(CameraMode::FollowingShells),
6 => DecodedPacketPayload::CameraMode(CameraMode::FollowingPlanes),
7 => DecodedPacketPayload::CameraMode(CameraMode::DockModule),
8 => DecodedPacketPayload::CameraMode(CameraMode::FollowingShip),
9 => DecodedPacketPayload::CameraMode(CameraMode::FreeFlying),
10 => DecodedPacketPayload::CameraMode(CameraMode::ReplayFpc),
11 => DecodedPacketPayload::CameraMode(CameraMode::FollowingSubmarine),
12 => DecodedPacketPayload::CameraMode(CameraMode::TacticalConsumables),
13 => DecodedPacketPayload::CameraMode(CameraMode::RespawnMap),
19 => DecodedPacketPayload::CameraMode(CameraMode::DockFlags),
20 => DecodedPacketPayload::CameraMode(CameraMode::DockEnsign),
21 => DecodedPacketPayload::CameraMode(CameraMode::DockLootbox),
22 => DecodedPacketPayload::CameraMode(CameraMode::DockNavalFlag),
23 => DecodedPacketPayload::CameraMode(CameraMode::IdleGame),
_ => {
if audit {
DecodedPacketPayload::Audit(format!("CameraMode({})", mode))
} else {
DecodedPacketPayload::CameraMode(CameraMode::Unknown(*mode))
}
}
}
}
}
PacketType::CameraFreeLook(freelook) => match freelook {
0 => DecodedPacketPayload::CameraFreeLook(false),
1 => DecodedPacketPayload::CameraFreeLook(true),
_ => {
if audit {
DecodedPacketPayload::Audit(format!("CameraFreeLook({})", freelook))
} else {
DecodedPacketPayload::CameraFreeLook(true)
}
}
},
PacketType::CruiseState(cs) => match cs.key {
0 => DecodedPacketPayload::CruiseState {
state: CruiseState::Throttle,
value: cs.value,
},
1 => DecodedPacketPayload::CruiseState {
state: CruiseState::Rudder,
value: cs.value,
},
2 => DecodedPacketPayload::CruiseState {
state: CruiseState::DiveDepth,
value: cs.value,
},
_ => {
if audit {
DecodedPacketPayload::Audit(format!(
"CruiseState(unknown={}, {})",
cs.key, cs.value
))
} else {
DecodedPacketPayload::CruiseState {
state: CruiseState::Unknown(cs.key),
value: cs.value,
}
}
}
},
PacketType::Map(map) => {
if audit && map.unknown != 0 && map.unknown != 1 {
DecodedPacketPayload::Audit(format!(
"Map: Unknown bool is not a bool (is {})",
map.unknown
))
} else if audit
&& map.matrix
!= [
0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
128, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 63,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 63,
]
{
DecodedPacketPayload::Audit(format!(
"Map: Unit matrix is not a unit matrix (is {:?})",
map.matrix
))
} else {
DecodedPacketPayload::Map(map)
}
}
PacketType::EntityProperty(p) => DecodedPacketPayload::EntityProperty(p),
PacketType::Position(pos) => DecodedPacketPayload::Position((*pos).clone()),
PacketType::PlayerOrientation(pos) => {
DecodedPacketPayload::PlayerOrientation((*pos).clone())
}
PacketType::BasePlayerCreate(b) => DecodedPacketPayload::BasePlayerCreate(b),
PacketType::CellPlayerCreate(c) => DecodedPacketPayload::CellPlayerCreate(c),
PacketType::EntityEnter(e) => DecodedPacketPayload::EntityEnter(e),
PacketType::EntityLeave(e) => DecodedPacketPayload::EntityLeave(e),
PacketType::EntityCreate(e) => DecodedPacketPayload::EntityCreate(e),
PacketType::PropertyUpdate(update) => DecodedPacketPayload::PropertyUpdate(update),
PacketType::Version(version) => DecodedPacketPayload::Version(version.clone()),
PacketType::Unknown(u) => {
if packet_type == 0x18 {
if audit
&& u != &[
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00,
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00,
00, 00, 00, 00, 00, 00, 0x80, 0xbf, 00, 00, 0x80, 0xbf, 00, 00, 0x80,
0xbf,
]
{
DecodedPacketPayload::Audit("Camera18 unexpected value!".to_string())
} else {
DecodedPacketPayload::Unknown(u)
}
} else {
DecodedPacketPayload::Unknown(u)
}
}
PacketType::Invalid(u) => DecodedPacketPayload::Invalid(u),
PacketType::BattleResults(results) => DecodedPacketPayload::BattleResults(results),
PacketType::EntityControl(ec) => DecodedPacketPayload::EntityControl(ec),
PacketType::SmokeScreenDrift(sd) => DecodedPacketPayload::SmokeScreenDrift(sd),
PacketType::ViewDirection(vd) => DecodedPacketPayload::ViewDirection(vd),
PacketType::ServerTimestamp(st) => DecodedPacketPayload::ServerTimestamp(st.timestamp),
PacketType::OwnShip(os) => DecodedPacketPayload::OwnShip(os),
PacketType::VehicleRef(vr) => DecodedPacketPayload::VehicleRef(vr),
PacketType::ServerTick(tick) => DecodedPacketPayload::ServerTick(*tick),
}
}
fn extract_vec3(val: Option<&ArgValue>) -> (f32, f32, f32) {
match val {
Some(ArgValue::Vector3((x, y, z))) => (*x, *y, *z),
Some(ArgValue::Array(a)) if a.len() >= 3 => {
let x: f32 = (&a[0]).try_into().unwrap_or(0.0);
let y: f32 = (&a[1]).try_into().unwrap_or(0.0);
let z: f32 = (&a[2]).try_into().unwrap_or(0.0);
(x, y, z)
}
_ => (0.0, 0.0, 0.0),
}
}
fn from_entity_method(
version: &Version,
audit: bool,
packet: &'rawpacket EntityMethodPacket<'argtype>,
battle_constants: Option<&wowsunpack::game_constants::BattleConstants>,
) -> Self {
let entity_id = &packet.entity_id;
let method = &packet.method;
let args = &packet.args;
if *method == "onChatMessage" {
let target = match &args[1] {
ArgValue::String(s) => s,
_ => panic!("foo"),
};
let message = match &args[2] {
ArgValue::String(s) => s,
_ => panic!("foo"),
};
let sender_id = match &args[0] {
ArgValue::Int32(i) => i,
_ => panic!("foo"),
};
let mut extra_data = None;
if *sender_id == 0 && args.len() >= 4 {
let extra = pickled::de::value_from_slice(
args[3].string_ref().expect("failed"),
pickled::de::DeOptions::new(),
)
.expect("value is not pickled");
let mut extra_dict: HashMap<String, Value> = HashMap::from_iter(
extra
.dict()
.expect("value is not a dictionary")
.inner()
.iter()
.map(|(key, value)| {
let key = match key {
pickled::HashableValue::Bytes(bytes) => {
String::from_utf8(bytes.inner().clone())
.expect("key is not a valid utf-8 sequence")
}
pickled::HashableValue::String(string) => string.inner().clone(),
other => {
panic!("unexpected key type {:?}", other)
}
};
let value = match value {
Value::Bytes(bytes) => {
if let Ok(result) = String::from_utf8(bytes.inner().clone()) {
Value::String(result.into())
} else {
Value::Bytes(bytes.clone())
}
}
other => other.clone(),
};
(key, value)
}),
);
let extra = ChatMessageExtra {
pre_battle_sign: extra_dict
.remove("preBattleSign")
.unwrap()
.i64()
.expect("preBattleSign is not an i64"),
pre_battle_id: extra_dict
.remove("prebattleId")
.unwrap()
.i64()
.expect("preBattleId is not an i64"),
player_clan_tag: extra_dict
.remove("playerClanTag")
.unwrap()
.string()
.expect("playerClanTag is not a string")
.inner()
.clone(),
typ: extra_dict
.remove("type")
.unwrap()
.i64()
.expect("type is not an i64"),
player_avatar_id: AccountId::from(
extra_dict
.remove("playerAvatarId")
.unwrap()
.i64()
.expect("playerAvatarId is not an i64"),
),
player_name: extra_dict
.remove("playerName")
.unwrap()
.string()
.expect("playerName is not a string")
.inner()
.clone(),
};
assert!(extra_dict.is_empty());
extra_data = Some(extra);
}
DecodedPacketPayload::Chat {
entity_id: *entity_id,
sender_id: AccountId::from(*sender_id),
audience: std::str::from_utf8(target).unwrap(),
message: std::str::from_utf8(message).unwrap(),
extra_data,
}
} else if *method == "receive_CommonCMD" {
let (sender_id, message, is_global) = if version
.is_at_least(&Version::from_client_exe("0,12,8,0"))
{
let sender = *args[0]
.int_32_ref()
.expect("receive_CommonCMD: sender is not an i32");
let blob = args[1]
.blob_ref()
.expect("receive_CommonCMD: second argument is not a blob");
let (_reminader, (message_type, is_global)) =
match parse_receive_common_cmd_blob(blob.as_ref()) {
Ok(result) => result,
Err(e) => {
eprintln!("Warning: receive_CommonCMD: failed to parse blob: {:?}", e);
(&[][..], (VoiceLine::Unknown(0), false))
}
};
(sender, message_type, is_global)
} else {
let (audience, sender_id, line, a, b) =
unpack_rpc_args!(args, u8, i32, u8, u32, u64);
let is_global = match audience {
0 => false,
1 => true,
_ => {
panic!(
"Got unknown audience {} sender=0x{:x} line={} a={:x} b={:x}",
audience, sender_id, line, a, b
);
}
};
let message = match line {
1 => VoiceLine::AttentionToSquare(a, b as u32),
2 => VoiceLine::QuickTactic(a as u16, b),
3 => VoiceLine::RequestingSupport(None),
5 => VoiceLine::Wilco,
6 => VoiceLine::Negative,
7 => VoiceLine::WellDone, 8 => VoiceLine::FairWinds,
9 => VoiceLine::Curses,
10 => VoiceLine::DefendTheBase,
11 => VoiceLine::ProvideAntiAircraft,
12 => VoiceLine::Retreat(if b != 0 { Some(b as i32) } else { None }),
13 => VoiceLine::IntelRequired,
14 => VoiceLine::SetSmokeScreen,
15 => VoiceLine::UsingRadar,
16 => VoiceLine::UsingHydroSearch,
17 => VoiceLine::FollowMe,
18 => VoiceLine::MapPointAttention(a as f32, b as f32),
19 => VoiceLine::UsingSubmarineLocator,
_ => {
eprintln!("Warning: Unknown voice line {} a={:x} b={:x}!", line, a, b);
VoiceLine::Unknown(line as i64)
}
};
(sender_id, message, is_global)
};
DecodedPacketPayload::VoiceLine {
sender_id: AccountId::from(sender_id),
is_global,
message,
}
} else if *method == "onGameRoomStateChanged" {
let player_states = pickled::de::value_from_slice(
args[0].blob_ref().expect("player_states arg is not a blob"),
pickled::de::DeOptions::new(),
)
.expect("failed to deserialize player_states");
let player_states = try_convert_pickle_to_string(player_states);
let mut players_out = vec![];
if let pickled::value::Value::List(players) = &player_states {
for player in players.inner().iter() {
let raw_values = convert_flat_dict_to_real_dict(player);
let mapped_values =
PlayerStateData::convert_raw_dict(&raw_values, version, false);
players_out.push(mapped_values);
}
}
DecodedPacketPayload::OnGameRoomStateChanged {
player_states: players_out,
}
} else if *method == "onArenaStateReceived" {
let (arg0, arg1) = unpack_rpc_args!(args, i64, i8);
let value = pickled::de::value_from_slice(
match &args[2] {
ArgValue::Blob(x) => x,
_ => panic!("foo"),
},
pickled::de::DeOptions::new(),
)
.unwrap();
let value = match value {
pickled::value::Value::Dict(d) => d,
_ => panic!(),
};
let mut arg2 = HashMap::new();
for (k, v) in value.inner().iter() {
let k = match k {
pickled::value::HashableValue::I64(i) => *i,
_ => panic!(),
};
let v = match v {
pickled::value::Value::List(l) => l,
_ => panic!(),
};
let v: Vec<_> = v
.inner()
.iter()
.map(|elem| match elem {
pickled::value::Value::Dict(d) => Some(
d.inner()
.iter()
.map(|(k, v)| {
let k = match k {
pickled::value::HashableValue::Bytes(b) => {
std::str::from_utf8(b.inner()).unwrap().to_string()
}
_ => panic!(),
};
let v = format!("{:?}", v);
(k, v)
})
.collect(),
),
pickled::value::Value::None => None,
_ => panic!(),
})
.collect();
arg2.insert(k, v);
}
let value = pickled::de::value_from_slice(
match &args[3] {
ArgValue::Blob(x) => x,
_ => panic!("foo"),
},
pickled::de::DeOptions::new(),
)
.unwrap();
let value = try_convert_pickle_to_string(value);
let mut players_out = vec![];
if let pickled::value::Value::List(players) = &value {
for player in players.inner().iter() {
players_out.push(PlayerStateData::from_pickle(player, version, false));
}
}
let mut bots_out = vec![];
if let Some(ArgValue::Blob(blob)) = args.get(4)
&& let Ok(value) =
pickled::de::value_from_slice(blob, pickled::de::DeOptions::new())
{
let value = try_convert_pickle_to_string(value);
if let pickled::value::Value::List(bots) = &value {
for bot in bots.inner().iter() {
bots_out.push(PlayerStateData::from_pickle(bot, version, true));
}
}
}
DecodedPacketPayload::OnArenaStateReceived {
arena_id: arg0,
team_build_type_id: arg1,
pre_battles_info: arg2,
player_states: players_out,
bot_states: bots_out,
}
} else if *method == "receiveDamageStat" {
let value = pickled::de::value_from_slice(
match &args[0] {
ArgValue::Blob(x) => x,
_ => panic!("foo"),
},
pickled::de::DeOptions::new(),
)
.unwrap();
let mut stats = vec![];
match value {
pickled::value::Value::Dict(d) => {
for (k, v) in d.inner().iter() {
let k = match k {
pickled::value::HashableValue::Tuple(t) => {
let t = t.inner();
assert!(t.len() == 2);
(
match &t[0] {
pickled::value::HashableValue::I64(i) => *i,
_ => panic!("foo"),
},
match &t[1] {
pickled::value::HashableValue::I64(i) => *i,
_ => panic!("foo"),
},
)
}
_ => panic!("foo"),
};
let v = match v {
pickled::value::Value::List(t) => {
let t = t.inner();
assert!(t.len() == 2);
(
match &t[0] {
pickled::value::Value::I64(i) => *i,
_ => panic!("foo"),
},
match &t[1] {
pickled::value::Value::F64(i) => *i,
pickled::value::Value::I64(i) => *i as f64,
_ => panic!("foo"),
},
)
}
_ => panic!("foo"),
};
stats.push((k, v));
}
}
_ => panic!("foo"),
}
DecodedPacketPayload::DamageStat(stats)
} else if *method == "receiveVehicleDeath" {
let (victim, killer, cause) = unpack_rpc_args!(args, i32, i32, u32);
let cause = if let Some(dc) = battle_constants
.and_then(|bc| bc.death_reason(cause as i32))
.map(DeathCause::from_name)
{
dc
} else {
match cause {
0 => DeathCause::None,
1 => DeathCause::Artillery,
2 => DeathCause::Secondaries,
3 => DeathCause::Torpedo,
4 => DeathCause::DiveBomber,
5 => DeathCause::AerialTorpedo,
6 => DeathCause::Fire,
7 => DeathCause::Ramming,
8 => DeathCause::Terrain,
9 => DeathCause::Flooding,
10 => DeathCause::Mirror,
11 => DeathCause::SeaMine,
12 => DeathCause::Special,
13 => DeathCause::DepthCharge,
14 => DeathCause::AerialRocket,
15 => DeathCause::Detonation,
16 => DeathCause::Health,
17 => DeathCause::ApShell,
18 => DeathCause::HeShell,
19 => DeathCause::CsShell,
20 => DeathCause::Fel,
21 => DeathCause::Portal,
22 => DeathCause::SkipBombs,
23 => DeathCause::SectorWave,
24 => DeathCause::Acid,
25 => DeathCause::Laser,
26 => DeathCause::Match,
27 => DeathCause::Timer,
28 => DeathCause::AerialDepthCharge,
29 => DeathCause::Event1,
30 => DeathCause::Event2,
31 => DeathCause::Event3,
32 => DeathCause::Event4,
33 => DeathCause::Event5,
34 => DeathCause::Event6,
35 => DeathCause::Missile,
cause => {
if audit {
return DecodedPacketPayload::Audit(format!(
"receiveVehicleDeath(victim={}, killer={}, unknown cause {})",
victim, killer, cause
));
} else {
DeathCause::Unknown(cause)
}
}
}
};
DecodedPacketPayload::ShipDestroyed {
victim: EntityId::from(victim),
killer: EntityId::from(killer),
cause,
}
} else if *method == "onRibbon" {
let (ribbon,) = unpack_rpc_args!(args, i8);
let ribbon = match ribbon {
1 => Ribbon::TorpedoHit,
3 => Ribbon::PlaneShotDown,
4 => Ribbon::Incapacitation,
5 => Ribbon::Destroyed,
6 => Ribbon::SetFire,
7 => Ribbon::Flooding,
8 => Ribbon::Citadel,
9 => Ribbon::Defended,
10 => Ribbon::Captured,
11 => Ribbon::AssistedInCapture,
13 => Ribbon::SecondaryHit,
14 => Ribbon::OverPenetration,
15 => Ribbon::Penetration,
16 => Ribbon::NonPenetration,
17 => Ribbon::Ricochet,
19 => Ribbon::Spotted,
21 => Ribbon::DiveBombPenetration,
25 => Ribbon::RocketPenetration,
26 => Ribbon::RocketNonPenetration,
27 => Ribbon::ShotDownByAircraft,
28 => Ribbon::TorpedoProtectionHit,
30 => Ribbon::RocketTorpedoProtectionHit,
31 => Ribbon::DepthChargeHit,
33 => Ribbon::BuffSeized,
39 => Ribbon::SonarOneHit,
40 => Ribbon::SonarTwoHits,
41 => Ribbon::SonarNeutralized,
ribbon => {
if audit {
return DecodedPacketPayload::Audit(format!(
"onRibbon(unknown ribbon {})",
ribbon
));
} else {
Ribbon::Unknown(ribbon)
}
}
};
DecodedPacketPayload::Ribbon(ribbon)
} else if *method == "receiveDamagesOnShip" {
let mut v = vec![];
for elem in match &args[0] {
ArgValue::Array(a) => a,
_ => panic!(),
} {
let map = match elem {
ArgValue::FixedDict(m) => m,
_ => panic!(),
};
let aggressor_raw: i32 = map.get("vehicleID").unwrap().try_into().unwrap();
v.push(DamageReceived {
aggressor: EntityId::from(aggressor_raw),
damage: map.get("damage").unwrap().try_into().unwrap(),
});
}
DecodedPacketPayload::DamageReceived {
victim: *entity_id,
aggressors: v,
}
} else if *method == "onCheckGamePing" {
let (ping,) = unpack_rpc_args!(args, u64);
DecodedPacketPayload::CheckPing(ping)
} else if *method == "updateMinimapVisionInfo" {
let v = match &args[0] {
ArgValue::Array(a) => a,
_ => panic!(),
};
let mut updates = vec![];
for minimap_update in v.iter() {
let minimap_update = match minimap_update {
ArgValue::FixedDict(m) => m,
_ => panic!(),
};
let vehicle_id = minimap_update.get("vehicleID").unwrap();
let packed_data: u32 = minimap_update
.get("packedData")
.unwrap()
.try_into()
.unwrap();
let update = RawMinimapUpdate::from_bytes(packed_data.to_le_bytes());
let heading = update.heading() as f32 / 256. * 360. - 180.;
let x = update.x() as f32 / 512. - 1.5;
let y = update.y() as f32 / 512. - 1.5;
updates.push(MinimapUpdate {
entity_id: match vehicle_id {
ArgValue::Uint32(u) => EntityId::from(*u),
_ => panic!(),
},
position: NormalizedPos { x, y },
heading,
disappearing: update.is_disappearing(),
unknown: update.unknown(),
})
}
let args1 = match &args[1] {
ArgValue::Array(a) => a,
_ => panic!(),
};
DecodedPacketPayload::MinimapUpdate {
updates,
arg1: args1,
}
} else if *method == "onBattleEnd" {
let (winning_team, finish_type) = if args.len() >= 2 {
let (winning_team, raw_finish) = unpack_rpc_args!(args, i8, u8);
let ft = if let Some(name) =
battle_constants.and_then(|bc| bc.finish_type(raw_finish as i32))
{
FinishType::from_name(name)
} else {
FinishType::from_id(raw_finish)
};
(Some(winning_team), Some(ft))
} else {
(None, None)
};
DecodedPacketPayload::BattleEnd {
winning_team,
finish_type,
}
} else if *method == "consumableUsed" || *method == "onConsumableUsed" {
let consumable: i8 = match &args[0] {
ArgValue::Int8(v) => *v,
ArgValue::Uint8(v) => *v as i8,
ArgValue::Int16(v) => *v as i8,
ArgValue::Uint16(v) => *v as i8,
ArgValue::Int32(v) => *v as i8,
other => panic!(
"onConsumableUsed: unexpected consumable arg type: {:?}",
other
),
};
let duration: f32 = match &args[1] {
ArgValue::Float32(v) => *v,
ArgValue::Float64(v) => *v as f32,
other => panic!(
"onConsumableUsed: unexpected duration arg type: {:?}",
other
),
};
let raw_consumable = consumable;
let consumable = match consumable {
0 => Consumable::DamageControl, 1 => Consumable::SpottingAircraft, 2 => Consumable::DefensiveAntiAircraft, 3 => Consumable::SpeedBoost, 4 => Consumable::MainBatteryReloadBooster, 6 => Consumable::Smoke, 8 => Consumable::RepairParty, 9 => Consumable::CatapultFighter, 10 => Consumable::HydroacousticSearch, 11 => Consumable::TorpedoReloadBooster, 12 => Consumable::Radar, 19 => Consumable::Invulnerable, 20 => Consumable::HealForsage, 21 => Consumable::CallFighters, 22 => Consumable::RegenerateHealth, 26 => Consumable::DepthCharges, 34 => Consumable::WeaponReloadBooster, 35 => Consumable::Hydrophone, 36 => Consumable::EnhancedRudders, 37 => Consumable::ReserveBattery, 41 => Consumable::SubmarineSurveillance, _ => {
if audit {
return DecodedPacketPayload::Audit(format!(
"consumableUsed({},{},{})",
entity_id, raw_consumable, duration
));
} else {
Consumable::Unknown(consumable)
}
}
};
DecodedPacketPayload::Consumable {
entity: *entity_id,
consumable,
duration,
}
} else if *method == "receiveArtilleryShots" {
let salvos_array = match &args[0] {
ArgValue::Array(a) => a,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let mut salvos = Vec::new();
for salvo_val in salvos_array.iter() {
let salvo_dict = match salvo_val {
ArgValue::FixedDict(m) => m,
_ => continue,
};
let owner_id: i32 = salvo_dict
.get("ownerID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let params_id: u32 = salvo_dict
.get("paramsID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let salvo_id: u32 = salvo_dict
.get("salvoID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let shots_array = match salvo_dict.get("shots") {
Some(ArgValue::Array(a)) => a,
_ => continue,
};
let mut shots = Vec::new();
for shot_val in shots_array.iter() {
let shot_dict = match shot_val {
ArgValue::FixedDict(m) => m,
_ => continue,
};
let pos = Self::extract_vec3(shot_dict.get("pos"));
let tar_pos = Self::extract_vec3(shot_dict.get("tarPos"));
let shot_id: u32 = shot_dict
.get("shotID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let speed: f32 = shot_dict
.get("speed")
.and_then(|v| v.try_into().ok())
.unwrap_or(0.0);
shots.push(ArtilleryShotData {
origin: pos,
target: tar_pos,
shot_id,
speed,
});
}
salvos.push(ArtillerySalvo {
owner_id: EntityId::from(owner_id),
params_id: GameParamId::from(params_id),
salvo_id,
shots,
});
}
DecodedPacketPayload::ArtilleryShots {
entity_id: *entity_id,
salvos,
}
} else if *method == "receiveTorpedoes" {
let salvos_array = match &args[0] {
ArgValue::Array(a) => a,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let mut torpedoes = Vec::new();
for salvo_val in salvos_array.iter() {
let salvo_dict = match salvo_val {
ArgValue::FixedDict(m) => m,
_ => continue,
};
let owner_id: i32 = salvo_dict
.get("ownerID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let params_id: u32 = salvo_dict
.get("paramsID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let salvo_id: u32 = salvo_dict
.get("salvoID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let torps_array = match salvo_dict.get("torpedoes") {
Some(ArgValue::Array(a)) => a,
_ => continue,
};
for torp_val in torps_array.iter() {
let torp_dict = match torp_val {
ArgValue::FixedDict(m) => m,
_ => continue,
};
let pos = Self::extract_vec3(torp_dict.get("pos"));
let dir = Self::extract_vec3(torp_dict.get("dir"));
let shot_id: u32 = torp_dict
.get("shotID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
torpedoes.push(TorpedoData {
owner_id: EntityId::from(owner_id),
params_id: GameParamId::from(params_id),
salvo_id,
shot_id,
origin: pos,
direction: dir,
});
}
}
DecodedPacketPayload::TorpedoesReceived {
entity_id: *entity_id,
torpedoes,
}
} else if *method == "receiveShotKills" {
let packs = match &args[0] {
ArgValue::Array(a) => a,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let mut hits = Vec::new();
for pack in packs {
let pack_dict = match pack {
ArgValue::FixedDict(d) => d,
_ => continue,
};
let owner_id: i32 = pack_dict
.get("ownerID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
let kills_array = match pack_dict.get("kills") {
Some(ArgValue::Array(a)) => a,
_ => continue,
};
for kill in kills_array {
let kill_dict = match kill {
ArgValue::FixedDict(d) => d,
_ => continue,
};
let shot_id: u32 = kill_dict
.get("shotID")
.and_then(|v| v.try_into().ok())
.unwrap_or(0);
hits.push(ShotHit {
owner_id: EntityId::from(owner_id),
shot_id,
});
}
}
DecodedPacketPayload::ShotKills {
entity_id: *entity_id,
hits,
}
} else if *method == "receive_addMinimapSquadron" {
let plane_id: PlaneId = match &args[0] {
ArgValue::Uint64(v) => PlaneId::from(*v),
ArgValue::Int64(v) => PlaneId::from(*v),
ArgValue::Uint32(v) => PlaneId::from(*v as u64),
ArgValue::Int32(v) => PlaneId::from(*v as i64),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let team_id: u32 = match &args[1] {
ArgValue::Uint32(v) => *v,
ArgValue::Int32(v) => *v as u32,
ArgValue::Uint64(v) => *v as u32,
ArgValue::Int64(v) => *v as u32,
ArgValue::Uint8(v) => *v as u32,
ArgValue::Int8(v) => *v as u32,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let params_id: u64 = match &args[2] {
ArgValue::Uint64(v) => *v,
ArgValue::Int64(v) => *v as u64,
ArgValue::Uint32(v) => *v as u64,
ArgValue::Int32(v) => *v as u64,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let position = match &args[3] {
ArgValue::Array(a) if a.len() >= 2 => {
let x: f32 = (&a[0]).try_into().unwrap_or(0.0);
let y: f32 = (&a[1]).try_into().unwrap_or(0.0);
(x, y)
}
ArgValue::Vector2((x, y)) => (*x, *y),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
DecodedPacketPayload::PlaneAdded {
entity_id: *entity_id,
plane_id,
team_id,
params_id: GameParamId::from(params_id),
x: position.0,
y: position.1,
}
} else if *method == "receive_removeMinimapSquadron" {
let plane_id: PlaneId = match &args[0] {
ArgValue::Uint64(v) => PlaneId::from(*v),
ArgValue::Int64(v) => PlaneId::from(*v),
ArgValue::Uint32(v) => PlaneId::from(*v as u64),
ArgValue::Int32(v) => PlaneId::from(*v as i64),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
DecodedPacketPayload::PlaneRemoved {
entity_id: *entity_id,
plane_id,
}
} else if *method == "receive_updateMinimapSquadron" {
let plane_id: PlaneId = match &args[0] {
ArgValue::Uint64(v) => PlaneId::from(*v),
ArgValue::Int64(v) => PlaneId::from(*v),
ArgValue::Uint32(v) => PlaneId::from(*v as u64),
ArgValue::Int32(v) => PlaneId::from(*v as i64),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let position = match &args[1] {
ArgValue::Array(a) if a.len() >= 2 => {
let x: f32 = (&a[0]).try_into().unwrap_or(0.0);
let y: f32 = (&a[1]).try_into().unwrap_or(0.0);
(x, y)
}
ArgValue::Vector2((x, y)) => (*x, *y),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
DecodedPacketPayload::PlanePosition {
entity_id: *entity_id,
plane_id,
x: position.0,
y: position.1,
}
} else if *method == "syncGun" {
let group = match &args[0] {
ArgValue::Uint8(v) => *v as u32,
ArgValue::Int8(v) => *v as u32,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let turret = match &args[1] {
ArgValue::Uint8(v) => *v as u32,
ArgValue::Int8(v) => *v as u32,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let yaw = match &args[2] {
ArgValue::Float32(v) => *v,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let pitch = match &args[3] {
ArgValue::Float32(v) => *v,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
DecodedPacketPayload::GunSync {
entity_id: *entity_id,
group,
turret,
yaw,
pitch,
}
} else if *method == "setAmmoForWeapon" {
let weapon_type = match &args[0] {
ArgValue::Uint8(v) => *v as u32,
ArgValue::Int8(v) => *v as u32,
ArgValue::Uint32(v) => *v,
ArgValue::Int32(v) => *v as u32,
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let ammo_param_id = match &args[1] {
ArgValue::Uint32(v) => GameParamId::from(*v),
ArgValue::Int32(v) => GameParamId::from(*v as u32),
ArgValue::Uint64(v) => GameParamId::from(*v),
ArgValue::Int64(v) => GameParamId::from(*v),
_ => return DecodedPacketPayload::EntityMethod(packet),
};
let is_reload = if args.len() > 2 {
match &args[2] {
ArgValue::Uint8(v) => *v != 0,
ArgValue::Int8(v) => *v != 0,
_ => false,
}
} else {
false
};
DecodedPacketPayload::SetAmmoForWeapon {
entity_id: *entity_id,
weapon_type,
ammo_param_id,
is_reload,
}
} else {
DecodedPacketPayload::EntityMethod(packet)
}
}
}
#[derive(Debug, Serialize)]
pub struct DecodedPacket<'replay, 'argtype, 'rawpacket> {
pub packet_type: u32,
pub clock: crate::types::GameClock,
pub payload: DecodedPacketPayload<'replay, 'argtype, 'rawpacket>,
}
impl<'replay, 'argtype, 'rawpacket> DecodedPacket<'replay, 'argtype, 'rawpacket>
where
'rawpacket: 'replay,
'rawpacket: 'argtype,
{
pub fn from(
version: &Version,
audit: bool,
packet: &'rawpacket Packet<'_, '_>,
battle_constants: Option<&wowsunpack::game_constants::BattleConstants>,
) -> Self {
Self {
clock: packet.clock,
packet_type: packet.packet_type,
payload: DecodedPacketPayload::from(
version,
audit,
&packet.payload,
packet.packet_type,
battle_constants,
),
}
}
}
struct Decoder {
silent: bool,
output: Option<Box<dyn std::io::Write>>,
version: Version,
}
impl Decoder {
fn write(&mut self, line: &str) {
if !self.silent {
match self.output.as_mut() {
Some(f) => {
writeln!(f, "{}", line).unwrap();
}
None => {
println!("{}", line);
}
}
}
}
}
#[allow(dead_code)]
mod minimap_update {
use modular_bitfield::prelude::*;
#[bitfield]
pub(super) struct RawMinimapUpdate {
pub x: B11,
pub y: B11,
pub heading: B8,
pub unknown: bool,
pub is_disappearing: bool,
}
}
use minimap_update::RawMinimapUpdate;
impl Analyzer for Decoder {
fn finish(&mut self) {}
fn process(&mut self, packet: &Packet<'_, '_>) {
let decoded = DecodedPacket::from(&self.version, false, packet, None);
let encoded = serde_json::to_string(&decoded).unwrap();
self.write(&encoded);
}
}