@@ -1,6 +1,6 @@ | |||
[package] | |||
name = "libfj" | |||
version = "0.1.0" | |||
version = "0.4.1" | |||
authors = ["NGnius (Graham) <ngniusness@gmail.com>"] | |||
edition = "2018" | |||
description = "An unofficial collection of APIs used in FreeJam games and mods" | |||
@@ -6,12 +6,14 @@ use crate::cardlife::{AuthenticationInfo, AuthenticationPayload, LobbyInfo, Lobb | |||
const AUTHENTICATION_DOMAIN: &str = "https://live-auth.cardlifegame.com/"; | |||
const LOBBY_DOMAIN: &str = "https://live-lobby.cardlifegame.com/"; | |||
/// Cardlife live information API | |||
pub struct LiveAPI { | |||
client: Client, | |||
auth: Option<AuthenticationInfo>, | |||
} | |||
impl LiveAPI { | |||
/// Create a new instance | |||
pub fn new() -> LiveAPI { | |||
LiveAPI { | |||
client: Client::new(), | |||
@@ -19,6 +21,7 @@ impl LiveAPI { | |||
} | |||
} | |||
/// Create a new instance and login using email | |||
pub async fn login_email(email: &str, password: &str) -> Result<LiveAPI, Error> { | |||
let mut instance = LiveAPI::new(); | |||
let result = instance.authenticate_email(email, password).await; | |||
@@ -29,7 +32,8 @@ impl LiveAPI { | |||
return Err(result.err().unwrap()); | |||
} | |||
} | |||
/// Login using email and password | |||
pub async fn authenticate_email(&mut self, email: &str, password: &str) -> Result<AuthenticationInfo, Error> { | |||
let url = Url::parse(AUTHENTICATION_DOMAIN) | |||
.unwrap() | |||
@@ -51,7 +55,10 @@ impl LiveAPI { | |||
} | |||
Err(result.err().unwrap()) | |||
} | |||
// TODO username authentication | |||
/// Retrieve lobby information for all active Cardlife servers | |||
pub async fn lobbies(&self) -> Result<LobbyInfo, Error> { | |||
let url = Url::parse(LOBBY_DOMAIN) | |||
.unwrap() | |||
@@ -70,4 +77,4 @@ impl LiveAPI { | |||
} | |||
Err(result.err().unwrap()) | |||
} | |||
} | |||
} |
@@ -8,24 +8,36 @@ pub(crate) struct AuthenticationPayload { | |||
pub password: String, | |||
} | |||
/// Authentication information | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct AuthenticationInfo { | |||
/// User's public ID | |||
#[serde(rename = "PublicId")] | |||
pub public_id: String, | |||
/// User's email address | |||
#[serde(rename = "EmailAddress")] | |||
pub email_address: String, | |||
/// Account display name | |||
#[serde(rename = "DisplayName")] | |||
pub display_name: String, | |||
/// Account purchases (???) | |||
#[serde(rename = "Purchases")] | |||
purchases: Vec<String>, // ??? | |||
/// Account flags (dev, admin, etc.???) | |||
#[serde(rename = "Flags")] | |||
flags: Vec<String>, // ??? | |||
/// Is confirmed account? | |||
#[serde(rename = "Confirmed")] | |||
pub confirmed: bool, | |||
/// Temporary account token | |||
#[serde(rename = "Token")] | |||
pub token: String, | |||
/// Steam ID | |||
/// | |||
/// Since Steam users cannot be authenticated using this lib, this will always be blank or None | |||
#[serde(rename = "SteamId")] | |||
steam_id: Option<String>, // ??? | |||
/// User ID | |||
#[serde(rename = "ID")] | |||
pub id: usize, | |||
} | |||
@@ -42,38 +54,54 @@ pub(crate) struct LobbyPayload { | |||
pub public_id: String, | |||
} | |||
/// Lobby information for available Cardlife servers | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct LobbyInfo { | |||
#[serde(rename = "Games")] | |||
/// Available servers' information | |||
pub games: Vec<LiveGameInfo>, | |||
} | |||
/// Server information for a single Cardlife server | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct LiveGameInfo { | |||
/// Server game ID | |||
#[serde(rename = "Id")] | |||
pub id: usize, | |||
/// World name | |||
#[serde(rename = "WorldName")] | |||
pub world_name: String, | |||
/// Max players | |||
#[serde(rename = "MaxPlayers")] | |||
pub max_players: usize, | |||
/// Current player count | |||
#[serde(rename = "CurrentPlayers")] | |||
pub current_players: usize, | |||
/// Server version | |||
#[serde(rename = "GameVersion")] | |||
pub game_version: String, | |||
/// Ping latency | |||
#[serde(rename = "Ping")] | |||
pub ping: usize, | |||
/// Account has already joined this server? | |||
#[serde(rename = "HasPlayed")] | |||
pub has_played: bool, | |||
/// Server is password protected? | |||
#[serde(rename = "HasPassword")] | |||
pub has_password: bool, | |||
/// PvP is enabled on this server? | |||
#[serde(rename = "IsPvp")] | |||
pub is_pvp: bool, | |||
/// EasyAntiCheat is enabled on this server? | |||
#[serde(rename = "IsAntiCheatEnabled")] | |||
pub is_anticheat_enabled: bool, | |||
/// Official server? | |||
#[serde(rename = "IsOfficial")] | |||
pub is_official: bool, | |||
/// Mods installed on this server | |||
#[serde(rename = "ModInfo")] | |||
pub mod_info: String, | |||
/// Server region | |||
#[serde(rename = "Region")] | |||
pub region: String, | |||
} | |||
@@ -82,4 +110,4 @@ impl std::string::ToString for LiveGameInfo { | |||
fn to_string(&self) -> String { | |||
format!("{} ({}):{}/{}", self.world_name, self.id, self.current_players, self.max_players) | |||
} | |||
} | |||
} |
@@ -1,3 +1,6 @@ | |||
//! Cardlife vanilla and modded (CLre) APIs (WIP) | |||
//! LiveAPI and CLreServer are mostly complete, but some other APIs are missing. | |||
mod client; | |||
mod server; | |||
@@ -2,12 +2,14 @@ use reqwest::{Client, IntoUrl, Error}; | |||
use url::{Origin, Url}; | |||
use crate::cardlife::{GameInfo, StatusInfo}; | |||
/// CLre_server web server API implemenation | |||
pub struct CLreServer { | |||
client: Client, | |||
addr: Url, | |||
} | |||
impl CLreServer { | |||
/// Create a new instance | |||
pub fn new<U: IntoUrl>(url: U) -> Result<CLreServer, ()> { | |||
let url_result = url.into_url(); | |||
if let Ok(uri) = url_result { | |||
@@ -25,6 +27,7 @@ impl CLreServer { | |||
Err(()) | |||
} | |||
/// Retrieve the current game info | |||
pub async fn game_info(&self) -> Result<GameInfo, Error> { | |||
let response = self.client.get(self.addr.join("/c/game.json").unwrap()) | |||
.send().await; | |||
@@ -34,6 +37,7 @@ impl CLreServer { | |||
Err(response.err().unwrap()) | |||
} | |||
/// Retrieve CLre_server information | |||
pub async fn status_info(&self) -> Result<StatusInfo, Error> { | |||
let response = self.client.get(self.addr.join("/status.json").unwrap()) | |||
.send().await; | |||
@@ -1,23 +1,33 @@ | |||
use serde::{Deserialize, Serialize}; | |||
/// CLre game info | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct GameInfo { | |||
/// Max allowed player count | |||
#[serde(rename = "MaxPlayers")] | |||
pub max_players: usize, | |||
/// Server world ID | |||
#[serde(rename = "GameId")] | |||
pub game_id: usize, | |||
/// Server world GUID | |||
#[serde(rename = "GameGuid")] | |||
pub game_guid: String, | |||
/// World name | |||
#[serde(rename = "WorldName")] | |||
pub world_name: String, | |||
/// Game host type | |||
#[serde(rename = "GameHostType")] | |||
pub game_host_type: usize, | |||
/// Is PvP enabled? | |||
#[serde(rename = "PvP")] | |||
pub pvp: bool, | |||
/// Photon server region override | |||
#[serde(rename = "PhotonRegionOverride")] | |||
pub photon_region_override: String, | |||
/// Server password | |||
#[serde(rename = "ServerPassword")] | |||
pub server_password: String, | |||
/// Admin priviledge password | |||
#[serde(rename = "AdminPassword")] | |||
pub admin_password: String, | |||
} | |||
@@ -28,30 +38,42 @@ impl std::string::ToString for GameInfo { | |||
} | |||
} | |||
/// CLre_server status information | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct StatusInfo { | |||
/// Maximum player count | |||
#[serde(rename = "PlayersMax")] | |||
pub max_players: usize, | |||
/// Current player count | |||
#[serde(rename = "PlayerCount")] | |||
pub player_count: usize, | |||
/// Server status (enum as string) | |||
#[serde(rename = "Status")] | |||
pub status: String, | |||
/// Information on all online players in this server | |||
#[serde(rename = "OnlinePlayers")] | |||
pub online_players: Vec<PlayerStatusInfo> | |||
} | |||
/// A single online player's information | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct PlayerStatusInfo { | |||
/// Player public ID | |||
#[serde(rename = "id")] | |||
pub id: String, | |||
/// Player name | |||
#[serde(rename = "name")] | |||
pub name: String, | |||
/// Is the player a developer? | |||
#[serde(rename = "isDev")] | |||
pub is_dev: bool, | |||
/// Player's location on x-axis | |||
#[serde(rename = "x")] | |||
pub x: f32, | |||
/// Player's location on y-axis | |||
#[serde(rename = "y")] | |||
pub y: f32, | |||
/// Player's location on z-axis | |||
#[serde(rename = "z")] | |||
pub z: f32, | |||
} | |||
@@ -1 +1,3 @@ | |||
//! Simple, blocking Cardlife API (WIP) | |||
//! Nothing is here yet, sorry! | |||
// TODO |
@@ -1,3 +1,8 @@ | |||
//! An unofficial collection of APIs used in Robocraft and Cardlife. | |||
//! | |||
//! This crate is WIP, but the available APIs are tested and very usable. | |||
#![warn(missing_docs)] | |||
pub mod cardlife; | |||
pub mod robocraft; | |||
#[cfg(feature = "simple")] | |||
@@ -1,10 +1,13 @@ | |||
use crate::robocraft::{DEFAULT_TOKEN}; | |||
/// Token generator for authenticated API endpoints | |||
pub trait ITokenProvider { | |||
/// Retrieve the token to use | |||
fn token(&self) -> Result<String, ()>; | |||
} | |||
/// Token provider which uses DEFAULT_TOKEN | |||
pub struct DefaultTokenProvider { | |||
} | |||
@@ -3,13 +3,22 @@ use std::io::Read; | |||
// TODO(maybe) parse iteratively instead of one-shot | |||
/// A collection of cube data | |||
/// | |||
/// This holds all data parsed from cube_data and colour_data. | |||
/// Individual Cube structs can be iterated through. | |||
#[derive(Clone)] | |||
pub struct Cubes { | |||
/// Parsed cube count (the first 32 bits of data parsed to `u32`) | |||
pub provided_len: u32, | |||
cubes: Vec<Cube>, | |||
} | |||
impl Cubes { | |||
/// Process the raw bytes containing block data from a Robocraft CRF bot | |||
/// | |||
/// `cube_data` and `colour_data` correspond to the `cube_data` and `colour_data` fields of FactoryRobotGetInfo. | |||
/// In generally, you should use `Cubes::from<FactoryRobotGetInfo>(data)` instead of this lower-level function. | |||
pub fn parse(cube_data: &mut Vec<u8>, colour_data: &mut Vec<u8>) -> Result<Self, ()> { | |||
// read first 4 bytes (cube count) from both arrays and make sure they match | |||
let mut cube_buf = [0; 4]; | |||
@@ -66,6 +75,13 @@ impl Cubes { | |||
}) | |||
} | |||
/// Dump the raw bytes containing block data for a Robocraft bot. | |||
/// | |||
/// The first tuple item is cube data, and the second item is colour data. | |||
/// Use this to write a modified robot to file. | |||
/// This is the inverse of `Cubes::parse(...)`. | |||
/// | |||
/// I'm not sure what this would actually be useful for... | |||
pub fn dump(&self) -> (Vec<u8>, Vec<u8>) { | |||
let mut cube_buf = Vec::new(); | |||
let mut colour_buf = Vec::new(); | |||
@@ -78,6 +94,10 @@ impl Cubes { | |||
(cube_buf, colour_buf) | |||
} | |||
/// Get the actual amount of cubes. | |||
/// | |||
/// This differs from `provided_len` by being the amount of cubes parsed (successfully), instead of something parsed from block data. | |||
/// For any valid robot data, `data.provided_len == data.len()`. | |||
pub fn len(&self) -> usize { | |||
self.cubes.len() | |||
} | |||
@@ -93,13 +113,22 @@ impl<'a> std::iter::IntoIterator for &'a Cubes { | |||
} | |||
} | |||
/// A single block in a Robocraft robot. | |||
/// | |||
/// From the front of a Robocraft garage bay, looking at the back, all positions are measured from the back bottom right corner. | |||
#[derive(Copy, Clone)] | |||
pub struct Cube { | |||
/// The cube id | |||
pub id: u32, | |||
/// The cube's x position (left to right) | |||
pub x: u8, // left to right | |||
/// The cube's y position (bottom to top) | |||
pub y: u8, // bottom to top | |||
/// The cube's z position (back to front) | |||
pub z: u8, // back to front | |||
/// The cube's orientation | |||
pub orientation: u8, | |||
/// The cube's colour, one of the 24 possible colours in Robocraft | |||
pub colour: u8, | |||
} | |||
@@ -143,11 +172,17 @@ impl Cube { | |||
Ok(4) | |||
} | |||
/// Dump the raw cube data as used in the Robocraft CRF. | |||
/// | |||
/// This is useless by itself, use `Cubes.dump()` for a valid robot. | |||
pub fn dump_cube_data(&self) -> [u8; 8] { | |||
let id_buf = self.id.to_le_bytes(); | |||
[id_buf[0], id_buf[1], id_buf[2], id_buf[3], self.x, self.y, self.z, self.orientation] | |||
} | |||
/// Dump the raw colour data as used in the Robocraft CRF. | |||
/// | |||
/// This is useless by itself, use `Cubes.dump()` for a valid robot. | |||
pub fn dump_colour_data(&self) -> [u8; 4] { | |||
[self.colour, self.x, self.y, self.z] | |||
} | |||
@@ -4,14 +4,17 @@ use url::{Url}; | |||
use crate::robocraft::{ITokenProvider, DefaultTokenProvider, FactoryInfo, FactorySearchBuilder, RoboShopItemsInfo, FactoryRobotGetInfo}; | |||
use crate::robocraft::factory_json::ListPayload; | |||
/// Community Factory Robot root URL | |||
pub const FACTORY_DOMAIN: &str = "https://factory.robocraftgame.com/"; | |||
/// CRF API implementation | |||
pub struct FactoryAPI { | |||
client: Client, | |||
token: Box<dyn ITokenProvider>, | |||
} | |||
impl FactoryAPI { | |||
/// Create a new instance, using `DefaultTokenProvider`. | |||
pub fn new() -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
@@ -19,6 +22,7 @@ impl FactoryAPI { | |||
} | |||
} | |||
/// Create a new instance using the provided token provider. | |||
pub fn with_auth(token_provider: Box<dyn ITokenProvider>) -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
@@ -26,6 +30,9 @@ impl FactoryAPI { | |||
} | |||
} | |||
/// Retrieve CRF robots on the main page. | |||
/// | |||
/// For searching, use `list_builder()` instead. | |||
pub async fn list(&self) -> Result<FactoryInfo<RoboShopItemsInfo>, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -44,6 +51,9 @@ impl FactoryAPI { | |||
Err(result.err().unwrap()) | |||
} | |||
/// Build a CRF search query. | |||
/// | |||
/// This follows the builder pattern, so functions can be chained. | |||
pub fn list_builder(&self) -> FactorySearchBuilder { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -57,6 +67,9 @@ impl FactoryAPI { | |||
FactorySearchBuilder::new(request_builder, token_opt) | |||
} | |||
/// Get in-depth info on a CRF robot. | |||
/// | |||
/// `item_id` corresponds to the field with the same name for FactoryRobotGetInfo and FactoryRobotListInfo. | |||
pub async fn get(&self, item_id: usize) -> Result<FactoryInfo<FactoryRobotGetInfo>, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -82,60 +82,90 @@ impl ListPayload { | |||
} | |||
} | |||
/// Standard factory response format. | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct FactoryInfo<T> { | |||
#[serde(rename = "response")] | |||
/// The response data | |||
pub response: T, | |||
#[serde(rename = "statusCode")] | |||
/// HTTP status code for the query | |||
pub status_code: usize, | |||
} | |||
/// Collection of robots in response to a list query. | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct RoboShopItemsInfo { | |||
#[serde(rename = "roboShopItems")] | |||
/// Robot items | |||
pub roboshop_items: Vec<FactoryRobotListInfo>, | |||
} | |||
/// Information about a single robot in response to a list query. | |||
/// | |||
/// This does not include robot block data since it is not returned by this API endpoint. | |||
/// Use `FactoryAPI.get(data.item_id)` to retrieve all info for a single robot. | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct FactoryRobotListInfo { | |||
/// Item ID | |||
#[serde(rename = "itemId")] | |||
pub item_id: usize, | |||
/// Robot name | |||
#[serde(rename = "itemName")] | |||
pub item_name: String, | |||
/// Robot description | |||
#[serde(rename = "itemDescription")] | |||
pub item_description: String, | |||
/// Thumbnail URL, as displayed to preview the robot. | |||
#[serde(rename = "thumbnail")] | |||
pub thumbnail: String, // url | |||
/// Robot author's username or UUID | |||
#[serde(rename = "addedBy")] | |||
pub added_by: String, | |||
/// Robot author's display name | |||
#[serde(rename = "addedByDisplayName")] | |||
pub added_by_display_name: String, | |||
/// Date added, in standard ISO format | |||
#[serde(rename = "addedDate")] | |||
pub added_date: String, // ISO date | |||
/// Expiry date, in standard ISO format | |||
#[serde(rename = "expiryDate")] | |||
pub expiry_date: String, // ISO date | |||
/// Robot CPU value | |||
#[serde(rename = "cpu")] | |||
pub cpu: usize, | |||
/// Robot RR | |||
#[serde(rename = "totalRobotRanking")] | |||
pub total_robot_ranking: usize, | |||
/// Robot's rentals | |||
#[serde(rename = "rentCount")] | |||
pub rent_count: usize, | |||
/// Robot's purchases | |||
#[serde(rename = "buyCount")] | |||
pub buy_count: usize, | |||
/// Is this robot buyable? (probably yes, unless you're a mod/admin or missing parts) | |||
#[serde(rename = "buyable")] | |||
pub buyable: bool, | |||
/// Removed date, in standard ISO format (probably None, unless authenticated as a mod/admin) | |||
#[serde(rename = "removedDate")] | |||
pub removed_date: Option<String>, | |||
/// Author ban date, in standard ISO format (probable None) | |||
#[serde(rename = "banDate")] | |||
pub ban_date: Option<String>, | |||
/// Is this robot featured? | |||
#[serde(rename = "featured")] | |||
pub featured: bool, | |||
/// CRF Banner message | |||
#[serde(rename = "bannerMessage")] | |||
pub banner_message: Option<String>, | |||
/// Robot's combat rating, out of 5 | |||
#[serde(rename = "combatRating")] | |||
pub combat_rating: f32, | |||
/// Robot's cosmetic rating, out of 5 | |||
#[serde(rename = "cosmeticRating")] | |||
pub cosmetic_rating: f32, | |||
/// Robot's count of (some?) blocks it uses | |||
#[serde(rename = "cubeAmounts")] | |||
pub cube_amounts: String, // JSON as str | |||
} | |||
@@ -147,51 +177,74 @@ impl std::string::ToString for FactoryRobotListInfo { | |||
} | |||
// get/<item_id> endpoint | |||
/// Complete information about a single robot in response to a get query. | |||
/// Please refer to FactoryRobotListInfo for more in-depth documentation of fields. | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct FactoryRobotGetInfo { | |||
/// Item ID | |||
#[serde(rename = "id")] | |||
pub item_id: usize, | |||
/// Robot name | |||
#[serde(rename = "name")] | |||
pub item_name: String, | |||
/// Robot description | |||
#[serde(rename = "description")] | |||
pub item_description: String, | |||
/// Robot thumbnail URL | |||
#[serde(rename = "thumbnail")] | |||
pub thumbnail: String, // url | |||
/// Robot author's username or UUID | |||
#[serde(rename = "addedBy")] | |||
pub added_by: String, | |||
/// Robot author's display name | |||
#[serde(rename = "addedByDisplayName")] | |||
pub added_by_display_name: String, | |||
/// ISO date added | |||
#[serde(rename = "addedDate")] | |||
pub added_date: String, // ISO date | |||
/// ISO date expiring | |||
#[serde(rename = "expiryDate")] | |||
pub expiry_date: String, // ISO date | |||
/// CPU | |||
#[serde(rename = "cpu")] | |||
pub cpu: usize, | |||
/// RR | |||
#[serde(rename = "totalRobotRanking")] | |||
pub total_robot_ranking: usize, | |||
/// Robot rent count | |||
#[serde(rename = "rentCount")] | |||
pub rent_count: usize, | |||
/// Robot buy count | |||
#[serde(rename = "buyCount")] | |||
pub buy_count: usize, | |||
/// Robot is buyable? | |||
#[serde(rename = "buyable")] | |||
pub buyable: bool, | |||
/// ISO date removed | |||
#[serde(rename = "removedDate")] | |||
pub removed_date: Option<String>, | |||
/// ISO date banned | |||
#[serde(rename = "banDate")] | |||
pub ban_date: Option<String>, | |||
/// Robot is featured? | |||
#[serde(rename = "featured")] | |||
pub featured: bool, | |||
/// CRF banner message | |||
#[serde(rename = "bannerMessage")] | |||
pub banner_message: Option<String>, | |||
/// Robot's combat rating, out of 5 | |||
#[serde(rename = "combatRating")] | |||
pub combat_rating: f32, | |||
/// Robot's cosmetic rating, out of 5 | |||
#[serde(rename = "cosmeticRating")] | |||
pub cosmetic_rating: f32, | |||
/// Robot block cube and position data | |||
#[serde(rename = "cubeData")] | |||
pub cube_data: String, | |||
/// Robot block colour data | |||
#[serde(rename = "colourData")] | |||
pub colour_data: String, | |||
/// Cube counts | |||
#[serde(rename = "cubeAmounts")] | |||
pub cube_amounts: String, // JSON as str | |||
} | |||
@@ -4,57 +4,93 @@ use num_enum::{TryFromPrimitive}; | |||
use crate::robocraft::{FactoryInfo, RoboShopItemsInfo}; | |||
use crate::robocraft::factory_json::ListPayload; | |||
/// Factory list response ordering | |||
#[derive(Eq, PartialEq, TryFromPrimitive)] | |||
#[repr(u8)] | |||
pub enum FactoryOrderType { | |||
/// Suggested (default) | |||
Suggested = 0, | |||
/// Combat rating (decreasing?) | |||
CombatRating = 1, | |||
/// Cosmetic rating (decreasing?) | |||
CosmeticRating = 2, | |||
/// Date added (oldest first?) | |||
Added = 3, | |||
/// CPU value (decreasing?) | |||
CPU = 4, | |||
/// Purchases (decreasing) | |||
MostBought = 5, | |||
} | |||
/// Robot movement categories | |||
#[derive(Eq, PartialEq, TryFromPrimitive)] | |||
#[repr(u32)] | |||
pub enum FactoryMovementType { | |||
/// Vrooooom | |||
Wheels = 100000, | |||
/// Woooooosh | |||
Hovers = 200000, | |||
/// Fwoooosh | |||
Aerofoils=300000, | |||
/// Also fwoooosh (but actually a different movement type, trust me) | |||
Thrusters=400000, | |||
/// Also also fwoooosh (but also a different movement type) | |||
Rudders=500000, | |||
/// Ewwww | |||
InsectLegs=600000, | |||
/// Mechs are cool | |||
MechLegs=700000, | |||
/// Skis and turning skis | |||
Skis=800000, | |||
/// All tank treads | |||
TankTreads=900000, | |||
/// Wrrrrrrrrrr | |||
Rotors=1000000, | |||
/// Mech legs, but faster | |||
Sprinters=1100000, | |||
/// Wrrrrr but for Fwoooosh | |||
Propellers=1200000 | |||
} | |||
/// Robot weapon categories | |||
#[derive(Eq, PartialEq, TryFromPrimitive)] | |||
#[repr(u32)] | |||
pub enum FactoryWeaponType { | |||
/// All laser weapons (aka Lasor, SMG) | |||
Laser=10000000, | |||
/// All plasma launcher weapons | |||
PlasmaLauncher=20000000, | |||
/// Mortar | |||
GyroMortar=25000000, | |||
/// All rails | |||
RailCannon=30000000, | |||
/// All healing weapons | |||
NanoDisruptor=40000000, | |||
/// All tesla blade melee weapons | |||
TeslaBlade=50000000, | |||
/// All aeroflak weapons | |||
AeroflakCannon=60000000, | |||
/// All shotgun weapons | |||
IonCannon=65000000, | |||
/// Lol | |||
ProtoSeeker=70100000, | |||
/// All chain weapons | |||
ChainShredder=75000000, | |||
} | |||
/// Text field search modes | |||
#[derive(Eq, PartialEq, TryFromPrimitive)] | |||
#[repr(u8)] | |||
pub enum FactoryTextSearchType { | |||
/// Search players and robot names | |||
All=0, | |||
/// Search players only | |||
Player=1, | |||
/// Search robot names only | |||
Name=2, | |||
} | |||
/// Factory API list query builder | |||
pub struct FactorySearchBuilder { | |||
reqwest_builder: RequestBuilder, | |||
payload: ListPayload, | |||
@@ -70,16 +106,19 @@ impl FactorySearchBuilder { | |||
} | |||
} | |||
/// Retrieve list page page_number | |||
pub fn page(mut self, page_number: isize) -> Self { | |||
self.payload.page = page_number; | |||
self | |||
} | |||
/// Retrieve page_size items per page (this is unreliable) | |||
pub fn items_per_page(mut self, page_size: isize) -> Self { | |||
self.payload.page_size = page_size; | |||
self | |||
} | |||
/// Order list by order_type | |||
pub fn order(mut self, order_type: FactoryOrderType) -> Self { | |||
self.payload.order = order_type as isize; | |||
self | |||
@@ -93,6 +132,10 @@ impl FactorySearchBuilder { | |||
} | |||
*/ | |||
/// Retrieve items with movement type. | |||
/// | |||
/// Multiple calls to this function will cause logical OR behaviour. | |||
/// e.g. results will contain robots with Wheels OR Aerofoils (or both). | |||
pub fn movement_or(mut self, movement_type: FactoryMovementType) -> Self { | |||
if self.payload.movement_filter == "" { | |||
self.payload.movement_filter = format!("{},{}", &self.payload.movement_filter, movement_type as isize); | |||
@@ -103,12 +146,17 @@ impl FactorySearchBuilder { | |||
self | |||
} | |||
/// Override allowed movement types | |||
pub fn movement_raw(mut self, filter: String) -> Self { | |||
self.payload.movement_filter = filter.clone(); | |||
self.payload.movement_category_filter = filter.clone(); | |||
self | |||
} | |||
/// Retrieve items with weapon type. | |||
/// | |||
/// Multiple calls to this function will cause logical OR behaviour. | |||
/// e.g. results will contain robots with ChainShredder OR GyroMortar (or both). | |||
pub fn weapon_or(mut self, weapon_type: FactoryWeaponType) -> Self { | |||
if self.payload.weapon_filter == "" { | |||
self.payload.weapon_filter = format!("{},{}", &self.payload.weapon_filter, weapon_type as isize); | |||
@@ -119,64 +167,81 @@ impl FactorySearchBuilder { | |||
self | |||
} | |||
/// Override allowed weapon types | |||
pub fn weapon_raw(mut self, filter: String) -> Self { | |||
self.payload.weapon_filter = filter.clone(); | |||
self.payload.weapon_category_filter = filter.clone(); | |||
self | |||
} | |||
/// Retrieve items within the specified CPU min and max values | |||
pub fn cpu_range(mut self, min: isize, max: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
/// Retrieve items with CPU no lower than min | |||
/// overrides cpu_range() | |||
pub fn min_cpu(mut self, min: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self | |||
} | |||
/// Retrieve items with CPU no greater than max | |||
/// overrides cpu_range() | |||
pub fn max_cpu(mut self, max: isize) -> Self { | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
/// Retrieve items with any minimum CPU | |||
pub fn no_minimum_cpu(mut self) -> Self { | |||
self.payload.minimum_cpu = -1; | |||
self | |||
} | |||
/// Retrieve items with any maximum CPU | |||
pub fn no_maximum_cpu(mut self) -> Self { | |||
self.payload.maximum_cpu = -1; | |||
self | |||
} | |||
/// Retrieve items which match text | |||
pub fn text(mut self, t: String) -> Self { | |||
self.payload.text_filter = t; | |||
self | |||
} | |||
/// Text filter searches search_type | |||
pub fn text_search_type(mut self, search_type: FactoryTextSearchType) -> Self { | |||
self.payload.text_search_field = search_type as isize; | |||
self | |||
} | |||
// setting buyable to false while using the default token provider will cause HTTP status 500 error | |||
/// Retrieve only items which are buyable for current account? (default: false) | |||
/// Buyable means that the account owns all blocks required. | |||
/// This will cause an error when using DEFAULT_TOKEN | |||
pub fn buyable(mut self, b: bool) -> Self { | |||
self.payload.buyable = b; | |||
self | |||
} | |||
/// Retrieve items with featured robot at start? (default: false) | |||
pub fn prepend_featured(mut self, b: bool) -> Self { | |||
self.payload.prepend_featured_robot = b; | |||
self | |||
} | |||
/// Retrieve default robot list? (default: false) | |||
/// The default page is the CRF landing page (I think?) | |||
pub fn default_page(mut self, b: bool) -> Self { | |||
self.payload.default_page = b; | |||
self | |||
} | |||
/// Execute list query | |||
pub async fn send(mut self) -> Result<FactoryInfo<RoboShopItemsInfo>, Error> { | |||
self.reqwest_builder = self.reqwest_builder.json(&self.payload); | |||
if let Some(token) = self.token.clone() { | |||
@@ -1,3 +1,6 @@ | |||
//! Robocraft APIs for the CRF and leaderboards (WIP) | |||
//! FactoryAPI is mostly complete, but many other APIs are missing. | |||
mod factory; | |||
mod factory_json; | |||
mod factory_request_builder; | |||
@@ -13,4 +16,5 @@ pub use self::cubes::{Cube, Cubes}; | |||
mod auth; | |||
pub use self::auth::{ITokenProvider, DefaultTokenProvider}; | |||
/// Token defined in a javascript file from Freejam which never expires | |||
pub const DEFAULT_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg"; |
@@ -6,12 +6,17 @@ use crate::robocraft::{ITokenProvider, DefaultTokenProvider, FACTORY_DOMAIN, Fac | |||
use crate::robocraft::{ListPayload}; | |||
use crate::robocraft_simple::FactorySearchBuilder; | |||
/// Simpler CRF API implementation. | |||
/// Refer to libfj::robocraft::FactoryAPI for in-depth documentation. | |||
/// The only API difference is that this API is blocking (i.e. no async). | |||
/// This version also works with Wine and Proton since it does not rely on tokio. | |||
pub struct FactoryAPI { | |||
client: Agent, | |||
token: Box<dyn ITokenProvider>, | |||
} | |||
impl FactoryAPI { | |||
/// Create a new instance using `DefaultTokenProvider`. | |||
pub fn new() -> FactoryAPI { | |||
FactoryAPI { | |||
client: Agent::new(), | |||
@@ -19,6 +24,7 @@ impl FactoryAPI { | |||
} | |||
} | |||
/// List CRF robots | |||
pub fn list(&self) -> Result<FactoryInfo<RoboShopItemsInfo>, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -41,6 +47,7 @@ impl FactoryAPI { | |||
Err(result.err().unwrap()) | |||
} | |||
/// Build a list query | |||
pub fn list_builder(&self) -> FactorySearchBuilder { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -54,6 +61,7 @@ impl FactoryAPI { | |||
FactorySearchBuilder::new(request_builder, token_opt) | |||
} | |||
/// Get complete information on a robot. | |||
pub fn get(&self, item_id: usize) -> Result<FactoryInfo<FactoryRobotGetInfo>, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
@@ -3,6 +3,9 @@ use ureq::{Request, Response, Error}; | |||
use crate::robocraft::{FactoryInfo, RoboShopItemsInfo, FactoryTextSearchType, FactoryWeaponType, FactoryMovementType, FactoryOrderType}; | |||
use crate::robocraft::{ListPayload}; | |||
/// Factory API list query builder. | |||
/// This is the simpler, blocking equivalent of libfj::robocraft::FactorySearchBuilder. | |||
/// Please refer to that struct's documentation for details. | |||
#[derive(Clone)] | |||
pub struct FactorySearchBuilder { | |||
reqwest_builder: Request, | |||
@@ -19,16 +22,19 @@ impl FactorySearchBuilder { | |||
} | |||
} | |||
/// Set page number | |||
pub fn page(mut self, page_number: isize) -> Self { | |||
self.payload.page = page_number; | |||
self | |||
} | |||
/// Set page size | |||
pub fn items_per_page(mut self, page_size: isize) -> Self { | |||
self.payload.page_size = page_size; | |||
self | |||
} | |||
/// Set results ordering | |||
pub fn order(mut self, order_type: FactoryOrderType) -> Self { | |||
self.payload.order = order_type as isize; | |||
self | |||
@@ -42,12 +48,14 @@ impl FactorySearchBuilder { | |||
} | |||
*/ | |||
/// Override movement filter | |||
pub fn movement_raw(mut self, filter: String) -> Self { | |||
self.payload.movement_filter = filter.clone(); | |||
self.payload.movement_category_filter = filter.clone(); | |||
self | |||
} | |||
/// Add allowed movement type | |||
pub fn movement_or(mut self, movement_type: FactoryMovementType) -> Self { | |||
if self.payload.movement_filter == "" { | |||
self.payload.movement_filter = format!("{},{}", &self.payload.movement_filter, movement_type as isize); | |||
@@ -58,12 +66,14 @@ impl FactorySearchBuilder { | |||
self | |||
} | |||
/// Override weapon filter | |||
pub fn weapon_raw(mut self, filter: String) -> Self { | |||
self.payload.weapon_filter = filter.clone(); | |||
self.payload.weapon_category_filter = filter.clone(); | |||
self | |||
} | |||
/// Add allowed weapon type | |||
pub fn weapon_or(mut self, weapon_type: FactoryWeaponType) -> Self { | |||
if self.payload.weapon_filter == "" { | |||
self.payload.weapon_filter = format!("{},{}", &self.payload.weapon_filter, weapon_type as isize); | |||
@@ -74,58 +84,69 @@ impl FactorySearchBuilder { | |||
self | |||
} | |||
/// Set CPU value min and max | |||
pub fn cpu_range(mut self, min: isize, max: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
/// Set CPU minimum value | |||
pub fn min_cpu(mut self, min: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self | |||
} | |||
/// Set CPU maximum value | |||
pub fn max_cpu(mut self, max: isize) -> Self { | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
/// Removem minimum CPU limit | |||
pub fn no_minimum_cpu(mut self) -> Self { | |||
self.payload.minimum_cpu = -1; | |||
self | |||
} | |||
/// Remove maximum CPU limit | |||
pub fn no_maximum_cpu(mut self) -> Self { | |||
self.payload.maximum_cpu = -1; | |||
self | |||
} | |||
/// Set text filter | |||
pub fn text(mut self, t: String) -> Self { | |||
self.payload.text_filter = t; | |||
self | |||
} | |||
/// Set fields which text filter searches | |||
pub fn text_search_type(mut self, search_type: FactoryTextSearchType) -> Self { | |||
self.payload.text_search_field = search_type as isize; | |||
self | |||
} | |||
// setting buyable to false while using the default token provider will cause HTTP status 500 error | |||
/// Only search robots which can be bought by the current account? | |||
pub fn buyable(mut self, b: bool) -> Self { | |||
self.payload.buyable = b; | |||
self | |||
} | |||
/// Prepend a featured robot to the response? | |||
pub fn prepend_featured(mut self, b: bool) -> Self { | |||
self.payload.prepend_featured_robot = b; | |||
self | |||
} | |||
/// Retrieve default CRF page? | |||
pub fn default_page(mut self, b: bool) -> Self { | |||
self.payload.default_page = b; | |||
self | |||
} | |||
/// Execute list query | |||
pub fn send(mut self) -> Result<FactoryInfo<RoboShopItemsInfo>, Error> { | |||
self.reqwest_builder = self.reqwest_builder; | |||
if let Some(token) = self.token.clone() { | |||
@@ -1,3 +1,5 @@ | |||
//! Simple, blocking Robocraft API | |||
mod factory; | |||
mod factory_request_builder; | |||
pub use factory::{FactoryAPI}; | |||