use winnow::Parser;
use winnow::binary::le_u32;
use winnow::combinator::repeat;
use super::Version;
use super::parser_utils::WResult;
use crate::game_types::GameParamId;
use crate::rpc::typedefs::ArgValue;
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ShipConfig {
ship_params_id: GameParamId,
abilities: Vec<GameParamId>,
modernization: Vec<GameParamId>,
units: Vec<GameParamId>,
exteriors: Vec<GameParamId>,
ensigns: Vec<GameParamId>,
ecoboosts: Vec<GameParamId>,
naval_flag: Option<u32>,
last_boarded_crew: Option<u32>,
}
impl ShipConfig {
pub fn stock(ship_params_id: GameParamId) -> Self {
Self { ship_params_id, ..Default::default() }
}
pub fn ship_params_id(&self) -> GameParamId {
self.ship_params_id
}
pub fn exteriors(&self) -> &[GameParamId] {
self.exteriors.as_ref()
}
pub fn units(&self) -> &[GameParamId] {
self.units.as_ref()
}
pub fn modernization(&self) -> &[GameParamId] {
self.modernization.as_ref()
}
pub fn hull(&self) -> Option<GameParamId> {
self.units.first().copied()
}
pub fn abilities(&self) -> &[GameParamId] {
self.abilities.as_ref()
}
pub fn ensigns(&self) -> &[GameParamId] {
self.ensigns.as_ref()
}
pub fn ecoboosts(&self) -> &[GameParamId] {
self.ecoboosts.as_ref()
}
pub fn naval_flag(&self) -> Option<u32> {
self.naval_flag
}
pub fn last_boarded_crew(&self) -> Option<u32> {
self.last_boarded_crew
}
}
pub fn parse_ship_config_value(value: &ArgValue, version: &Version) -> Option<ShipConfig> {
match value {
ArgValue::Blob(blob) => parse_ship_config(blob, version).ok(),
ArgValue::FixedDict(m) | ArgValue::NullableFixedDict(Some(m)) => {
let ship_id = m.get("shipId").and_then(|v| v.as_u32())?;
Some(ShipConfig::stock(GameParamId::from(ship_id)))
}
_ => None,
}
}
pub fn parse_ship_config(blob: &[u8], version: &Version) -> WResult<ShipConfig> {
let i = &mut &*blob;
let _version = le_u32.parse_next(i)?;
let ship_params_id = le_u32.parse_next(i)?;
let _element_count = le_u32.parse_next(i)?;
let unit_count = le_u32.parse_next(i)?;
let units: Vec<u32> = repeat(unit_count as usize, le_u32).parse_next(i)?;
if version.is_at_least(&Version::base(13, 2, 0)) {
let _unk = le_u32.parse_next(i)?;
}
fn take_u32(i: &mut &[u8]) -> Option<u32> {
let (head, rest) = i.split_first_chunk::<4>()?;
*i = rest;
Some(u32::from_le_bytes(*head))
}
fn take_section(i: &mut &[u8]) -> Vec<u32> {
let Some(count) = take_u32(i) else { return Vec::new() };
(0..count).map_while(|_| take_u32(i)).collect()
}
let modernization = take_section(i);
let exteriors = take_section(i);
let _supply_state = take_u32(i);
if let Some(color_scheme_count) = take_u32(i) {
for _ in 0..color_scheme_count {
take_u32(i);
take_u32(i);
}
}
let abilities = take_section(i);
let ensigns = take_section(i);
let ecoboosts = take_section(i);
let naval_flag = take_u32(i);
let _is_owned = take_u32(i);
let last_boarded_crew = take_u32(i);
let to_ids = |v: Vec<u32>| v.into_iter().map(GameParamId::from).collect();
Ok(ShipConfig {
ship_params_id: GameParamId::from(ship_params_id),
abilities: to_ids(abilities),
modernization: to_ids(modernization),
units: to_ids(units),
exteriors: to_ids(exteriors),
ensigns: to_ids(ensigns),
ecoboosts: to_ids(ecoboosts),
naval_flag,
last_boarded_crew,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn push_u32(buf: &mut Vec<u8>, val: u32) {
buf.extend_from_slice(&val.to_le_bytes());
}
#[allow(clippy::too_many_arguments)]
fn build_blob(
ship_params_id: u32,
units: &[u32],
modernization: &[u32],
exteriors: &[u32],
color_schemes: &[(u32, u32)],
abilities: &[u32],
ensigns: &[u32],
ecoboosts: &[u32],
naval_flag: u32,
last_boarded_crew: u32,
include_v13_2_field: bool,
) -> Vec<u8> {
let mut buf = Vec::new();
push_u32(&mut buf, 1); push_u32(&mut buf, ship_params_id);
push_u32(&mut buf, 0);
push_u32(&mut buf, units.len() as u32);
for &u in units {
push_u32(&mut buf, u);
}
if include_v13_2_field {
push_u32(&mut buf, 0);
}
push_u32(&mut buf, modernization.len() as u32);
for &m in modernization {
push_u32(&mut buf, m);
}
push_u32(&mut buf, exteriors.len() as u32);
for &e in exteriors {
push_u32(&mut buf, e);
}
push_u32(&mut buf, 0);
push_u32(&mut buf, color_schemes.len() as u32);
for &(slot, scheme) in color_schemes {
push_u32(&mut buf, slot);
push_u32(&mut buf, scheme);
}
push_u32(&mut buf, abilities.len() as u32);
for &a in abilities {
push_u32(&mut buf, a);
}
push_u32(&mut buf, ensigns.len() as u32);
for &e in ensigns {
push_u32(&mut buf, e);
}
push_u32(&mut buf, ecoboosts.len() as u32);
for &e in ecoboosts {
push_u32(&mut buf, e);
}
push_u32(&mut buf, naval_flag);
push_u32(&mut buf, 1); push_u32(&mut buf, last_boarded_crew);
buf
}
fn version_15_1() -> Version {
Version::base(15, 1, 0)
}
fn version_12_3() -> Version {
Version::base(12, 3, 0)
}
#[test]
fn parse_minimal_config() {
let blob = build_blob(
4_293_001_168, &[100], &[],
&[],
&[],
&[],
&[],
&[],
0,
0,
true,
);
let config = parse_ship_config(&blob, &version_15_1()).unwrap();
assert_eq!(config.ship_params_id().raw(), 4_293_001_168);
assert_eq!(config.hull().map(|h| h.raw()), Some(100));
assert_eq!(config.units().len(), 1);
assert!(config.modernization().is_empty());
assert!(config.exteriors().is_empty());
assert!(config.abilities().is_empty());
assert!(config.ensigns().is_empty());
assert!(config.ecoboosts().is_empty());
assert_eq!(config.naval_flag(), Some(0));
assert_eq!(config.last_boarded_crew(), Some(0));
}
#[test]
fn parse_config_v13_2_extra_field() {
let blob = build_blob(
1000,
&[10, 20, 30],
&[200, 201],
&[300],
&[],
&[400, 401, 402],
&[],
&[500, 501, 502, 503],
7,
9999,
true, );
let config = parse_ship_config(&blob, &version_15_1()).unwrap();
assert_eq!(config.ship_params_id().raw(), 1000);
assert_eq!(config.hull().map(|h| h.raw()), Some(10));
assert_eq!(config.units().len(), 3);
assert_eq!(config.modernization().len(), 2);
assert_eq!(config.exteriors().len(), 1);
assert_eq!(config.abilities().len(), 3);
assert!(config.ensigns().is_empty());
assert_eq!(config.ecoboosts().len(), 4);
assert_eq!(config.naval_flag(), Some(7));
assert_eq!(config.last_boarded_crew(), Some(9999));
}
#[test]
fn parse_config_pre_v13_2() {
let blob = build_blob(
2000,
&[50, 60],
&[150],
&[250, 251, 252],
&[],
&[350],
&[450],
&[],
3,
8888,
false, );
let config = parse_ship_config(&blob, &version_12_3()).unwrap();
assert_eq!(config.ship_params_id().raw(), 2000);
assert_eq!(config.hull().map(|h| h.raw()), Some(50));
assert_eq!(config.units().len(), 2);
assert_eq!(config.modernization().len(), 1);
assert_eq!(config.exteriors().len(), 3);
assert_eq!(config.abilities().len(), 1);
assert_eq!(config.ensigns().len(), 1);
assert!(config.ecoboosts().is_empty());
assert_eq!(config.naval_flag(), Some(3));
assert_eq!(config.last_boarded_crew(), Some(8888));
}
#[test]
fn parse_empty_slots() {
let blob = build_blob(
42,
&[1], &[],
&[],
&[],
&[],
&[],
&[],
0,
0,
true,
);
let config = parse_ship_config(&blob, &version_15_1()).unwrap();
assert!(config.modernization().is_empty());
assert!(config.exteriors().is_empty());
assert!(config.abilities().is_empty());
assert!(config.ensigns().is_empty());
assert!(config.ecoboosts().is_empty());
}
#[test]
fn parse_config_with_many_modules() {
let units: Vec<u32> = (100..114).collect();
let mods: Vec<u32> = (200..206).collect();
let exteriors: Vec<u32> = (300..308).collect();
let abilities: Vec<u32> = (400..404).collect();
let ensigns: Vec<u32> = vec![500, 501];
let ecoboosts: Vec<u32> = vec![600, 601, 602, 603];
let color_schemes: Vec<(u32, u32)> = vec![(0, 10), (1, 11), (2, 12)];
let blob = build_blob(
5000,
&units,
&mods,
&exteriors,
&color_schemes,
&abilities,
&ensigns,
&ecoboosts,
15,
7777,
true,
);
let config = parse_ship_config(&blob, &version_15_1()).unwrap();
assert_eq!(config.ship_params_id().raw(), 5000);
assert_eq!(config.hull().map(|h| h.raw()), Some(100)); assert_eq!(config.units().len(), 14);
assert_eq!(config.modernization().len(), 6);
assert_eq!(config.exteriors().len(), 8);
assert_eq!(config.abilities().len(), 4);
assert_eq!(config.ensigns().len(), 2);
assert_eq!(config.ecoboosts().len(), 4);
assert_eq!(config.naval_flag(), Some(15));
assert_eq!(config.last_boarded_crew(), Some(7777));
assert_eq!(config.units()[5].raw(), 105);
assert_eq!(config.modernization()[3].raw(), 203);
assert_eq!(config.exteriors()[7].raw(), 307);
assert_eq!(config.abilities()[2].raw(), 402);
}
#[test]
fn parse_truncated_old_blob_yields_partial_loadout() {
let mut blob = build_blob(2000, &[10, 20, 30], &[200, 201], &[300], &[], &[400, 401], &[], &[], 0, 0, false);
blob.truncate(blob.len() - 16);
let config = parse_ship_config(&blob, &version_12_3()).unwrap();
assert_eq!(config.units().len(), 3);
assert_eq!(config.hull().map(|h| h.raw()), Some(10));
assert_eq!(config.modernization().len(), 2);
assert_eq!(config.abilities().len(), 2);
assert!(config.ecoboosts().is_empty());
assert_eq!(config.naval_flag(), None);
assert_eq!(config.last_boarded_crew(), None);
}
#[test]
fn default_ship_config() {
let config = ShipConfig::default();
assert_eq!(config.ship_params_id().raw(), 0);
assert_eq!(config.hull(), None);
assert!(config.units().is_empty());
assert!(config.modernization().is_empty());
assert!(config.exteriors().is_empty());
assert!(config.abilities().is_empty());
assert!(config.ensigns().is_empty());
assert!(config.ecoboosts().is_empty());
assert_eq!(config.naval_flag(), None);
assert_eq!(config.last_boarded_crew(), None);
}
}