mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
369 lines
19 KiB
Rust
369 lines
19 KiB
Rust
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<sea_orm::DatabaseConnection>,
|
|
cdn: std::sync::Arc<String>,
|
|
upload_duration: std::time::Duration,
|
|
}
|
|
|
|
impl FactoryDatabase {
|
|
pub async fn init(uri: &str, cdn: std::sync::Arc<String>) -> Result<Self, sea_orm::DbErr>{
|
|
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<sea_orm::DatabaseConnection>, cdn: std::sync::Arc<String>) -> 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<Option<(crate::schema::factory::vehicle::Model, crate::schema::user::Model)>, 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<Vec<(crate::schema::factory::vehicle_query::VehicleInfo, crate::schema::factory::vehicle_query::VehicleOwnerInfo)>, 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::<crate::schema::factory::vehicle_query::VehicleInfo, crate::schema::factory::vehicle_query::VehicleOwnerInfo>();
|
|
//.into_partial_model::<crate::schema::factory::vehicle_query::VehicleInfo, crate::schema::factory::vehicle_query::VehicleOwnerInfo>();
|
|
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<u32, u32> = 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<String, u32> = 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<Option<(VehicleInfo, VehicleQueryInfo)>, Box<dyn std::error::Error>> {
|
|
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<Vec<VehicleQueryInfo>, Box<dyn std::error::Error>> {
|
|
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<VehicleThumbnailInfo, Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<Vec<u8>>, colour_data: Option<Vec<u8>>) -> Result<(), Box<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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<dyn std::error::Error>> {
|
|
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(())
|
|
}
|
|
}
|