1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

feat-webgarages-121 (#129)

### Description

Closes #121

### Please confirm

- [x] I am the legal owner or represent the legal owner of all work submitted (including LLM-generated code, if any)
- [x] I consent to my changes being added to this FOSS project
- [x] I have confirmed that this does not add new errors or warnings with `utils/clippy.sh`
- [ ] This PR used LLMs to generate some or all of the code changes

Reviewed-on: https://git.ngram.ca/OpenJam/rc-servers/pulls/129
This commit is contained in:
NG (Graham)
2026-06-15 01:02:52 +00:00
committed by NGnius
parent a222745841
commit d2626bb47f
40 changed files with 4012 additions and 282 deletions

View File

@@ -28,17 +28,28 @@ impl CubeConfig {
pub fn load(root: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let file = std::fs::File::open(root.as_ref().join(CUBE_CONFIG_FILENAME))?;
let buffered = std::io::BufReader::new(file);
let result = serde_json::from_reader(buffered)?;
#[cfg(debug_assertions)]
{
let filename = root.as_ref().join(format!("{}.expanded.json", CUBE_CONFIG_FILENAME.trim_end_matches(".json")));
let file = std::fs::File::create(filename)?;
let buffered = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(buffered, &result)?;
let result = serde_json::from_reader::<_, Self>(buffered)?;
let result = result.expand_with_context();
let expanded_filename = root.as_ref().join(format!("{}.expanded.json", CUBE_CONFIG_FILENAME.trim_end_matches(".json")));
match std::fs::File::create(&expanded_filename) {
Ok(file) => {
let buffered = std::io::BufWriter::new(file);
serde_json::to_writer_pretty(buffered, &result)?;
},
Err(e) => {
log::info!("Skipping creating {}: {}", expanded_filename.display(), e);
}
}
Ok(result)
}
/// Populate more of the config for parts of it which need more context for choosing default values
fn expand_with_context(mut self) -> Self {
// TODO
self.cubes.values_mut().for_each(crate::persist::conversion::expand_conversion_data);
self
}
/// Performs configuration checks
/// Returns true if validation succeeds, false if failed
pub fn self_validate(&self, data_path: impl AsRef<std::path::Path>) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ use serde::{Serialize, Deserialize};
use polariton::operation::Typed;
use super::{WeaponData, WeaponUpgradeInfo, TechTreeData, MovementData};
use super::{WeaponData, WeaponUpgradeInfo, TechTreeData, MovementData, CubeConversionData};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Cube {
@@ -14,6 +14,7 @@ pub struct Cube {
pub weapon_upgrade: Option<WeaponUpgradeInfo>,
pub movement: Option<MovementData>,
pub tree: Option<TechTreeData>,
pub conversion: Option<CubeConversionData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@@ -50,6 +50,9 @@ pub use team_chooser::TeamChooser;
mod vehicle_validator;
pub use vehicle_validator::VehicleValidator;
pub(crate) mod conversion;
pub use conversion::{CubeConversionData, FromConversionData, ToConversionData};
const VALID_ROBOT: &[u8] = &[64,
0,
0,

View File

@@ -396,7 +396,7 @@ impl UserData {
Ok(())
}
async fn all_vehicles(&self) -> Result<Vec<oj_rc_database::schema::garage::Model>, oj_rc_database::sea_orm::DbErr> {
pub(super) async fn all_vehicles(&self) -> Result<Vec<oj_rc_database::schema::garage::Model>, oj_rc_database::sea_orm::DbErr> {
self.db.garages_by_user_id(self.account.id).await
}
@@ -1123,7 +1123,7 @@ impl <C: Clone + Send> super::User<C> for UserData {
let total_cost: u32 = self.garage_upgrades.increments.iter()
.map(|x| if x.cpu > minimum_upgrade_to_cpu { 0 } else { x.cost })
.sum();
log::info!("Bay CPU upgrade costs {}", total_cost);
log::debug!("Bay CPU upgrade costs {}", total_cost);
self.currency_sub_checked(super::CurrencyType::Free, total_cost as u64).await.map_err(|e| {
log::error!("Failed to debit user for cpu upgrade during fresh clone to slot {} for user_id {}: {}", slot, self.account.id, e);
DATABASE_ERR

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides, WebUser, GarageWebInfo};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;

View File

@@ -676,7 +676,19 @@ pub trait FactoryUser {
#[async_trait::async_trait]
pub trait WebUser: CommonUser {
async fn garages(&self) -> Result<Vec<GarageWebInfo>, Box<dyn std::error::Error>>;
async fn garage_by_id(&self, id: i32) -> Result<Option<VehicleData>, Box<dyn std::error::Error>>;
async fn save_garage(&self, vehicle: crate::persist::user::VehicleData, garage_id: Option<i32>, cpu_counter: &crate::cubes::CpuListParser, weapon_orderer: &crate::cubes::WeaponListParser) -> Result<(), Box<dyn std::error::Error>>;
async fn garage_id_selected(&self) -> Result<Option<i32>, Box<dyn std::error::Error>>;
}
pub struct GarageWebInfo {
pub id: i32,
pub slot: i32,
pub total_robot_cpu: i32,
pub bay_cpu: i32,
pub name: String,
pub creation_time: i64,
}
#[async_trait::async_trait]

View File

@@ -1,4 +1,109 @@
#[async_trait::async_trait]
impl super::WebUser for super::account_json::UserData {
// TODO
async fn garages(&self) -> Result<Vec<super::GarageWebInfo>, Box<dyn std::error::Error>> {
let vehicles = self.all_vehicles().await?;
Ok(vehicles.into_iter()
.map(|v| super::GarageWebInfo {
id: v.id,
slot: v.slot,
total_robot_cpu: v.total_robot_cpu,
bay_cpu: v.bay_cpu,
name: v.name.clone(),
creation_time: v.creation_time,
})
.collect()
)
}
async fn garage_by_id(&self, id: i32) -> Result<Option<super::VehicleData>, Box<dyn std::error::Error>> {
let garage = self.db.garage_by_id(id).await?;
Ok(garage.and_then(|g|
if g.user_id == self.account.id {
Some(super::VehicleData {
name: Some(g.name),
slot: g.slot,
robot_data: g.robot_data,
colour_data: g.colour_data,
weapon_order: Vec::default(),
crf_id: g.crf_id,
was_rated: Some(g.was_rated),
})
} else {
None
}
))
}
async fn save_garage(
&self,
vehicle: super::VehicleData,
garage_id: Option<i32>,
cpu_counter: &crate::cubes::CpuListParser,
weapon_orderer: &crate::cubes::WeaponListParser,
) -> Result<(), Box<dyn std::error::Error>> {
let cpu_counts = cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&vehicle.robot_data));
let weapon_order = weapon_orderer.guess_weapons(&mut std::io::Cursor::new(&vehicle.robot_data));
let mut entity = oj_rc_database::schema::garage::ActiveModel {
id: garage_id.map(oj_rc_database::sea_orm::ActiveValue::Set).unwrap_or(oj_rc_database::sea_orm::ActiveValue::NotSet),
user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id),
weapon_order: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::dump_csv(&weapon_order)),
robot_data: oj_rc_database::sea_orm::ActiveValue::Set(vehicle.robot_data),
colour_data: oj_rc_database::sea_orm::ActiveValue::Set(vehicle.colour_data),
crf_id: oj_rc_database::sea_orm::ActiveValue::NotSet,
name: if let Some(new_name) = vehicle.name {
oj_rc_database::sea_orm::ActiveValue::Set(format!("{} (imported)", new_name))
} else {
oj_rc_database::sea_orm::ActiveValue::Set(format!(
"Import {}",
garage_id.map(|x| x.to_string()).unwrap_or_else(|| "(new)".to_owned())
))
},
total_robot_cpu: oj_rc_database::sea_orm::ActiveValue::Set(cpu_counts.total as _),
total_cosmetic_cpu: oj_rc_database::sea_orm::ActiveValue::Set(cpu_counts.cosmetic as _),
was_rated: oj_rc_database::sea_orm::ActiveValue::Set(false),
movement_categories: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()), // FIXME
total_robot_ranking: oj_rc_database::sea_orm::ActiveValue::Set(0), // FIXME
bay_cpu: oj_rc_database::sea_orm::ActiveValue::Set(10_000),
//tutorial_robot: oj_rc_database::sea_orm::ActiveValue::Set(false),
mastery_level: oj_rc_database::sea_orm::ActiveValue::Set(0),
..super::initial_data::default_reset_slot()
};
// TODO charge currency for new or higher CPU bay
/*let total_cost: u32 = self.garage_upgrades.increments.iter()
.map(|x| if x.cpu > minimum_upgrade_to_cpu { 0 } else { x.cost })
.sum();
log::debug!("Bay CPU upgrade costs {}", total_cost);
self.currency_sub_checked(super::CurrencyType::Free, total_cost as u64).await.map_err(|e| {
log::error!("Failed to debit user for cpu upgrade during create of slot {} for user_id {}: {}", slot, self.account.id, e);
DATABASE_ERR
})?;*/
if let Some(garage_id) = garage_id {
// technically we don't need an account id to get the slot
// but this way ensures the user also owns the slot we retrieve
// (and no slot will be retrieved if the user doesn't own the slot, even if the garage id exists)
if let Some(slot) = self.db.slot_of_garage_by_id_and_user_id(garage_id, self.account.id).await? {
entity.slot = oj_rc_database::sea_orm::ActiveValue::Set(slot);
self.db.update_garage_by_user_id_and_slot(entity, self.account.id, slot).await?;
Ok(())
} else {
Err(format!("Invalid vehicle ID {} for user {}", garage_id, self.account.id).into())
}
} else {
let now = chrono::Utc::now().timestamp();
let new_slot = self.db.garage_max_slot_by_user_id(self.account.id).await? + 1;
let uuid = super::uuid_sanitize(now);
entity.slot = oj_rc_database::sea_orm::ActiveValue::Set(new_slot);
entity.creation_time = oj_rc_database::sea_orm::ActiveValue::Set(now);
entity.uuid = oj_rc_database::sea_orm::ActiveValue::Set(uuid);
entity.thumbnail_version = oj_rc_database::sea_orm::ActiveValue::Set(0);
entity.selected = oj_rc_database::sea_orm::ActiveValue::Set(false);
self.db.insert_garage(entity).await?;
Ok(())
}
}
async fn garage_id_selected(&self) -> Result<Option<i32>, Box<dyn std::error::Error>> {
Ok(self.db.garage_selected(self.account.id).await?
.map(|x| x.id))
}
}