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:
1
rc_society/src/api/garage/mod.rs
Normal file
1
rc_society/src/api/garage/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod plugins;
|
||||
124
rc_society/src/api/garage/plugins/bobocraft.rs
Normal file
124
rc_society/src/api/garage/plugins/bobocraft.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use oj_convert::{Bobocraft, BobocraftBlock};
|
||||
|
||||
pub struct RexLike {
|
||||
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
|
||||
}
|
||||
|
||||
impl RexLike {
|
||||
pub fn new(convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>) -> Self {
|
||||
Self {
|
||||
convert,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for RexLike {
|
||||
fn file_ext(&self) -> &'static str {
|
||||
"bobo"
|
||||
}
|
||||
|
||||
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
let data = Bobocraft::parse(upload).map_err(|e| {
|
||||
log::error!("Failed to parse bobocraft import: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let cubes = data.cubes.iter()
|
||||
.map(|b| oj_rc_core::cubes::Cube {
|
||||
id: b.cube_id,
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
z: b.z,
|
||||
orientation: b.orientation,
|
||||
}).collect();
|
||||
let cube_data = oj_rc_core::cubes::Cube::dump_list(cubes)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to write cube data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let colours = data.cubes.iter()
|
||||
.map(|b| oj_rc_core::cubes::Colour {
|
||||
colour: b.color.unwrap_or(0),
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
z: b.z,
|
||||
}).collect();
|
||||
let colour_data = oj_rc_core::cubes::Colour::dump_list(colours)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to write colour data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let upgraded = self.convert.upgrade_to_modern(
|
||||
&mut std::io::Cursor::new(&cube_data),
|
||||
&mut std::io::Cursor::new(&colour_data),
|
||||
).map_err(|e| {
|
||||
log::warn!("Failed to upgrade bobo import data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
|
||||
})?;
|
||||
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
|
||||
cube_data: upgraded.cube_data,
|
||||
colour_data: upgraded.colour_data,
|
||||
vehicle_name: Some(data.item_name),
|
||||
vehicle_author: Some(data.added_by_display_name),
|
||||
})
|
||||
}
|
||||
|
||||
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
let cubes = oj_rc_core::cubes::Cube::parse_list(&mut std::io::Cursor::new(&data.cube_data))
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to read cube data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let colours = oj_rc_core::cubes::Colour::parse_list(&mut std::io::Cursor::new(&data.colour_data))
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to read colour data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
if colours.len() != cubes.len() {
|
||||
log::error!("Failed to parse the same amount of cubes and colours (aborting)");
|
||||
return Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid);
|
||||
}
|
||||
let mut placements = Vec::with_capacity(colours.len());
|
||||
for i in 0..cubes.len() {
|
||||
let colour = &colours[i];
|
||||
let cube = &cubes[i];
|
||||
// assumption: cube and colours are in the same order
|
||||
let placement = BobocraftBlock {
|
||||
cube_id: cube.id,
|
||||
x: cube.x,
|
||||
y: cube.y,
|
||||
z: cube.z,
|
||||
orientation: cube.orientation,
|
||||
color: if (cube.x, cube.y, cube.z) == (colour.x, colour.y, colour.z) { Some(colour.colour) } else { None },
|
||||
};
|
||||
placements.push(placement);
|
||||
}
|
||||
let bobo = Bobocraft {
|
||||
item_id: 0,
|
||||
item_name: data.vehicle_name.clone().unwrap_or_else(|| "Exported vehicle".to_owned()),
|
||||
item_description: format!("Exported by OpenJam {} {}", env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_VERSION")),
|
||||
thumbnail: String::default(),
|
||||
added_by: data.vehicle_author.clone().unwrap_or_default(),
|
||||
added_by_display_name: data.vehicle_author.clone().unwrap_or_default(),
|
||||
added_date: chrono::Utc::now().naive_utc(),
|
||||
expiry_date: chrono::Utc::now().naive_utc(),
|
||||
cpu: 0, // TODO?
|
||||
total_robot_ranking: None, // TODO?
|
||||
buyable: true,
|
||||
buy_count: 0,
|
||||
unknown_count: 0,
|
||||
combat_rating: 5.0,
|
||||
cosmetic_rating: 5.0,
|
||||
featured: false,
|
||||
banner_message: None,
|
||||
offset: (0, 0, 0),
|
||||
cubes: placements,
|
||||
};
|
||||
bobo.dump()
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to write bobocraft format: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::Plugin for RexLike {}
|
||||
103
rc_society/src/api/garage/plugins/classic.rs
Normal file
103
rc_society/src/api/garage/plugins/classic.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use oj_convert::{Classic, ClassicBlock};
|
||||
|
||||
pub struct FifteenLike {
|
||||
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
|
||||
base_image: std::sync::Arc<Vec<u8>>,
|
||||
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>,
|
||||
}
|
||||
|
||||
impl FifteenLike {
|
||||
pub fn new(
|
||||
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
|
||||
base_image: Vec<u8>,
|
||||
cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>,
|
||||
) -> Self {
|
||||
Self {
|
||||
convert,
|
||||
base_image: std::sync::Arc::new(base_image),
|
||||
cpu_counter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for FifteenLike {
|
||||
fn file_ext(&self) -> &'static str {
|
||||
"png"
|
||||
}
|
||||
|
||||
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
let data = Classic::parse(upload).map_err(|e| {
|
||||
log::error!("Failed to parse RC15 import: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let cubes = data.cubes.iter()
|
||||
.map(|b| oj_rc_core::cubes::Cube {
|
||||
id: b.cube_id,
|
||||
x: b.x,
|
||||
y: b.y,
|
||||
z: b.z,
|
||||
orientation: b.orientation,
|
||||
}).collect();
|
||||
let cube_data = oj_rc_core::cubes::Cube::dump_list(cubes)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to write cube data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let converted = self.convert.convert_to_modern(
|
||||
&mut std::io::Cursor::new(&cube_data),
|
||||
).map_err(|e| {
|
||||
log::warn!("Failed to modernize RC15 import data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
|
||||
})?;
|
||||
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
|
||||
cube_data: converted.cube_data,
|
||||
colour_data: converted.colour_data,
|
||||
vehicle_name: Some(data.item_name),
|
||||
vehicle_author: data.item_author,
|
||||
})
|
||||
}
|
||||
|
||||
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
let cpu_count = self.cpu_counter.calculate_cpu(&mut std::io::Cursor::new(&data.cube_data)).total;
|
||||
let classified = self.convert.convert_to_classic(
|
||||
&mut std::io::Cursor::new(&data.cube_data),
|
||||
&mut std::io::Cursor::new(&data.colour_data),
|
||||
).map_err(|e| {
|
||||
log::warn!("Failed to classic-ify RC15 export data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
|
||||
})?;
|
||||
let cubes = oj_rc_core::cubes::Cube::parse_list(&mut std::io::Cursor::new(&classified))
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to read cube data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let mut placements = Vec::with_capacity(cubes.len());
|
||||
for cube in cubes.iter() {
|
||||
let placement = ClassicBlock {
|
||||
cube_id: cube.id,
|
||||
x: cube.x,
|
||||
y: cube.y,
|
||||
z: cube.z,
|
||||
orientation: cube.orientation,
|
||||
};
|
||||
placements.push(placement);
|
||||
}
|
||||
let classic = Classic {
|
||||
item_author: data.vehicle_author.clone(),
|
||||
game_version: Some(1), // once told me
|
||||
export_time: Some(chrono::Utc::now().to_rfc3339()),
|
||||
item_name: data.vehicle_name.clone().unwrap_or_else(|| "Exported vehicle".to_owned()),
|
||||
item_description: Some(format!("Exported by OpenJam {} {}", env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_VERSION"))),
|
||||
item_tier: 1, // TODO
|
||||
item_cpu: cpu_count,
|
||||
cubes: placements,
|
||||
};
|
||||
classic.dump(&self.base_image)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to dump the RC15 PNG data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::Plugin for FifteenLike {}
|
||||
102
rc_society/src/api/garage/plugins/factory.rs
Normal file
102
rc_society/src/api/garage/plugins/factory.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use libfj::robocraft::{FactoryRobotGetInfo, FactoryInfo};
|
||||
|
||||
pub struct FactoryLike {
|
||||
convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,
|
||||
}
|
||||
|
||||
impl FactoryLike {
|
||||
pub fn new(convert: std::sync::Arc<oj_rc_core::cubes::CubeConversionParser>,) -> Self {
|
||||
Self {
|
||||
convert,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_guess_json_format(upload: &[u8]) -> Option<FactoryRobotGetInfo> {
|
||||
let reader = Cursor::new(upload);
|
||||
match serde_json::from_reader::<_, FactoryRobotGetInfo>(reader) {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
let reader = Cursor::new(upload);
|
||||
match serde_json::from_reader::<_, FactoryInfo<FactoryRobotGetInfo>>(reader) {
|
||||
Ok(data) => Some(data.response),
|
||||
Err(e2) => {
|
||||
log::error!("Failed to parse json for factory-like vehicle import: (1){} (2){}", e, e2);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::vehicle_import::VehicleImportPlugin for FactoryLike {
|
||||
fn file_ext(&self) -> &'static str {
|
||||
"rcbup"
|
||||
}
|
||||
|
||||
fn import(&self, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
if let Some(upload_data) = Self::try_guess_json_format(upload) {
|
||||
use base64::Engine;
|
||||
let cube_data = base64::engine::general_purpose::STANDARD.decode(&upload_data.cube_data).map_err(|e| {
|
||||
log::error!("Bad base64 encoding of cube data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let colour_data = base64::engine::general_purpose::STANDARD.decode(&upload_data.cube_data).map_err(|e| {
|
||||
log::error!("Bad base64 encoding of colour data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
let upgraded = self.convert.upgrade_to_modern(
|
||||
&mut std::io::Cursor::new(&cube_data),
|
||||
&mut std::io::Cursor::new(&colour_data),
|
||||
).map_err(|e| {
|
||||
log::warn!("Failed to upgrade rcbup import data: {}", e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported
|
||||
})?;
|
||||
Ok(oj_rc_plugins::vehicle_import::VehicleImportData {
|
||||
cube_data: upgraded.cube_data,
|
||||
colour_data: upgraded.colour_data,
|
||||
vehicle_name: Some(upload_data.item_name),
|
||||
vehicle_author: Some(upload_data.added_by_display_name),
|
||||
})
|
||||
} else {
|
||||
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
fn export(&self, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
use base64::Engine;
|
||||
let json_data = FactoryRobotGetInfo {
|
||||
item_id: 0,
|
||||
item_name: data.vehicle_name.clone().unwrap_or_default(),
|
||||
item_description: String::default(),
|
||||
thumbnail: String::default(),
|
||||
added_by: data.vehicle_author.clone().unwrap_or_default(),
|
||||
added_by_display_name: data.vehicle_author.clone().unwrap_or_default(),
|
||||
added_date: "1970-01-01T00:00:01".to_owned(),
|
||||
expiry_date: "2100-01-01T00:00:01".to_owned(),
|
||||
rent_count: 0,
|
||||
buy_count: 0,
|
||||
cpu: 0, // TODO
|
||||
total_robot_ranking: 0, // TODO
|
||||
buyable: true,
|
||||
removed_date: None,
|
||||
ban_date: None,
|
||||
featured: false,
|
||||
banner_message: None,
|
||||
combat_rating: 5.0,
|
||||
cosmetic_rating: 5.0,
|
||||
cube_data: base64::engine::general_purpose::STANDARD.encode(&data.cube_data),
|
||||
colour_data: base64::engine::general_purpose::STANDARD.encode(&data.colour_data),
|
||||
cube_amounts: String::default(),
|
||||
};
|
||||
let output = serde_json::to_vec_pretty(&json_data).map_err(|e| {
|
||||
log::error!("Failed to convert vehicle `{:?}` to JSON: {}", json_data.item_name, e);
|
||||
oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid
|
||||
})?;
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl oj_rc_plugins::Plugin for FactoryLike {}
|
||||
61
rc_society/src/api/garage/plugins/mod.rs
Normal file
61
rc_society/src/api/garage/plugins/mod.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
mod factory;
|
||||
pub use factory::FactoryLike;
|
||||
|
||||
mod bobocraft;
|
||||
pub use bobocraft::RexLike;
|
||||
|
||||
mod classic;
|
||||
pub use classic::FifteenLike;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ImportPlugins {
|
||||
map: std::collections::HashMap<String, Box<dyn oj_rc_plugins::vehicle_import::VehicleImportPlugin>>,
|
||||
}
|
||||
|
||||
impl ImportPlugins {
|
||||
pub fn standard(
|
||||
assets_path: impl AsRef<std::path::Path>,
|
||||
parsers: &oj_rc_core::cubes::CubeParsers,
|
||||
) -> Self {
|
||||
let mut map = std::collections::HashMap::with_capacity(3);
|
||||
map.insert("rcbup".to_owned(), Box::new(FactoryLike::new(parsers.converter())) as _);
|
||||
map.insert("bobo".to_owned(), Box::new(RexLike::new(parsers.converter())) as _);
|
||||
let image_path = assets_path.as_ref().join("rc_export.png");
|
||||
log::debug!("Loading PNG for RC15 export steganography from {}", image_path.display());
|
||||
let image_data = std::fs::read(image_path).expect("Failed to read rc_export.png file required for exporting RC15 vehicles");
|
||||
map.insert("rc15".to_owned(), Box::new(FifteenLike::new(parsers.converter(), image_data, parsers.cpu_counter())) as _);
|
||||
Self {
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plugin_names(&self) -> impl std::iter::Iterator<Item=&'_ String> {
|
||||
let mut to_sort = self.map.keys().collect::<Vec<_>>();
|
||||
to_sort.sort_by_key(|x| x.to_lowercase());
|
||||
to_sort.into_iter()
|
||||
}
|
||||
|
||||
pub fn file_ext(&self, name: &str) -> Result<&'static str, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
if let Some(plugin) = self.map.get(name) {
|
||||
Ok(plugin.file_ext())
|
||||
} else {
|
||||
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_by_name(&self, name: &str, upload: &[u8]) -> Result<oj_rc_plugins::vehicle_import::VehicleImportData, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
if let Some(plugin) = self.map.get(name) {
|
||||
plugin.import(upload)
|
||||
} else {
|
||||
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_by_name(&self, name: &str, data: &oj_rc_plugins::vehicle_import::VehicleImportData) -> Result<Vec<u8>, oj_rc_plugins::vehicle_import::VehicleImportErrorCode> {
|
||||
if let Some(plugin) = self.map.get(name) {
|
||||
plugin.export(data)
|
||||
} else {
|
||||
Err(oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Unsupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod garage;
|
||||
|
||||
Reference in New Issue
Block a user