diff --git a/Cargo.lock b/Cargo.lock index 65b8eee..3a87dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2589,6 +2589,7 @@ dependencies = [ "polariton", "polariton_server", "rc_database", + "rc_factory", "serde", "serde_json", "tokio", @@ -2603,6 +2604,18 @@ dependencies = [ "sea-orm-migration", ] +[[package]] +name = "rc_factory" +version = "0.2.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "chrono", + "libfj", + "log", + "sea-orm", +] + [[package]] name = "rc_microtransactions" version = "0.2.0" @@ -2636,12 +2649,14 @@ dependencies = [ "clap", "env_logger", "hex", + "libfj", "log", "polariton", "polariton_auth", "polariton_server", "rand 0.9.0", "rc_core", + "rc_factory", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 18bbb2f..3e57696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ "rc_social", "rc_social_room", "rc_chat", "rc_chat_room", "rc_singleplayer", "rc_singleplayer_room", - "rc_core", "rc_database", + "rc_core", "rc_database", "rc_factory", ] [workspace.dependencies] @@ -34,3 +34,4 @@ polariton_server = { version = "0.2", path = "../polariton/server", features = [ serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" async-trait = "0.1" +chrono = "0.4" diff --git a/README.md b/README.md index 62b8d8f..972840b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The current PC's MAC address is also sent to the server (this is a Robocraft cli #### Extra crates -There are two crates that are expected to be in the same folder parent folder as this project, since they may change as a result of OpenJam server development. +There are two crates that are expected to be in the same parent folder as this project, since they may change as a result of OpenJam server development. - https://git.ngram.ca/OpenJam/libfj (Public-facing FreeJam API and data structures) - https://git.ngram.ca/OpenJam/polariton (Photon Unity Network packet and server) diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index a61c2f8..6ab636f 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14423,6 +14423,12 @@ "jam_club" ] }, + "factory": { + "adapter": { + "variant": "Arc", + "uri": "sqlite:../../arc/rc_archive.db?mode=ro" + } + }, "settings": { "banners": [ { diff --git a/rc_core/Cargo.toml b/rc_core/Cargo.toml index 0dad21a..5501cca 100644 --- a/rc_core/Cargo.toml +++ b/rc_core/Cargo.toml @@ -24,3 +24,4 @@ jsonwebtoken = "9" argon2 = { version = "0.5", features = [ "std" ] } rc_database = { version = "0.2", path = "../rc_database" } +rc_factory = { version = "0.2", path = "../rc_factory" } diff --git a/rc_core/src/data/mod.rs b/rc_core/src/data/mod.rs index 98ec3bd..66b7d66 100644 --- a/rc_core/src/data/mod.rs +++ b/rc_core/src/data/mod.rs @@ -28,6 +28,22 @@ pub fn encode_7_bit_i32(mut src: i32) -> Vec { out } +pub fn decode_7_bit_i32(reader: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 1]; + let mut out: i32 = 0; + for _ in 0..5 { + reader.read_exact(&mut buf)?; + let byte = buf[0]; + let has_more = byte & 0x80; + let number = byte & 0x7F; + out = (out << 7) | (number as i32); + if has_more == 0 { + return Ok(out); + } + } + Ok(out) +} + pub fn write_str_for_binreader(s: &str, writer: &mut dyn std::io::Write) -> std::io::Result { let s_bytes = s.as_bytes(); let mut total_len = writer.write(&encode_7_bit_i32(s_bytes.len() as i32))?; @@ -35,6 +51,13 @@ pub fn write_str_for_binreader(s: &str, writer: &mut dyn std::io::Write) -> std: Ok(total_len) } +pub fn read_str_for_binwriter(reader: &mut dyn std::io::Read) -> std::io::Result { + let len = decode_7_bit_i32(reader)?; + let mut buf = vec![0u8; len as usize]; + reader.read_exact(&mut buf)?; + Ok(String::from_utf8_lossy(&buf).into_owned()) +} + pub fn cube_id_to_str(id: u32) -> String { hex::encode(id.to_be_bytes()).into() } diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs new file mode 100644 index 0000000..0438c63 --- /dev/null +++ b/rc_core/src/factory/adapter_enum.rs @@ -0,0 +1,33 @@ +pub enum Factory { + Arc(rc_factory::arc::ArcAdapter), + Custom(Box), + None, +} + +#[async_trait::async_trait] +impl rc_factory::VehicleFactoryAdapter for Factory { + async fn vehicle(&self, id: u32) -> Result, Box> { + match self { + Self::Arc(x) => x.vehicle(id).await, + Self::Custom(x) => x.vehicle(id).await, + Self::None => Ok(None), + } + } + + async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { + match self { + Self::Arc(x) => x.list(query).await, + Self::Custom(x) => x.list(query).await, + Self::None => Ok(Vec::default()), + } + } +} + +impl Factory { + pub async fn from_config(conf: &crate::persist::FactoryConfig) -> Result> { + Ok(match &conf.adapter { + crate::persist::AdapterSettings::Arc(x) => Self::Arc(rc_factory::arc::ArcAdapter::init(&x.uri, x.show_expired).await?), + crate::persist::AdapterSettings::None => Self::None, + }) + } +} diff --git a/rc_core/src/factory/mod.rs b/rc_core/src/factory/mod.rs new file mode 100644 index 0000000..f2dc197 --- /dev/null +++ b/rc_core/src/factory/mod.rs @@ -0,0 +1,2 @@ +mod adapter_enum; +pub use adapter_enum::Factory; diff --git a/rc_core/src/lib.rs b/rc_core/src/lib.rs index 8e15c36..9b63dd8 100644 --- a/rc_core/src/lib.rs +++ b/rc_core/src/lib.rs @@ -9,3 +9,5 @@ pub use persist::user::{UserImpl, UserProvider, UserAuthenticator}; pub use persist::config::{ConfigImpl, ConfigProvider}; pub mod polariton; + +pub mod factory; diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 80b0e1d..1e5576f 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -5,7 +5,7 @@ use serde::{Serialize, Deserialize}; use polariton::operation::{Typed, Dict}; use polariton::serdes::TypePrefix; -use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig}; +use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig}; const CUBE_CONFIG_FILENAME: &str = "config.json"; @@ -16,6 +16,7 @@ pub struct CubeConfig { lerp_value: f32, battle: BattleConfig, chat: ChatConfig, + factory: FactoryConfig, settings: Settings, } @@ -28,6 +29,7 @@ impl CubeConfig { } } +#[async_trait::async_trait] impl super::ConfigProvider for CubeConfig { fn cube_list(&self) -> Typed { Typed::Dict(Dict { @@ -271,4 +273,8 @@ impl super::ConfigProvider for CubeConfig { }).collect(), } } + + async fn factory(&self) -> Result> { + crate::factory::Factory::from_config(&self.factory).await + } } diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 127d5d6..ece3866 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -1,5 +1,6 @@ use polariton::operation::Typed; +#[async_trait::async_trait] pub trait ConfigProvider { fn cube_list(&self) -> Typed; fn movement_list(&self) -> Typed; @@ -20,6 +21,7 @@ pub trait ConfigProvider { fn public_channels(&self) -> Typed; fn server_config(&self) -> ServerConfig; fn garage_upgrades(&self) -> GarageUpgrades; + async fn factory(&self) -> Result>; } pub struct CompleteCampaignProvider { diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index bbe0d59..93513b2 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -32,6 +32,9 @@ pub use settings::Settings; mod chat; pub use chat::ChatConfig; +mod vehicle_factory; +pub use vehicle_factory::{FactoryConfig, AdapterSettings, ArcFactorySettings}; + pub(self) const VALID_ROBOT: &[u8] = &[64, 0, 0, diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index ea623ac..0a8d3e0 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -387,6 +387,7 @@ impl super::User for UserData { slot: polariton::operation::Typed::Int(model.slot as _), bay_cpu: polariton::operation::Typed::Int(model.bay_cpu as _), mastery_level: polariton::operation::Typed::Int(model.mastery_level as _), + slot_i: model.slot as _, }) } diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 6ce5649..9d2f0af 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -100,6 +100,7 @@ pub struct NewSlotData { pub slot: polariton::operation::Typed, pub bay_cpu: polariton::operation::Typed, pub mastery_level: polariton::operation::Typed, + pub slot_i: i32, } pub struct VehicleData { diff --git a/rc_core/src/persist/vehicle_factory.rs b/rc_core/src/persist/vehicle_factory.rs new file mode 100644 index 0000000..c562cd6 --- /dev/null +++ b/rc_core/src/persist/vehicle_factory.rs @@ -0,0 +1,30 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct FactoryConfig { + #[serde(default = "default_variant")] + pub adapter: AdapterSettings, +} + +fn default_variant() -> AdapterSettings { + AdapterSettings::None +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "variant")] +pub enum AdapterSettings { + #[serde(alias = "sqlite")] + Arc(ArcFactorySettings), + None, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ArcFactorySettings { + pub uri: String, + #[serde(default = "default_true")] + pub show_expired: bool, +} + +fn default_true() -> bool { + true +} diff --git a/rc_factory/Cargo.toml b/rc_factory/Cargo.toml new file mode 100644 index 0000000..1c00238 --- /dev/null +++ b/rc_factory/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "rc_factory" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +sea-orm = { version = "1.1.10", features = [ "runtime-tokio-rustls", "macros" ] } +async-trait.workspace = true +libfj.workspace = true +chrono.workspace = true +log.workspace = true +base64 = "0.22" diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs new file mode 100644 index 0000000..a2d39d9 --- /dev/null +++ b/rc_factory/src/arc/adapter.rs @@ -0,0 +1,122 @@ +use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder}; + +pub struct ArcAdapter { + orm: sea_orm::DatabaseConnection, + ignore_expiry: bool, +} + +impl ArcAdapter { + pub async fn init(uri: &str, show_expired: bool) -> Result{ + log::debug!("Connecting to Archive of RoboCraft (ARC) vehicle factory database URI: {}", uri); + let db = sea_orm::Database::connect(uri).await?; + Ok(Self { + orm: db, + ignore_expiry: show_expired + }) + } + + fn default_query(&self) -> sea_orm::Select { + super::entities::robot_metadata::Entity::find() + .order_by_desc(super::entities::robot_metadata::Column::RentCount) + } +} + +#[async_trait::async_trait] +impl crate::VehicleFactoryAdapter for ArcAdapter { + async fn vehicle(&self, id: u32) -> Result, Box> { + log::debug!("Get vehicle id {}", id); + let cubes = super::entities::robot_cubes::Entity::find_by_id(id).one(&self.orm).await?; + if let Some(cubes) = cubes { + use base64::Engine; + Ok(Some(crate::VehicleInfo { + id: id as _, + cube_data: base64::prelude::BASE64_STANDARD.decode(cubes.cube_data.as_bytes()).unwrap_or_default(), + colour_data: base64::prelude::BASE64_STANDARD.decode(cubes.colour_data.as_bytes()).unwrap_or_default(), + })) + } else { + Ok(None) + } + + } + + async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { + log::debug!("Search vehicles with query {:?}", query); + let query_params = if query.default_page { + log::debug!("Default vehicle list query"); + self.default_query() + } else { + let mut query_builder = super::entities::robot_metadata::Entity::find(); + match query.order { + libfj::robocraft::FactoryOrderType::Suggested => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::RentCount); }, + libfj::robocraft::FactoryOrderType::CombatRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CombatRating); }, + libfj::robocraft::FactoryOrderType::CosmeticRating => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::CosmeticRating); }, + libfj::robocraft::FactoryOrderType::Added => { query_builder = query_builder.order_by_asc(super::entities::robot_metadata::Column::AddedDate); }, + libfj::robocraft::FactoryOrderType::CPU => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::Cpu); }, + libfj::robocraft::FactoryOrderType::MostBought => { query_builder = query_builder.order_by_desc(super::entities::robot_metadata::Column::BuyCount); }, + } + if !query.text_filter.is_empty() { + if query.player_filter { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedBy.like(query.text_filter.clone())) + .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query.text_filter)) + ); + } else { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(super::entities::robot_metadata::Column::AddedBy.like(query.text_filter.clone())) + .add(super::entities::robot_metadata::Column::AddedByDisplayName.like(query.text_filter.clone())) + .add(super::entities::robot_metadata::Column::Name.like(query.text_filter.clone())) + .add(super::entities::robot_metadata::Column::Description.like(query.text_filter.clone())) + ); + } + } + // movement filters not supported + // weapon filters not supported + if query.minimum_cpu > 0 { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.gte(query.minimum_cpu as u32)); + } + if query.maximum_cpu < usize::MAX { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Cpu.lte(query.maximum_cpu as u32)); + } + if query.buyable { + query_builder = query_builder.filter(super::entities::robot_metadata::Column::Buyable.ne(0)); + } + query_builder + }; + + // FIXME add support for query.prepend_featured_bot + let metadata_pages = query_params.paginate(&self.orm, query.page_size as u64); + let metadatas = metadata_pages.fetch_page(query.page as u64).await?; + let mut infos = Vec::with_capacity(metadatas.len()); + for meta in metadatas { + //let cube_amounts = super::entities::robot_cubes::Entity::find_by_id(meta.id).one(&self.orm).await?.map(|x| x.cube_amounts).unwrap_or_else(|| "".to_owned()); + infos.push( + crate::VehicleQueryInfo { + id: meta.id as _, + name: meta.name, + description: meta.description, + thumbnail: meta.thumbnail, + added_by: meta.added_by, + added_by_display_name: meta.added_by_display_name, + added_date: crate::traits::parse_rc_date(&meta.added_date).unwrap_or_default(), + expiry_date: if self.ignore_expiry { chrono::Utc::now() + chrono::Duration::weeks(2) } else { crate::traits::parse_rc_date(&meta.expiry_date).unwrap_or_default() }, + cpu: meta.cpu as _, + total_robot_ranking: meta.total_robot_ranking as _, + rent_count: meta.rent_count as _, + buy_count: meta.buy_count as _, + buyable: meta.buyable != 0, + removed_date: Default::default(), + ban_date: Default::default(), + featured: meta.featured != 0, + banner_message: Default::default(), + combat_rating: meta.combat_rating, + cosmetic_rating: meta.cosmetic_rating, + cube_amounts: Default::default(), + } + ); + } + log::debug!("Search vehicles returned {} results", infos.len()); + Ok(infos) + } +} diff --git a/rc_factory/src/arc/entities/mod.rs b/rc_factory/src/arc/entities/mod.rs new file mode 100644 index 0000000..9d3a93b --- /dev/null +++ b/rc_factory/src/arc/entities/mod.rs @@ -0,0 +1,7 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 + +//pub mod prelude; + +pub mod robot_cubes; +pub mod robot_metadata; +//pub mod state; diff --git a/rc_factory/src/arc/entities/prelude.rs b/rc_factory/src/arc/entities/prelude.rs new file mode 100644 index 0000000..ac866c8 --- /dev/null +++ b/rc_factory/src/arc/entities/prelude.rs @@ -0,0 +1,5 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 + +pub use super::robot_cubes::Entity as RobotCubes; +pub use super::robot_metadata::Entity as RobotMetadata; +//pub use super::state::Entity as State; diff --git a/rc_factory/src/arc/entities/robot_cubes.rs b/rc_factory/src/arc/entities/robot_cubes.rs new file mode 100644 index 0000000..e92eaf8 --- /dev/null +++ b/rc_factory/src/arc/entities/robot_cubes.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "ROBOT_CUBES")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: u32, + #[sea_orm(column_type = "Text")] + pub cube_data: String, + #[sea_orm(column_type = "Text")] + pub colour_data: String, + #[sea_orm(column_type = "Text")] + pub cube_amounts: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_factory/src/arc/entities/robot_metadata.rs b/rc_factory/src/arc/entities/robot_metadata.rs new file mode 100644 index 0000000..b32910a --- /dev/null +++ b/rc_factory/src/arc/entities/robot_metadata.rs @@ -0,0 +1,39 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, DeriveEntityModel)] +#[sea_orm(table_name = "ROBOT_METADATA")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: u32, + #[sea_orm(column_type = "Text")] + pub name: String, + #[sea_orm(column_type = "Text")] + pub description: String, + #[sea_orm(column_type = "Text")] + pub thumbnail: String, + #[sea_orm(column_type = "Text")] + pub added_by: String, + #[sea_orm(column_type = "Text")] + pub added_by_display_name: String, + #[sea_orm(column_type = "Text")] + pub added_date: String, + #[sea_orm(column_type = "Text")] + pub expiry_date: String, + pub cpu: u32, + pub total_robot_ranking: i32, + pub rent_count: i32, + pub buy_count: i32, + pub buyable: i32, + pub featured: i32, + #[sea_orm(column_type = "Float")] + pub combat_rating: f64, + #[sea_orm(column_type = "Float")] + pub cosmetic_rating: f64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_factory/src/arc/entities/state.rs b/rc_factory/src/arc/entities/state.rs new file mode 100644 index 0000000..dad4a36 --- /dev/null +++ b/rc_factory/src/arc/entities/state.rs @@ -0,0 +1,18 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.11 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "STATE")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: i32, + pub next_page: i32, + pub last_page_size: i32, + pub last_sequential_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_factory/src/arc/mod.rs b/rc_factory/src/arc/mod.rs new file mode 100644 index 0000000..7b47802 --- /dev/null +++ b/rc_factory/src/arc/mod.rs @@ -0,0 +1,6 @@ +//! Adapter for sqlite database generated by https://github.com/NGnius/arc + +mod adapter; +pub use adapter::ArcAdapter; + +mod entities; diff --git a/rc_factory/src/lib.rs b/rc_factory/src/lib.rs new file mode 100644 index 0000000..0f3746a --- /dev/null +++ b/rc_factory/src/lib.rs @@ -0,0 +1,4 @@ +pub mod arc; + +mod traits; +pub use traits::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo}; diff --git a/rc_factory/src/traits.rs b/rc_factory/src/traits.rs new file mode 100644 index 0000000..79a7205 --- /dev/null +++ b/rc_factory/src/traits.rs @@ -0,0 +1,52 @@ +#[async_trait::async_trait] +pub trait VehicleFactoryAdapter: Send + Sync + 'static { + async fn vehicle(&self, id: u32) -> Result, Box>; + async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box>; +} + +#[derive(Debug, Clone)] +pub struct VehicleInfo { + pub id: i32, + pub cube_data: Vec, + pub colour_data: Vec, +} + +pub fn parse_rc_date(s: &str) -> chrono::ParseResult> { + let naive = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S")?; + Ok(chrono::DateTime::from_naive_utc_and_offset(naive, chrono::Utc)) +} + +impl std::convert::From for VehicleInfo { + fn from(value: libfj::robocraft::FactoryRobotGetInfo) -> Self { + use base64::Engine; + Self { + id: value.item_id as _, + cube_data: base64::prelude::BASE64_STANDARD.decode(value.cube_data.as_bytes()).unwrap_or_default(), + colour_data: base64::prelude::BASE64_STANDARD.decode(value.colour_data.as_bytes()).unwrap_or_default(), + } + } +} + +#[derive(Debug)] +pub struct VehicleQueryInfo { + pub id: i32, + pub name: String, + pub description: String, + pub thumbnail: String, // url + pub added_by: String, + pub added_by_display_name: String, + pub added_date: chrono::DateTime, + pub expiry_date: chrono::DateTime, + pub cpu: u32, + pub total_robot_ranking: u32, + pub rent_count: u32, + pub buy_count: u32, + pub buyable: bool, + pub removed_date: Option>, + pub ban_date: Option>, + pub featured: bool, + pub banner_message: Option, + pub combat_rating: f64, + pub cosmetic_rating: f64, + pub cube_amounts: std::collections::HashMap, +} diff --git a/rc_services_room/Cargo.toml b/rc_services_room/Cargo.toml index ec25406..49a562e 100644 --- a/rc_services_room/Cargo.toml +++ b/rc_services_room/Cargo.toml @@ -19,7 +19,9 @@ base64 = "0.22" hex = "0.4" serde.workspace = true serde_json.workspace = true -chrono = "0.4" +chrono.workspace = true rc_core = { version = "*", path = "../rc_core" } +rc_factory = { version = "*", path = "../rc_factory" } rand = "0.9" async-trait.workspace = true +libfj.workspace = true diff --git a/rc_services_room/src/data/crf.rs b/rc_services_room/src/data/crf.rs new file mode 100644 index 0000000..1ed6e01 --- /dev/null +++ b/rc_services_room/src/data/crf.rs @@ -0,0 +1,249 @@ +use std::i64; + +#[allow(dead_code)] +pub struct ShopItemListFilters { + pub page: u32, + pub page_size: u32, + pub weapon_filter: i32, + pub movement_filter: i32, + pub weapon_groups: String, + pub movement_groups: String, + pub player: bool, + pub sort_mode: i32, + pub min_cpu: i32, + pub max_cpu: i32, + pub min_robot_ranking: i32, + pub max_robot_ranking: i32, + pub text: String, + pub text_search_field: i32, + pub show_featured: bool, + pub show_hidden: bool, // dev-only? + pub no_filters: bool, +} + +impl ShopItemListFilters { + pub fn parse(r: &mut dyn std::io::Read) -> std::io::Result { + Ok(Self { + page: read_u32(r)?, + page_size: read_u32(r)?, + weapon_filter: read_i32(r)?, + movement_filter: read_i32(r)?, + weapon_groups: rc_core::data::read_str_for_binwriter(r)?, + movement_groups: rc_core::data::read_str_for_binwriter(r)?, + player: read_bool(r)?, + sort_mode: read_i32(r)?, + min_cpu: read_i32(r)?, + max_cpu: read_i32(r)?, + min_robot_ranking: read_i32(r)?, + max_robot_ranking: read_i32(r)?, + text: rc_core::data::read_str_for_binwriter(r)?, + text_search_field: read_i32(r)?, + show_featured: read_bool(r)?, + show_hidden: read_bool(r)?, + no_filters: read_bool(r)?, + }) + } + + pub fn into_core(self) -> libfj::robocraft::ListQuery { + let weapon_groups = split_u32(&self.weapon_groups); + let movement_groups = split_u32(&self.movement_groups); + libfj::robocraft::ListQuery { + page: self.page as _, + page_size: self.page_size as _, + order: libfj::robocraft::FactoryOrderType::try_from(self.sort_mode as u8).unwrap_or(libfj::robocraft::FactoryOrderType::Suggested), + player_filter: self.player, + movement_filter: movement_groups.clone(), + movement_category_filter: movement_groups, + weapon_filter: weapon_groups.clone(), + weapon_category_filter: weapon_groups, + minimum_cpu: if self.min_cpu <= 0 { 0 } else { self.min_cpu as _ }, + maximum_cpu: if self.max_cpu <= 0 { usize::MAX } else { self.max_cpu as _ }, + text_filter: self.text, + text_search_field: libfj::robocraft::FactoryTextSearchField::try_from(self.text_search_field as u8).unwrap_or(libfj::robocraft::FactoryTextSearchField::All), + buyable: true, + prepend_featured_robot: true, + featured_only: self.show_featured, + default_page: self.no_filters, + } + } +} + +pub struct ItemResult { + pub id: i32, + pub name: String, + pub description: String, + pub thumbnail: String, + pub style_rating: f64, + pub combat_rating: f64, + pub cpu: i32, + pub total_robot_ranking: i32, + pub expiry_date: i64, // ticks until expiry (from now) + pub buyable: bool, + pub added_by: String, + pub added_by_display_name: String, + pub added_date: i64, // tick until added (from now -- probably negative) + pub rent_count: i32, + pub buy_count: i32, + pub featured: bool, + pub banner_message: String, + pub cube_counts: Vec<(u32, u32)>, +} + +// a tick is 100ns + +impl ItemResult { + pub fn dump(&self, w: &mut dyn std::io::Write) -> std::io::Result { + let mut total_len = write_i32(w, self.id)?; + total_len += rc_core::data::write_str_for_binreader(&self.name, w)?; + total_len += rc_core::data::write_str_for_binreader(&self.description, w)?; + total_len += rc_core::data::write_str_for_binreader(&self.thumbnail, w)?; + total_len += write_f64(w, self.style_rating)?; + total_len += write_f64(w, self.combat_rating)?; + total_len += write_i32(w, self.cpu)?; + total_len += write_i32(w, self.total_robot_ranking)?; + total_len += write_i64(w, self.expiry_date)?; + total_len += write_bool(w, self.buyable)?; + total_len += rc_core::data::write_str_for_binreader(&self.added_by, w)?; + total_len += rc_core::data::write_str_for_binreader(&self.added_by_display_name, w)?; + total_len += write_i64(w, self.added_date)?; + total_len += write_i32(w, self.rent_count)?; + total_len += write_i32(w, self.buy_count)?; + total_len += write_bool(w, self.featured)?; + total_len += rc_core::data::write_str_for_binreader(&self.banner_message, w)?; + total_len += write_i32(w, self.cube_counts.len() as i32)?; + for (key, val) in self.cube_counts.iter() { + total_len += write_u32(w, *key)?; + total_len += write_u32(w, *val)?; + } + Ok(total_len) + } + + pub fn dump_many(items: &[Self], w: &mut dyn std::io::Write) -> std::io::Result { + let mut total_len = write_i32(w, items.len() as _)?; + for item in items.iter() { + total_len += item.dump(w)?; + } + Ok(total_len) + } + + pub fn as_transmissible(items: &[Self]) -> polariton::operation::Typed { + let mut buf = Vec::new(); + Self::dump_many(items, &mut buf).unwrap(); + polariton::operation::Typed::Bytes(buf.into()) + } +} + +fn ticks_from_now(time: &chrono::DateTime) -> i64 { + let dur = time.signed_duration_since(chrono::offset::Utc::now()); + dur.num_nanoseconds() + .map(|x| x/100) + .unwrap_or_else(|| dur.num_milliseconds() * 1_000_000 / 100) +} + +impl std::convert::From for ItemResult { + fn from(value: rc_factory::VehicleQueryInfo) -> Self { + Self { + id: value.id, + name: value.name, + description: value.description, + thumbnail: value.thumbnail, + style_rating: value.cosmetic_rating, + combat_rating: value.combat_rating, + cpu: value.cpu as i32, + total_robot_ranking: value.total_robot_ranking as i32, + //expiry_date: ticks_from_now(&value.expiry_date), + expiry_date: i32::MAX as _, + buyable: value.buyable, + added_by: value.added_by, + added_by_display_name: value.added_by_display_name, + added_date: ticks_from_now(&value.added_date), + rent_count: value.rent_count as _, + buy_count: value.buy_count as _, + featured: value.featured, + banner_message: value.banner_message.unwrap_or_default(), + cube_counts: value.cube_amounts.into_iter().collect(), + } + } +} + +pub struct ItemData { + pub index: i32, + pub cube_data: Vec, + pub colour_data: Vec, +} + +impl ItemData { + pub fn as_transmissible(&self) -> polariton::operation::Typed { + polariton::operation::Typed::HashMap(vec![ + (polariton::operation::Typed::Str("itemIndex".into()), polariton::operation::Typed::Int(self.index)), + (polariton::operation::Typed::Str("cubeData".into()), polariton::operation::Typed::Bytes(self.cube_data.clone().into())), + (polariton::operation::Typed::Str("colourData".into()), polariton::operation::Typed::Bytes(self.colour_data.clone().into())), + ].into()) + } +} + +impl std::convert::From for ItemData { + fn from(value: rc_factory::VehicleInfo) -> Self { + Self { + index: value.id, + cube_data: value.cube_data, + colour_data: value.colour_data, + } + } +} + +#[inline] +fn read_i32(r: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf)?; + Ok(i32::from_le_bytes(buf)) +} + +#[inline] +fn write_i32(w: &mut dyn std::io::Write, num: i32) -> std::io::Result { + w.write_all(&num.to_le_bytes())?; + Ok(4) +} + +#[inline] +fn write_i64(w: &mut dyn std::io::Write, num: i64) -> std::io::Result { + w.write_all(&num.to_le_bytes())?; + Ok(8) +} + +#[inline] +fn write_f64(w: &mut dyn std::io::Write, num: f64) -> std::io::Result { + w.write_all(&num.to_le_bytes())?; + Ok(8) +} + +#[inline] +fn read_u32(r: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 4]; + r.read_exact(&mut buf)?; + Ok(u32::from_le_bytes(buf)) +} + +#[inline] +fn write_u32(w: &mut dyn std::io::Write, num: u32) -> std::io::Result { + w.write_all(&num.to_le_bytes())?; + Ok(4) +} + +#[inline] +fn read_bool(r: &mut dyn std::io::Read) -> std::io::Result { + let mut buf = [0u8; 1]; + r.read_exact(&mut buf)?; + Ok(buf[0] != 0) +} + +#[inline] +fn write_bool(w: &mut dyn std::io::Write, b: bool) -> std::io::Result { + w.write_all(&[b as u8])?; + Ok(1) +} + +#[inline] +fn split_u32(s: &str) -> Vec { + s.split(',').filter_map(|x| x.parse().ok()).collect() +} diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs index 8aa3611..81cdb36 100644 --- a/rc_services_room/src/data/mod.rs +++ b/rc_services_room/src/data/mod.rs @@ -28,3 +28,4 @@ pub use rc_core::data::error_codes; //pub use rc_core::data::game_mode; pub mod score_multipliers; //pub use rc_core::data::campaign; +pub mod crf; diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs index c9ee279..5444ff7 100644 --- a/rc_services_room/src/main.rs +++ b/rc_services_room/src/main.rs @@ -16,6 +16,7 @@ pub type UserTy = rc_core::UserState<()>; pub struct InitConfig { pub cubes: rc_core::persist::config::ConfigImpl, pub users: std::sync::Arc, + pub factory: std::sync::Arc, } #[tokio::main] @@ -26,9 +27,11 @@ async fn main() -> std::io::Result<()> { let cubes = rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(rc_core::persist::user::UserImpl::load(&args.data, &cubes).await.expect("Bad user data")); + let factory = std::sync::Arc::new(>::factory::<'_, '_>(&cubes).await.expect("Bad vehicle factory (CRF) config")); let init_ctx = std::sync::Arc::new(InitConfig { cubes, users, + factory, }); let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new())); diff --git a/rc_services_room/src/operations/crf_earnings.rs b/rc_services_room/src/operations/crf_earnings.rs new file mode 100644 index 0000000..3281a18 --- /dev/null +++ b/rc_services_room/src/operations/crf_earnings.rs @@ -0,0 +1,15 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const PARAM_KEY: u8 = 96; + +pub(super) fn robot_shop_user_earnings_provider() -> SimpleFunc<88, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, Typed::HashMap(vec![ + (Typed::Str("buyCount".into()), Typed::Int(0)), + (Typed::Str("earnings".into()), Typed::Int(0)), + ].into())); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/crf_list_query.rs b/rc_services_room/src/operations/crf_list_query.rs new file mode 100644 index 0000000..62189d5 --- /dev/null +++ b/rc_services_room/src/operations/crf_list_query.rs @@ -0,0 +1,51 @@ +use polariton_server::operations::{Operation, OperationCode}; +use polariton::operation::{Typed, ParameterTable}; +use rc_factory::VehicleFactoryAdapter; + +const CODE: u8 = 86; + +const FILTERS_PARAM_KEY: u8 = 92; +const ITEMS_PARAM_KEY: u8 = 93; + +async fn do_handling(params: ParameterTable<()>, _user: &crate::UserTy, factory: &std::sync::Arc) -> Result { + let mut params = params.to_dict(); + if let Some(Typed::Bytes(filters)) = params.remove(&FILTERS_PARAM_KEY) { + let mut cursor = std::io::Cursor::new(filters.vec); + let filters = crate::data::crf::ShopItemListFilters::parse(&mut cursor).map_err(|e| { + log::error!("Failed to parse factory item query: {}", e); + rc_core::data::error_codes::WebServicesError::UnexpectedError as i16 + })?; + let vehicles = factory.list(filters.into_core()).await.map_err(|e| { + log::error!("Failed to retrieve vehicles from factory: {}", e); + rc_core::data::error_codes::WebServicesError::DatabaseError as i16 + })?; + let vehicles: Vec<_> = vehicles.into_iter().map(|x| crate::data::crf::ItemResult::from(x)).collect(); + params.insert(ITEMS_PARAM_KEY, crate::data::crf::ItemResult::as_transmissible(&vehicles)); + } + Ok(params.into()) +} + +pub struct CrfItemListProvider { + factory: std::sync::Arc, +} + +#[async_trait::async_trait] +impl Operation<()> for CrfItemListProvider { + type User = crate::UserTy; + + async fn handle_async(&self, params: polariton::operation::ParameterTable<()>, user: &Self::User) -> polariton::operation::OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_handling(params, user, &self.factory).await) + } +} + +impl OperationCode for CrfItemListProvider { + fn op_code() -> u8 { + CODE + } +} + +pub(super) fn crf_item_list_query_provider(factory: &std::sync::Arc) -> CrfItemListProvider { + CrfItemListProvider { + factory: factory.to_owned(), + } +} diff --git a/rc_services_room/src/operations/crf_purchase.rs b/rc_services_room/src/operations/crf_purchase.rs new file mode 100644 index 0000000..2cc7a4b --- /dev/null +++ b/rc_services_room/src/operations/crf_purchase.rs @@ -0,0 +1,69 @@ +use polariton::operation::{ParameterTable, Typed, OperationResponse}; +use rc_factory::VehicleFactoryAdapter; + +const CODE: u8 = 166; + +const SLOT_PARAM_KEY: u8 = 43; // in; int +//const FREE_CURRENCY_COST_PARAM_KEY: u8 = 5; // in; int +//const PREMIUM_CURRENCY_COST_PARAM_KEY: u8 = 6; // in; int +const FACTORY_ID_PARAM_KEY: u8 = 94; // in; int + + +async fn do_handling(params: ParameterTable<()>, user: &crate::UserTy, factory: &std::sync::Arc) -> Result { + let mut params = params.to_dict(); + let user_info = user.user()?; + let slot = if let Some(Typed::Int(slot)) = params.remove(&SLOT_PARAM_KEY) { + user_info.new_slot(Some(slot)).await?; + slot + } else { + user_info.new_slot(None).await?.slot_i + }; + if let Some(Typed::Int(factory_id)) = params.remove(&FACTORY_ID_PARAM_KEY) { + // TODO charge for robot? + let vehicle_to_copy = factory.vehicle(factory_id as _).await.map_err(|e| { + log::error!("Failed to retrieve vehicle {} (for copy-construct) from factory: {}", factory_id, e); + rc_core::data::error_codes::WebServicesError::DatabaseError as i16 + })?; + if let Some(vehicle_to_copy) = vehicle_to_copy { + let to_save = rc_core::persist::user::VehicleData { + slot, + robot_data: vehicle_to_copy.cube_data, + colour_data: vehicle_to_copy.colour_data, + weapon_order: Vec::default(), // FIXME calculate this somehow? Or maybe get the factory adapter to calculate this + }; + user_info.save_slot(to_save).await?; + } else { + log::warn!("Failed to retrieve (for copy-construct) non-existent factory vehicle {}", factory_id); + return Err(rc_core::data::error_codes::WebServicesError::DatabaseError as i16); + } + + } + + Ok(params.into()) +} + +pub struct CrfItemPurchaseProvider { + factory: std::sync::Arc, +} + +#[async_trait::async_trait] +impl polariton_server::operations::Operation<()> for CrfItemPurchaseProvider { + type User = crate::UserTy; + + async fn handle_async(&self, params: ParameterTable<()>, user: &Self::User) -> OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_handling(params, user, &self.factory).await) + } +} + +impl polariton_server::operations::OperationCode for CrfItemPurchaseProvider { + fn op_code() -> u8 { + CODE + } +} + + +pub(super) fn crf_copy_to_bay_provider(factory: &std::sync::Arc) -> CrfItemPurchaseProvider { + CrfItemPurchaseProvider { + factory: factory.to_owned(), + } +} diff --git a/rc_services_room/src/operations/crf_vehicle_data.rs b/rc_services_room/src/operations/crf_vehicle_data.rs new file mode 100644 index 0000000..5d10126 --- /dev/null +++ b/rc_services_room/src/operations/crf_vehicle_data.rs @@ -0,0 +1,51 @@ +use polariton_server::operations::{Operation, OperationCode}; +use polariton::operation::{Typed, ParameterTable}; +use rc_factory::VehicleFactoryAdapter; + +const CODE: u8 = 87; + +const ID_PARAM_KEY: u8 = 94; +const DATA_PARAM_KEY: u8 = 95; + +async fn do_handling(params: ParameterTable<()>, _user: &crate::UserTy, factory: &std::sync::Arc) -> Result { + let mut params = params.to_dict(); + if let Some(Typed::Int(id)) = params.remove(&ID_PARAM_KEY) { + let vehicle = factory.vehicle(id as _).await.map_err(|e| { + log::error!("Failed to retrieve vehicle {} from factory: {}", id, e); + rc_core::data::error_codes::WebServicesError::DatabaseError as i16 + })?; + if let Some(vehicle) = vehicle { + let vehicle_data = crate::data::crf::ItemData::from(vehicle); + params.insert(DATA_PARAM_KEY, vehicle_data.as_transmissible()); + } else { + log::warn!("Failed to retrieve non-existent factory vehicle {}", id); + return Err(rc_core::data::error_codes::WebServicesError::InvalidRobot as i16); + } + } + Ok(params.into()) +} + +pub struct CrfItemDataProvider { + factory: std::sync::Arc, +} + +#[async_trait::async_trait] +impl Operation<()> for CrfItemDataProvider { + type User = crate::UserTy; + + async fn handle_async(&self, params: polariton::operation::ParameterTable<()>, user: &Self::User) -> polariton::operation::OperationResponse<()> { + polariton_server::operations::result_to_op_resp::(do_handling(params, user, &self.factory).await) + } +} + +impl OperationCode for CrfItemDataProvider { + fn op_code() -> u8 { + CODE + } +} + +pub(super) fn crf_item_data_provider(factory: &std::sync::Arc) -> CrfItemDataProvider { + CrfItemDataProvider { + factory: factory.to_owned(), + } +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 2b7435f..e7309a8 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -86,6 +86,10 @@ mod garage_slots_order; mod garage_slot_select; mod garage_slot_dismantle; mod garage_slot_upgrade; +mod crf_earnings; +mod crf_list_query; +mod crf_vehicle_data; +mod crf_purchase; use polariton_server::operations::OperationsHandler; @@ -194,4 +198,8 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(garage_slot_select::garage_slot_selector()) .add(garage_slot_dismantle::garage_slot_dismantler()) .add(garage_slot_upgrade::garage_slot_upgrage_provider()) + .add(crf_earnings::robot_shop_user_earnings_provider()) + .add(crf_list_query::crf_item_list_query_provider(&init_ctx.factory)) + .add(crf_vehicle_data::crf_item_data_provider(&init_ctx.factory)) + .add(crf_purchase::crf_copy_to_bay_provider(&init_ctx.factory)) }