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(()) } }