From 3b909327d84e8196753998dfea086ac96914d9e8 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 1 Feb 2026 18:14:16 -0500 Subject: [PATCH] Implement factory in main database, close #80 --- Cargo.lock | 4 + assets/robocraft/config.json | 2 +- rc_core/src/data/crf.rs | 4 +- rc_core/src/factory/adapter_enum.rs | 20 +- rc_core/src/persist/config/cubes_json.rs | 4 +- rc_core/src/persist/config/traits.rs | 2 +- rc_core/src/persist/user/account_json.rs | 4 + rc_core/src/persist/user/common.rs | 4 + rc_core/src/persist/user/factory.rs | 2 + rc_core/src/persist/user/traits.rs | 1 + rc_core/src/persist/vehicle_factory.rs | 2 + rc_database/Cargo.toml | 7 +- rc_database/src/factory_wrapper.rs | 368 ++++++++++++++++++ rc_database/src/lib.rs | 5 + ...126_000001_create_factory_vehicle_table.rs | 69 ++++ rc_database/src/migration/mod.rs | 4 + rc_database/src/schema/factory/mod.rs | 2 + rc_database/src/schema/factory/vehicle.rs | 58 +++ .../src/schema/factory/vehicle_query.rs | 32 ++ rc_database/src/schema/garage.rs | 8 + rc_database/src/schema/mod.rs | 2 + rc_database/src/schema/user.rs | 8 + rc_database/src/wrapper.rs | 118 +++--- rc_factory/src/arc/adapter.rs | 24 +- rc_factory/src/traits.rs | 3 + rc_factory_web/src/cli.rs | 4 + rc_factory_web/src/main.rs | 3 +- rc_lobby_room/src/main.rs | 2 +- rc_multiplayer/src/main.rs | 2 +- rc_services_room/src/main.rs | 2 +- rc_services_room/src/operations/crf_remove.rs | 44 +++ rc_services_room/src/operations/mod.rs | 3 + rc_singleplayer_room/src/main.rs | 2 +- 33 files changed, 741 insertions(+), 78 deletions(-) create mode 100644 rc_database/src/factory_wrapper.rs create mode 100644 rc_database/src/migration/m20260126_000001_create_factory_vehicle_table.rs create mode 100644 rc_database/src/schema/factory/mod.rs create mode 100644 rc_database/src/schema/factory/vehicle.rs create mode 100644 rc_database/src/schema/factory/vehicle_query.rs create mode 100644 rc_services_room/src/operations/crf_remove.rs diff --git a/Cargo.lock b/Cargo.lock index 8db16bc..86fcf32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2546,9 +2546,13 @@ dependencies = [ name = "oj_rc_database" version = "1.1.0" dependencies = [ + "async-trait", "itertools", + "libfj", + "oj_rc_factory", "sea-orm", "sea-orm-migration", + "serde_json", ] [[package]] diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index 215f5f5..8829f87 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14531,7 +14531,7 @@ }, "factory": { "adapter": { - "variant": "Arc", + "variant": "BuiltIn", "uri": "sqlite:../data/robocraft/rc_archive.db?mode=ro" } }, diff --git a/rc_core/src/data/crf.rs b/rc_core/src/data/crf.rs index 7eb9e96..043c9bb 100644 --- a/rc_core/src/data/crf.rs +++ b/rc_core/src/data/crf.rs @@ -149,8 +149,8 @@ impl std::convert::From for ItemResult { 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 _, + 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, diff --git a/rc_core/src/factory/adapter_enum.rs b/rc_core/src/factory/adapter_enum.rs index 353496c..b05c28a 100644 --- a/rc_core/src/factory/adapter_enum.rs +++ b/rc_core/src/factory/adapter_enum.rs @@ -1,5 +1,6 @@ pub enum Factory { Arc(oj_rc_factory::arc::ArcAdapter), + Primary(oj_rc_database::FactoryDatabase), Custom(Box), None, } @@ -9,6 +10,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { async fn vehicle(&self, id: i32) -> Result, Box> { match self { Self::Arc(x) => x.vehicle(id).await, + Self::Primary(x) => x.vehicle(id).await, Self::Custom(x) => x.vehicle(id).await, Self::None => Ok(None), } @@ -17,6 +19,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { match self { Self::Arc(x) => x.list(query).await, + Self::Primary(x) => x.list(query).await, Self::Custom(x) => x.list(query).await, Self::None => Ok(Vec::default()), } @@ -25,6 +28,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { async fn upload(&self, vehicle: oj_rc_factory::VehicleUploadInfo) -> Result> { match self { Self::Arc(x) => x.upload(vehicle).await, + Self::Primary(x) => x.upload(vehicle).await, Self::Custom(x) => x.upload(vehicle).await, Self::None => Ok(oj_rc_factory::VehicleThumbnailInfo { id: i32::MIN, @@ -37,6 +41,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { async fn rate_vehicle(&self, id: i32, combat: i32, cosmetic: i32) -> Result<(), Box> { match self { Self::Arc(x) => x.rate_vehicle(id, combat, cosmetic).await, + Self::Primary(x) => x.rate_vehicle(id, combat, cosmetic).await, Self::Custom(x) => x.rate_vehicle(id, combat, cosmetic).await, Self::None => Ok(()), } @@ -45,6 +50,7 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { async fn purchase(&self, id: i32) -> Result<(), Box> { match self { Self::Arc(x) => x.purchase(id).await, + Self::Primary(x) => x.purchase(id).await, Self::Custom(x) => x.purchase(id).await, Self::None => Ok(()), } @@ -53,14 +59,25 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { 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::Primary(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(()), } } + async fn remove_vehicle(&self, id: i32, user_id: i32) -> Result<(), Box> { + match self { + Self::Arc(x) => x.remove_vehicle(id, user_id).await, + Self::Primary(x) => x.remove_vehicle(id, user_id).await, + Self::Custom(x) => x.remove_vehicle(id, user_id).await, + Self::None => Ok(()), + } + } + async fn set_featured(&self, id: i32, is_featured: bool) -> Result<(), Box> { match self { Self::Arc(x) => x.set_featured(id, is_featured).await, + Self::Primary(x) => x.set_featured(id, is_featured).await, Self::Custom(x) => x.set_featured(id, is_featured).await, Self::None => Ok(()), } @@ -68,9 +85,10 @@ impl oj_rc_factory::VehicleFactoryAdapter for Factory { } impl Factory { - pub async fn from_config(conf: &crate::persist::FactoryConfig, settings: &crate::persist::config::ServerConfig) -> Result> { + pub async fn from_config(conf: &crate::persist::FactoryConfig, settings: &crate::persist::config::ServerConfig, builtin_factory_provider: &(dyn (Fn() -> oj_rc_database::FactoryDatabase) + Sync)) -> Result> { Ok(match &conf.adapter { crate::persist::AdapterSettings::Arc(x) => Self::Arc(oj_rc_factory::arc::ArcAdapter::init(&x.uri, x.show_expired, settings.cdn_url.to_owned(), x.override_cdn, x.spoof_username).await?), + crate::persist::AdapterSettings::BuiltIn => Self::Primary(builtin_factory_provider()), crate::persist::AdapterSettings::None => Self::None, }) } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 7faeb42..5181823 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -310,8 +310,8 @@ impl super::ConfigProvider for CubeConfig { } } - async fn factory(&self) -> Result> { - crate::factory::Factory::from_config(&self.factory, &>::server_config(self)).await + async fn factory(&self, builtin_factory_provider: &(dyn (Fn() -> oj_rc_database::FactoryDatabase) + Sync)) -> Result> { + crate::factory::Factory::from_config(&self.factory, &>::server_config(self), builtin_factory_provider).await } fn cubes(&self) -> &'_ indexmap::IndexMap { diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index b522d20..46d3201 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -20,7 +20,7 @@ pub trait ConfigProvider { fn public_channels(&self) -> Typed; fn server_config(&self) -> ServerConfig; fn garage_upgrades(&self) -> GarageUpgrades; - async fn factory(&self) -> Result>; + async fn factory(&self, builtin_factory_provider: &(dyn (Fn() -> oj_rc_database::FactoryDatabase) + Sync)) -> Result>; fn cubes(&self) -> &'_ indexmap::IndexMap; fn chat_system_config(&self) -> ChatSystemConfig; fn gamemode_events(&self) -> GameEventSequence; diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 22fb658..6bae82a 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -41,6 +41,10 @@ impl AccountProvider { }) } + pub fn factory_impl(&self) -> oj_rc_database::FactoryDatabase { + self.db.vehicle_factory(self.cdn.clone()) + } + pub async fn multiplayer_init(&self) -> Result<(), oj_rc_database::sea_orm::DbErr> { self.db.complete_all_games().await } diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs index b293868..dadb5eb 100644 --- a/rc_core/src/persist/user/common.rs +++ b/rc_core/src/persist/user/common.rs @@ -6,6 +6,10 @@ fn u64_negative_on_err(result: Result) -> i64 { #[async_trait::async_trait] impl super::CommonUser for UserData { + fn account_id(&self) -> i32 { + self.account.id + } + fn public_id(&self) -> &'_ str { &self.account.public_id } diff --git a/rc_core/src/persist/user/factory.rs b/rc_core/src/persist/user/factory.rs index 0e28e2f..43baf3e 100644 --- a/rc_core/src/persist/user/factory.rs +++ b/rc_core/src/persist/user/factory.rs @@ -23,6 +23,8 @@ impl super::FactoryUser for UserData { thumbnail: vehicle.thumbnail, added_by: self.account.public_id.clone(), added_by_display_name: self.account.display_name.clone(), + added_by_id: self.account.id, + garage_id: slot.id, cpu: slot.total_robot_cpu as u32, total_robot_ranking: slot.total_robot_ranking as u32, build_version: vehicle.version, diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index f5893e9..d9e378e 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -410,6 +410,7 @@ pub struct ResolvedVehicle { #[async_trait::async_trait] pub trait CommonUser: Send + Sync { + fn account_id(&self) -> i32; async fn resolve_config_vehicle(&self, vehicle: &crate::persist::config::VehicleInfo, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, cpu_counter: &crate::cubes::CpuListParser) -> Result; fn public_id(&self) -> &'_ str; fn is_mod(&self) -> bool; diff --git a/rc_core/src/persist/vehicle_factory.rs b/rc_core/src/persist/vehicle_factory.rs index 669d412..52ff28c 100644 --- a/rc_core/src/persist/vehicle_factory.rs +++ b/rc_core/src/persist/vehicle_factory.rs @@ -23,6 +23,8 @@ fn default_variant() -> AdapterSettings { pub enum AdapterSettings { #[serde(alias = "sqlite")] Arc(ArcFactorySettings), + #[serde(alias = "integrated")] + BuiltIn, None, } diff --git a/rc_database/Cargo.toml b/rc_database/Cargo.toml index f6cd3fb..c0d7cbb 100644 --- a/rc_database/Cargo.toml +++ b/rc_database/Cargo.toml @@ -12,9 +12,14 @@ all_databases = ["mysql", "postgres", "sqlite"] mysql = [ "sea-orm/sqlx-mysql" ] postgres = [ "sea-orm/sqlx-postgres" ] sqlite = [ "sea-orm/sqlx-sqlite" ] -default = [ "all_databases" ] +default = [ "all_databases", "factory" ] +factory = [ "oj_rc_factory", "libfj", "async-trait", "sea-orm/with-chrono", "serde_json" ] [dependencies] sea-orm = { version = "1.1.10", features = [ "runtime-tokio-rustls", "macros" ] } sea-orm-migration = "1.1.10" itertools = "0.14" +oj_rc_factory = { version = "*", path = "../rc_factory", optional = true } +async-trait = { workspace = true, optional = true } +libfj = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } diff --git a/rc_database/src/factory_wrapper.rs b/rc_database/src/factory_wrapper.rs new file mode 100644 index 0000000..213a929 --- /dev/null +++ b/rc_database/src/factory_wrapper.rs @@ -0,0 +1,368 @@ +use oj_rc_factory::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo, VehicleThumbnailInfo, VehicleUploadInfo}; +use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, sqlx::types::chrono, ActiveModelTrait, TransactionTrait}; + +pub struct FactoryDatabase { + orm: std::sync::Arc, + cdn: std::sync::Arc, + upload_duration: std::time::Duration, +} + +impl FactoryDatabase { + pub async fn init(uri: &str, cdn: std::sync::Arc) -> Result{ + let db = sea_orm::Database::connect(uri).await?; + Ok(Self { + orm: std::sync::Arc::new(db), + cdn, + upload_duration: std::time::Duration::from_secs(365 * 24 * 60 * 60), // 1 year + }) + } + + pub fn init_preconnected(orm: std::sync::Arc, cdn: std::sync::Arc) -> Self { + Self { + orm, + cdn, + upload_duration: std::time::Duration::from_secs(365 * 24 * 60 * 60), // 1 year + } + } + + async fn full_vehicle_by_id(&self, id: i32) -> Result, sea_orm::DbErr> { + Ok(crate::schema::factory::vehicle::Entity::find_by_id(id) + .find_also_related(crate::schema::user::Entity) + .one(self.orm.as_ref()) + .await? + .and_then(|(vehicle, user_opt)| user_opt.map(|user| (vehicle, user)))) + } + + async fn list_query(&self, query: libfj::robocraft::ListQuery) -> Result, sea_orm::DbErr> { + let mut query_builder = crate::schema::factory::vehicle::Entity::find() + .filter(crate::schema::factory::vehicle::Column::RemovedDate.is_null()) + .filter(crate::schema::factory::vehicle::Column::BanDate.is_null()) + .filter(crate::schema::factory::vehicle::Column::Buyable.eq(true)); + + match query.order { + libfj::robocraft::FactoryOrderType::Suggested => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::GetCount); }, + libfj::robocraft::FactoryOrderType::CombatRating => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::CombatRating); }, + libfj::robocraft::FactoryOrderType::CosmeticRating => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::CosmeticRating); }, + libfj::robocraft::FactoryOrderType::Added => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::AddedTime); }, + libfj::robocraft::FactoryOrderType::CPU => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::Cpu); }, + libfj::robocraft::FactoryOrderType::MostBought => { query_builder = query_builder.order_by_desc(crate::schema::factory::vehicle::Column::GetCount); }, + } + + if !query.text_filter.is_empty() { + let query_text = format!("%{}%", query.text_filter.replace('%', "")); + if query.player_filter { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(crate::schema::user::Column::PublicId.like(query_text.clone())) + .add(crate::schema::user::Column::DisplayName.like(query_text)) + ); + } else { + match query.text_search_field { + libfj::robocraft::FactoryTextSearchField::All => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(crate::schema::user::Column::PublicId.like(query_text.clone())) + .add(crate::schema::user::Column::DisplayName.like(query_text.clone())) + .add(crate::schema::factory::vehicle::Column::Name.like(query_text.clone())) + .add(crate::schema::factory::vehicle::Column::Description.like(query_text)) + ); + }, + libfj::robocraft::FactoryTextSearchField::Name => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(crate::schema::factory::vehicle::Column::Name.like(query_text.clone())) + ); + }, + libfj::robocraft::FactoryTextSearchField::Player => { + query_builder = query_builder.filter( + sea_orm::sea_query::Condition::any() + .add(crate::schema::user::Column::PublicId.like(query_text.clone())) + .add(crate::schema::user::Column::DisplayName.like(query_text)) + ); + }, + } + } + } + // TODO support movement and weapon filters + if query.minimum_cpu > 0 { + query_builder = query_builder.filter(crate::schema::factory::vehicle::Column::Cpu.gte(query.minimum_cpu as u32)); + } + if query.maximum_cpu < usize::MAX { + query_builder = query_builder.filter(crate::schema::factory::vehicle::Column::Cpu.lte(query.maximum_cpu as u32)); + } + + let query_builder = query_builder + .find_also_related(crate::schema::user::Entity) + .select_only() + // WARNING: jank sea_orm query generation (fix) + .column_as(crate::schema::factory::vehicle::Column::Id, "A_id") + .column_as(crate::schema::factory::vehicle::Column::UserId, "A_user_id") + .column_as(crate::schema::factory::vehicle::Column::CreationTime, "A_creation_time") + .column_as(crate::schema::factory::vehicle::Column::Name, "A_name") + .column_as(crate::schema::factory::vehicle::Column::Description, "A_description") + .column_as(crate::schema::factory::vehicle::Column::AddedTime, "A_added_time") + .column_as(crate::schema::factory::vehicle::Column::ExpiryTime, "A_expiry_time") + .column_as(crate::schema::factory::vehicle::Column::Cpu, "A_cpu") + .column_as(crate::schema::factory::vehicle::Column::TotalRobotRanking, "A_total_robot_ranking") + .column_as(crate::schema::factory::vehicle::Column::GetCount, "A_get_count") + .column_as(crate::schema::factory::vehicle::Column::Buyable, "A_buyable") + .column_as(crate::schema::factory::vehicle::Column::RemovedDate, "A_removed_date") + .column_as(crate::schema::factory::vehicle::Column::BanDate, "A_ban_date") + .column_as(crate::schema::factory::vehicle::Column::Featured, "A_featured") + .column_as(crate::schema::factory::vehicle::Column::BannerMessage, "A_banner_message") + .column_as(crate::schema::factory::vehicle::Column::CombatRating, "A_combat_rating") + .column_as(crate::schema::factory::vehicle::Column::CosmeticRating, "A_cosmetic_rating") + .column_as(crate::schema::factory::vehicle::Column::CubeAmounts, "A_cube_amounts") + .column_as(crate::schema::user::Column::PublicId, "B_public_id") + .column_as(crate::schema::user::Column::DisplayName, "B_display_name") + .into_model::(); + //.into_partial_model::(); + let pagination = query_builder.paginate(self.orm.as_ref(), query.page_size as u64); + Ok(pagination.fetch_page((query.page - 1) as u64).await? + .into_iter() + .filter_map(|(vehicle, user_opt)| user_opt.map(|user| (vehicle, user))) + .collect()) + } + + fn thumbnail_url(&self, id: i32) -> String { + format!("{}/roboshop/Live/{}", self.cdn, id) + } + + fn calculate_cube_amounts(&self, data: &[u8]) -> String { + let mut counts: std::collections::HashMap = std::collections::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: std::collections::HashMap = std::collections::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] +impl VehicleFactoryAdapter for FactoryDatabase { + async fn vehicle(&self, id: i32) -> Result, Box> { + let entities = self.full_vehicle_by_id(id).await?; + Ok(entities.map(|(vehicle, user)| ( + VehicleInfo { + id, + cube_data: vehicle.cube_data, + colour_data: vehicle.colour_data, + }, + VehicleQueryInfo { + id, + name: vehicle.name, + description: vehicle.description, + thumbnail: self.thumbnail_url(id), + added_by: user.public_id, + added_by_display_name: user.display_name, + added_date: vehicle.added_time.and_utc(), + expiry_date: vehicle.expiry_time.and_utc(), + cpu: vehicle.cpu as _, + total_robot_ranking: vehicle.total_robot_ranking as _, + rent_count: vehicle.get_count as _, + buy_count: vehicle.get_count as _, + removed_date: vehicle.removed_date.map(|t| t.and_utc()), + ban_date: vehicle.ban_date.map(|t| t.and_utc()), + featured: vehicle.featured, + buyable: vehicle.buyable, + banner_message: vehicle.banner_message, + combat_rating: vehicle.combat_rating, + cosmetic_rating: vehicle.cosmetic_rating, + cube_amounts: serde_json::from_str(&vehicle.cube_amounts).unwrap_or_default(), + }, + ))) + } + + async fn list(&self, query: libfj::robocraft::ListQuery) -> Result, Box> { + let entities = self.list_query(query).await?; + Ok(entities.into_iter().map(|(vehicle, user)| VehicleQueryInfo { + id: vehicle.id, + name: vehicle.name, + description: vehicle.description, + thumbnail: self.thumbnail_url(vehicle.id), + added_by: user.public_id, + added_by_display_name: user.display_name, + added_date: vehicle.added_time.and_utc(), + expiry_date: vehicle.expiry_time.and_utc(), + cpu: vehicle.cpu as _, + total_robot_ranking: vehicle.total_robot_ranking as _, + rent_count: vehicle.get_count as _, + buy_count: vehicle.get_count as _, + removed_date: vehicle.removed_date.map(|t| t.and_utc()), + ban_date: vehicle.ban_date.map(|t| t.and_utc()), + featured: vehicle.featured, + buyable: vehicle.buyable, + banner_message: vehicle.banner_message, + combat_rating: vehicle.combat_rating, + cosmetic_rating: vehicle.cosmetic_rating, + cube_amounts: serde_json::from_str(&vehicle.cube_amounts).unwrap_or_default(), + }).collect()) + } + + async fn upload(&self, vehicle: VehicleUploadInfo) -> Result> { + let now = chrono::Utc::now(); + let entity = crate::schema::factory::vehicle::ActiveModel { + id: sea_orm::ActiveValue::NotSet, + user_id: sea_orm::ActiveValue::Set(vehicle.added_by_id), + garage_id: sea_orm::ActiveValue::Set(vehicle.garage_id), + creation_time: sea_orm::ActiveValue::Set(now.timestamp()), + name: sea_orm::ActiveValue::Set(vehicle.name), + description: sea_orm::ActiveValue::Set(vehicle.description), + added_time: sea_orm::ActiveValue::Set(now.naive_utc()), + expiry_time: sea_orm::ActiveValue::Set((now + self.upload_duration).naive_utc()), + cpu: sea_orm::ActiveValue::Set(vehicle.cpu as i32), + total_robot_ranking: sea_orm::ActiveValue::Set(vehicle.total_robot_ranking as i32), + get_count: sea_orm::ActiveValue::Set(0), + buyable: sea_orm::ActiveValue::Set(true), + removed_date: sea_orm::ActiveValue::Set(None), + ban_date: sea_orm::ActiveValue::Set(None), + featured: sea_orm::ActiveValue::Set(false), + banner_message: sea_orm::ActiveValue::Set(None), + combat_rating: sea_orm::ActiveValue::Set(2.5), + cosmetic_rating: sea_orm::ActiveValue::Set(2.5), + cube_amounts: sea_orm::ActiveValue::Set(self.calculate_cube_amounts(&vehicle.cube_data)), + cube_data: sea_orm::ActiveValue::Set(vehicle.cube_data), + colour_data: sea_orm::ActiveValue::Set(vehicle.colour_data), + }; + let saved_entity = entity.insert(self.orm.as_ref()).await?; + Ok(VehicleThumbnailInfo { + id: saved_entity.id, + thumbnail: vehicle.thumbnail, + needs_upload: true, + }) + } + + async fn rate_vehicle(&self, id: i32, combat: i32, cosmetic: i32) -> Result<(), Box> { + let result = self.orm.as_ref().transaction::<'_, '_, _, (), sea_orm::DbErr>(|txn| { + Box::pin(async move { + let select_q = crate::schema::factory::vehicle::Entity::find_by_id(id) + .select_only() + .columns([ + crate::schema::factory::vehicle::Column::CombatRating, + crate::schema::factory::vehicle::Column::CosmeticRating, + crate::schema::factory::vehicle::Column::GetCount, + ]) + .into_tuple::<(f64, f64, i32)>(); + if let Some((curr_combat_rating, curr_cosmetic_rating, get_count)) = select_q.one(txn).await? { + let get_count = get_count.clamp(1, 100) as f64; + let next_combat_rating = curr_combat_rating + (((combat as f64) - curr_combat_rating) / get_count); + let next_cosmetic_rating = curr_cosmetic_rating + (((cosmetic as f64) - curr_cosmetic_rating) / get_count); + crate::schema::factory::vehicle::ActiveModel { + id: sea_orm::ActiveValue::Set(id), + cosmetic_rating: sea_orm::ActiveValue::Set(next_cosmetic_rating), + combat_rating: sea_orm::ActiveValue::Set(next_combat_rating), + ..Default::default() + }.update(txn).await?; + } + Ok(()) + }) + }).await; + if let Err(e) = result { + return match e { + sea_orm::TransactionError::Connection(x) => Err(x.into()), + sea_orm::TransactionError::Transaction(x) => Err(x.into()), + }; + } + Ok(()) + } + + /// Just update any purchase trackers + async fn purchase(&self, id: i32) -> Result<(), Box> { + crate::schema::factory::vehicle::Entity::update_many() + .col_expr(crate::schema::factory::vehicle::Column::GetCount, crate::schema::factory::vehicle::Column::GetCount.into_expr().add(1)) + .filter(crate::schema::factory::vehicle::Column::Id.eq(id)) + .exec(self.orm.as_ref()).await?; + Ok(()) + } + + async fn update_vehicle(&self, id: i32, cube_data: Option>, colour_data: Option>) -> Result<(), Box> { + let cube_amounts = if let Some(cube_data) = &cube_data { + sea_orm::ActiveValue::Set(self.calculate_cube_amounts(cube_data)) + } else { + sea_orm::ActiveValue::NotSet + }; + let result = self.orm.as_ref().transaction::<'_, '_, _, (), sea_orm::DbErr>(|txn| { + Box::pin(async move { + let select_q = crate::schema::factory::vehicle::Entity::find_by_id(id) + .select_only() + .columns([ + crate::schema::factory::vehicle::Column::Id, + ]); + if select_q.count(txn).await? == 1 { + let mut to_update = crate::schema::factory::vehicle::ActiveModel { + id: sea_orm::ActiveValue::Set(id), + cube_amounts, + ..Default::default() + }; + if let Some(cube_data) = cube_data { + to_update.cube_data = sea_orm::ActiveValue::Set(cube_data); + } + if let Some(colour_data) = colour_data { + to_update.colour_data = sea_orm::ActiveValue::Set(colour_data); + } + to_update.update(txn).await?; + } + Ok(()) + }) + }).await; + if let Err(e) = result { + return match e { + sea_orm::TransactionError::Connection(x) => Err(x.into()), + sea_orm::TransactionError::Transaction(x) => Err(x.into()), + }; + } + Ok(()) + } + + async fn remove_vehicle(&self, id: i32, user_id: i32) -> Result<(), Box> { + let now = chrono::Utc::now(); + let result = self.orm.as_ref().transaction::<'_, '_, _, (), sea_orm::DbErr>(|txn| { + Box::pin(async move { + let select_q = crate::schema::factory::vehicle::Entity::find_by_id(id) + .select_only() + .columns([ + crate::schema::factory::vehicle::Column::Id, + crate::schema::factory::vehicle::Column::UserId, + ]) + .into_tuple::<(i32, i32)>(); + if let Some((_selected_id, selected_user_id)) = select_q.one(txn).await? { + if selected_user_id != user_id { + //log::warn!("User {} tried to remove factory vehicle owned by {}", user_id, selected_user_id); + return Ok(()); + } + crate::schema::factory::vehicle::ActiveModel { + id: sea_orm::ActiveValue::Set(id), + buyable: sea_orm::ActiveValue::Set(false), + removed_date: sea_orm::ActiveValue::Set(Some(now.naive_utc())), + ..Default::default() + }.update(txn).await?; + } + Ok(()) + }) + }).await; + if let Err(e) = result { + return match e { + sea_orm::TransactionError::Connection(x) => Err(x.into()), + sea_orm::TransactionError::Transaction(x) => Err(x.into()), + }; + } + Ok(()) + } + + async fn set_featured(&self, id: i32, is_featured: bool) -> Result<(), Box> { + crate::schema::factory::vehicle::ActiveModel { + id: sea_orm::ActiveValue::Set(id), + featured: sea_orm::ActiveValue::Set(is_featured), + ..Default::default() + }.update(self.orm.as_ref()).await?; + Ok(()) + } +} diff --git a/rc_database/src/lib.rs b/rc_database/src/lib.rs index d30f75c..8a110d7 100644 --- a/rc_database/src/lib.rs +++ b/rc_database/src/lib.rs @@ -9,4 +9,9 @@ pub use wrapper::Database; mod metrics; pub use metrics::DatabaseMetrics; +#[cfg(feature = "factory")] +mod factory_wrapper; +#[cfg(feature = "factory")] +pub use factory_wrapper::FactoryDatabase; + pub use sea_orm; diff --git a/rc_database/src/migration/m20260126_000001_create_factory_vehicle_table.rs b/rc_database/src/migration/m20260126_000001_create_factory_vehicle_table.rs new file mode 100644 index 0000000..4e71b7a --- /dev/null +++ b/rc_database/src/migration/m20260126_000001_create_factory_vehicle_table.rs @@ -0,0 +1,69 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20260126_000001_create_factory_vehicle_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Factory Vehicles table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::factory::vehicle::Entity) + .col( + ColumnDef::new(crate::schema::factory::vehicle::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::UserId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-factory_vehicles-user_id") + .from(crate::schema::factory::vehicle::Entity, crate::schema::factory::vehicle::Column::UserId) + .to(crate::schema::user::Entity, crate::schema::user::Column::Id), + ) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::GarageId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-factory_vehicles-garage_id") + .from(crate::schema::factory::vehicle::Entity, crate::schema::factory::vehicle::Column::GarageId) + .to(crate::schema::garage::Entity, crate::schema::garage::Column::Id), + ) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::Name).string().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::Description).string().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::AddedTime).date_time().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::ExpiryTime).date_time().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::Cpu).integer().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::TotalRobotRanking).integer().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::GetCount).integer().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::Buyable).boolean().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::RemovedDate).date_time().null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::BanDate).date_time().null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::Featured).boolean().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::BannerMessage).string().null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CombatRating).double().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CosmeticRating).double().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CubeAmounts).string().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::CubeData).binary().not_null()) + .col(ColumnDef::new(crate::schema::factory::vehicle::Column::ColourData).binary().not_null()) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Factory Vehicles table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::factory::vehicle::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index e1af97c..b9ac0cf 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -13,6 +13,8 @@ mod m20250722_000001_create_game_event_table; mod m20250816_000001_add_fake_players; mod m20250918_000001_add_player_variant; mod m20251228_000001_create_score_table; +#[cfg(feature = "factory")] +mod m20260126_000001_create_factory_vehicle_table; pub struct Migrator; @@ -33,6 +35,8 @@ impl MigratorTrait for Migrator { Box::new(m20250816_000001_add_fake_players::Migration), Box::new(m20250918_000001_add_player_variant::Migration), Box::new(m20251228_000001_create_score_table::Migration), + #[cfg(feature = "factory")] + Box::new(m20260126_000001_create_factory_vehicle_table::Migration), ] } } diff --git a/rc_database/src/schema/factory/mod.rs b/rc_database/src/schema/factory/mod.rs new file mode 100644 index 0000000..2c5dc56 --- /dev/null +++ b/rc_database/src/schema/factory/mod.rs @@ -0,0 +1,2 @@ +pub mod vehicle; +pub mod vehicle_query; diff --git a/rc_database/src/schema/factory/vehicle.rs b/rc_database/src/schema/factory/vehicle.rs new file mode 100644 index 0000000..f28be12 --- /dev/null +++ b/rc_database/src/schema/factory/vehicle.rs @@ -0,0 +1,58 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, DeriveEntityModel)] +#[sea_orm(table_name = "factory_vehicles")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub user_id: i32, + pub garage_id: i32, + pub creation_time: i64, // seconds since unix epoch + pub name: String, + pub description: String, + pub added_time: DateTime, + pub expiry_time: DateTime, + pub cpu: i32, + pub total_robot_ranking: i32, + pub get_count: i32, + 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: String, + pub cube_data: Vec, + pub colour_data: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::super::user::Entity", + from = "Column::UserId", + to = "super::super::user::Column::Id" + )] + User, + #[sea_orm( + belongs_to = "super::super::garage::Entity", + from = "Column::UserId", + to = "super::super::garage::Column::Id" + )] + Garage, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/schema/factory/vehicle_query.rs b/rc_database/src/schema/factory/vehicle_query.rs new file mode 100644 index 0000000..0ab39b2 --- /dev/null +++ b/rc_database/src/schema/factory/vehicle_query.rs @@ -0,0 +1,32 @@ +use sea_orm::{DerivePartialModel, FromQueryResult}; +use sea_orm::entity::prelude::DateTime; + +#[derive(FromQueryResult, DerivePartialModel)] +#[sea_orm(entity = "crate::schema::factory::vehicle::Entity")] +pub struct VehicleInfo { + pub id: i32, + pub user_id: i32, + pub creation_time: i64, // seconds since unix epoch + pub name: String, + pub description: String, + pub added_time: DateTime, + pub expiry_time: DateTime, + pub cpu: i32, + pub total_robot_ranking: i32, + pub get_count: i32, + 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: String, +} + +#[derive(FromQueryResult, DerivePartialModel)] +#[sea_orm(entity = "crate::schema::user::Entity")] +pub struct VehicleOwnerInfo { + pub public_id: String, + pub display_name: String, +} diff --git a/rc_database/src/schema/garage.rs b/rc_database/src/schema/garage.rs index 3fe70db..c51f629 100644 --- a/rc_database/src/schema/garage.rs +++ b/rc_database/src/schema/garage.rs @@ -54,6 +54,8 @@ pub enum Relation { to = "super::user::Column::Id" )] User, + #[sea_orm(has_many = "super::factory::vehicle::Entity")] + FactoryUploads, } impl Related for Entity { @@ -62,6 +64,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::FactoryUploads.def() + } +} + impl ActiveModelBehavior for ActiveModel {} #[repr(i32)] diff --git a/rc_database/src/schema/mod.rs b/rc_database/src/schema/mod.rs index ceded99..4f332df 100644 --- a/rc_database/src/schema/mod.rs +++ b/rc_database/src/schema/mod.rs @@ -10,6 +10,8 @@ pub mod multiplayer_game; pub mod multiplayer_game_player; pub mod game_event; pub mod multiplayer_game_score; +#[cfg(feature = "factory")] +pub mod factory; pub fn parse_int_csv(s: &str) -> Vec { s.split(',').filter_map(|i_as_s| { diff --git a/rc_database/src/schema/user.rs b/rc_database/src/schema/user.rs index d6d291c..0c5e012 100644 --- a/rc_database/src/schema/user.rs +++ b/rc_database/src/schema/user.rs @@ -25,6 +25,8 @@ pub enum Relation { Campaigns, #[sea_orm(has_many = "super::multiplayer_game_player::Entity")] Player, + #[sea_orm(has_many = "super::factory::vehicle::Entity")] + FactoryUploads, } impl Related for Entity { @@ -57,4 +59,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::FactoryUploads.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index 4c8ff14..13f87cc 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -2,7 +2,7 @@ use sea_orm_migration::MigratorTrait; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, TransactionTrait}; pub struct Database { - orm: sea_orm::DatabaseConnection, + orm: std::sync::Arc, metrics: std::sync::Arc>, } @@ -14,14 +14,18 @@ impl Database { //let schema_manager = SchemaManager::new(&db); super::Migrator::up(&db, None).await?; Ok(Self { - orm: db, + orm: std::sync::Arc::new(db), metrics: metrics_data, }) } + pub fn vehicle_factory(&self, cdn: std::sync::Arc) -> super::FactoryDatabase { + super::FactoryDatabase::init_preconnected(self.orm.clone(), cdn) + } + pub async fn user_count(&self) -> Result { crate::schema::user::Entity::find() - .count(&self.orm) + .count(self.orm.as_ref()) .await } @@ -31,14 +35,14 @@ impl Database { sea_orm::sea_query::Func::lower(crate::schema::user::Column::DisplayName.into_expr()) ).eq(public_id.to_lowercase()) ) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn user_by_steam_id(&self, steam_id: u64) -> Result, sea_orm::DbErr> { crate::schema::user::Entity::find() .filter(crate::schema::user::Column::SteamId.eq(Some(steam_id.to_string()))) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -48,7 +52,7 @@ impl Database { sea_orm::sea_query::Func::lower(crate::schema::user::Column::Email.into_expr()) ).eq(email.to_lowercase()) ) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -67,13 +71,13 @@ impl Database { } pub async fn insert_user(&self, entity: crate::schema::user::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn user_aux_by_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { crate::schema::user_aux::Entity::find() .filter(crate::schema::user_aux::Column::UserId.eq(user_id)) - .all(&self.orm) + .all(self.orm.as_ref()) .await } @@ -81,12 +85,12 @@ impl Database { crate::schema::user_aux::Entity::find() .filter(crate::schema::user_aux::Column::UserId.eq(user_id)) .filter(crate::schema::user_aux::Column::Descriptor.eq(descriptor)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn insert_user_aux(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; + crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; Ok(()) } @@ -97,13 +101,13 @@ impl Database { .filter(crate::schema::user_aux::Column::UserId.eq(user_id)) .filter(crate::schema::user_aux::Column::Descriptor.eq(descriptor.clone())) .into_model::() - .one(&self.orm) + .one(self.orm.as_ref()) .await?; if let Some(id) = id_opt { // update entity.id = sea_orm::ActiveValue::Set(id.id); Ok(Some(crate::schema::user_aux::Entity::update(entity) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?)) } else { Ok(None) @@ -142,12 +146,12 @@ impl Database { pub async fn perms_by_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { crate::schema::permissions::Entity::find() .filter(crate::schema::permissions::Column::UserId.eq(user_id)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn insert_perms(&self, entity: crate::schema::permissions::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn update_perms_by_user_id(&self, mut entity: crate::schema::permissions::ActiveModel, user_id: i32) -> Result, sea_orm::DbErr> { @@ -156,12 +160,12 @@ impl Database { .column(crate::schema::permissions::Column::Id) .filter(crate::schema::permissions::Column::UserId.eq(user_id)) .into_model::() - .one(&self.orm) + .one(self.orm.as_ref()) .await?; if let Some(id) = id_opt { entity.id = sea_orm::ActiveValue::Set(id.id); Ok(Some(crate::schema::permissions::Entity::update(entity) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?)) } else { Ok(None) @@ -170,7 +174,7 @@ impl Database { pub async fn garage_count(&self) -> Result { crate::schema::garage::Entity::find() - .count(&self.orm) + .count(self.orm.as_ref()) .await } @@ -180,7 +184,7 @@ impl Database { .column_as(crate::schema::garage::Column::Slot.max(), "column") .filter(crate::schema::garage::Column::UserId.eq(user_id)) .into_model::>() - .one(&self.orm) + .one(self.orm.as_ref()) .await?; Ok(result.map(|x| *x).unwrap_or(0)) } @@ -189,7 +193,7 @@ impl Database { crate::schema::garage::Entity::find() .filter(crate::schema::garage::Column::UserId.eq(user_id)) .filter(crate::schema::garage::Column::Selected.eq(true)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -197,7 +201,7 @@ impl Database { crate::schema::garage::Entity::find() .filter(crate::schema::garage::Column::UserId.eq(user_id)) .filter(crate::schema::garage::Column::Slot.eq(garage_slot)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -205,35 +209,35 @@ impl Database { crate::schema::garage::Entity::find() .filter(crate::schema::garage::Column::UserId.eq(user_id)) .order_by_asc(crate::schema::garage::Column::Slot) - .all(&self.orm) + .all(self.orm.as_ref()) .await } pub async fn garage_by_uuid(&self, uuid: i64) -> Result, sea_orm::DbErr> { crate::schema::garage::Entity::find() .filter(crate::schema::garage::Column::Uuid.eq(uuid)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn garage_by_id(&self, id: i32) -> Result, sea_orm::DbErr> { crate::schema::garage::Entity::find_by_id(id) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn insert_garages(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::garage::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; + crate::schema::garage::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; Ok(()) } pub async fn insert_garage(&self, entity: crate::schema::garage::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn update_garage(&self, entity: crate::schema::garage::ActiveModel) -> Result { crate::schema::garage::Entity::update(entity) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await } @@ -244,12 +248,12 @@ impl Database { .filter(crate::schema::garage::Column::UserId.eq(user_id)) .filter(crate::schema::garage::Column::Slot.eq(slot)) .into_model::() - .one(&self.orm) + .one(self.orm.as_ref()) .await?; if let Some(id) = id_opt { entity.id = sea_orm::ActiveValue::Set(id.id); Ok(Some(crate::schema::garage::Entity::update(entity) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?)) } else { Ok(None) @@ -262,12 +266,12 @@ impl Database { .column(crate::schema::garage::Column::Id) .filter(crate::schema::garage::Column::Uuid.eq(uuid)) .into_model::() - .one(&self.orm) + .one(self.orm.as_ref()) .await?; if let Some(id) = id_opt { entity.id = sea_orm::ActiveValue::Set(id.id); Ok(Some(crate::schema::garage::Entity::update(entity) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?)) } else { Ok(None) @@ -307,7 +311,7 @@ impl Database { .filter(crate::schema::sanction::Column::UserId.eq(user_id)) .filter(crate::schema::sanction::Column::Descriptor.eq(desc)) .filter(crate::schema::sanction::Column::Acknowledged.is_null()) - .count(&self.orm) + .count(self.orm.as_ref()) .await } @@ -316,17 +320,17 @@ impl Database { .filter(crate::schema::sanction::Column::UserId.eq(user_id)) .filter(crate::schema::sanction::Column::Acknowledged.is_null()) .order_by_asc(crate::schema::sanction::Column::CreationTime) - .all(&self.orm) + .all(self.orm.as_ref()) .await } pub async fn insert_sanction(&self, entity: crate::schema::sanction::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn game_count(&self) -> Result { crate::schema::multiplayer_game::Entity::find() - .count(&self.orm) + .count(self.orm.as_ref()) .await } @@ -334,7 +338,7 @@ impl Database { crate::schema::multiplayer_game::Entity::find() .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .order_by_desc(crate::schema::multiplayer_game::Column::CreationTime) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -346,7 +350,7 @@ impl Database { .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) .order_by_desc(crate::schema::multiplayer_game::Column::CreationTime) //.into_model() - .one(&self.orm) + .one(self.orm.as_ref()) .await? .map(|(x, _)| x)) } @@ -359,7 +363,7 @@ impl Database { .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) .order_by_asc(crate::schema::multiplayer_game::Column::CreationTime) //.into_model() - .one(&self.orm) + .one(self.orm.as_ref()) .await? .and_then(|(game, player)| player.map(|player| (game, player)))) } @@ -368,7 +372,7 @@ impl Database { crate::schema::multiplayer_game::Entity::update_many() .col_expr(crate::schema::multiplayer_game::Column::IsComplete, sea_orm::sea_query::Expr::value(true)) .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?; Ok(()) } @@ -377,13 +381,13 @@ impl Database { crate::schema::multiplayer_game::Entity::update_many() .col_expr(crate::schema::multiplayer_game::Column::IsComplete, sea_orm::sea_query::Expr::value(true)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(false)) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?; Ok(()) } pub async fn insert_game(&self, entity: crate::schema::multiplayer_game::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn players_by_game_guid_and_completion(&self, game_guid: i64, is_complete: bool) -> Result, sea_orm::DbErr> { @@ -392,7 +396,7 @@ impl Database { .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) //.into_model::() - .all(&self.orm) + .all(self.orm.as_ref()) .await? .into_iter() .map(|(player, _)| player) @@ -407,7 +411,7 @@ impl Database { //.join(sea_orm::JoinType::InnerJoin, crate::schema::user::Relation::Player.def()) .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) - .all(&self.orm) + .all(self.orm.as_ref()) .await? .into_iter() .filter_map(|(player, user, _)| user.map(|user| (player, user))) @@ -423,7 +427,7 @@ impl Database { .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) .filter(crate::schema::multiplayer_game_player::Column::Team.eq(team)) - .all(&self.orm) + .all(self.orm.as_ref()) .await? .into_iter() .filter_map(|(player, user, _)| user.map(|user| (player, user))) @@ -438,7 +442,7 @@ impl Database { .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game::Column::IsComplete.eq(is_complete)) .filter(crate::schema::multiplayer_game_player::Column::Team.eq(team)) - .all(&self.orm) + .all(self.orm.as_ref()) .await? .into_iter() .map(|(player, _)| player) @@ -449,7 +453,7 @@ impl Database { Ok(crate::schema::multiplayer_game_player::Entity::find() .find_also_related(crate::schema::multiplayer_game::Entity) .filter(crate::schema::multiplayer_game::Column::Id.eq(game_id)) - .all(&self.orm) + .all(self.orm.as_ref()) .await? .into_iter() .map(|(player, _)| player) @@ -462,17 +466,17 @@ impl Database { .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) //.order_by_asc(crate::schema::multiplayer_game::Column::CreationTime) - .one(&self.orm) + .one(self.orm.as_ref()) .await? .and_then(|(_, player)| player)) } pub async fn insert_player(&self, entity: crate::schema::multiplayer_game_player::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn insert_players(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; + crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; Ok(()) } @@ -482,7 +486,7 @@ impl Database { is_claimed: sea_orm::Set(is_claimed), ..Default::default() }) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?; Ok(()) } @@ -493,18 +497,18 @@ impl Database { .filter(crate::schema::game_event::Column::End.gte(time)) .filter(crate::schema::game_event::Column::Variant.eq(variant)) .order_by_desc(crate::schema::game_event::Column::Start) - .one(&self.orm) + .one(self.orm.as_ref()) .await } pub async fn insert_game_event(&self, entity: crate::schema::game_event::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn score_by_player_id(&self, player_id: i32) -> Result, sea_orm::DbErr> { crate::schema::multiplayer_game_score::Entity::find() .filter(crate::schema::multiplayer_game_score::Column::PlayerId.eq(player_id)) - .one(&self.orm) + .one(self.orm.as_ref()) .await } @@ -514,17 +518,17 @@ impl Database { .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) .filter(crate::schema::multiplayer_game_score::Column::IsClaimed.eq(is_claimed)) .order_by_asc(crate::schema::multiplayer_game_player::Column::CreationTime) - .one(&self.orm) + .one(self.orm.as_ref()) .await .map(|opt| opt.and_then(|(_player, score)| score)) } pub async fn insert_score(&self, entity: crate::schema::multiplayer_game_score::ActiveModel) -> Result { - entity.insert(&self.orm).await + entity.insert(self.orm.as_ref()).await } pub async fn update_score(&self, entity: crate::schema::multiplayer_game_score::ActiveModel) -> Result { - crate::schema::multiplayer_game_score::Entity::update(entity).exec(&self.orm).await + crate::schema::multiplayer_game_score::Entity::update(entity).exec(self.orm.as_ref()).await } pub async fn score_claim(&self, score_id: i32) -> Result<(), sea_orm::DbErr> { @@ -533,7 +537,7 @@ impl Database { is_claimed: sea_orm::Set(true), ..Default::default() }) - .exec(&self.orm) + .exec(self.orm.as_ref()) .await?; Ok(()) } @@ -544,7 +548,7 @@ impl Database { .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) .filter(crate::schema::multiplayer_game_score::Column::IsClaimed.eq(is_claimed)) .order_by_asc(crate::schema::multiplayer_game_player::Column::CreationTime) - .count(&self.orm) + .count(self.orm.as_ref()) .await } diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs index 57442ac..6f02791 100644 --- a/rc_factory/src/arc/adapter.rs +++ b/rc_factory/src/arc/adapter.rs @@ -294,14 +294,10 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { if self.is_readonly { return Ok(()); } - // TODO this should probably be a transaction - if let Some(meta) = super::entities::robot_metadata::Entity::find_by_id(id as u32).one(&self.orm).await? { - super::entities::robot_metadata::ActiveModel { - id: sea_orm::ActiveValue::Set(id as u32), - buy_count: sea_orm::ActiveValue::Set(meta.buy_count + 1), - ..Default::default() - }.update(&self.orm).await?; - } + super::entities::robot_metadata::Entity::update_many() + .col_expr(super::entities::robot_metadata::Column::BuyCount, super::entities::robot_metadata::Column::BuyCount.into_expr().add(1)) + .filter(super::entities::robot_metadata::Column::Id.eq(id)) + .exec(&self.orm).await?; Ok(()) } @@ -325,6 +321,18 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { Ok(()) } + async fn remove_vehicle(&self, id: i32, _user_id: i32) -> Result<(), Box> { + if self.is_readonly { + return Ok(()); + } + super::entities::robot_metadata::ActiveModel { + id: sea_orm::ActiveValue::Set(id as u32), + buyable: sea_orm::ActiveValue::Set(0), + ..Default::default() + }.update(&self.orm).await?; + Ok(()) + } + async fn set_featured(&self, id: i32, is_featured: bool) -> Result<(), Box> { if self.is_readonly { return Ok(()); diff --git a/rc_factory/src/traits.rs b/rc_factory/src/traits.rs index fb5257e..e4720ff 100644 --- a/rc_factory/src/traits.rs +++ b/rc_factory/src/traits.rs @@ -7,6 +7,7 @@ pub trait VehicleFactoryAdapter: Send + Sync + 'static { /// 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>; + async fn remove_vehicle(&self, id: i32, user_id: i32) -> Result<(), Box>; async fn set_featured(&self, id: i32, is_featured: bool) -> Result<(), Box>; } @@ -64,6 +65,8 @@ pub struct VehicleUploadInfo { pub thumbnail: Vec, pub added_by: String, pub added_by_display_name: String, + pub added_by_id: i32, + pub garage_id: i32, pub cpu: u32, pub total_robot_ranking: u32, pub cube_data: Vec, diff --git a/rc_factory_web/src/cli.rs b/rc_factory_web/src/cli.rs index a07d819..1f2cfbe 100644 --- a/rc_factory_web/src/cli.rs +++ b/rc_factory_web/src/cli.rs @@ -14,6 +14,10 @@ pub struct CliArgs { /// Assets root #[arg(long, default_value_t = {"../assets/robocraft".to_string()})] pub assets: String, + + /// User data root + #[arg(long, default_value_t = {"../data/robocraft".to_string()})] + pub data: String, } impl CliArgs { diff --git a/rc_factory_web/src/main.rs b/rc_factory_web/src/main.rs index 8f82b71..6eb1202 100644 --- a/rc_factory_web/src/main.rs +++ b/rc_factory_web/src/main.rs @@ -25,7 +25,8 @@ async fn main() -> std::io::Result<()> { let cli_args = cli::CliArgs::get(); let conf = ConfigImpl::load(&cli_args.assets).map_err(io_error)?; - let factory_enum = >::factory(&conf).await.map_err(io_error)?; + let users = oj_rc_core::persist::user::UserImpl::load(&cli_args.data, &conf).await.map_err(io_error)?; + let factory_enum = >::factory(&conf, &|| users.factory_impl()).await.map_err(io_error)?; let factory_data = actix_web::web::Data::new(factory_enum); let mut handlebars_conf = handlebars::Handlebars::new(); diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index 53143ce..3ca234f 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -33,7 +33,7 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); - let factory = std::sync::Arc::new(>::factory::<'_, '_>(&config).await.expect("Bad vehicle factory (CRF) config")); + let factory = std::sync::Arc::new(>::factory(&config, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config")); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect, factory.clone(), parsers.cpu_counter(), parsers.weapon_order())); diff --git a/rc_multiplayer/src/main.rs b/rc_multiplayer/src/main.rs index 1abba0c..1082bf6 100644 --- a/rc_multiplayer/src/main.rs +++ b/rc_multiplayer/src/main.rs @@ -28,7 +28,7 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); users.multiplayer_init().await.expect("Multiplayer init task failed"); - let factory = std::sync::Arc::new(>::factory::<'_, '_>(&config).await.expect("Bad vehicle factory (CRF) config")); + let factory = std::sync::Arc::new(>::factory(&config, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config")); let parsers = std::sync::Arc::new(oj_rc_core::cubes::CubeParsers::new(&config)); let matches = matches::GameMatches::new(&config, parsers.clone(), factory.clone()); let matches_chann = matches.spawn(); diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs index 8ac0ead..e724d6e 100644 --- a/rc_services_room/src/main.rs +++ b/rc_services_room/src/main.rs @@ -33,7 +33,7 @@ async fn main() -> std::io::Result<()> { let cubes = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_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 factory = std::sync::Arc::new(>::factory(&cubes, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config")); let parsers = oj_rc_core::cubes::CubeParsers::new(&cubes); let init_ctx = std::sync::Arc::new(InitConfig { cubes, diff --git a/rc_services_room/src/operations/crf_remove.rs b/rc_services_room/src/operations/crf_remove.rs new file mode 100644 index 0000000..43f4d6a --- /dev/null +++ b/rc_services_room/src/operations/crf_remove.rs @@ -0,0 +1,44 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; +use oj_rc_factory::VehicleFactoryAdapter; + +const CODE: u8 = 91; + +const ID_PARAM_KEY: u8 = 94; // int; in +const EARNINGS_PARAM_KEY: u8 = 96; + +pub(super) struct FactoryRemoveVehicle { + factory: std::sync::Arc, +} + +#[async_trait::async_trait] +impl SimpleOperation for FactoryRemoveVehicle { + 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) { + let user_info = user.user()?; + let user_id = user_info.account_id(); + self.factory.remove_vehicle(factory_id, user_id).await.map_err(|e| { + log::error!("Failed to remove vehicle {} by user {} from factory: {}", factory_id, user_id, e); + SimpleOpError::with_message( + oj_rc_core::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to remove vehicle (for offset) from factory: {}", e) + ) + })?; + } + // TODO Factory earnings + params.insert(EARNINGS_PARAM_KEY, Typed::HashMap(vec![ + (Typed::Str("buyCount".into()), Typed::Int(0)), + (Typed::Str("earnings".into()), Typed::Int(0)), + ].into())); + Ok(params) + } +} + +pub(super) fn factory_remove_provider(factory: &std::sync::Arc) -> SimpleOpImpl { + SimpleOpImpl::new(FactoryRemoveVehicle { + factory: factory.to_owned() + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 32ba177..e0c4343 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -106,6 +106,7 @@ mod crf_shift_vehicle; mod crf_make_featured; mod crf_unmake_featured; mod tech_tree_unlock_cube; +mod crf_remove; use polariton_server::operations::OperationsHandler; @@ -236,4 +237,6 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(crf_unmake_featured::factory_featured_provider(&init_ctx.factory)) .add(polariton_server::operations::Ack::<101, _>::default()) // RestoreCRFFeaturedRobot unused and also redundant??? .add(tech_tree_unlock_cube::tech_tree_cube_unlock_provider(&init_ctx.cubes)) + .add(crf_remove::factory_remove_provider(&init_ctx.factory)) + .add(polariton_server::operations::Ack::<94, _>::default()) // ReportCommunityShopItemRequest FIXME: log reports } diff --git a/rc_singleplayer_room/src/main.rs b/rc_singleplayer_room/src/main.rs index 92646c7..9544afc 100644 --- a/rc_singleplayer_room/src/main.rs +++ b/rc_singleplayer_room/src/main.rs @@ -30,7 +30,7 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); - let factory = std::sync::Arc::new(>::factory::<'_, '_>(&config).await.expect("Bad vehicle factory (CRF) config")); + let factory = std::sync::Arc::new(>::factory(&config, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config")); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); let init_ctx = InitConfig {