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

@@ -0,0 +1,334 @@
const COLOUR_COUNT: usize = 32;
struct ClassicModernTranslation {
id_mapping: ModernMapping,
offset: (i16, i16, i16),
orientation_mapping: OrientationMapping,
}
impl ClassicModernTranslation {
fn convert(&self, cube: &super::Cube) -> (super::Cube, super::Colour){
let (new_id, new_colour) = self.id_mapping.convert(cube);
let new_orientation = self.orientation_mapping.convert(cube);
let new_cube = super::Cube {
id: new_id,
x: (cube.x as u16).saturating_add_signed(self.offset.0) as _,
y: (cube.y as u16).saturating_add_signed(self.offset.1) as _,
z: (cube.z as u16).saturating_add_signed(self.offset.2) as _,
orientation: new_orientation,
};
let new_colour = super::Colour {
colour: new_colour,
x: new_cube.x,
y: new_cube.y,
z: new_cube.z,
};
(new_cube, new_colour)
}
}
#[allow(dead_code)]
enum ModernMapping {
Always(u32, u8),
ByOrientation(Vec<(u32, u8)>),
}
impl ModernMapping {
fn convert(&self, cube: &super::Cube) -> (u32, u8) {
match self {
Self::Always(id, colour) => (*id, *colour),
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
fn pretty(&self) -> String {
match self {
Self::Always(id, colour) => format!("ID:{}|colour#{}", id, colour),
Self::ByOrientation(mapping) => format!("o[0]ID:{}|colour#{}", mapping[0].0, mapping[0].1),
}
}
}
struct ModernClassicTranslation {
id_mapping: ClassicMapping,
offset: (i16, i16, i16),
orientation_mapping: OrientationMapping,
}
impl ModernClassicTranslation {
fn convert(&self, cube: &super::Cube, colour: &super::Colour) -> super::Cube {
let new_id = self.id_mapping.convert(cube, colour);
let new_orientation = self.orientation_mapping.convert(cube);
super::Cube {
id: new_id,
x: (cube.x as u16).saturating_add_signed(self.offset.0) as _,
y: (cube.y as u16).saturating_add_signed(self.offset.1) as _,
z: (cube.z as u16).saturating_add_signed(self.offset.2) as _,
orientation: new_orientation,
}
}
}
#[allow(dead_code)]
enum ClassicMapping {
Always(u32),
ByColour(Vec<u32>),
ByOrientation(Vec<u32>),
}
impl ClassicMapping {
fn convert(&self, cube: &super::Cube, colour: &super::Colour) -> u32 {
match self {
Self::Always(x) => *x,
Self::ByColour(v) => v[colour.colour as usize],
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
}
#[allow(dead_code)]
enum OrientationMapping {
Passthrough,
ByOrientation(Vec<u8>),
}
impl OrientationMapping {
fn convert(&self, cube: &super::Cube) -> u8 {
match self {
Self::Passthrough => cube.orientation,
Self::ByOrientation(v) => v[cube.orientation as usize],
}
}
}
#[derive(Debug)]
pub enum ConversionError {
UnknownCube {
id: u32,
index: usize,
position: (u8, u8, u8)
},
CubeParse(std::io::Error),
CubeDump(std::io::Error),
ColourParse(std::io::Error),
ColourDump(std::io::Error),
}
impl core::fmt::Display for ConversionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownCube { id, index, position } => write!(f, "Unknown cube ID {} at {} {:?}", id, index, position),
Self::CubeParse(e) => write!(f, "Failed to parse cube data: {}", e),
Self::CubeDump(e) => write!(f, "Failed to dump cube data: {}", e),
Self::ColourParse(e) => write!(f, "Failed to parse colour data: {}", e),
Self::ColourDump(e) => write!(f, "Failed to dump colour data: {}", e),
}
}
}
impl core::error::Error for ConversionError {}
pub struct ModernConversionResult{
pub cube_data: Vec<u8>,
pub colour_data: Vec<u8>,
}
pub struct CubeConversionParser {
classic_to_modern: std::collections::HashMap<u32, ClassicModernTranslation>,
modern_to_classic: std::collections::HashMap<u32, ModernClassicTranslation>,
known: std::collections::HashSet<u32>,
}
impl CubeConversionParser {
pub fn with_cubes<'a, I: std::iter::Iterator<Item=&'a crate::persist::Cube>>(cubes: I) -> Self {
let mut classic_to_modern = std::collections::HashMap::<u32, ClassicModernTranslation>::new();
let mut modern_to_classic = std::collections::HashMap::<u32, ModernClassicTranslation>::new();
let mut known = std::collections::HashSet::new();
for cube in cubes {
// populate classic_to_modern
let conversion_data = cube.conversion.clone().unwrap_or_else(crate::persist::conversion::actual_default_conversion);
for from in conversion_data.from.iter() {
let mapping = match from {
crate::persist::FromConversionData::Simple(_id) => ClassicModernTranslation {
id_mapping: ModernMapping::Always(cube.id, 0),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::FromConversionData::Complex { id: _, offset, colour } => ClassicModernTranslation {
id_mapping: ModernMapping::Always(cube.id, *colour),
offset: offset.unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
},
};
#[cfg(debug_assertions)]
if let Some(old_mapping) = classic_to_modern.get(&from.id()) {
log::debug!("Not overriding mapping classic ID {} from modern {} to modern {}", from.id(), old_mapping.id_mapping.pretty(), mapping.id_mapping.pretty());
} else {
classic_to_modern.insert(from.id(), mapping);
}
#[cfg(not(debug_assertions))]
if !classic_to_modern.contains_key(&from.id) {
classic_to_modern.insert(from.id(), mapping);
}
}
// populate modern_to_classic
let mapping = if let Some(to) = conversion_data.to {
match to {
crate::persist::ToConversionData::Simple(id) => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::ToConversionData::Complex { id, offset } => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(id),
offset: offset.unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
}
}
} else if !conversion_data.from.is_empty() {
if conversion_data.from.len() == 1 {
let from_first = conversion_data.from.first().unwrap();
match from_first {
crate::persist::FromConversionData::Simple(id) => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(*id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
},
crate::persist::FromConversionData::Complex { id, offset, colour: _ } => ModernClassicTranslation {
id_mapping: ClassicMapping::Always(*id),
offset: offset.map(|(x, y, z)| (-x, -y, -z)).unwrap_or_default(),
orientation_mapping: OrientationMapping::Passthrough,
},
}
} else {
// more than one conversion, build map
let default_id = conversion_data.from.first().unwrap().id();
let mut colour_map: Vec<u32> = (0..COLOUR_COUNT).map(|_| default_id).collect();
let mut is_default_set = false;
for from in conversion_data.from.iter() {
let colour = match from {
crate::persist::FromConversionData::Simple(_) => 0,
crate::persist::FromConversionData::Complex { colour, .. } => *colour,
};
if colour_map[colour as usize] != default_id { continue; }
if colour == 0 {
if !is_default_set {
is_default_set = true;
} else {
continue;
}
}
colour_map[colour as usize] = from.id();
}
ModernClassicTranslation {
id_mapping: ClassicMapping::ByColour(colour_map),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
}
}
} else {
ModernClassicTranslation {
id_mapping: ClassicMapping::Always(cube.id),
offset: (0, 0, 0),
orientation_mapping: OrientationMapping::Passthrough,
}
};
modern_to_classic.insert(cube.id, mapping);
// populate known cubes
known.insert(cube.id);
}
Self {
classic_to_modern,
modern_to_classic,
known,
}
}
/// Replace modern cubes with classic equivalents (based on cube ID)
pub fn convert_to_classic(&self, cubes: &mut dyn std::io::Read, colours: &mut dyn std::io::Read) -> Result<Vec<u8>, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let colour_data = super::parser::Colour::parse_list(colours).map_err(ConversionError::ColourParse)?;
for (i, (cube, colour)) in cube_data.iter_mut().zip(colour_data.iter()).enumerate() {
if let Some(translation) = self.modern_to_classic.get(&cube.id) {
let new_cube = translation.convert(cube, colour);
*cube = new_cube;
} else {
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
}
}
super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)
}
/// Replace classic cubes with modern equivalents (based on cube ID)
pub fn convert_to_modern(&self, cubes: &mut dyn std::io::Read) -> Result<ModernConversionResult, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let mut colour_data = Vec::with_capacity(cube_data.len());
for (i, cube) in cube_data.iter_mut().enumerate() {
if let Some(translation) = self.classic_to_modern.get(&cube.id) {
let (new_cube, new_colour) = translation.convert(cube);
log::trace!("cube {} RC15 ID {} -> Modern ID {} colour {} ({}, {}, {})", i, cube.id, new_cube.id, new_colour.colour, cube.x, cube.y, cube.z);
*cube = new_cube;
colour_data.push(new_colour);
} else if !self.known.contains(&cube.id){
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
} else {
log::debug!("No translation found for cube {} ({}, {}, {}) id {} (a valid modern ID)", i, cube.x, cube.y, cube.z, cube.id);
colour_data.push(super::Colour {
colour: 0,
x: cube.x,
y: cube.y,
z: cube.z,
});
}
}
#[cfg(debug_assertions)]
if cube_data.len() != colour_data.len() {
log::error!("Emitted different lengths of cube and colour data; {} cubes != {} colours", cube_data.len(), colour_data.len());
return Err(ConversionError::UnknownCube {
id: 0,
index: cube_data.len(),
position: (0, 0, 0),
});
}
let cube_bytes = super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)?;
let colour_bytes = super::parser::Colour::dump_list(colour_data).map_err(ConversionError::ColourDump)?;
Ok(ModernConversionResult {
cube_data: cube_bytes,
colour_data: colour_bytes,
})
}
/// Replace unknown cubes with modern equivalents (based on cube ID), skipping known cube IDs
pub fn upgrade_to_modern(&self, cubes: &mut dyn std::io::Read, colours: &mut dyn std::io::Read) -> Result<ModernConversionResult, ConversionError> {
let mut cube_data = super::parser::Cube::parse_list(cubes).map_err(ConversionError::CubeParse)?;
let mut colour_data = super::parser::Colour::parse_list(colours).map_err(ConversionError::ColourParse)?;
for (i, (cube, colour)) in cube_data.iter_mut().zip(colour_data.iter_mut()).enumerate() {
if self.known.contains(&cube.id) { continue; }
if let Some(translation) = self.classic_to_modern.get(&cube.id) {
let (new_cube, new_colour) = translation.convert(cube);
*cube = new_cube;
*colour = new_colour;
} else {
return Err(ConversionError::UnknownCube {
id: cube.id,
index: i,
position: (cube.x, cube.y, cube.z),
});
}
}
let cube_bytes = super::parser::Cube::dump_list(cube_data).map_err(ConversionError::CubeDump)?;
let colour_bytes = super::parser::Colour::dump_list(colour_data).map_err(ConversionError::ColourDump)?;
Ok(ModernConversionResult {
cube_data: cube_bytes,
colour_data: colour_bytes,
})
}
}

View File

@@ -1,4 +1,5 @@
mod parser;
pub use parser::{Colour, Cube};
mod weapon_list;
pub use weapon_list::WeaponListParser;
@@ -21,6 +22,9 @@ pub use rotations::CUBE_ROTATIONS;
mod graph;
pub use graph::{CubeGraph, CellPoint};
mod conversion;
pub use conversion::CubeConversionParser;
pub const CRYSTAL_ID: u32 = 3950293873;
pub const CLASP_ID: u32 = 606866102;
@@ -31,6 +35,7 @@ pub struct CubeParsers {
cpu_counter: std::sync::Arc<CpuListParser>,
locations: std::sync::Arc<CubeLocationsParser>,
offset: std::sync::Arc<OffsetParser>,
converter: std::sync::Arc<CubeConversionParser>,
}
impl CubeParsers {
@@ -41,6 +46,7 @@ impl CubeParsers {
cpu_counter: std::sync::Arc::new(CpuListParser::with_cubes(cubes.values())),
locations: std::sync::Arc::new(CubeLocationsParser::with_cubes(cubes.values())),
offset: std::sync::Arc::new(OffsetParser::with_cubes(cubes.values())),
converter: std::sync::Arc::new(CubeConversionParser::with_cubes(cubes.values())),
}
}
@@ -59,4 +65,8 @@ impl CubeParsers {
pub fn offset(&self) -> std::sync::Arc<OffsetParser> {
self.offset.clone()
}
pub fn converter(&self) -> std::sync::Arc<CubeConversionParser> {
self.converter.clone()
}
}

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