diff --git a/Cargo.lock b/Cargo.lock index fc1ec7b..0a4f823 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ "actix-web", "bitflags", "bytes", - "derive_more", + "derive_more 2.1.1", "futures-core", "http-range", "log", @@ -57,7 +57,7 @@ dependencies = [ "brotli", "bytes", "bytestring", - "derive_more", + "derive_more 2.1.1", "encoding_rs", "flate2", "foldhash", @@ -90,7 +90,7 @@ dependencies = [ "actix-session", "actix-utils", "actix-web", - "derive_more", + "derive_more 2.1.1", "futures-core", "serde", "tracing", @@ -106,6 +106,44 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "actix-multipart" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5118a26dee7e34e894f7e85aa0ee5080ae4c18bf03c0e30d49a80e418f00a53" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "derive_more 0.99.20", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "rand 0.8.6", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11eb847f49a700678ea2fa73daeb3208061afa2b9d1a8527c03390f4c4a1c6b" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "actix-router" version = "0.5.4" @@ -167,7 +205,7 @@ dependencies = [ "actix-utils", "actix-web", "anyhow", - "derive_more", + "derive_more 2.1.1", "rand 0.9.4", "serde", "serde_json", @@ -203,7 +241,7 @@ dependencies = [ "bytestring", "cfg-if", "cookie 0.16.2", - "derive_more", + "derive_more 2.1.1", "encoding_rs", "foldhash", "futures-core", @@ -520,6 +558,15 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -915,6 +962,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -958,6 +1014,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.10.0" @@ -1059,6 +1121,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1256,6 +1324,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + [[package]] name = "derive_more" version = "2.1.1" @@ -1271,7 +1352,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -1395,6 +1476,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1465,6 +1558,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -1786,6 +1894,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1821,6 +1938,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -2457,6 +2588,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -2831,6 +2968,18 @@ dependencies = [ "zip", ] +[[package]] +name = "oj_convert" +version = "0.3.0" +dependencies = [ + "bytes", + "chrono", + "png", + "postcard", + "serde", + "serde_json", +] + [[package]] name = "oj_polariton_auth" version = "1.3.0" @@ -3163,8 +3312,10 @@ version = "1.3.0" dependencies = [ "actix-files", "actix-identity", + "actix-multipart", "actix-session", "actix-web", + "base64 0.22.1", "chrono", "clap", "cookie 0.18.1", @@ -3173,7 +3324,9 @@ dependencies = [ "handlebars", "libfj", "log", + "oj_convert", "oj_rc_core", + "oj_rc_plugins", "serde", "serde_json", "tokio", @@ -3299,6 +3452,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "parse-size" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b" + [[package]] name = "password-hash" version = "0.5.0" @@ -3436,6 +3595,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polariton" version = "0.6.0" @@ -3484,6 +3656,19 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -4097,6 +4282,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.40" @@ -4233,7 +4431,7 @@ dependencies = [ "async-trait", "bigdecimal", "chrono", - "derive_more", + "derive_more 2.1.1", "futures-util", "log", "mac_address", @@ -4473,6 +4671,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4977,6 +5184,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "2.0.18" diff --git a/Cargo.toml b/Cargo.toml index 311e08e..0db5ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,3 +49,5 @@ hex = "0.4" base64 = "0.22" #oj_serdes = { version = "0.3.0", path = "../oj_core/serdes" } oj_serdes = "0.3.0" +oj_convert = { version = "0.3.0", path = "../oj_core/convert" } +#oj_convert = "0.3.0" diff --git a/assets/robocraft/rc_export.png b/assets/robocraft/rc_export.png new file mode 100644 index 0000000..92aa315 Binary files /dev/null and b/assets/robocraft/rc_export.png differ diff --git a/assets/templates/rc_society/dashboard.html.hbs b/assets/templates/rc_society/dashboard.html.hbs index c4b1211..787c057 100644 --- a/assets/templates/rc_society/dashboard.html.hbs +++ b/assets/templates/rc_society/dashboard.html.hbs @@ -11,144 +11,19 @@ - + - + - + {{> stylesheet}}
@@ -200,7 +75,7 @@
Total
{{form.debug.user_id}}/{{form.debug.user_id}}
- +
View My Garages
diff --git a/assets/templates/rc_society/garage_import.html.hbs b/assets/templates/rc_society/garage_import.html.hbs new file mode 100644 index 0000000..a393782 --- /dev/null +++ b/assets/templates/rc_society/garage_import.html.hbs @@ -0,0 +1,79 @@ + + + + + + Garages - Openjam + + + + + + + + + + + + + + + + + + + + {{> stylesheet}} + + +
+
+ Welcome {{form.display_name}}! | + Home +
+

{{form.display_name}} Garage {{#if form.garage}}#{{form.garage.slot}}{{/if}} Import

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} + {{#if form.success}} +
+

{{form.success}}

+
+ {{/if}} + +
+
+ {{#if form.garage}} + + {{else}} + + {{/if}} +
+ + +
+
+ +
+
+ +
+
+
+ + +
+ + diff --git a/assets/templates/rc_society/garage_info.html.hbs b/assets/templates/rc_society/garage_info.html.hbs new file mode 100644 index 0000000..499e0a6 --- /dev/null +++ b/assets/templates/rc_society/garage_info.html.hbs @@ -0,0 +1,91 @@ + + + + + + Garage {{form.garage.slot}} - Openjam + + + + + + + + + + + + + + + + + + + + {{> stylesheet}} + + +
+
+ Welcome {{form.display_name}}! | + Home +
+

{{form.display_name}} Garage #{{form.garage.slot}}

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} + +
+
+
+ + {{#each form.exporters}} + + {{/each}} + +
+
+
+ +
+
Name
+
{{form.garage.name}}
+
Slot
+
{{form.garage.slot}}
+
Database ID
+
{{form.garage.garage_id}}
+
+
+
+ + +
+ + diff --git a/assets/templates/rc_society/garage_list.html.hbs b/assets/templates/rc_society/garage_list.html.hbs new file mode 100644 index 0000000..ae5c844 --- /dev/null +++ b/assets/templates/rc_society/garage_list.html.hbs @@ -0,0 +1,107 @@ + + + + + + Garages - Openjam + + + + + + + + + + + + + + + + + + + + {{> stylesheet}} + + +
+
+ Welcome {{form.display_name}}! | + Home +
+

{{form.display_name}} Garages

+ {{#if error}} +
+

{{error}}

+
+ {{/if}} + +
+ + {{#each form.garages}} +
+ +
+
Name
+
{{this.name}}
+
CPU
+
{{this.cpu}}/{{this.max_cpu}}
+
Slot
+
{{this.slot}}
+
Database ID
+
{{this.garage_id}}
+
Creation
+
+
+ +
+ View Details +
+
+
+ {{/each}} +
+ + +
+ + diff --git a/assets/templates/rc_society/index.html.hbs b/assets/templates/rc_society/index.html.hbs index 2d2a359..dc7c443 100644 --- a/assets/templates/rc_society/index.html.hbs +++ b/assets/templates/rc_society/index.html.hbs @@ -22,132 +22,7 @@ - + {{> stylesheet}}
diff --git a/assets/templates/rc_society/stylesheet.html.hbs b/assets/templates/rc_society/stylesheet.html.hbs new file mode 100644 index 0000000..88c05bb --- /dev/null +++ b/assets/templates/rc_society/stylesheet.html.hbs @@ -0,0 +1,158 @@ + + + diff --git a/rc_core/src/cubes/conversion.rs b/rc_core/src/cubes/conversion.rs new file mode 100644 index 0000000..70107a7 --- /dev/null +++ b/rc_core/src/cubes/conversion.rs @@ -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), + ByOrientation(Vec), +} + +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), +} + +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, + pub colour_data: Vec, +} + +pub struct CubeConversionParser { + classic_to_modern: std::collections::HashMap, + modern_to_classic: std::collections::HashMap, + known: std::collections::HashSet, +} + +impl CubeConversionParser { + pub fn with_cubes<'a, I: std::iter::Iterator>(cubes: I) -> Self { + let mut classic_to_modern = std::collections::HashMap::::new(); + let mut modern_to_classic = std::collections::HashMap::::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 = (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, 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 { + 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 { + 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, + }) + } +} diff --git a/rc_core/src/cubes/mod.rs b/rc_core/src/cubes/mod.rs index 4a7df69..d5d1577 100644 --- a/rc_core/src/cubes/mod.rs +++ b/rc_core/src/cubes/mod.rs @@ -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, locations: std::sync::Arc, offset: std::sync::Arc, + converter: std::sync::Arc, } 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 { self.offset.clone() } + + pub fn converter(&self) -> std::sync::Arc { + self.converter.clone() + } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index ef4e423..af0e783 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -28,17 +28,28 @@ impl CubeConfig { pub fn load(root: impl AsRef) -> std::io::Result { 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) -> bool { diff --git a/rc_core/src/persist/conversion.rs b/rc_core/src/persist/conversion.rs new file mode 100644 index 0000000..d7b4868 --- /dev/null +++ b/rc_core/src/persist/conversion.rs @@ -0,0 +1,1851 @@ +use serde::{Serialize, Deserialize}; + +pub(crate) fn actual_default_conversion() -> CubeConversionData { + CubeConversionData { + from: Vec::default(), + to: None, + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct CubeConversionData { + pub from: Vec, + pub to: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum FromConversionData { + Simple(u32), + Complex { + id: u32, + offset: Option<(i16, i16, i16)>, + #[serde(default)] + colour: u8, + }, +} + +impl FromConversionData { + pub(crate) fn id(&self) -> u32 { + match self { + Self::Simple(id) => *id, + Self::Complex { id, .. } => *id, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum ToConversionData { + Simple(u32), + Complex { + id: u32, + offset: Option<(i16, i16, i16)>, + }, +} + +pub(crate) fn expand_conversion_data(cube: &mut super::Cube) { + if cube.conversion.is_some() { return; } + let conversion = match cube.id { + 0 => CubeConversionData { + from: (0..24).map(|i| FromConversionData::Complex { + id: 0, + offset: Some((i as _, (i * 4) as _, (i * -2) as _)), + colour: i as u8, + }).collect(), + to: None, + }, + // cone + 1352234106 => CubeConversionData { // MediumCone + from: vec![ + FromConversionData::Simple(914659855), // ArmoredConeT10 + FromConversionData::Complex { // ArmoredConeT10Orange + id: 903181232, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredConeT10Yellow + id: 2073756085, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredConeT10Gold + id: 420704, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280721), // HallowConeT10 + FromConversionData::Complex { // ArmoredConeT10Blue + id: 603738000, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredConeT10DarkBlue + id: 2126078066, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367602), // RareConeT10 + FromConversionData::Simple(311721892), // WolfConeT10 + FromConversionData::Simple(3504767087), // QuarterbackConeT10 + FromConversionData::Complex { // ArmoredConeT10C6B + id: 1558067038, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredConeT10MatteWhite + id: 914659773, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredConeT10MatteRed + id: 865911715, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredConeT10C6O + id: 2786019727, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredConeT10SType + id: 1503181726, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredConeT10MatteGreen + id: 107293578, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredConeT10MatteBlue + id: 1824768921, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredConeT10MattePurple + id: 1911941389, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 1979640496 => only_to(914659855), // CompactCone to ArmoredConeT10 + 3886136035 => only_to(914659855), // HeavytCone [sic] + 1422041599 => only_to(914659855), // LightCone + 552682446 => only_to(914659855), // NeonCone + // full cubes + 1576857358 => reciprocal(2234346032), // CompactCube + 123901970 => CubeConversionData { // heavy cube + from: vec![ + FromConversionData::Simple(1654632428), // HeavyChassisCube + FromConversionData::Simple(366000), // special april fools heavy + ], + to: Some(ToConversionData::Simple(1654632428)), + }, + 227205318 => CubeConversionData { // medium cube + from: vec![ + FromConversionData::Complex { // ArmoredCubeT10 + id: 227205318, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredCubeT10Orange + id: 903181219, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredCubeT10Yellow + id: 2073756071, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredCubeT10Gold + id: 420691, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(81509462), // HallowCubeT10 + FromConversionData::Complex { // ArmoredCubeT10Blue + id: 603737987, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredCubeT10DarkBlue + id: 2126078053, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367589), // RareArmoredCubeT3 + FromConversionData::Simple(3111721879), // WolfCubeT5 + FromConversionData::Simple(3504767074), // QuarterbackCubeT4 + FromConversionData::Complex { // ArmoredCubeT10MatteWhite + id: 2553833528, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredCubeT10MatteRed + id: 2688683104, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredCubeT10SType + id: 2688683104, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredCubeT10MatteGreen + id: 107293565, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredCubeT10MatteBlue + id: 1824768908, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredCubeT10MattePurple + id: 1911941376, + offset: NO_OFFSET, + colour: PURPLE, + }, + // TODO colour-match the tiered cubes + FromConversionData::Simple(936473914), // ArmoredCubeT9 + FromConversionData::Simple(3130466754), // ArmoredCubeT8 + FromConversionData::Simple(1602312433), // ArmoredCubeT7 + FromConversionData::Simple(3217516252), // ArmoredCubeT6 + FromConversionData::Simple(3902577141), // ArmoredCubeT5 + FromConversionData::Simple(148547544), // ArmoredCubeT4 + FromConversionData::Simple(1428281037), // ArmoredCubeT3 + FromConversionData::Simple(3523011797), // ArmoredCubeT2 + FromConversionData::Simple(3795589065), // ArmoredCubeT1 + // end tiered + FromConversionData::Simple(2256925527), // ArmoredCubeT10White_STypeSymbol + FromConversionData::Simple(1733271378), // ArmoredCubeT10SType_STypeSymbol + // no equivalent translation + // non-building blocks + FromConversionData::Simple(3951497291), // Fusion Tower + FromConversionData::Simple(606866102), // Protonium Clasp + FromConversionData::Simple(3950293873), // Protonium Crystal + + FromConversionData::Simple(2000000000), // Camera Cube + ], + to: None, + }, + 1972224393 => reciprocal(2295832800), // light cube + 3367514907 => CubeConversionData { // CubewithC6Logo + from: vec![ + FromConversionData::Complex { // ArmoredCubeT10C6B + id: 501172250, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredCubeT10C6O + id: 3461973291, + offset: NO_OFFSET, + colour: ORANGE, + }, + ], + to: None, + }, + 2392661891 => CubeConversionData { // CubewithCARBONLetters + from: vec![ + FromConversionData::Complex { // ArmoredCubeT10C6B_Six + id: 3367514907, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredCubeT10C6B_Carbon + id: 2392661891, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredCubeT10C6O_Six + id: 457113130, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredCubeT10C6O_Carbon + id: 1562104498, + offset: NO_OFFSET, + colour: ORANGE, + }, + ], + to: Some(ToConversionData::Simple(3367514907)), + }, + 3327812742 => CubeConversionData { // GlassCube + from: vec![ + FromConversionData::Simple(3969548222), // WindshieldCubeT10 + FromConversionData::Simple(3327812742), // WindshieldCube + ], + to: None, + }, + 2077855120 => only_to(227205318), // MediumCubeCardLife + 150161008 => only_to(227205318), // NeonCube + 183067052 => only_to(227205318), // RetroCube + // inners + 2378018821 => CubeConversionData { // CompactInner + from: vec![ + FromConversionData::Simple(314459661), // ArmoredInnerTX1 + FromConversionData::Complex { // GoldArmoredInnerTX1 + id: 783649019, + offset: NO_OFFSET, + colour: GOLD, + }, + ], + to: None, + }, + 2577871627 => CubeConversionData { // HeavyInner + from: vec![ + FromConversionData::Simple(4181414569), // HeavyChassisInner + FromConversionData::Simple(366003), // special april fools heavy inner + ], + to: Some(ToConversionData::Simple(1654632428)), + }, + 3559488176 => CubeConversionData { // MediumInner + from: vec![ + FromConversionData::Simple(3559488176), // ArmoredInnerT10 + FromConversionData::Complex { // ArmoredInnerT10Orange + id: 903181222, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredInnerT10Yellow + id: 2073756074, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredInnerT10Gold + id: 420694, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(2292045362), // HallowInnerT10 + FromConversionData::Complex { // ArmoredInnerT10Blue + id: 603737990, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredInnerT10DarkBlue + id: 2126078056, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1478755203), // RareArmoredInnerT3 + FromConversionData::Simple(1730552257), // WolfInnerT5 + FromConversionData::Simple(3293765065), // QuarterbackInnerT4 + FromConversionData::Complex { // ArmoredInnerT10C6B + id: 4039842231, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteWhite + id: 3217177779, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteRed + id: 865911707, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredInnerT10C6O + id: 592303238, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredInnerT10SType + id: 1503181716, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteGreen + id: 107293568, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteBlue + id: 1824768911, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredInnerT10MattePurple + id: 1911941379, + offset: NO_OFFSET, + colour: PURPLE, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteGreen + id: 107293568, + offset: NO_OFFSET, + colour: GREEN, + }, + // TODO colour-match the tiered cubes + FromConversionData::Simple(2704279673), // ArmoredInnerT9 + FromConversionData::Simple(1664400308), // ArmoredInnerT8 + FromConversionData::Simple(3737084984), // ArmoredInnerT7 + FromConversionData::Simple(1717894314), // ArmoredInnerT6 + FromConversionData::Simple(1772266812), // ArmoredInnerT5 + FromConversionData::Simple(3514383790), // ArmoredInnerT4 + FromConversionData::Simple(3558861316), // ArmoredInnerT3 + FromConversionData::Simple(140476067), // ArmoredInnerT2 + FromConversionData::Simple(1661086464), // ArmoredInnerT1 + // no equivalent translation + // pyramid inner + FromConversionData::Simple(366009), // special april fools heavy pyramid inner + FromConversionData::Simple(914659851), // ArmoredPyramidInnerT10 + FromConversionData::Simple(903181228), // ArmoredRoundPyramidInnerT10Orange + FromConversionData::Simple(2073756081), // ArmoredPyramidInnerT10Yellow + FromConversionData::Simple(420700), // ArmoredPyramidInnerT10Gold + FromConversionData::Simple(1837280717), // HallowPyramidInnerT10 + FromConversionData::Simple(603737996), // ArmoredPyramidInnerT10Blue + FromConversionData::Simple(2126078062), // ArmoredPyramidInnerT10DarkBlue + FromConversionData::Simple(1727367598), // RarePyramidInnerT10 + FromConversionData::Simple(311721888), // WolfPyramidInnerT10 + FromConversionData::Simple(3504767083), // QuarterbackPyramidInnerT10 + FromConversionData::Simple(1558067034), // ArmoredPyramidInnerT10C6B + FromConversionData::Simple(914659769), // ArmoredPyramidInnerT10MatteWhite + FromConversionData::Simple(865911711), // ArmoredPyramidInnerT10MatteRed + FromConversionData::Simple(2786019731), // ArmoredPyramidInnerT10C6O + FromConversionData::Simple(1503181722), // ArmoredPyramidInnerT10SType + FromConversionData::Simple(107293574), // ArmoredPyramidInnerT10MatteGreen + FromConversionData::Simple(1824768917), // ArmoredPyramidInnerT10MatteBlue + FromConversionData::Simple(1911941385), // ArmoredPyramidInnerT10MattePurple + ], + to: None, + }, + 1941682825 => reciprocal(3920646105), // light inner + 445857183 => CubeConversionData { // GlassInner + from: vec![ + FromConversionData::Simple(2305492794), // WindshieldInnerT10 + FromConversionData::Simple(445857183), // WindshieldInner + ], + to: None, + }, + 1616217456 => only_to(3559488176), // NeonInner + 3985049715 => only_to(3559488176), // RetroInner + // edges aka prisms + 327960333 => CubeConversionData { // CompactEdge + from: vec![ + FromConversionData::Simple(3003461135), // ArmoredPrismTX1 + FromConversionData::Complex { // GoldArmoredPrismTX1 + id: 776898512, + offset: NO_OFFSET, + colour: GOLD, + }, + ], + to: None, + }, + 1225928721 => CubeConversionData { // HeavyEdge + from: vec![ + FromConversionData::Simple(2686466102), // HeavyChassisPrism + FromConversionData::Simple(366001), // special april fools heavy prism + ], + to: Some(ToConversionData::Simple(1654632428)), + }, + 227917916 => CubeConversionData { // MediumEdge + from: vec![ + FromConversionData::Simple(227917916), // ArmoredPrismT10 + FromConversionData::Complex { // ArmoredPrismT10Orange + id: 903181220, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredPrismT10Yellow + id: 2073756072, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredPrismT10Gold + id: 420692, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1309620559), // HallowPrismT10 + FromConversionData::Complex { // ArmoredPrismT10Blue + id: 603737988, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredPrismT10DarkBlue + id: 2126078054, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(2662788350), // RareArmoredPrismT3 + FromConversionData::Simple(3197803309), // WolfPrismT5 + FromConversionData::Simple(46150324), // QuarterbackPrismT4 + FromConversionData::Complex { // ArmoredPrismT10C6B + id: 2752826359, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredPrismT10MatteWhite + id: 2067295656, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredPrismT10MatteRed + id: 1097117136, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredInnerT10C6O + id: 2006058694, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredPrismT10SType + id: 1503181714, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredPrismT10MatteGreen + id: 107293566, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredPrismT10MatteBlue + id: 1824768909, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredPrismT10MattePurple + id: 1911941377, + offset: NO_OFFSET, + colour: PURPLE, + }, + FromConversionData::Complex { // ArmoredInnerT10MatteGreen + id: 107293568, + offset: NO_OFFSET, + colour: GREEN, + }, + // TODO colour-match the tiered cubes + FromConversionData::Simple(3159626035), // ArmoredPrismT9 + FromConversionData::Simple(3129475416), // ArmoredPrismT8 + FromConversionData::Simple(117694164), // ArmoredPrismT7 + FromConversionData::Simple(3218622022), // ArmoredPrismT6 + FromConversionData::Simple(2954764240), // ArmoredPrismT5 + FromConversionData::Simple(147162946), // ArmoredPrismT4 + FromConversionData::Simple(228806888), // ArmoredPrismT3 + FromConversionData::Simple(3521365071), // ArmoredPrismT2 + FromConversionData::Simple(3133051372), // ArmoredPrismT1 + // no equivalent translation + ], + to: None, + }, + 4157111053 => reciprocal(2231909735), // light edge + 4235300269 => CubeConversionData { // GlassEdge + from: vec![ + FromConversionData::Simple(2956906195), // WindshieldPrismT10 + FromConversionData::Simple(4235300269), // WindshieldPrism + ], + to: None, + }, + 348383505 => only_to(227917916), // MediumEdgeCardLife + 1183051379 => only_to(227917916), // NeonEdge + 1150929327 => only_to(227917916), // RetroEdge + // pyramids aka pyramid tetras + 2686602224 => CubeConversionData { // MediumPyramid + from: vec![ + FromConversionData::Simple(914659850), // ArmoredPyramidTetraT10 + FromConversionData::Complex { // ArmoredPyramidTetraT10Orange + id: 903181227, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10Yellow + id: 2073756080, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10Gold + id: 420699, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280716), // HallowPyramidTetraT10 + FromConversionData::Complex { // ArmoredPyramidTetraT10Blue + id: 603737995, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10DarkBlue + id: 2126078061, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367597), // RarePyramidTetraT10 + FromConversionData::Simple(311721887), // WolfPyramidTetraT10 + FromConversionData::Simple(3504767082), // QuarterbackPyramidTetraT10 + FromConversionData::Complex { // ArmoredPyramidTetraT10C6B + id: 1558067033, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10MatteWhite + id: 914659768, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10MatteRed + id: 865911710, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10C6O + id: 2786019732, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10SType + id: 1503181721, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10MatteGreen + id: 107293573, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10MatteBlue + id: 1824768916, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredPyramidTetraT10MattePurple + id: 1911941384, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 979123471 => only_to(914659850), // CompactPyramid to ArmoredPyramidTetraT10 + 4184274974 => only_to(914659850), // HeavyPyramid to ArmoredPyramidTetraT10 + 885415789 => only_to(914659850), // LightPyramid + 3447434946 => only_to(914659850), // NeonPyramid + // no glass pyramid exists + // wedges (just the headlamp) + 1367205243 => CubeConversionData { // Headlamp + from: vec![ + FromConversionData::Simple(1367205243), // Headlamp + FromConversionData::Simple(366007), // special april fools heavy wedge + FromConversionData::Simple(914659766), // ArmoredWedgeT10 + FromConversionData::Simple(903181226), // ArmoredWedgeT10Orange + FromConversionData::Simple(2073756079), // ArmoredWedgeT10Yellow + FromConversionData::Simple(420698), // ArmoredWedgeT10Gold + FromConversionData::Simple(1837280715), // HallowWedgeT10 + FromConversionData::Simple(603737994), // ArmoredWedgeT10Blue + FromConversionData::Simple(2126078060), // ArmoredWedgeT10DarkBlue + FromConversionData::Simple(1727367596), // RareWedgeT10 + FromConversionData::Simple(311721886), // WolfWedgeT10 + FromConversionData::Simple(3504767081), // QuarterbackWedgeT10 + FromConversionData::Simple(1558067032), // ArmoredWedgeT10C6B + FromConversionData::Simple(914659767), // ArmoredWedgeT10White + FromConversionData::Simple(865911709), // ArmoredWedgeT10MatteRed + FromConversionData::Simple(2786019733), // ArmoredWedgeT10C6O + FromConversionData::Simple(1503181720), // ArmoredWedgeT10SType + FromConversionData::Simple(107293572), // ArmoredWedgeT10MatteGreen + FromConversionData::Simple(1824768915), // ArmoredWedgeT10MatteBlue + FromConversionData::Simple(1911941383), // ArmoredWedgeT10MattePurple + ], + to: Some(ToConversionData::Simple(1367205243)), + }, + // Inner round aka round inner (lol) + 1837286858 => CubeConversionData { // MediumInnerRound + from: vec![ + FromConversionData::Simple(1837286858), // ArmoredInnerRoundT10 + FromConversionData::Complex { // ArmoredRoundInnerT10Orange + id: 903181225, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10Yellow + id: 2073756078, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10Gold + id: 420697, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280714), // HallowInnerRoundT10 + FromConversionData::Complex { // ArmoredRoundInnerT10Blue + id: 603737993, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10DarkBlue + id: 2126078059, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367595), // RareRoundInnerT10 + FromConversionData::Simple(311721885), // WolfRoundInnerT10 + FromConversionData::Simple(3504767080), // QuarterbackRoundInnerT10 + FromConversionData::Complex { // ArmoredRoundInnerT10C6B + id: 1558067031, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10MatteWhite + id: 3217177780, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10MatteRed + id: 865911708, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10C6O + id: 2786019734, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10SType + id: 1503181719, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10MatteGreen + id: 107293571, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10MatteBlue + id: 1824768914, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundInnerT10MattePurple + id: 1911941382, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 925214026 => only_to(1837286858), // CompactInnerRound to ArmoredInnerRoundT10 + 665761850 => CubeConversionData { + from: vec![ + FromConversionData::Simple(366006), // special april fools heavy round inner + ], + to: Some(ToConversionData::Simple(1837286858)), // ArmoredInnerRoundT10 + }, + 1578378954 => only_to(1837286858), // LightInnerRound + 1774712137 => only_to(1837286858), // NeonInnerRound + 3066776961 => reciprocal(477710581), // GlassInnerRound to WindshieldRoundInnerT10 + // edge round aka round prism + 447126572 => CubeConversionData { // MediumEdgeRound + from: vec![ + FromConversionData::Simple(447126572), // ArmoredPrismRoundT10 + FromConversionData::Complex { // ArmoredRoundPrismT10Orange + id: 903181223, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10Yellow + id: 2073756075, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10Gold + id: 420695, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(447124524), // HallowPrismRoundT10 + FromConversionData::Complex { // ArmoredRoundPrismT10Blue + id: 603737991, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10DarkBlue + id: 2126078057, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367593), // RareRoundPrismT10 + FromConversionData::Simple(311721883), // WolfRoundPrismT10 + FromConversionData::Simple(3504767078), // QuarterbackRoundPrismT10 + FromConversionData::Complex { // ArmoredRoundPrismT10C6B + id: 1018457748, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10MatteWhite + id: 2067295657, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10MatteRed + id: 1097117137, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10C6O + id: 3328512085, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10SType + id: 1503181717, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10MatteGreen + id: 107293569, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10MatteBlue + id: 1824768912, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundPrismT10MattePurple + id: 1911941380, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 676446887 => only_to(447126572), // CompactEdgeRound to ArmoredPrismRoundT10 + 368051733 => CubeConversionData { + from: vec![ + FromConversionData::Simple(366006), // special april fools heavy round inner + ], + to: Some(ToConversionData::Simple(447126572)), // ArmoredPrismRoundT10 + }, + 1993583577 => only_to(447126572), // LightEdgeRound + 2623263862 => only_to(447126572), // NeonEdgeRound + 3629216992 => reciprocal(2407011807), // GlassEdgeRound to WindshieldRoundPrismT10 + // corner round aka round tetra + 2589418111 => CubeConversionData { // MediumCornerRound + from: vec![ + FromConversionData::Simple(2589418111), // ArmoredTetraRoundT10 + FromConversionData::Complex { // ArmoredRoundTetraT10Orange + id: 903181224, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10Yellow + id: 2073756076, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10Gold + id: 420696, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(2089416063), // HallowTetraRoundT10 + FromConversionData::Complex { // ArmoredRoundTetraT10Blue + id: 603737992, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10DarkBlue + id: 2126078058, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367594), // RareRoundTetraT10 + FromConversionData::Simple(311721884), // WolfRoundTetraT10 + FromConversionData::Simple(3504767079), // QuarterbackRoundTetraT10 + FromConversionData::Complex { // ArmoredRoundTetraT10C6B + id: 4200979110, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10MatteWhite + id: 904092408, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10MatteRed + id: 2704843488, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10C6O + id: 11838567, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10SType + id: 1503181718, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10MatteGreen + id: 107293570, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10MatteBlue + id: 1824768913, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredRoundTetraT10MattePurple + id: 1911941381, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 1728084091 => only_to(2589418111), // CompactCornerRound to ArmoredTetraRoundT10 + 2148353926 => CubeConversionData { // HeavyCornerRound + from: vec![ + FromConversionData::Simple(366005), // special april fools heavy round tetra + ], + to: Some(ToConversionData::Simple(2589418111)), // ArmoredTetraRoundT10 + }, + 582052348 => only_to(2589418111), // LightCornerRound + 3195590950 => only_to(2589418111), // NeonCornerRound + 2380494106 => reciprocal(1907975281), // GlassCornerRound to WindshieldRoundTetraT10 + // inner slope aka sloped inner + 495400745 => CubeConversionData { // MediumInnerSlope + from: vec![ + FromConversionData::Simple(914659854), // ArmoredSlopedInnerT10 + FromConversionData::Complex { // ArmoredSlopedInnerT10Orange + id: 903181231, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10Yellow + id: 2073756084, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10Gold + id: 420703, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280720), // HallowInnerSlopedT10 + FromConversionData::Complex { // ArmoredSlopedInnerT10Blue + id: 603737999, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10DarkBlue + id: 2126078065, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367601), // RareSlopedInnerT10 + FromConversionData::Simple(311721891), // WolfSlopedInnerT10 + FromConversionData::Simple(3504767086), // QuarterbackSlopedInnerT10 + FromConversionData::Complex { // ArmoredSlopedInnerT10C6B + id: 1558067037, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10MatteWhite + id: 914659772, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10MatteRed + id: 865911714, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10C6O + id: 2786019728, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10SType + id: 1503181725, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10MatteGreen + id: 107293577, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10MatteBlue + id: 1824768920, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedInnerT10MattePurple + id: 1911941388, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 1834966563 => only_to(914659854), // CompactInnerSlope to ArmoredSlopedInnerT10 + 2111062867 => only_to(914659854), // HeavyInnerSlope + 2426642454 => only_to(914659854), // LightInnerSlope + 868027936 => only_to(914659854), // NeonInnerSlope + // no glass inner slope + // edge slope aka sloped prism + 1538778047 => CubeConversionData { // MediumEdgeSlope + from: vec![ + FromConversionData::Simple(914659852), // ArmoredSlopedPrismT10 + FromConversionData::Complex { // ArmoredSlopedPrismT10Orange + id: 903181229, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10Yellow + id: 2073756082, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10Gold + id: 420701, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280718), // HallowPrismSlopedT10 + FromConversionData::Complex { // ArmoredSlopedPrismT10Blue + id: 603737997, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10DarkBlue + id: 2126078063, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367599), // RareSlopedPrismT10 + FromConversionData::Simple(311721889), // WolfSlopedPrismT10 + FromConversionData::Simple(3504767084), // QuarterbackSlopedPrismT10 + FromConversionData::Complex { // ArmoredSlopedPrismT10C6B + id: 1558067035, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10MatteWhite + id: 914659770, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10MatteRed + id: 865911712, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredSlopedPrism10C6O + id: 2786019730, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredTetraT10SType [sic?] + id: 1503181723, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10MatteGreen + id: 107293575, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10MatteBlue + id: 1824768918, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedPrismT10MattePurple + id: 1911941386, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 1915435470 => only_to(914659852), // CompactEdgeSlope to ArmoredSlopedPrismT10 + 1334508924 => only_to(914659852), // HeavyEdgeSlope + 3093572869 => only_to(914659852), // LightEdgeSlope + 3324063519 => only_to(914659852), // NeonEdgeSlope + // corner slope aka sloped tetra + 557016830 => CubeConversionData { // MediumCornerSlope + from: vec![ + FromConversionData::Simple(914659852), // ArmoredSlopedTetraT10 + FromConversionData::Complex { // ArmoredSlopedTetraT10Orange + id: 903181230, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10Yellow + id: 2073756083, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10Gold + id: 420702, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1837280719), // HallowTetraSlopedT10 + FromConversionData::Complex { // ArmoredSlopedTetraT10Blue + id: 603737998, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10DarkBlue + id: 2126078064, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(1727367600), // RareSlopedTetraT10 + FromConversionData::Simple(311721890), // WolfSlopedTetraT10 + FromConversionData::Simple(3504767085), // QuarterbackSlopedTetraT10 + FromConversionData::Complex { // ArmoredSlopedTetraT10C6B + id: 1558067036, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10MatteWhite + id: 914659771, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10MatteRed + id: 865911713, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10C6O + id: 2786019729, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10SType + id: 1503181724, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10MatteGreen + id: 107293576, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10MatteBlue + id: 1824768919, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredSlopedTetraT10MattePurple + id: 1911941387, + offset: NO_OFFSET, + colour: PURPLE, + }, + // no equivalent translation + ], + to: None, + }, + 1881278783 => only_to(914659853), // CompactCornerSlope to ArmoredSlopedTetraT10 + 3665280239 => only_to(914659853), // HeavyCornerSlope + 3959877408 => only_to(914659853), // LightCornerSlope + 3825345103 => only_to(914659853), // NeonCornerSlope + // corner aka tetra + 126353766 => CubeConversionData { // CompactCorner + from: vec![ + FromConversionData::Simple(1362505337), // ArmoredTetraTX + FromConversionData::Complex { // GoldArmouredTetraTX + id: 734598081, + offset: NO_OFFSET, + colour: GOLD, + }, + ], + to: None, + }, + 222074329 => CubeConversionData { // HeavyCorner + from: vec![ + FromConversionData::Simple(2157492751), // HeavyChassisTetra + FromConversionData::Simple(366002), // special april fools heavy tetra + ], + to: Some(ToConversionData::Simple(2157492751)), + }, + 1931676396 => CubeConversionData { // MediumCorner + from: vec![ + FromConversionData::Simple(1931676396), // ArmoredTetraT10 + FromConversionData::Complex { // ArmoredTetraT10Orange + id: 903181221, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredTetraT10Yellow + id: 2073756073, + offset: NO_OFFSET, + colour: YELLOW, + }, + FromConversionData::Complex { // ArmoredTetraT10Gold + id: 420693, + offset: NO_OFFSET, + colour: GOLD, + }, + FromConversionData::Simple(1857534838), // HallowTetraT10 + FromConversionData::Complex { // ArmoredTetraT10Blue + id: 603737989, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredTetraT10DarkBlue + id: 2126078055, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Simple(3188616903), // RareArmoredTetraT3 + FromConversionData::Simple(3224129949), // WolfTetraT5 + FromConversionData::Simple(578334861), // QuarterbackTetraT4 + FromConversionData::Complex { // ArmoredTetraT10C6B + id: 2388981877, + offset: NO_OFFSET, + colour: BLACK, + }, + FromConversionData::Complex { // ArmoredTetraT10MatteWhite + id: 904092407, + offset: NO_OFFSET, + colour: WHITE, + }, + FromConversionData::Complex { // ArmoredTetraT10MatteRed + id: 2704843487, + offset: NO_OFFSET, + colour: RED, + }, + FromConversionData::Complex { // ArmoredTetraT10C6O + id: 1575105860, + offset: NO_OFFSET, + colour: ORANGE, + }, + FromConversionData::Complex { // ArmoredTetraT10SType + id: 1503181715, + offset: NO_OFFSET, + colour: STYPE, + }, + FromConversionData::Complex { // ArmoredTetraT10MatteGreen + id: 107293567, + offset: NO_OFFSET, + colour: GREEN, + }, + FromConversionData::Complex { // ArmoredTetraT10MatteBlue + id: 1824768910, + offset: NO_OFFSET, + colour: BLUE, + }, + FromConversionData::Complex { // ArmoredTetraT10MattePurple + id: 1911941378, + offset: NO_OFFSET, + colour: PURPLE, + }, + // TODO colour-match the tiered cubes + FromConversionData::Simple(3265763652), // ArmoredTetraT9 + FromConversionData::Simple(3292449768), // ArmoredTetraT8 + FromConversionData::Simple(2041910372), // ArmoredTetraT7 + FromConversionData::Simple(3245247734), // ArmoredTetraT6 + FromConversionData::Simple(3467171168), // ArmoredTetraT5 + FromConversionData::Simple(1987268082), // ArmoredTetraT4 + FromConversionData::Simple(1930795608), // ArmoredTetraT3 + FromConversionData::Simple(2941587199), // ArmoredTetraT2 + FromConversionData::Simple(3288882012), // ArmoredTetraT1 + // end tiered + // no equivalent translation + ], + to: None, + }, + 1347255791 => reciprocal(1754827363), // LightCorner to LightChassisTetra + 1094997616 => CubeConversionData { // GlassCorner + from: vec![ + FromConversionData::Simple(1593042212), // WindshieldTetraT10 + FromConversionData::Simple(1094997616), // WindshieldTetra + ], + to: None, + }, + 2333317284 => only_to(1931676396), // NeonCorner + 1387792676 => only_to(1931676396), // RetroCorner + // Helium (sort of a chassic cube) + 3226650954 => CubeConversionData { // Helium + from: vec![ + FromConversionData::Simple(3425997036), + FromConversionData::Simple(3305279060), + FromConversionData::Simple(1914654544), + FromConversionData::Simple(2001668174), + FromConversionData::Simple(3226650954), + ], + to: Some(ToConversionData::Simple(3226650954)), + }, + // country flags + 1385335421 => CubeConversionData { // Canada (also used as fallback country flag) + from: vec![ + FromConversionData::Simple(1385335421), // Canada + FromConversionData::Simple(1958111701), // DominicanRepublic + FromConversionData::Simple(1958111702), // Egypt + FromConversionData::Simple(1958111703), // Estonia + FromConversionData::Simple(1958111704), // Greece + FromConversionData::Simple(1958111705), // HongKong + FromConversionData::Simple(1958111706), // Hungary + FromConversionData::Simple(1958111708), // Latvia + FromConversionData::Simple(1958111709), // Liechtenstein + FromConversionData::Simple(1958111710), // Lithuania + FromConversionData::Simple(1958111711), // Mongolia + FromConversionData::Simple(1958111712), // Sardinia + FromConversionData::Simple(1958111713), // Taiwan + FromConversionData::Simple(1958111714), // Thailand + //FromConversionData::Simple(1958111715), // Turkey + // Unknown countries + FromConversionData::Simple(1395013882), // ??? + FromConversionData::Simple(2877144578), // ??? + FromConversionData::Simple(1875812779), // ??? + FromConversionData::Simple(2997211596), // ??? + FromConversionData::Simple(987493977), // ??? + FromConversionData::Simple(1122928047), // ??? + FromConversionData::Simple(1971217946), // ??? + FromConversionData::Simple(766565997), // ??? + ], + to: None, + }, + 4065608086 => only_to(68277216), // Russia to Ukraine + 68277216 => CubeConversionData { // Ukraine + from: vec![ + FromConversionData::Simple(68277216), // Ukraine + FromConversionData::Simple(4065608086), // Russia + ], + to: Some(ToConversionData::Simple(68277216)), + }, + 4175295691 => reciprocal(4175295691), // UK + 3559676835 => reciprocal(3559676835), // USA + 2020064218 => reciprocal(2020064218), // Japan + 2730691972 => reciprocal(2730691972), // Poland + 2596479965 => reciprocal(2596479965), // Germany + 3330651823 => reciprocal(3330651823), // Kazakhstan + 1768151340 => reciprocal(1768151340), // Belarus + 4166201860 => reciprocal(4166201860), // Italy + 3447406366 => reciprocal(3447406366), // Ireland + 664490215 => reciprocal(664490215), // Australia + 3114896263 => reciprocal(3114896263), // Spain + 2243089188 => reciprocal(2243089188), // Brazil + 88051662 => reciprocal(88051662), // Netherlands + 354715741 => reciprocal(354715741), // Argentina + 1786715320 => reciprocal(1786715320), // Sweden + 3910121366 => reciprocal(3910121366), // China + 2373812955 => reciprocal(2373812955), // France + 460733827 => reciprocal(460733827), // South Korea + // newer country flags (added after RC15) + 2855651676 => only_to(1385335421), // New Zealand to Canada + 932603502 => only_to(1385335421), // Denmark to Canada + 1796059677 => only_to(1385335421), // IcelandHoloflag + 1798659636 => only_to(1385335421), // BelgiumHoloFlag + 4070927930 => reciprocal(1958111715), // Turkiye/Turkey has a classic equivalent + // general flags + 3024755361 => reciprocal(3024755361), // Humble Bundle + 1039436321 => reciprocal(1039436321), // Curse + 1958111892 => CubeConversionData { // Robocraft (also used as fallback flag) + from: vec![ + FromConversionData::Simple(1958111892), // Robocraft + // These are all sequential, so likely added by client mods + //FromConversionData::Simple(1958111896), // pirate + FromConversionData::Simple(1958111895), // mars + FromConversionData::Simple(1958111897), // ??? + FromConversionData::Simple(1958111898), // ??? + FromConversionData::Simple(1958111901), // anniversary flag ??? + FromConversionData::Simple(1958111902), // anniversary flag ??? + FromConversionData::Simple(1958111903), // anniversary flag ??? + FromConversionData::Simple(1958111904), // anniversary flag ??? + FromConversionData::Simple(1754621522), // hex badge ??? + FromConversionData::Simple(1754621523), // hex badge ??? + FromConversionData::Simple(1754621523), // hex badge ??? + FromConversionData::Simple(1958111893), // ??? + FromConversionData::Simple(1958111707), // unused (for now?) + FromConversionData::Simple(1958111897), // E14 + FromConversionData::Simple(1958111898), // CF-Alliance + ], + to: Some(ToConversionData::Simple(1958111892)), + }, + 4200145444 => reciprocal(4200145444), // Dev Supporter + 3370765914 => reciprocal(3370765914), // Gold Dev Supporter + 3996493866 => reciprocal(1754621525), // 3YearsHoloflag (to badge equiv) + 507555577 => reciprocal(1754621526), // 4YearsHoloflag (to badge equiv) + 4124816651 => reciprocal(1958111896), // PirateHoloflag + // non-translatable flags + 3411783298 => only_to(1385335421), // RoboPassSeason2Holoflag + 2663263630 => only_to(1385335421), // BunnyHoloflag + 3131616996 => only_to(1385335421), // EasterEggHoloflag + 3266439135 => only_to(1385335421), // FlowersHoloflag + 216301182 => only_to(1385335421), // 2019Holoflag + 2412453676 => only_to(1385335421), // CandyCaneHoloflag + 2990880144 => only_to(1385335421), // SantaCrayHoloflag + 3058601525 => only_to(1385335421), // RoboPassSeason1Holoflag + 327459709 => only_to(1385335421), // SnowflakeHoloflag + 2537158398 => only_to(1385335421), // 6YearsHoloflag + 757806708 => only_to(1385335421), // TopHundredHoloflag + 1020482017 => only_to(1385335421), // OverwolfHoloflag + 1607707925 => only_to(1385335421), // AbleGamersCharityHoloflag + 4079824960 => only_to(1385335421), // YogscastHoloflag + 862329300 => only_to(1385335421), // ChronoGGHoloflag + 1042028710 => only_to(1385335421), // IntelHoloflag/Nyan Cray/Rainbow + 2345098755 => only_to(1385335421), // AlienwareHoloflag + 3210649465 => only_to(1385335421), // 5YearsHoloflag + // badges + 1301942811 => only_to(1754621522), // BronzeLeagueBadge to some badge (yes I know league badges aren't the same shape) + 4157599523 => only_to(1754621522), // SilverLeagueBadge + 1893098069 => only_to(1754621522), // GoldLeagueBadge + 608517870 => only_to(1754621522), // DiamondLeagueBadge + 2046961989 => only_to(1754621522), // ProtoniumLeagueBadge + 1988022506 => only_to(1754621522), // ProtoniumLeagueFiveStarsBadge + 1551110695 => only_to(1754621522), // SilverLeagueBadgeArena + 2514755079 => only_to(1754621522), // GoldLeagueBadgeArena + 988387568 => only_to(1754621522), // BronzeLeagueBadgeArena + // masks + // cockpit + 2530225659 => reciprocal(2530225659), + 3970675339 => reciprocal(3970675339), + 1354711861 => reciprocal(1354711861), + 717544517 => reciprocal(717544517), + 2711624874 => reciprocal(2711624874), + 3688596442 => reciprocal(3688596442), + // other masks + 451055140 => reciprocal(451055140), // ??? + 1620644180 => reciprocal(1620644180), // ??? + 3561288328 => reciprocal(3561288328), // ??? + 2923392504 => reciprocal(2923392504), // ??? + 2479898264 => reciprocal(2479898264), // ??? + 3920339432 => reciprocal(3920339432), // ??? + 2906446214 => reciprocal(2906446214), // ??? + 3611772662 => reciprocal(3611772662), // ??? + 1217647354 => reciprocal(1217647354), // ??? + 854085002 => reciprocal(854085002), // ??? + 604924218 => reciprocal(604924218), // ??? + 1584789066 => reciprocal(1584789066), // ??? + 4155139139 => reciprocal(4155139139), // ??? + 2379348787 => reciprocal(2379348787), // ??? + 2123945215 => reciprocal(2123945215), // ??? + 82004879 => reciprocal(82004879), // ??? + 1554012392 => reciprocal(1554012392), // ??? + 651937688 => reciprocal(651937688), // ??? + 1268201954 => reciprocal(1268201954), // ??? + 837592722 => reciprocal(837592722), // ??? + 1108501665 => reciprocal(1108501665), // ??? + 946469841 => reciprocal(946469841), // ??? + 1839067602 => reciprocal(1839067602), // ??? + 400977570 => reciprocal(400977570), // ??? + 1762483815 => reciprocal(1762483815), // ??? + 326549783 => reciprocal(326549783), // ??? + 670637166 => reciprocal(670637166), // ??? + 1568834334 => reciprocal(1568834334), // ??? + 438539744 => reciprocal(438539744), // ??? + 1616382608 => reciprocal(1616382608), // ??? + 4200383087 => reciprocal(4200383087), // ??? + 2150063391 => reciprocal(2150063391), // ??? + 3060186489 => reciprocal(3060186489), // ??? + 3424461321 => reciprocal(3424461321), // ??? + 1936618195 => reciprocal(1936618195), // ??? + 152432035 => reciprocal(152432035), // ??? + 2075163291 => reciprocal(2075163291), // ??? + 30139883 => reciprocal(30139883), // ??? + 2915102202 => reciprocal(2915102202), // ??? + 3619385994 => reciprocal(3619385994), // ??? + 665365647 => reciprocal(665365647), // ??? + 1574122495 => reciprocal(1574122495), // ??? + // misc cosmetics + 688123941 => reciprocal(688123941), // SpikeDagger + 2782198650 => reciprocal(2782198650), // RobotNameBanner + 2820370110 => reciprocal(2820370110), // Altimeter + 3531325242 => reciprocal(3531325242), // Speedometer + 3610833057 => reciprocal(3610833057), // VaporTrailSingle + 3137009174 => only_to(3610833057), // VaporTrailSingleFlower + 1810319153 => only_to(3610833057), // VaporTrailSingleFirework + 3297983510 => only_to(3610833057), // VaporTrailSingleSnowflake + 236893100 => reciprocal(236893100), // VaporTrailTwin + // electroplates + 3823106277 => reciprocal(3823106277), // ElectroshieldIL + 1889152405 => reciprocal(1889152405), // ElectroshieldIR + 3001609712 => reciprocal(3001609712), // ElectroshieldJL + 3365106304 => reciprocal(3365106304), // ElectroshieldJR + 991790322 => reciprocal(991790322), // ElectroshieldKL + 1097210754 => reciprocal(1097210754), // ElectroshieldKR + 726724318 => reciprocal(726724318), // T3ElectroShieldDLSpiked + 1361801646 => reciprocal(1361801646), // T3ElectroShieldDRSpiked + 3248328684 => reciprocal(3248328684), // T3ElectroShieldDLFootball + 3152416924 => reciprocal(3152416924), // T3ElectroShieldDRFootball + // T1 + 1264237222 => reciprocal(1264237222), + 2965095316 => reciprocal(2965095316), + // T2 + 2457663915 => reciprocal(2457663915), + 1771929753 => reciprocal(1771929753), + // T3 + 627128495 => reciprocal(627128495), + 3732947357 => reciprocal(3732947357), + // T4 + 540114865 => reciprocal(540114865), + 3687843459 => reciprocal(3687843459), + // T5 + 2536292021 => reciprocal(2536292021), + 1825425287 => reciprocal(1825425287), + // T6 + 1309343160 => reciprocal(1309343160), + 3052110986 => reciprocal(3052110986), + // T7 + 4179049660 => reciprocal(4179049660), + 49694094 => reciprocal(49694094), + // T8 + 1152021380 => reciprocal(1152021380), + 3209366198 => reciprocal(3209366198), + // T9 + 4088902272 => reciprocal(4088902272), + 139905970 => reciprocal(139905970), + // Weapons + // Nanos + 1761903423 => reciprocal(1761903423), // T2Nano + 1671695619 => variant_dual(1671695619, 3726197307), // T3Nano to T7/T8 + 2364175165 => variant_dual(2364175165, 3568946183), // T4Nano to T9/T10 + // Plasma + 2655409492 => fallback_conversion(), // T0Plasma + 1293532710 => variant_dual(1412971690, 1293532710), // T1Plasma to T3/T4 + 2446895092 => variant_dual(3810917806, 2446895092), // T2Plasma to T5/T6 + 3953551555 => variant_dual(3866475184, 3953551555), // T3Plasma to T7/T8 + 929526033 => variant_dual(1365801908, 929526033), // T4Plasma to T9/T10 + 1593059007 => only_to(929526033), // T4PlasmaCarbon6 + 171448327 => reciprocal(171448327), // T5Plasma (mega) + 2943284935 => reciprocal(2943284935), // T5PlasmaCarbon6 (mega) + 3594602569 => only_to(171448327), // T5PlasmaGolden + // Rail + 1640043587 => reciprocal(1640043587), // T1Rail to T4 + 2870850840 => variant_dual(360854742, 2870850840), // T2Rail to T5/T6 + 1779831198 => variant_dual(282227656, 1779831198), // T3Rail to T7/T8 + 2697638085 => variant_dual(2815406796, 2697638085), // T4Rail to T9/T10 + 2083340214 => only_to(2697638085), // T5Rail to T10; mega does not exist in classic game + 447003593 => only_to(2697638085), // T5RailGolden + // SMG front mounts + 3402350870 => variant_dual(1337963121, 3402350870), // T0FrontLaser to T1/T2 + 2494440442 => variant_dual(776342052, 2494440442), // T1FrontLaser to T3/T4 + 1584562849 => variant_dual(1497447090, 1584562849), // T2FrontLaser to T5/T6 + 2675516967 => variant_dual(3341135633, 2675516967), // T3FrontLaser to T7/T8 + 1436911484 => variant_dual(2955059079, 1436911484), // T4FrontLaser to T9/T10 + // SMG top mounts + 3809895395 => variant_dual(2929054885, 3809895395), // T0Laser to T1/T2 + 3178463503 => variant_dual(2985370719, 3178463503), // T1Laser to T3/T4 + 2007965780 => variant_dual(3338015945, 2007965780), // T2Laser to T5/T6 + 3064235218 => variant_dual(1485996394, 3064235218), // T3Laser to T7/T8 + //2088248713 => variant_dual(798339580, 2088248713), // T4Laser to T9/T10 + 2088248713 => CubeConversionData { // T4Laser + from: vec![ + FromConversionData::Simple(798339580), // T9 + FromConversionData::Simple(2088248713), // T10 + FromConversionData::Simple(201410), // T10 retro + FromConversionData::Simple(1201410), // T10 retro ??? + FromConversionData::Simple(8201410), // T10 retro gold + ], + to: Some(ToConversionData::Simple(2088248713)), + }, + 3632954889 => only_to(2088248713), // T4LaserCarbon6 + 3209000021 => reciprocal(3209000021), // T5Laser (mega) + 1199458351 => reciprocal(1199458351), // T5LaserCarbon6 + 3851710054 => only_to(3209000021), // T5LaserGolden + // Tesla + 2017654588 => reciprocal(2017654588), // T2Tesla + 3479123512 => reciprocal(3479123512), // T3Tesla + 2703353899 => reciprocal(2703353899), // T4Tesla + // Modern-only weapons + 3947892032 => fallback_conversion(), // T4Aeroflak + 73607413 => fallback_conversion(), // T5Aeroflak + 48150119 => fallback_conversion(), // T3Seeker + 350778877 => fallback_conversion(), // T4Seeker + 2419806013 => fallback_conversion(), // T5Seeker + 3437168371 => fallback_conversion(), // T5SeekerFirework + 3541341953 => fallback_conversion(), // T4Ion + 1184153980 => fallback_conversion(), // T5Ion + 2295212085 => fallback_conversion(), // T5IonCardLife + 105639928 => fallback_conversion(), // T5IonGolden + 1318129004 => fallback_conversion(), // T4Chaingun + 1795842454 => fallback_conversion(), // T5Chaingun + 1885850433 => fallback_conversion(), // T5Mortar + 194724715 => fallback_conversion(), // T5ChaingunGolden + 3832533619 => fallback_conversion(), // T5SeekerGolden + 2256680330 => fallback_conversion(), // T5MortarGolden + 3087866254 => fallback_conversion(), // MortarEggLauncher + // TODO + // Movement + // Rudders + 2369778644 => reciprocal(2369778644), // T2Rudder to T4 + 3808719325 => variant_dual(975319760, 3808719325), // T3Rudder to T5/T6 + 3919904737 => variant_dual(1410969817, 3919904737), // T4Rudder to T7/T8 + 2404311559 => reciprocal(2404311559), // T4RudderBat + 2608683011 => variant_dual(1589198565, 2608683011), // T5Rudder to T9/T10 + 2928920064 => only_to(2608683011), // T5RudderBat + // Wings/Aerofoils + 3980928804 => reciprocal(3980928804), // T2Wing to T4 + 2205394221 => variant_dual(1515578912, 2205394221), // T3Wing to T5/T6 + 2312317713 => variant_dual(879649833, 2312317713), // T4Wing to T7/T8 + 2865954846 => reciprocal(2865954846), // T4WingBat + 2464192350 => variant_dual(1053747733, 2464192350), // T5Wing to T9/T10 + 2863842421 => only_to(2464192350), // T5WingBat + // Hover + 1851713730 => reciprocal(1851713730), // T0Hover to T2 + 3170960304 => variant_dual(947628072, 3170960304), // T1Hover to T3/T4 + 1639979618 => variant_dual(1333565630, 1639979618), // T2Hover to T5/T6 + 465491285 => variant_dual(3508014365, 465491285), // T3Hover to T7/T8 + 3347041415 => variant_dual(2787065227, 3347041415), // T4Hover to T10 Low/T10 Regular + 3835205776 => reciprocal(3835205776), // T5Hover (mega) + 2217471839 => only_to(3835205776), // T5HoverGolden + // Ski + 347391203 => reciprocal(347391203), // T2Ski (why is this in hovers_common.xml!?) + 3080624795 => reciprocal(3080624795), // T2SteeringSki + // Insect Legs + 2671394050 => reciprocal(2671394050), // T3InsectLegBladed + 1668760037 => variant_dual(1889322041, 1668760037), // T4InsectLeg to T9/T10 (Jumper) + 875393229 => only_to(1668760037), // T4InsectLegSpider + 127661231 => variant_dual(2535778622, 127661231), // T3InsectLeg to T7/T8 (Leaper) + // Mech Legs + 3066502430 => fallback_conversion(), // T0MechLeg + 76192516 => fallback_conversion(), // T1MechLeg + 1789991181 => fallback_conversion(), // T2MechLeg + 1611765553 => reciprocal(706930465), // T3MechLeg to T10 Compact + 2452579137 => reciprocal(706930466), // T4MechLeg to T10 + 4140335767 => reciprocal(706930467), // T5MechLeg to TX-1 + 3660410821 => only_to(706930467), // T5MechLegGolden + // Sprinter Legs + 3345472872 => fallback_conversion(), // T3SprinterLeg + 1169080311 => fallback_conversion(), // T4SprinterLeg + 2474131397 => fallback_conversion(), // T4SprinterLegCarbon6 + // Rotors + 3947193935 => variant_dual(845358402, 3947193935), // T2Rotor to T5/T6 + 3789940851 => variant_dual(1549313867, 3789940851), // T3Rotor to T7/T8 + 1802864184 => variant_dual(1459104119, 1802864184), // T4Rotor to T9/T10 + // Thrusters + 844778245 => variant_dual(2766875446, 844778245), // T1Thruster to T1/T2 + 2498076128 => variant_dual(4276453881, 2498076128), // T2Thruster to T3/T4 + 3764614578 => variant_dual(1572019677, 3764614578), // T3Thruster to T5/T6 + 3938712462 => variant_dual(3285682302, 3938712462), // T4Thruster to T7/T8 + 2969949587 => variant_dual(3033577704, 2969949587), // T5Thruster to T9/T10 + 944347375 => reciprocal(944347375), // T5ThrusterCarbon6 + // Tracks + 4050988656 => fallback_conversion(), // T1TankTrack + 1691117057 => variant_dual(1637622047, 1691117057), // T2TankTrack to T3/T4 + 184017928 => variant_dual(3553732357, 184017928), // T3TankTrack to T5/T6 + 5728820 => variant_dual(3186238732, 5728820), // T4TankTrack to T7/T8 + 431289744 => variant_dual(3075124016, 431289744), // T5TankTrack to T9/T10 + // NOTE: wheels are not the same IDs, presumably because of the wheels rework between classic and modern versions + // Wheels steering + 932552373 => variant_dual(3769194911, 3158825472), // T0SteeringWheel to T5/T6 + 169839823 => variant_dual(1526222854, 3539316064), // T1SteeringWheel to T5/T6 + 2958657062 => variant_dual(3435681649, 1701822652), // T2SteeringWheel to T5/T6 + 1177366035 => variant_dual(1868379119, 244400800), // T3SteeringWheel to T7/T8 + //345048522 => variant_dual(4184683672, 3104518012), // T4SteeringWheel to T9/T10 + 345048522 => CubeConversionData { // T4SteeringWheel + from: vec![ + FromConversionData::Simple(4184683672), // T9 + FromConversionData::Simple(3104518012), // T10 + FromConversionData::Simple(2682515905), // T10 Large + ], + to: Some(ToConversionData::Simple(3104518012)), // T10 + }, + 32187483 => only_to(3104518012), // T4SteeringWheelCardLife + 48208435 => reciprocal(48208435), // T5SteeringWheel (mega) + 3546777424 => only_to(48208435), // T5SteeringWheelGolden + // Wheels non-steering + 1810876016 => variant_dual(2866040116, 299251682), // T0Wheel + 4172855773 => variant_dual(182430853, 2137276546), // T1Wheel + 2924196438 => variant_dual(2632952818, 3372100958), // T2Wheel + 3028949761 => variant_dual(1065013100, 2734731074), // T3Wheel + //1223384335 => variant_dual(, ), // T4Wheel + 1223384335 => CubeConversionData { // T4Wheel + from: vec![ + FromConversionData::Simple(2840250395), // T9 + FromConversionData::Simple(345686686), // T10 + FromConversionData::Simple(3625806184), // T10 Large + ], + to: Some(ToConversionData::Simple(345686686)), // T10 Large + }, + 2105254905 => only_to(345686686), // T4WheelCardLife + 3301162279 => reciprocal(3301162279), // T5Wheel (mega) + 2305706992 => only_to(3301162279), // T5WheelGolden + // Modern-only movement + 3286209719 => fallback_conversion(), // T3Propeller + 2784549582 => fallback_conversion(), // T4Propeller + // TODO + // Jammers + 198393630 => reciprocal(3805965640), // JammerSmall + 3106939802 => reciprocal(1439149132), // JammerMedium + //575087658 => reciprocal(1352133458), // JammerLarge + 575087658 => CubeConversionData { // JammerLarge + from: vec![ + FromConversionData::Simple(1352133458), // T7 + FromConversionData::Simple(3884591702), // T9 has no modern equivalent + ], + to: Some(ToConversionData::Simple(1352133458)), + }, + // Radars + 3782348815 => reciprocal(3123991804), // RadarSmall to T1 + 2115785469 => reciprocal(591201606), // RadarMedium to T5 + 3370519355 => reciprocal(1413101008), // RadarLarge to T9 + // Radar Receivers + 2330463949 => reciprocal(2460830034), // ReceiverSmall to T5 + 2312945847 => reciprocal(2549943884), // ReceiverMedium to T7 + 2742818297 => reciprocal(551604040), // ReceiverLarge to T9 + // Flippers aka alignment rectifiers (why does classic only have 1?) + 2297667466 => reciprocal(2014070946), // FlipperLarge + 2707915966 => only_to(2014070946), // FlipperSmall + 3083647184 => only_to(2014070946), // FlipperMedium + // Seats + 2325944783 => reciprocal(1692614192), // PilotSeatMega to Mega seat + 2843561040 => reciprocal(3186524732), // PilotSeatCray to actual pilot seat + 1427441647 => only_to(3186524732), // PilotSeatGene to actual pilot seat + 2743059293 => only_to(3186524732), // PilotSeatRetro to actual pilot seat + 1061758673 => only_to(3186524732), // PilotSeatBunny + // Modules + 1692614192 => only_to(1692614192), // T5EnergyModule to Mega Seat + 3263542661 => fallback_conversion(), // T5ShieldModule + 3987245394 => fallback_conversion(), // T5BlinkModule + 4230376397 => fallback_conversion(), // T5GhostModule + 658488560 => fallback_conversion(), // T5EmpModule + 344080878 => only_to(1413101008), // T5WindowmakerModule to Radar T9 + // Rods + 1818686721 => fallback_conversion(), // Rod + 1312806203 => fallback_conversion(), // RodShort + 3489647384 => fallback_conversion(), // RodLong + 2454932271 => fallback_conversion(), // RodArc + 3568811832 => fallback_conversion(), // RodArcShort + 394503911 => fallback_conversion(), // RodDiagonal2D + 1316425218 => fallback_conversion(), // RodDiagonal2DShort + 3705632066 => fallback_conversion(), // RodDiagonal3D + 1750720388 => fallback_conversion(), // RodPlus + 651695911 => fallback_conversion(), // RodSkewedPlus + 4265819694 => fallback_conversion(), // RodCross + 3950939011 => fallback_conversion(), // RodSpring (cosmetic rod) + 2828600518 => fallback_conversion(), // RodShortSpring (cosmetic rod) + 1051732041 => fallback_conversion(), // RodLongSpring (cosmetic rod) + 2183972675 => fallback_conversion(), // FairyLight (cosmetic rod) + // Struts + 4262490755 => fallback_conversion(), // Strut + 1648206355 => fallback_conversion(), // StrutShort + 3084940372 => fallback_conversion(), // StrutLong + 743433251 => fallback_conversion(), // StrutArc + 2126360617 => fallback_conversion(), // StrutSlice + 2608938070 => fallback_conversion(), // StrutSliceShort + 3999885169 => fallback_conversion(), // StrutRamp + 14694903 => fallback_conversion(), // StrutDiagonal2D + 3562021587 => fallback_conversion(), // StrutDiagonal2DShort + 2028802540 => fallback_conversion(), // StrutDiagonal3DL + 2002864041 => fallback_conversion(), // StrutDiagonal3DR + 511164535 => fallback_conversion(), // StrutPlus + 1521447199 => fallback_conversion(), // StrutSkewedPlus + 787409845 => fallback_conversion(), // StrutCross + 4206005137 => fallback_conversion(), // StrutArcShort + 3903776284 => fallback_conversion(), // StrutRampShort + 1319987665 => fallback_conversion(), // StrutDiagonal3DLShort + 3923036903 => fallback_conversion(), // StrutDiagonal3DRShort + // misc masks and mask-likes + 1704793982 => fallback_conversion(), // VigilantEyeL + 3558508089 => fallback_conversion(), // VigilantEyeR + 767825476 => fallback_conversion(), // CatEyeL + 683945560 => fallback_conversion(), // CatEyeR + 1609561971 => fallback_conversion(), // CyborgEye + 108680691 => fallback_conversion(), // SpikePin + 955529870 => fallback_conversion(), // SpikeNeedle + 1991524976 => fallback_conversion(), // SpikeClaw + 38268230 => fallback_conversion(), // EagleNeckL + 2017227318 => fallback_conversion(), // EagleNeckR + 2829693731 => fallback_conversion(), // EagleFeathersL + 3536989267 => fallback_conversion(), // EagleFeathersR + 1797279669 => fallback_conversion(), // EagleFaceL + 291229893 => fallback_conversion(), // EagleFaceR + 699401910 => fallback_conversion(), // AlienwareMaskEarL + 3136672710 => fallback_conversion(), // AlienwareMaskEarR + 3969591264 => fallback_conversion(), // AlienwareMaskHeadL + 2145317520 => fallback_conversion(), // AlienwareMaskHeadR + 1314825875 => fallback_conversion(), // AlienwareMaskJawL + 3709563875 => fallback_conversion(), // AlienwareMaskJawR + 1390502124 => fallback_conversion(), // NinjaMaskBandanaL + 3248797084 => fallback_conversion(), // NinjaMaskBandanaR + 989441293 => fallback_conversion(), // NinjaMaskHeadL + 2847701117 => fallback_conversion(), // NinjaMaskHeadR + 2503490917 => fallback_conversion(), // NinjaMaskL + 108914709 => fallback_conversion(), // NinjaMaskR + 799302218 => fallback_conversion(), // TrexHeadL + 981970925 => fallback_conversion(), // TrexHeadR + 4069461414 => fallback_conversion(), // TrexNoseL + 669458802 => fallback_conversion(), // TrexNoseR + 3618891370 => fallback_conversion(), // TrexJawL + 3670645745 => fallback_conversion(), // TrexJawR + // silly blocks + 149580437 => fallback_conversion(), // ExhaustBlower + 3489925411 => fallback_conversion(), // ExhaustStackL + 1679554409 => fallback_conversion(), // ExhaustStackR + 138993629 => fallback_conversion(), // PresentSmall + 568116457 => fallback_conversion(), // PresentLarge + 2484686408 => fallback_conversion(), // BubbleBlower + 346272734 => fallback_conversion(), // Balloon + // dev blocks + 3950293873 => fallback_conversion(), // ProtoniumCrystal + 2771514423 => fallback_conversion(), // Equalizer + 606866102 => fallback_conversion(), // ProtoniumClasp + 3951497291 => fallback_conversion(), // FusionTower + unhandled_id => { + log::warn!("Unimplemented modern ID {}", unhandled_id); + fallback_conversion() + } + }; + cube.conversion = Some(conversion); +} + +fn reciprocal(classic_id: u32) -> CubeConversionData { + CubeConversionData { + from: vec![ + FromConversionData::Simple(classic_id), + ], + to: Some(ToConversionData::Simple(classic_id)), + } +} + +fn only_to(classic_id: u32) -> CubeConversionData { + CubeConversionData { + from: Vec::default(), + to: Some(ToConversionData::Simple(classic_id)), + } +} + +fn variant_dual(lower_tier: u32, higher_tier: u32) -> CubeConversionData { + CubeConversionData { + from: vec![ + FromConversionData::Simple(lower_tier), + FromConversionData::Simple(higher_tier), + ], + to: Some(ToConversionData::Simple(higher_tier)), + } +} + +fn fallback_conversion() -> CubeConversionData { + CubeConversionData { + from: Vec::default(), + to: Some(ToConversionData::Simple(2000000000)), // camera cube + } +} + +// these are positions in the array of colours, not the UI position... thanks FJ +const NO_OFFSET: Option<(i16, i16, i16)> = Some((0, 0, 0)); +const ORANGE: u8 = 3; +//const ORANGE: u8 = 11; +const YELLOW: u8 = 6; +const GOLD: u8 = 6; +//const LIGHT_BLUE: u8 = 3; +const BLUE: u8 = 9; +//const DARK_BLUE: u8 = 24; +const WHITE: u8 = 0; +const BLACK: u8 = 4; +const RED: u8 = 5; +const STYPE: u8 = 6; // yellow-ish? +const GREEN: u8 = 7; +const PURPLE: u8 = 10; diff --git a/rc_core/src/persist/cube_data.rs b/rc_core/src/persist/cube_data.rs index 39cd741..6d1b0b2 100644 --- a/rc_core/src/persist/cube_data.rs +++ b/rc_core/src/persist/cube_data.rs @@ -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, pub movement: Option, pub tree: Option, + pub conversion: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index 04cd70b..7150cd9 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -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, diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 6584556..5841f5e 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -396,7 +396,7 @@ impl UserData { Ok(()) } - async fn all_vehicles(&self) -> Result, oj_rc_database::sea_orm::DbErr> { + pub(super) async fn all_vehicles(&self) -> Result, oj_rc_database::sea_orm::DbErr> { self.db.garages_by_user_id(self.account.id).await } @@ -1123,7 +1123,7 @@ impl super::User 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 diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index e16c21b..9a5a43c 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -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; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 977f6a4..3d033a7 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -676,7 +676,19 @@ pub trait FactoryUser { #[async_trait::async_trait] pub trait WebUser: CommonUser { + async fn garages(&self) -> Result, Box>; + async fn garage_by_id(&self, id: i32) -> Result, Box>; + async fn save_garage(&self, vehicle: crate::persist::user::VehicleData, garage_id: Option, cpu_counter: &crate::cubes::CpuListParser, weapon_orderer: &crate::cubes::WeaponListParser) -> Result<(), Box>; + async fn garage_id_selected(&self) -> Result, Box>; +} +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] diff --git a/rc_core/src/persist/user/web.rs b/rc_core/src/persist/user/web.rs index aa2d0e9..bdcd593 100644 --- a/rc_core/src/persist/user/web.rs +++ b/rc_core/src/persist/user/web.rs @@ -1,4 +1,109 @@ #[async_trait::async_trait] impl super::WebUser for super::account_json::UserData { - // TODO + async fn garages(&self) -> Result, Box> { + 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, Box> { + 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, + cpu_counter: &crate::cubes::CpuListParser, + weapon_orderer: &crate::cubes::WeaponListParser, + ) -> Result<(), Box> { + 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, Box> { + Ok(self.db.garage_selected(self.account.id).await? + .map(|x| x.id)) + } } diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index a80dce5..0ac472c 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -129,7 +129,7 @@ impl Database { } pub async fn insert_user_aux(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::user_aux::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; + crate::schema::user_aux::Entity::insert_many(entities).exec(self.orm.as_ref()).await?; Ok(()) } @@ -217,6 +217,13 @@ impl Database { .await } + pub async fn garage_count_by_user_id(&self, user_id: i32) -> Result { + crate::schema::garage::Entity::find() + .filter(crate::schema::garage::Column::UserId.eq(user_id)) + .count(self.orm.as_ref()) + .await + } + pub async fn garage_max_slot_by_user_id(&self, user_id: i32) -> Result { let result = crate::schema::garage::Entity::find() .select_only() @@ -265,8 +272,20 @@ impl Database { .await } + pub async fn slot_of_garage_by_id_and_user_id(&self, garage_id: i32, user_id: i32) -> Result, sea_orm::DbErr> { + let opt = crate::schema::garage::Entity::find() + .select_only() + .column_as(crate::schema::garage::Column::Slot, "column") + .filter(crate::schema::garage::Column::Id.eq(garage_id)) + .filter(crate::schema::garage::Column::UserId.eq(user_id)) + .into_model::>() + .one(self.orm.as_ref()) + .await?; + Ok(opt.map(|x| *x)) + } + pub async fn insert_garages(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::garage::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; + crate::schema::garage::Entity::insert_many(entities).exec(self.orm.as_ref()).await?; Ok(()) } @@ -515,7 +534,7 @@ impl Database { } pub async fn insert_players(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { - crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(self.orm.as_ref()).await?; + crate::schema::multiplayer_game_player::Entity::insert_many(entities).exec(self.orm.as_ref()).await?; Ok(()) } diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 2be7bdc..0504909 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -658,7 +658,7 @@ impl QueueHandler { count += q_entry.users.len(); if let Some(values) = new_queue_map.get_mut(&key) { values.users.append(&mut q_entry.users); - values.platoons.extend(q_entry.platoons.into_iter()); + values.platoons.extend(q_entry.platoons); } else { new_queue_map.insert(key.clone(), q_entry); } diff --git a/rc_plugins/src/lib.rs b/rc_plugins/src/lib.rs index ffcaa56..b4c658e 100644 --- a/rc_plugins/src/lib.rs +++ b/rc_plugins/src/lib.rs @@ -1,6 +1,7 @@ pub mod chat; pub mod vehicle_validation; pub mod team_selection; +pub mod vehicle_import; pub trait Plugin: Send + Sync { fn self_check(&self) -> bool { diff --git a/rc_plugins/src/vehicle_import/mod.rs b/rc_plugins/src/vehicle_import/mod.rs new file mode 100644 index 0000000..5fcc142 --- /dev/null +++ b/rc_plugins/src/vehicle_import/mod.rs @@ -0,0 +1,2 @@ +mod plugin; +pub use plugin::{VehicleImportPlugin, VehicleImportErrorCode, VehicleImportData}; diff --git a/rc_plugins/src/vehicle_import/plugin.rs b/rc_plugins/src/vehicle_import/plugin.rs new file mode 100644 index 0000000..74ce4c9 --- /dev/null +++ b/rc_plugins/src/vehicle_import/plugin.rs @@ -0,0 +1,35 @@ +#[repr(u8)] +#[derive(Debug)] +pub enum VehicleImportErrorCode { + Invalid, + Unsupported, + UnknownCube, +} + +impl core::fmt::Display for VehicleImportErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Invalid => write!(f, "Invalid/corrupt data"), + Self::Unsupported => write!(f, "Unsupported/unrecognized data"), + Self::UnknownCube => write!(f, "Unknown cube in data"), + } + } +} + +impl core::error::Error for VehicleImportErrorCode {} + +#[derive(Debug)] +pub struct VehicleImportData { + pub cube_data: Vec, + pub colour_data: Vec, + pub vehicle_name: Option, + pub vehicle_author: Option, +} + +pub trait VehicleImportPlugin: crate::Plugin { + //fn supports(&self) -> Vec; + /// Preferred file extension for the raw data + fn file_ext(&self) -> &'static str; + fn import(&self, upload: &[u8]) -> Result; + fn export(&self, data: &VehicleImportData) -> Result, VehicleImportErrorCode>; +} diff --git a/rc_society/Cargo.toml b/rc_society/Cargo.toml index 8df84f4..a346b10 100644 --- a/rc_society/Cargo.toml +++ b/rc_society/Cargo.toml @@ -11,6 +11,7 @@ readme.workspace = true actix-web = { workspace = true, features = [ "secure-cookies" ] } actix-session = { version = "0.11", features = [ "cookie-session" ], default-features = false } actix-identity = "0.9" +actix-multipart = "0.7" cookie = "0.18" actix-files.workspace = true #actix-ws = "0.3" @@ -21,8 +22,11 @@ tokio = { version = "1.43", features = [ "rt-multi-thread" ] } clap.workspace = true handlebars = { version = "6", features = ["dir_source"] } oj_rc_core = { version = "*", path = "../rc_core" } +oj_rc_plugins = { version = "*", path = "../rc_plugins" } +oj_convert.workspace = true libfj.workspace = true git-version.workspace = true serde.workspace = true serde_json.workspace = true +base64.workspace = true chrono.workspace = true diff --git a/rc_society/src/api/garage/mod.rs b/rc_society/src/api/garage/mod.rs new file mode 100644 index 0000000..25a0616 --- /dev/null +++ b/rc_society/src/api/garage/mod.rs @@ -0,0 +1 @@ +pub mod plugins; diff --git a/rc_society/src/api/garage/plugins/bobocraft.rs b/rc_society/src/api/garage/plugins/bobocraft.rs new file mode 100644 index 0000000..5b43732 --- /dev/null +++ b/rc_society/src/api/garage/plugins/bobocraft.rs @@ -0,0 +1,124 @@ +use oj_convert::{Bobocraft, BobocraftBlock}; + +pub struct RexLike { + convert: std::sync::Arc, +} + +impl RexLike { + pub fn new(convert: std::sync::Arc) -> 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 { + 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, 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 {} diff --git a/rc_society/src/api/garage/plugins/classic.rs b/rc_society/src/api/garage/plugins/classic.rs new file mode 100644 index 0000000..ff3dc60 --- /dev/null +++ b/rc_society/src/api/garage/plugins/classic.rs @@ -0,0 +1,103 @@ +use oj_convert::{Classic, ClassicBlock}; + +pub struct FifteenLike { + convert: std::sync::Arc, + base_image: std::sync::Arc>, + cpu_counter: std::sync::Arc, +} + +impl FifteenLike { + pub fn new( + convert: std::sync::Arc, + base_image: Vec, + cpu_counter: std::sync::Arc, + ) -> 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 { + 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, 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 {} diff --git a/rc_society/src/api/garage/plugins/factory.rs b/rc_society/src/api/garage/plugins/factory.rs new file mode 100644 index 0000000..0c3d9f2 --- /dev/null +++ b/rc_society/src/api/garage/plugins/factory.rs @@ -0,0 +1,102 @@ +use std::io::Cursor; + +use libfj::robocraft::{FactoryRobotGetInfo, FactoryInfo}; + +pub struct FactoryLike { + convert: std::sync::Arc, +} + +impl FactoryLike { + pub fn new(convert: std::sync::Arc,) -> Self { + Self { + convert, + } + } + + fn try_guess_json_format(upload: &[u8]) -> Option { + 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>(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 { + 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, 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 {} diff --git a/rc_society/src/api/garage/plugins/mod.rs b/rc_society/src/api/garage/plugins/mod.rs new file mode 100644 index 0000000..7f21f61 --- /dev/null +++ b/rc_society/src/api/garage/plugins/mod.rs @@ -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>, +} + +impl ImportPlugins { + pub fn standard( + assets_path: impl AsRef, + 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 { + let mut to_sort = self.map.keys().collect::>(); + 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 { + 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, 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) + } + } +} diff --git a/rc_society/src/api/mod.rs b/rc_society/src/api/mod.rs index e69de29..46b5f68 100644 --- a/rc_society/src/api/mod.rs +++ b/rc_society/src/api/mod.rs @@ -0,0 +1 @@ +pub mod garage; diff --git a/rc_society/src/main.rs b/rc_society/src/main.rs index 1473258..230e0f4 100644 --- a/rc_society/src/main.rs +++ b/rc_society/src/main.rs @@ -27,6 +27,14 @@ async fn main() -> std::io::Result<()> { let server_settings = actix_web::web::Data::new(>::server_config(&config)); + let parsers = oj_rc_core::cubes::CubeParsers::new(&config); + + let vehicle_importers = crate::api::garage::plugins::ImportPlugins::standard(&cli_args.assets_robocraft, &parsers); + log::info!("Loaded {} vehicle import/export plugins", vehicle_importers.plugin_names().count()); + let importers_ref = actix_web::web::Data::new(vehicle_importers); + + let parsers_ref = actix_web::web::Data::new(parsers); + let users = oj_rc_core::UserImpl::load(&cli_args.data_robocraft, &config).await.expect("Bad user data"); let auth_ref = actix_web::web::Data::new(Box::new(users)); @@ -65,12 +73,22 @@ async fn main() -> std::io::Result<()> { .app_data(handlebars_ref.clone()) .app_data(server_settings.clone()) .app_data(auth_ref.clone()) + .app_data(importers_ref.clone()) + .app_data(parsers_ref.clone()) .service(version_info) .service(web::login::form_submit) .service(web::login::form_load) .service(web::favicon::favicon_standard) .service(web::dashboard::get) + .service(web::dashboard::post) // for login redirect .service(web::index::get) + .service(web::garage::list::get) + .service(web::garage::info::get) + .service(web::garage::export::get) + .service(web::garage::import::get_existing) + .service(web::garage::import::get_new) + .service(web::garage::import::post) + .service(web::garage::selected::get) }) .bind((cli_args.ip, cli_args.port))? .run() diff --git a/rc_society/src/web/dashboard.rs b/rc_society/src/web/dashboard.rs index ea760e7..eced6c8 100644 --- a/rc_society/src/web/dashboard.rs +++ b/rc_society/src/web/dashboard.rs @@ -1,4 +1,4 @@ -use actix_web::{get, web::Data, Responder, HttpRequest}; +use actix_web::{get, post, web::Data, Responder, HttpRequest}; use actix_identity::Identity; use serde::{Serialize, Deserialize}; @@ -29,12 +29,12 @@ struct PermissionData { banned: bool, } -#[get("/dashboard")] -pub async fn get(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { +pub async fn dashboard_impl(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { match super::try_auth_user(user_opt, auth.as_ref(), &req).await? { super::LoginReturn::AuthFail(resp) => Ok(resp), super::LoginReturn::Success(user) => { // TODO + log::debug!("Rendering user's dashboard"); let creation_time = user.creation(); let creation_time_chrono = chrono::DateTime::::from_timestamp_secs(creation_time).unwrap_or_default(); Ok(super::render_ok( @@ -63,3 +63,13 @@ pub async fn get(handlebars_ref: Data>, auth: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + dashboard_impl(handlebars_ref, auth, user_opt, req).await +} + +#[post("/dashboard")] +pub async fn post(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + dashboard_impl(handlebars_ref, auth, user_opt, req).await +} diff --git a/rc_society/src/web/garage/export.rs b/rc_society/src/web/garage/export.rs new file mode 100644 index 0000000..87f97ad --- /dev/null +++ b/rc_society/src/web/garage/export.rs @@ -0,0 +1,52 @@ +use actix_web::{HttpRequest, HttpResponse, Responder, get, http::StatusCode, web::{Data, Path, Query}}; +use actix_identity::Identity; + +use oj_rc_plugins::vehicle_import::VehicleImportData; + +use crate::web::{LoginReturn, try_auth_user}; +use crate::api::garage::plugins::ImportPlugins; + +#[get("/garages/{id}/export")] +pub async fn get(id: Path, query: Query, exporter: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + match user.garage_by_id(*id).await { + Ok(Some(garage)) => { + let data = VehicleImportData { + cube_data: garage.robot_data, + colour_data: garage.colour_data, + vehicle_name: garage.name, + vehicle_author: Some(user.display_name().to_owned()), + }; + match exporter.export_by_name(&query.plugin, &data) { + Ok(export) => { + //export.push(b'\n'); + use actix_web::http::header::{ContentDisposition, TryIntoHeaderPair}; + let ext = exporter.file_ext(&query.plugin).unwrap(); + // TODO sanitise vehicle name and include it in the filename + let dispo = ContentDisposition::attachment(format!("export-{}-{}.{}", &query.plugin, *id, ext)); + let (key, val) = dispo.try_into_pair().unwrap(); + let mut resp = HttpResponse::with_body(StatusCode::OK, export); + resp.headers_mut().append(key, val); + Ok(resp.map_into_boxed_body()) + }, + Err(e) => { + log::error!("Failed export of vehicle {} for user {}: {}", id, user.public_id(), e); + Err(super::PluginPortError::from(e).into()) + } + } + }, + Ok(None) => { + Ok(HttpResponse::NotFound() + .finish()) + }, + Err(e) => { + log::error!("Failed to load garage ID for user {}: {}", user.public_id(), e); + Ok(HttpResponse::InternalServerError() + .finish()) + } + } + } + } +} diff --git a/rc_society/src/web/garage/import.rs b/rc_society/src/web/garage/import.rs new file mode 100644 index 0000000..ccd40f2 --- /dev/null +++ b/rc_society/src/web/garage/import.rs @@ -0,0 +1,221 @@ +use actix_web::{get, post, web::{Data, Path}, Responder, HttpRequest}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +use crate::web::{LoginReturn, try_auth_user, render_ok, render_err}; +use crate::api::garage::plugins::ImportPlugins; + +const FORM_NAME: &str = "garage_import"; + +#[derive(Serialize, Deserialize)] +struct RenderData { + display_name: String, + public_id: String, + garage: Option, + importers: Vec, + selected_importer: Option, + success: Option, +} + +#[derive(Serialize, Deserialize)] +struct GarageData { + garage_id: i32, + slot: i32, +} + +async fn import_impl(id: Option, handlebars_ref: Data>, importer: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let html = if let Some(id) = id { + match user.garage_by_id(id).await { + Ok(Some(garage)) => { + render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: Some(GarageData { + garage_id: id, + slot: garage.slot, + }), + importers: importer.plugin_names() + .map(|x| x.to_owned()) + .collect(), + selected_importer: None, + success: None, + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + }, + Ok(None) => { + log::error!("Failed to find vehicle {} for user {}", id, user.public_id()); + render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: Some(GarageData { + garage_id: -1, + slot: -1, + }), + importers: Vec::new(), + selected_importer: None, + success: None, + }, + "Vehicle not found".to_owned(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + }, + Err(e) => { + log::error!("Failed to retrieve vehicle {} for user {}: {}", id, user.public_id(), e); + render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: Some(GarageData { + garage_id: -1, + slot: -1, + }), + importers: Vec::new(), + selected_importer: None, + success: None, + }, + e.to_string(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + } + } + } else { + render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: None, + importers: importer.plugin_names() + .map(|x| x.to_owned()) + .collect(), + selected_importer: None, + success: None, + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + }; + Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} + +#[get("/garages/{id}/import")] +pub async fn get_existing(id: Path, handlebars_ref: Data>, importer: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + import_impl(Some(*id), handlebars_ref, importer, auth, user_opt, req).await +} + +#[get("/garages/import")] +pub async fn get_new(handlebars_ref: Data>, importer: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + import_impl(None, handlebars_ref, importer, auth, user_opt, req).await +} + +#[derive(Debug, actix_multipart::form::MultipartForm)] +struct ImportForm { + //#[multipart] + files: Vec, + garage_id: actix_multipart::form::text::Text, + plugin: actix_multipart::form::text::Text, + +} + +#[post("/garages/import")] +pub async fn post(form: actix_multipart::form::MultipartForm, handlebars_ref: Data>, importer: Data, parsers: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let total_size: usize = form.files.iter().map(|f| f.data.len()).sum(); + if form.files.is_empty() { + return Err(super::PluginPortError { + code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid, + }.into()); + } + if *form.garage_id != -1 && form.files.len() != 1 { + return Err(super::PluginPortError { + code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode::Invalid, + }.into()); + } + for (i, file) in form.files.iter().enumerate() { + #[cfg(debug_assertions)] + log::trace!("import {} file {} data: {:?}", &*form.plugin, i, &file.data[..]); + let import_data = match importer.import_by_name(&form.plugin, &file.data) + .map_err(|e| super::PluginPortError { code: e }) { + Ok(x) => x, + Err(e) => { + let html = render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: None, + importers: importer.plugin_names() + .map(|x| x.to_owned()) + .collect(), + selected_importer: Some(form.plugin.0.clone()), + success: None, + }, + format!("Failed to import file {}: {}", i, e), + handlebars_ref.as_ref(), + FORM_NAME, + ); + return Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ); + }, + }; + let vehicle_data = oj_rc_core::persist::user::VehicleData { + name: import_data.vehicle_name, + slot: -1, // will be overriden in oj_rc_core + robot_data: import_data.cube_data, + colour_data: import_data.colour_data, + weapon_order: Vec::default(), // will be overriden in oj_rc_core + crf_id: None, // irrelevant + was_rated: None, // irrelevant + }; + user.save_garage( + vehicle_data, + if *form.garage_id < 0 { None } else { Some(*form.garage_id) }, // multipart crate silliness + parsers.cpu_counter().as_ref(), + parsers.weapon_order().as_ref(), + ).await?; + } + let html = render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: None, + importers: importer.plugin_names() + .map(|x| x.to_owned()) + .collect(), + selected_importer: Some(form.plugin.0.clone()), + success: Some(format!( + "Imported {} vehicle{} ({:.2}KiB)", + form.files.len(), + if form.files.len() > 1 { "s" } else { "" }, + total_size as f64 / 1024.0, + )), + }, + handlebars_ref.as_ref(), + FORM_NAME, + ); + Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} diff --git a/rc_society/src/web/garage/info.rs b/rc_society/src/web/garage/info.rs new file mode 100644 index 0000000..998057c --- /dev/null +++ b/rc_society/src/web/garage/info.rs @@ -0,0 +1,93 @@ +use actix_web::{get, web::{Data, Path}, Responder, HttpRequest}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +use crate::web::{LoginReturn, try_auth_user, render_ok, render_err}; +use crate::api::garage::plugins::ImportPlugins; + +const FORM_NAME: &str = "garage_info"; + +#[derive(Serialize, Deserialize)] +struct RenderData { + display_name: String, + public_id: String, + garage: GarageData, + exporters: Vec, +} + +#[derive(Serialize, Deserialize)] +struct GarageData { + garage_id: i32, + slot: i32, + name: String, +} + +#[get("/garages/{id}/info")] +pub async fn get(id: Path, handlebars_ref: Data>, exporter: Data, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let html = match user.garage_by_id(*id).await { + Ok(Some(garage)) => { + render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: GarageData { + garage_id: *id, + slot: garage.slot, + name: garage.name.unwrap_or_default(), + }, + exporters: exporter.plugin_names() + .map(|x| x.to_owned()) + .collect(), + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + }, + Ok(None) => { + log::error!("Failed to find vehicle {} for user {}", id, user.public_id()); + render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: GarageData { + garage_id: -1, + slot: -1, + name: String::default(), + }, + exporters: Vec::new(), + }, + "Vehicle not found".to_owned(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + }, + Err(e) => { + log::error!("Failed to retrieve vehicle {} for user {}: {}", id, user.public_id(), e); + render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garage: GarageData { + garage_id: -1, + slot: -1, + name: String::default(), + }, + exporters: Vec::new(), + }, + e.to_string(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + } + }; + Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} diff --git a/rc_society/src/web/garage/list.rs b/rc_society/src/web/garage/list.rs new file mode 100644 index 0000000..5fea689 --- /dev/null +++ b/rc_society/src/web/garage/list.rs @@ -0,0 +1,76 @@ +use actix_web::{get, web::Data, Responder, HttpRequest}; +use actix_identity::Identity; +use serde::{Serialize, Deserialize}; + +use crate::web::{LoginReturn, try_auth_user, render_ok, render_err}; + +const FORM_NAME: &str = "garage_list"; + +#[derive(Serialize, Deserialize)] +struct RenderData { + display_name: String, + public_id: String, + garages: Vec, +} + +#[derive(Serialize, Deserialize)] +struct GarageData { + garage_id: i32, + slot: i32, + name: String, + cpu: i32, + max_cpu: i32, + creation_time_unix: i64, + creation_time_iso: String, +} + +#[get("/garages/list")] +pub async fn get(handlebars_ref: Data>, auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let html = match user.garages().await { + Ok(mut garage_infos) => { + garage_infos.sort_by_key(|g| g.slot); + render_ok( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garages: garage_infos.into_iter() + .map(|g| GarageData { + garage_id: g.id, + slot: g.slot, + name: g.name, + cpu: g.total_robot_cpu, + max_cpu: g.bay_cpu, + creation_time_unix: g.creation_time, + creation_time_iso: chrono::DateTime::::from_timestamp_secs(g.creation_time).unwrap_or_default().to_rfc3339(), + }) + .collect(), + }, + handlebars_ref.as_ref(), + FORM_NAME, + ) + }, + Err(e) => { + log::error!("Failed to retrieve garages for user {}: {}", user.public_id(), e); + render_err( + RenderData { + display_name: user.display_name().to_owned(), + public_id: user.public_id().to_owned(), + garages: Vec::new(), + }, + e.to_string(), + handlebars_ref.as_ref(), + FORM_NAME, + ) + } + }; + Ok( + html + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} diff --git a/rc_society/src/web/garage/mod.rs b/rc_society/src/web/garage/mod.rs new file mode 100644 index 0000000..1976bfe --- /dev/null +++ b/rc_society/src/web/garage/mod.rs @@ -0,0 +1,37 @@ +pub mod list; +pub mod export; +pub mod import; +pub mod info; +pub mod selected; + +#[derive(serde::Deserialize)] +struct PortQuery { + pub plugin: String, +} + +#[derive(Debug)] +struct PluginPortError { + code: oj_rc_plugins::vehicle_import::VehicleImportErrorCode, +} + +impl std::convert::From for PluginPortError { + fn from(value: oj_rc_plugins::vehicle_import::VehicleImportErrorCode) -> Self { + Self { + code: value, + } + } +} + +impl core::fmt::Display for PluginPortError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.code) + } +} + +impl core::error::Error for PluginPortError {} + +impl actix_web::error::ResponseError for PluginPortError { + fn status_code(&self) -> actix_web::http::StatusCode { + actix_web::http::StatusCode::UNPROCESSABLE_ENTITY + } +} diff --git a/rc_society/src/web/garage/selected.rs b/rc_society/src/web/garage/selected.rs new file mode 100644 index 0000000..661c232 --- /dev/null +++ b/rc_society/src/web/garage/selected.rs @@ -0,0 +1,29 @@ +use actix_web::{get, web::{Data, Redirect}, Responder, HttpRequest}; +use actix_identity::Identity; + +use crate::web::{LoginReturn, try_auth_user}; + +#[get("/garages/selected")] +pub async fn get(auth: Data>, user_opt: Option, req: HttpRequest) -> Result { + match try_auth_user(user_opt, auth.as_ref(), &req).await? { + LoginReturn::AuthFail(resp) => Ok(resp), + LoginReturn::Success(user) => { + let resp = if let Some(selected_garage) = user.garage_id_selected().await? { + Redirect::to(format!("/garages/{}/info", selected_garage)) + .respond_to(&req) + .map_into_boxed_body() + + } else { + log::error!("User {} has no garage selected (bad database state?)", user.display_name()); + Redirect::to("/garages/list".to_owned()) + .respond_to(&req) + .map_into_boxed_body() + }; + Ok( + resp + .respond_to(&req) + .map_into_boxed_body() + ) + } + } +} diff --git a/rc_society/src/web/mod.rs b/rc_society/src/web/mod.rs index 2422763..cee20ec 100644 --- a/rc_society/src/web/mod.rs +++ b/rc_society/src/web/mod.rs @@ -2,6 +2,7 @@ pub mod dashboard; pub mod login; pub mod favicon; pub mod index; +pub mod garage; use serde::Serialize; use actix_web::{web::{Html, Redirect}, Responder}; @@ -51,19 +52,25 @@ async fn try_auth_user(user_opt: Option, auth: &oj_rc_ if let Some(user) = user_opt { let user_id = user.id()?; match >::web_authenticate(auth, user_id.clone()).await { - Ok(user) => Ok(LoginReturn::Success(user)), + Ok(user) => { + //log::debug!("Web auth success"); + Ok(LoginReturn::Success(user)) + }, Err(e) => { log::warn!("Failed to login with token {}: {} ({:?})", user_id, e.message, e.code); Ok(LoginReturn::AuthFail( Redirect::to("/login") + .temporary() .respond_to(req) .map_into_boxed_body() )) } } } else { + log::debug!("No user identity, redirecting to login page"); Ok(LoginReturn::AuthFail( Redirect::to("/login") + .temporary() .respond_to(req) .map_into_boxed_body() ))