diff --git a/rc_core/src/cubes/mod.rs b/rc_core/src/cubes/mod.rs index 9be8683..6a3d8c7 100644 --- a/rc_core/src/cubes/mod.rs +++ b/rc_core/src/cubes/mod.rs @@ -9,12 +9,16 @@ pub use cpu_count::CpuListParser; mod locations_of; pub use locations_of::{CubeLocationsParser, CubeLocationInfo}; +mod offsetter; +pub use offsetter::OffsetParser; + //pub mod prefabs; pub struct CubeParsers { weapon_list: std::sync::Arc, cpu_counter: std::sync::Arc, locations: std::sync::Arc, + offset: std::sync::Arc, } impl CubeParsers { @@ -24,6 +28,7 @@ impl CubeParsers { weapon_list: std::sync::Arc::new(WeaponListParser::with_cubes(cubes.values())), cpu_counter: std::sync::Arc::new(CpuListParser::with_cubes(cubes.values())), locations: std::sync::Arc::new(CubeLocationsParser::with_cubes(cubes.values())), + offset: std::sync::Arc::new(OffsetParser::with_cubes(cubes.values())), } } @@ -38,4 +43,8 @@ impl CubeParsers { pub fn locations_of(&self) -> std::sync::Arc { self.locations.clone() } + + pub fn offset(&self) -> std::sync::Arc { + self.offset.clone() + } } diff --git a/rc_core/src/cubes/offsetter.rs b/rc_core/src/cubes/offsetter.rs new file mode 100644 index 0000000..ded628e --- /dev/null +++ b/rc_core/src/cubes/offsetter.rs @@ -0,0 +1,26 @@ +pub struct OffsetParser; + +impl OffsetParser { + pub fn with_cubes<'a, I: std::iter::Iterator>(_iter: I) -> Self { + Self + } + + /// offset is (x, y, z) + pub fn offset_inplace_by(&self, cubes: &mut [u8], colours: &mut [u8], offset: (i16, i16, i16)) { + // this assumes cubes and colours are valid and length-prefixed + let cubes = &mut cubes[4..]; + let colours = &mut colours[4..]; + for cube in cubes.chunks_mut(8) { + // bytes 4, 5, 6 are x, y, z (respectively) + cube[4] = (cube[4] as i16 + offset.0) as _; + cube[5] = (cube[5] as i16 + offset.1) as _; + cube[6] = (cube[6] as i16 + offset.2) as _; + } + for colour in colours.chunks_mut(4) { + // last 3 bytes are x, y, z (respectively) + colour[1] = (colour[1] as i16 + offset.0) as _; + colour[2] = (colour[2] as i16 + offset.1) as _; + colour[3] = (colour[3] as i16 + offset.2) as _; + } + } +} diff --git a/rc_core/src/cubes/parser.rs b/rc_core/src/cubes/parser.rs index 4fb1659..b643284 100644 --- a/rc_core/src/cubes/parser.rs +++ b/rc_core/src/cubes/parser.rs @@ -85,4 +85,19 @@ impl Colour { } Ok(cubes) } + + pub fn dump(&self, w: &mut dyn std::io::Write) -> std::io::Result { + w.write_all(&[self.colour, self.x, self.y, self.z])?; + Ok(4) + } + + pub fn dump_list(items: Vec) -> std::io::Result> { + let mut buf = Vec::with_capacity(4 + (items.len() * 8)); + let mut dumped = std::io::Cursor::new(&mut buf); + dumped.write_all(&(items.len() as u32).to_le_bytes())?; + for item in items { + item.dump(&mut dumped)?; + } + Ok(buf) + } } diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs index 99f8fa1..8714224 100644 --- a/rc_core/src/factory/adapter_enum.rs +++ b/rc_core/src/factory/adapter_enum.rs @@ -49,6 +49,14 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { Self::None => Ok(()), } } + + async fn update_vehicle(&self, id: i32, cube_data: Option>, colour_data: Option>) -> Result<(), Box> { + match self { + Self::Arc(x) => x.update_vehicle(id, cube_data, colour_data).await, + Self::Custom(x) => x.update_vehicle(id, cube_data, colour_data).await, + Self::None => Ok(()), + } + } } impl Factory { diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs index 8d7fe4c..e323d3c 100644 --- a/rc_factory/src/arc/adapter.rs +++ b/rc_factory/src/arc/adapter.rs @@ -1,4 +1,4 @@ -use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, ActiveModelTrait, Set, TransactionTrait}; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, Set, TransactionTrait}; use base64::Engine; use std::collections::HashMap; @@ -46,6 +46,23 @@ impl ArcAdapter { fn parse_cube_amounts(&self, cube_amounts: &str) -> std::collections::HashMap { serde_json::from_str(cube_amounts).unwrap_or_default() } + + fn calculate_cube_amounts(&self, data: &[u8]) -> String { + let mut counts: HashMap = HashMap::new(); + let data = &data[4..]; + for chunk in data.chunks_exact(8) { + let id_bytes: [u8; 4] = chunk[0..4].try_into().unwrap(); + let part_id = u32::from_le_bytes(id_bytes); + + *counts.entry(part_id).or_insert(0) += 1; + } + let mut str_map: HashMap = HashMap::new(); + for (k, v) in counts { + str_map.insert(k.to_string(), v); + } + + serde_json::to_string(&str_map).unwrap_or_else(|_| "{}".to_string()) + } } #[async_trait::async_trait] @@ -208,22 +225,7 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { } let transaction = self.orm.begin().await?; - let cube_amounts = { - let mut counts: HashMap = HashMap::new(); - let data = &vehicle.cube_data[4..]; - for chunk in data.chunks_exact(8) { - let id_bytes: [u8; 4] = chunk[0..4].try_into().unwrap(); - let part_id = u32::from_le_bytes(id_bytes); - - *counts.entry(part_id).or_insert(0) += 1; - } - let mut str_map: HashMap = HashMap::new(); - for (k, v) in counts { - str_map.insert(k.to_string(), v); - } - - serde_json::to_string(&str_map).unwrap_or_else(|_| "{}".to_string()) - }; + let cube_amounts = self.calculate_cube_amounts(&vehicle.cube_data); let cubes = super::entities::robot_cubes::ActiveModel { id: sea_orm::ActiveValue::NotSet, cube_data: Set(base64::prelude::BASE64_STANDARD.encode(&vehicle.cube_data)), @@ -299,4 +301,24 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { } Ok(()) } + + async fn update_vehicle(&self, id: i32, cube_data: Option>, colour_data: Option>) -> Result<(), Box> { + if self.is_readonly { + //return Ok(()); // this soft-locks the client into an infinite reload cycle + return Err("Unsupported: cannot update factory in read-only mode".into()); // this works though + } + // TODO this should probably be a transaction + if let Some(cubes) = super::entities::robot_cubes::Entity::find_by_id(id as u32).one(&self.orm).await? { + let mut to_update = cubes.into_active_model(); + if let Some(cube_data) = cube_data { + to_update.cube_data = Set(base64::prelude::BASE64_STANDARD.encode(&cube_data)); + to_update.cube_amounts = Set(self.calculate_cube_amounts(&cube_data)); + } + if let Some(colour_data) = colour_data { + to_update.colour_data = Set(base64::prelude::BASE64_STANDARD.encode(&colour_data)); + } + to_update.update(&self.orm).await?; + } + Ok(()) + } } diff --git a/rc_factory/src/traits.rs b/rc_factory/src/traits.rs index 6f8f850..f5c8e16 100644 --- a/rc_factory/src/traits.rs +++ b/rc_factory/src/traits.rs @@ -6,6 +6,7 @@ pub trait VehicleFactoryAdapter: Send + Sync + 'static { async fn rate_vehicle(&self, id: i32, combat: i32, cosmetic: i32) -> Result<(), Box>; /// Just update any purchase trackers async fn purchase(&self, id: i32) -> Result<(), Box>; + async fn update_vehicle(&self, id: i32, cube_data: Option>, colour_data: Option>) -> Result<(), Box>; } #[derive(Debug, Clone)] diff --git a/rc_services_room/src/operations/crf_shift_vehicle.rs b/rc_services_room/src/operations/crf_shift_vehicle.rs new file mode 100644 index 0000000..493c0b6 --- /dev/null +++ b/rc_services_room/src/operations/crf_shift_vehicle.rs @@ -0,0 +1,66 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; +use oj_rc_factory::VehicleFactoryAdapter; + +const CODE: u8 = 97; + +const ID_PARAM_KEY: u8 = 45; // int; in +const OFFSET_X_PARAM_KEY: u8 = 104; // int; in +const OFFSET_Z_PARAM_KEY: u8 = 105; // int; in +const EXPECTED_FIRST_X_PARAM_KEY: u8 = 106; // int; in +const EXPECTED_FIRST_Y_PARAM_KEY: u8 = 107; // int; in +const EXPECTED_FIRST_Z_PARAM_KEY: u8 = 108; // int; in + +pub(super) struct FactoryVehicleOffsetApplier { + factory: std::sync::Arc, + offsetter: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FactoryVehicleOffsetApplier { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + if let Some(Typed::Int(factory_id)) = params.remove(&ID_PARAM_KEY) { + if let Some(Typed::Int(offset_x)) = params.remove(&OFFSET_X_PARAM_KEY) { + if let Some(Typed::Int(offset_z)) = params.remove(&OFFSET_Z_PARAM_KEY) { + if let Some(Typed::Int(expected_first_x)) = params.remove(&EXPECTED_FIRST_X_PARAM_KEY) { + if let Some(Typed::Int(expected_first_y)) = params.remove(&EXPECTED_FIRST_Y_PARAM_KEY) { + if let Some(Typed::Int(expected_first_z)) = params.remove(&EXPECTED_FIRST_Z_PARAM_KEY) { + let _ = user.user()?; // double-check they're authenticated + log::debug!("Factory vehicle {} has first expected cube at (x:{}, y:{}, z:{}) offset (x:{}, z:{})", factory_id, expected_first_x, expected_first_y, expected_first_z, offset_x, offset_z); + let vehicle_opt = self.factory.vehicle(factory_id).await.map_err(|e| { + log::error!("Failed to retrieve vehicle {} (for offset) from factory: {}", factory_id, e); + SimpleOpError::with_message( + oj_rc_core::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to retrieve vehicle (for offset) from factory: {}", e) + ) + })?; + if let Some((mut vehicle, _)) = vehicle_opt { + self.offsetter.offset_inplace_by(&mut vehicle.cube_data, &mut vehicle.colour_data, (offset_x as _, 0, offset_z as _)); + self.factory.update_vehicle(factory_id, Some(vehicle.cube_data), Some(vehicle.colour_data)).await + .map_err(|e| { + log::error!("Failed to update factory vehicle {} (for offset): {}", factory_id, e); + SimpleOpError::with_message( + oj_rc_core::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to retrieve factory vehicle (for offset): {}", e) + ) + })?; + } + } + } + } + } + } + } + Ok(ParameterTable::with_capacity(1)) + } +} + +pub(super) fn factory_offset_provider(factory: &std::sync::Arc, offsetter: std::sync::Arc) -> SimpleOpImpl { + SimpleOpImpl::new(FactoryVehicleOffsetApplier { + factory: factory.to_owned(), + offsetter, + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 9635079..1e3dbc6 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -102,6 +102,7 @@ mod campaign_save_result; mod item_shop_purchase; mod code_redeem; mod crf_rate_vehicle; +mod crf_shift_vehicle; use polariton_server::operations::OperationsHandler; @@ -224,7 +225,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(garage_slot_copy::garage_slot_copy_provider()) .add(polariton_server::operations::Ack::<12, _>::default()) // TODO handle UpdatePlayerDailyQuestProgressRequest instead of ignoring it .add(crf_rate_vehicle::crf_rating_provider(&init_ctx.factory)) - //.add(polariton_server::operations::Ack::<97, _>::default()) // TODO handle UpdateShopRobotOffsetRequest instead of ignoring it (this seems to break newly-uploaded vehicles for now) + .add(crf_shift_vehicle::factory_offset_provider(&init_ctx.factory, init_ctx.parsers.offset())) .add(steam_promo::steam_promos_provider()) .add(item_shop_purchase::item_purchase_provider(&init_ctx.cubes)) .add(code_redeem::code_redeem_provider(&init_ctx.cubes))