mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Implement factory in main database, close #80
This commit is contained in:
1 parent
a346299de8
commit
3b909327d8
33 files changed
+741
-78
No files matched your search
@@ -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<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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user