From 16f07613807920e22cc4a44a7b36c73482b73ab2 Mon Sep 17 00:00:00 2001 From: MaxSignal Date: Wed, 14 Jan 2026 06:56:00 +0000 Subject: [PATCH] Implementing the factory web API --- Cargo.toml | 2 +- assets/robocraft/servenvmulti.config | 1 + rc_factory/src/arc/adapter.rs | 2 +- rc_factory_api/Cargo.toml | 21 +++ rc_factory_api/run_debug.sh | 3 + rc_factory_api/src/cli.rs | 23 +++ rc_factory_api/src/main.rs | 43 +++++ .../src/robocraft/factory/crf_api.rs | 154 ++++++++++++++++++ rc_factory_api/src/robocraft/factory/mod.rs | 1 + rc_factory_api/src/robocraft/mod.rs | 1 + 10 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 rc_factory_api/Cargo.toml create mode 100755 rc_factory_api/run_debug.sh create mode 100644 rc_factory_api/src/cli.rs create mode 100644 rc_factory_api/src/main.rs create mode 100644 rc_factory_api/src/robocraft/factory/crf_api.rs create mode 100644 rc_factory_api/src/robocraft/factory/mod.rs create mode 100644 rc_factory_api/src/robocraft/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 99623c0..b265199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ "rc_singleplayer", "rc_singleplayer_room", "rc_lobby", "rc_lobby_room", "rc_core", "rc_database", "rc_factory", - "rc_multiplayer", + "rc_factory_api" ,"rc_multiplayer", "rc_plugins", ] diff --git a/assets/robocraft/servenvmulti.config b/assets/robocraft/servenvmulti.config index c190b1a..610927f 100644 --- a/assets/robocraft/servenvmulti.config +++ b/assets/robocraft/servenvmulti.config @@ -46,6 +46,7 @@ http://domain_or_ip_address:8001/ http://domain_or_ip_address:8010/live/data.json http://domain_or_ip_address:8011/ + http://domain_or_ip_address:8012/ domain_or_ip_address:4542 diff --git a/rc_factory/src/arc/adapter.rs b/rc_factory/src/arc/adapter.rs index 9bd8e39..8db88b2 100644 --- a/rc_factory/src/arc/adapter.rs +++ b/rc_factory/src/arc/adapter.rs @@ -85,7 +85,7 @@ impl crate::VehicleFactoryAdapter for ArcAdapter { id: meta.id as _, name: meta.name, description: meta.description, - thumbnail: meta.thumbnail, + thumbnail: self.thumbnail_url(meta.thumbnail, meta.id), added_by: if self.spoof_users { hex::encode(meta.added_by.as_bytes()) } else { meta.added_by }, added_by_display_name: meta.added_by_display_name, added_date: crate::traits::parse_rc_date(&meta.added_date).unwrap_or_default(), diff --git a/rc_factory_api/Cargo.toml b/rc_factory_api/Cargo.toml new file mode 100644 index 0000000..e98c4db --- /dev/null +++ b/rc_factory_api/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "oj_factory_api" +version.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +authors.workspace = true +readme.workspace = true + +[dependencies] +actix-web.workspace = true +clap.workspace = true +log.workspace = true +env_logger.workspace = true +git-version.workspace = true +base64.workspace = true +serde_json.workspace = true + +libfj.workspace = true +oj_rc_core = { version = "*", path = "../rc_core" } +oj_rc_factory = { version = "*", path = "../rc_factory" } diff --git a/rc_factory_api/run_debug.sh b/rc_factory_api/run_debug.sh new file mode 100755 index 0000000..a6e46e5 --- /dev/null +++ b/rc_factory_api/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_LOG=debug cargo run diff --git a/rc_factory_api/src/cli.rs b/rc_factory_api/src/cli.rs new file mode 100644 index 0000000..a07d819 --- /dev/null +++ b/rc_factory_api/src/cli.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 8012)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Assets root + #[arg(long, default_value_t = {"../assets/robocraft".to_string()})] + pub assets: String, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_factory_api/src/main.rs b/rc_factory_api/src/main.rs new file mode 100644 index 0000000..19ef79e --- /dev/null +++ b/rc_factory_api/src/main.rs @@ -0,0 +1,43 @@ +mod cli; +mod robocraft; + +use actix_web::{App, HttpServer, Responder}; +use oj_rc_core::persist::config::{ConfigImpl, ConfigProvider}; + +#[actix_web::get("/")] +async fn index() -> impl Responder { + let name = env!("CARGO_PKG_NAME"); + let version = env!("CARGO_PKG_VERSION"); + let git_version = git_version::git_version!(args = ["--always", "--dirty=+"]); + let authors = env!("CARGO_PKG_AUTHORS"); + let license = env!("CARGO_PKG_LICENSE"); + let repo = env!("CARGO_PKG_REPOSITORY"); + format!("{} {}:{} by [{}]\n{}\n{}", name, version, git_version, authors, license, repo) +} + +fn io_error(e: impl ToString) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let cli_args = cli::CliArgs::get(); + + let conf = ConfigImpl::load(&cli_args.assets).map_err(io_error)?; + let factory_enum = >::factory(&conf).await.map_err(io_error)?; + + let factory_data = actix_web::web::Data::new(factory_enum); + + HttpServer::new(move || { + App::new() + .app_data(factory_data.clone()) + .service(index) + .service(robocraft::factory::crf_api::list) + .service(robocraft::factory::crf_api::list_default) + .service(robocraft::factory::crf_api::get) + }) + .bind((cli_args.ip, cli_args.port))? + .run() + .await +} diff --git a/rc_factory_api/src/robocraft/factory/crf_api.rs b/rc_factory_api/src/robocraft/factory/crf_api.rs new file mode 100644 index 0000000..59e74bd --- /dev/null +++ b/rc_factory_api/src/robocraft/factory/crf_api.rs @@ -0,0 +1,154 @@ +use actix_web::{get, post, web::{Data, Json, Path}, HttpResponse}; +use base64::Engine; + +use libfj::robocraft::{ + FactoryInfo, RoboShopItemsInfo, FactoryRobotGetInfo, FactoryRobotListInfo, + ListPayload, ListQuery, FactoryOrderType, FactoryTextSearchField, +}; +use oj_rc_factory::{VehicleFactoryAdapter, VehicleInfo, VehicleQueryInfo}; + +fn parse_filter(filter: &str) -> Vec { + if filter.trim().is_empty() { + return Vec::new(); + } + filter.split(',') + .filter_map(|x| x.trim().parse::().ok()) + .collect() +} + +fn payload_to_query(payload: &ListPayload) -> ListQuery { + ListQuery { + page: (payload.page.max(1)) as usize, + page_size: (payload.page_size.max(1).min(100)) as usize, + order: FactoryOrderType::try_from(payload.order.clamp(0, u8::MAX.into()) as u8).unwrap_or(FactoryOrderType::Suggested), + player_filter: payload.player_filter, + movement_filter: parse_filter(&payload.movement_filter), + movement_category_filter: parse_filter(&payload.movement_category_filter), + weapon_filter: parse_filter(&payload.weapon_filter), + weapon_category_filter: parse_filter(&payload.weapon_category_filter), + minimum_cpu: if payload.minimum_cpu <= 0 { 0 } else { payload.minimum_cpu as usize }, + maximum_cpu: if payload.maximum_cpu <= 0 { usize::MAX } else { payload.maximum_cpu as usize }, + text_filter: payload.text_filter.clone(), + text_search_field: FactoryTextSearchField::try_from(payload.text_search_field.clamp(0, u8::MAX.into()) as u8).unwrap_or(FactoryTextSearchField::All), + buyable: payload.buyable, + prepend_featured_robot: payload.prepend_featured_robot, + featured_only: payload.featured_only, + default_page: payload.default_page, + } +} + +fn list_info(vi: VehicleQueryInfo) -> FactoryRobotListInfo { + FactoryRobotListInfo { + item_id: vi.id as usize, + item_name: vi.name, + item_description: vi.description, + thumbnail: vi.thumbnail, + added_by: vi.added_by, + added_by_display_name: vi.added_by_display_name, + added_date: vi.added_date.format("%Y-%m-%dT%H:%M:%S").to_string(), + expiry_date: vi.expiry_date.format("%Y-%m-%dT%H:%M:%S").to_string(), + cpu: vi.cpu as usize, + total_robot_ranking: vi.total_robot_ranking as usize, + rent_count: vi.rent_count as usize, + buy_count: vi.buy_count as usize, + buyable: vi.buyable, + removed_date: vi.removed_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()), + ban_date: vi.ban_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()), + featured: vi.featured, + banner_message: vi.banner_message, + combat_rating: vi.combat_rating as f32, + cosmetic_rating: vi.cosmetic_rating as f32, + cube_amounts: serde_json::to_string(&vi.cube_amounts).unwrap_or_else(|_| "{}".to_string()), + } +} + +fn get_info(qi: VehicleQueryInfo, vi: VehicleInfo) -> FactoryRobotGetInfo { + FactoryRobotGetInfo { + item_id: qi.id as usize, + item_name: qi.name, + item_description: qi.description, + thumbnail: qi.thumbnail, + added_by: qi.added_by, + added_by_display_name: qi.added_by_display_name, + added_date: qi.added_date.format("%Y-%m-%dT%H:%M:%S").to_string(), + expiry_date: qi.expiry_date.format("%Y-%m-%dT%H:%M:%S").to_string(), + cpu: qi.cpu as usize, + total_robot_ranking: qi.total_robot_ranking as usize, + rent_count: qi.rent_count as usize, + buy_count: qi.buy_count as usize, + buyable: qi.buyable, + removed_date: qi.removed_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()), + ban_date: qi.ban_date.as_ref().map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string()), + featured: qi.featured, + banner_message: qi.banner_message, + combat_rating: qi.combat_rating as f32, + cosmetic_rating: qi.cosmetic_rating as f32, + cube_data: base64::prelude::BASE64_STANDARD.encode(&vi.cube_data), + colour_data: base64::prelude::BASE64_STANDARD.encode(&vi.colour_data), + cube_amounts: serde_json::to_string(&qi.cube_amounts).unwrap_or_else(|_| "{}".to_string()), + } +} + +#[post("/api/roboShopItems/list")] +pub async fn list(factory: Data, body: Json) -> HttpResponse { + match factory.get_ref() { + oj_rc_core::factory::Factory::None => { + return HttpResponse::ServiceUnavailable().body("factory adapter disabled"); + } + _ => {} + } + + let query = payload_to_query(&body.into_inner()); + + match factory.list(query).await { + Ok(items) => { + let out: Vec = items.into_iter().map(list_info).collect(); + HttpResponse::Ok().json(FactoryInfo { + response: RoboShopItemsInfo { roboshop_items: out }, + status_code: 200, + }) + } + Err(e) => HttpResponse::InternalServerError().body(format!("factory list error: {e}")), + } +} + +#[get("/api/roboShopItems/list")] +pub async fn list_default(factory: Data) -> HttpResponse { + match factory.get_ref() { + oj_rc_core::factory::Factory::None => { + return HttpResponse::ServiceUnavailable().body("factory adapter disabled"); + } + _ => {} + } + + let query = payload_to_query(&ListPayload::default()); + + match factory.list(query).await { + Ok(items) => { + let out: Vec = items.into_iter().map(list_info).collect(); + HttpResponse::Ok().json(FactoryInfo { + response: RoboShopItemsInfo { roboshop_items: out }, + status_code: 200, + }) + } + Err(e) => HttpResponse::InternalServerError().body(format!("factory list error: {e}")), + } +} + +#[get("/api/roboShopItems/get/{id}")] +pub async fn get(factory: Data, id: Path) -> HttpResponse { + match factory.get_ref() { + oj_rc_core::factory::Factory::None => { + return HttpResponse::ServiceUnavailable().body("factory adapter disabled"); + } + _ => {} + } + match factory.vehicle(*id).await { + Ok(Some((vehicle_info, query_info))) => HttpResponse::Ok().json(FactoryInfo { + response: get_info(query_info, vehicle_info), + status_code: 200, + }), + Ok(None) => HttpResponse::NotFound().body("robot not found"), + Err(e) => HttpResponse::InternalServerError().body(format!("factory get error: {e}")), + } +} diff --git a/rc_factory_api/src/robocraft/factory/mod.rs b/rc_factory_api/src/robocraft/factory/mod.rs new file mode 100644 index 0000000..a9fef9c --- /dev/null +++ b/rc_factory_api/src/robocraft/factory/mod.rs @@ -0,0 +1 @@ +pub mod crf_api; diff --git a/rc_factory_api/src/robocraft/mod.rs b/rc_factory_api/src/robocraft/mod.rs new file mode 100644 index 0000000..a106d20 --- /dev/null +++ b/rc_factory_api/src/robocraft/mod.rs @@ -0,0 +1 @@ +pub mod factory;