From 2c5b7da8165e3f8b90748e8ced1de0e3e8d52309 Mon Sep 17 00:00:00 2001 From: lazydancer Date: Sun, 22 Dec 2024 17:33:11 -0500 Subject: Initial --- rust_backend/.vscode/launch.json | 45 +++++ rust_backend/Cargo.toml | 18 ++ rust_backend/src/eve_api.rs | 319 +++++++++++++++++++++++++++++++++ rust_backend/src/eve_service.rs | 42 +++++ rust_backend/src/main.rs | 95 ++++++++++ rust_backend/src/manager.rs | 90 ++++++++++ rust_backend/src/market/mod.rs | 3 + rust_backend/src/market/order.rs | 83 +++++++++ rust_backend/src/market/trade.rs | 63 +++++++ rust_backend/src/pathfinding/graph.rs | 7 + rust_backend/src/pathfinding/mod.rs | 15 ++ rust_backend/src/pathfinding/models.rs | 86 +++++++++ rust_backend/src/pathfinding/parser.rs | 46 +++++ rust_backend/src/route.rs | 108 +++++++++++ 14 files changed, 1020 insertions(+) create mode 100644 rust_backend/.vscode/launch.json create mode 100644 rust_backend/Cargo.toml create mode 100644 rust_backend/src/eve_api.rs create mode 100644 rust_backend/src/eve_service.rs create mode 100644 rust_backend/src/main.rs create mode 100644 rust_backend/src/manager.rs create mode 100644 rust_backend/src/market/mod.rs create mode 100644 rust_backend/src/market/order.rs create mode 100644 rust_backend/src/market/trade.rs create mode 100644 rust_backend/src/pathfinding/graph.rs create mode 100644 rust_backend/src/pathfinding/mod.rs create mode 100644 rust_backend/src/pathfinding/models.rs create mode 100644 rust_backend/src/pathfinding/parser.rs create mode 100644 rust_backend/src/route.rs (limited to 'rust_backend') diff --git a/rust_backend/.vscode/launch.json b/rust_backend/.vscode/launch.json new file mode 100644 index 0000000..cf1efa1 --- /dev/null +++ b/rust_backend/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'rust_backend'", + "cargo": { + "args": [ + "build", + "--bin=rust_backend", + "--package=rust_backend" + ], + "filter": { + "name": "rust_backend", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'rust_backend'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=rust_backend", + "--package=rust_backend" + ], + "filter": { + "name": "rust_backend", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/rust_backend/Cargo.toml b/rust_backend/Cargo.toml new file mode 100644 index 0000000..cace60d --- /dev/null +++ b/rust_backend/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rust_backend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = "4" +tokio = { version = "1", features = ["full"] } +ctrlc = "3.2" +serde = { version = "1.0", features = ["derive"] } +serde_derive = "1.0" +serde_json = "1.0" +reqwest = { version = "0.11", features = ["json"] } +dotenv = "*" +chrono = { version = "0.4", features = ["serde"] } +csv = "*" \ No newline at end of file diff --git a/rust_backend/src/eve_api.rs b/rust_backend/src/eve_api.rs new file mode 100644 index 0000000..18e1007 --- /dev/null +++ b/rust_backend/src/eve_api.rs @@ -0,0 +1,319 @@ +use dotenv::dotenv; +use std::env; +use std::error::Error; +use std::fs::{self, File}; +use std::io::{self, Read, Write}; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH, Duration}; +use std::sync::Arc; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use tokio::{task, time}; +use tokio::sync::Semaphore; + + +use crate::pathfinding::Location; +use crate::market::Order; + +const TOKEN_FILE: &str = "token.json"; + +use std::sync::Mutex; + + +#[derive(Deserialize, Serialize, Clone)] +struct TokenData { + access_token: String, + refresh_token: String, + expires_at: u64, +} + +#[derive(Deserialize)] +struct TokenResponse { + access_token: String, + refresh_token: String, + expires_in: u64, +} + +pub struct EveApiClient { + client_id: String, + client_secret: String, + character_id: String, + redirect_url: String, + access_token: Mutex>, + expires_at: Mutex>, +} + + +impl EveApiClient { + pub fn new() -> Result> { + dotenv().ok(); + + let client_id = env::var("CLIENT_ID").map_err(|e| format!("CLIENT_ID error: {}", e))?; + let client_secret = env::var("CLIENT_SECRET").map_err(|e| format!("CLIENT_SECRET error: {}", e))?; + let character_id = env::var("CHARACTER_ID").map_err(|e| format!("CHARACTER_ID error: {}", e))?; + let redirect_url = env::var("REDIRECT_URL").map_err(|e| format!("REDIRECT_URL error: {}", e))?; + + let token_data = match Self::load_token_from_file() { + Ok(data) => Some(data), + Err(_) => None, + }; + + Ok(EveApiClient { + client_id, + client_secret, + character_id, + redirect_url, + access_token: Mutex::new(token_data.clone().map(|data| data.access_token)), + expires_at: Mutex::new(token_data.map(|data| data.expires_at)), + }) + } + + pub fn get_authorization_url(&self) -> String { + let auth_url = format!( + "https://login.eveonline.com/v2/oauth/authorize/?response_type=code&client_id={}&redirect_uri={}&scope={}", + self.client_id, + self.redirect_url, + "esi-assets.read_assets.v1 esi-location.read_location.v1 esi-location.read_online.v1 esi-ui.open_window.v1 esi-ui.write_waypoint.v1".replace(" ", "%20") + ); + auth_url + } + + pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box> { + let client = reqwest::Client::new(); + let res = client + .post("https://login.eveonline.com/v2/oauth/token") + .form(&[ + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ("code", code), + ("redirect_uri", self.redirect_url.as_str()), + ("grant_type", "authorization_code"), + ]) + .send() + .await; + + let res = match res { + Ok(response) => response, + Err(e) => return Err(Box::new(e)), + }; + + let token_response: TokenResponse = match res.json().await { + Ok(data) => data, + Err(e) => return Err(Box::new(e)), + }; + + let expires_at = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") // Handle this error as per your need + .as_secs() + token_response.expires_in; + + let token_data = TokenData { + access_token: token_response.access_token, + refresh_token: token_response.refresh_token, + expires_at, + }; + + if let Err(e) = Self::save_token_to_file(&token_data) { + return Err(e); + } + + let mut token = self.access_token.lock().unwrap(); + *token = Some(token_data.access_token); + + Ok(()) + } + + pub async fn refresh_access_token(&self) -> Result<(), Box> { + let token_data = match Self::load_token_from_file() { + Ok(data) => data, + Err(_) => return Err("Failed to load token data".into()), + }; + + + let client = reqwest::Client::new(); + let res = client + .post("https://login.eveonline.com/v2/oauth/token") // Replace with the actual token endpoint + .form(&[ + ("client_id", self.client_id.as_str()), + ("client_secret", self.client_secret.as_str()), + ("refresh_token", token_data.refresh_token.as_str()), + ("grant_type", "refresh_token"), + ]) + .send() + .await?; + + let refreshed_token_response: TokenResponse = res.json().await?; + + let current_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + + let new_expires_at = current_timestamp + refreshed_token_response.expires_in; + + let new_token_data = TokenData { + access_token: refreshed_token_response.access_token, + refresh_token: refreshed_token_response.refresh_token, + expires_at: new_expires_at, + }; + + Self::save_token_to_file(&new_token_data)?; + + let mut access_token = self.access_token.lock().unwrap(); + *access_token = Some(new_token_data.access_token); + let mut expires_at = self.expires_at.lock().unwrap(); + *expires_at = Some(new_expires_at); + + Ok(()) + } + + + pub async fn request_location(&self) -> Result> { + self.ensure_valid_token().await?; + + let url = format!("https://esi.evetech.net/latest/characters/{}/location/", self.character_id); + + println!("{:?}", url); + + let client = reqwest::Client::new(); + + let response = client.get(url) + .bearer_auth(self.get_access_token().unwrap()) + .send() + .await?; + + println!("{:?}", response); + + if response.status().is_success() { + let data = response.json::().await?; + Ok(data) + } else { + Err(Box::new(response.error_for_status().unwrap_err())) + } + } + + pub async fn request_order_metadata(&self, region: u64) -> Result<(u64, DateTime), Box> { + let url = format!("https://esi.evetech.net/latest/markets/{}/orders/?datasource=tranquility&order_type=all&page=1", region); + + let client = reqwest::Client::new(); + let response = client.get(&url) + .send() + .await + .map_err(|e| e.to_string())?; + + let x_pages = match response.headers().get("x-pages") { + Some(x_pages_value) => { + match x_pages_value.to_str() { + Ok(value_str) => { + match value_str.parse::() { + Ok(value) => value, + Err(_) => return Err("Failed to parse x-pages value into u64".into()), + } + } + Err(_) => return Err("Invalid x-pages header value".into()), + } + } + None => return Err("X-Pages header not found".into()), + }; + + let expiry_unix_time = match response.headers().get("expires") { + Some(expiry_value) => { + match expiry_value.to_str() { + Ok(value_str) => { + match DateTime::parse_from_rfc2822(value_str) { + Ok(expiry_date) => expiry_date.with_timezone(&Utc), + Err(_) => return Err("Failed to parse expiry date".into()), + } + } + Err(_) => return Err("Invalid expiry header value".into()), + } + } + None => return Err("Expires header not found".into()), + }; + + Ok((x_pages, expiry_unix_time)) + } + + + + pub async fn request_orders(&self, region: u64) -> Result<(Vec, DateTime), Box> { + let (x_pages, expiry) = self.request_order_metadata(region).await?; + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(30)) + .build()?; + + let semaphore = Arc::new(Semaphore::new(10)); + let mut tasks = Vec::new(); + + for page in 1..=x_pages { + let permit = semaphore.clone().acquire_owned().await?; + let client = client.clone(); + let url = format!("https://esi.evetech.net/latest/markets/{}/orders/?datasource=tranquility&order_type=all&page={}", region, page); + + let task = task::spawn(async move { + time::sleep(Duration::from_millis(20)).await; + let response = client.get(&url).send().await?; + println!("{:?}", &url); + drop(permit); + response.error_for_status()?.json::>().await + }); + + tasks.push(task); + } + + let mut orders = Vec::new(); + for task in tasks { + match task.await { + Ok(result) => match result { + Ok(mut page_orders) => orders.append(&mut page_orders), + Err(e) => return Err(Box::new(e)), + }, + Err(e) => return Err(Box::new(e)), + } + } + + Ok((orders, expiry)) + } + + pub async fn ensure_valid_token(&self) -> Result<(), Box> { + let current_timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|e| format!("System time error: {}", e))? + .as_secs(); + + let should_refresh = { + let expires_at = self.expires_at.lock().unwrap(); + let expires_at_value = expires_at.unwrap_or(0); + current_timestamp > expires_at_value + }; + + if should_refresh { + self.refresh_access_token().await?; + } + + Ok(()) + } + pub fn get_access_token(&self) -> Option { + let token = self.access_token.lock().unwrap(); + token.clone() + } + + pub fn save_token_to_file(token_data: &TokenData) -> Result<(), Box> { + let token_json = serde_json::to_string(token_data)?; + fs::write(Path::new(TOKEN_FILE), token_json)?; + Ok(()) + } + + fn load_token_from_file() -> Result> { + let token_json = match fs::read_to_string(Path::new(TOKEN_FILE)) { + Ok(content) => content, + Err(_) => return Err("Unable to read token file".into()), + }; + let token_data: TokenData = serde_json::from_str(&token_json)?; + Ok(token_data) + } + + +} diff --git a/rust_backend/src/eve_service.rs b/rust_backend/src/eve_service.rs new file mode 100644 index 0000000..c831847 --- /dev/null +++ b/rust_backend/src/eve_service.rs @@ -0,0 +1,42 @@ +use crate::eve_api::EveApiClient; +use crate::pathfinding::Location; +use crate::market::Orders; + +use std::error::Error; + +pub struct EveService { + client: EveApiClient, +} + +impl EveService { + pub fn new() -> Result> { + let eve_api_client = EveApiClient::new()?; + Ok(EveService { client: eve_api_client, }) + } + + pub fn get_authorization_url(&self) -> String { + self.client.get_authorization_url() + } + + pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box> { + self.client.exchange_code_for_token(code).await + } + + pub async fn location(&self) -> Result> { + self.client.request_location().await.map_err(|e| e.into()) + } + + pub async fn updated_orders(&self, current_orders: &Orders) -> Result> { + let mut region_orders = Orders::new(); + + let regions = current_orders.expired_regions(); + + for ®ion in regions.iter() { + let (orders, expiry) = self.client.request_orders(region).await?; + region_orders.insert(region, orders, expiry); + } + + Ok(region_orders) + + } +} diff --git a/rust_backend/src/main.rs b/rust_backend/src/main.rs new file mode 100644 index 0000000..4a2e086 --- /dev/null +++ b/rust_backend/src/main.rs @@ -0,0 +1,95 @@ +mod manager; +mod eve_service; +mod eve_api; +mod pathfinding; +mod market; + +use manager::Manager; + +use actix_web::{web, App, HttpServer, Responder, HttpResponse, http::header}; +use std::sync::Mutex; +use tokio::sync::watch; +use serde::Deserialize; + + +async fn index() -> impl Responder { + "Hi" +} + +async fn location(manager: web::Data) -> impl Responder { + match manager.location() { + Some(location) => { + let station_id = location.station_id.map_or("None".to_string(), |id| id.to_string()); + let structure_id = location.structure_id.map_or("None".to_string(), |id| id.to_string()); + format!( + "Current location: Solar System ID: {}, Station ID: {}, Structure ID: {}", + location.solar_system_id, station_id, structure_id + ) + }, + None => "No location available".to_string(), + } +} + +async fn orders(manager: web::Data) -> impl Responder { + let orders = manager.orders(); + HttpResponse::Ok().json(orders) +} + +async fn start_oauth(manager: web::Data) -> HttpResponse { + let auth_url = manager.get_authorization_url(); + HttpResponse::Found() + .append_header((header::LOCATION, auth_url.as_str())) + .finish() +} + +#[derive(Deserialize)] +struct AuthCode { + code: String, +} + +async fn oauth_callback( + query: web::Query, + manager: web::Data, +) -> HttpResponse { + match manager.exchange_code_for_token(&query.code).await { + Ok(_) => HttpResponse::Ok().body("Token exchange successful"), + Err(e) => HttpResponse::InternalServerError().body(format!("Error: {}", e)), + } +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let manager = web::Data::new(Manager::new()); + + + let manager_clone = manager.clone(); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let manager_handle = tokio::spawn(async move { + manager_clone.run(shutdown_rx).await; + }); + + let server = HttpServer::new(move || { + App::new() + .app_data(manager.clone()) + .route("/", web::get().to(index)) + .route("/location", web::get().to(location)) + .route("/orders", web::get().to(orders)) + .route("/start_oauth", web::get().to(start_oauth)) + .route("/oauth_callback", web::get().to(oauth_callback)) + }) + .bind("127.0.0.1:8080")? + .run(); + + ctrlc::set_handler(move || { + let _ = shutdown_tx.send(true); + }) + .expect("Error setting Ctrl-C handler"); + + tokio::select! { + _ = server => {}, + _ = manager_handle => {}, + } + + Ok(()) +} diff --git a/rust_backend/src/manager.rs b/rust_backend/src/manager.rs new file mode 100644 index 0000000..b9cbe08 --- /dev/null +++ b/rust_backend/src/manager.rs @@ -0,0 +1,90 @@ +use std::sync::Mutex; +use tokio::sync::watch; + +use std::error::Error; + +use crate::eve_service::EveService; +use crate::pathfinding::{Location, build}; +use crate::market::{Order, Orders}; + +pub struct Manager { + // route_state: Mutex + // route_info_state: Mutex + orders_state: Mutex, + location: Mutex>, + // log_state: Mutex + eve_service: EveService, + +} + +impl Manager { + pub fn new() -> Manager { + + build(); + + Manager { + location: Mutex::new( None ), + eve_service: EveService::new().expect("Manager to generate EveService"), + orders_state: Mutex::new( Orders::new() ), + } + + } + + pub async fn run(&self, shutdown_signal: watch::Receiver) { + loop { + tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; + if *shutdown_signal.borrow() { + println!("Manager task is shutting down."); + break; + } + + match self.eve_service.location().await { + Ok(result) => { + match self.location.lock() { + Ok(mut location) => *location = Some(result), + Err(e) => eprintln!("Error locking location mutex: {}", e), + } + } + Err(e) => { + eprintln!("Error fetching location {}", e); + } + } + + + let orders_clone = match self.orders_state.lock() { + Ok(orders_lock) => orders_lock.clone(), + Err(e) => { + eprintln!("Error locking orders_state mutex: {}", e); + continue; + } + }; + + match self.eve_service.updated_orders(&orders_clone).await { + Ok(updated_orders) => { + match self.orders_state.lock() { + Ok(mut orders_state) => orders_state.update(updated_orders), + Err(e) => eprintln!("Error locking orders_state mutex: {}", e), + } + } + Err(e) => eprintln!("Error updating orders: {}", e), + } + } + } + pub fn get_authorization_url(&self) -> String { + self.eve_service.get_authorization_url() + } + + pub async fn exchange_code_for_token(&self, code: &str) -> Result<(), Box> { + self.eve_service.exchange_code_for_token(code).await + } + + pub fn location(&self) -> Option { + let location = self.location.lock().unwrap(); + location.clone() + } + + pub fn orders(&self) -> Vec { + let orders = self.orders_state.lock().unwrap(); + orders.orders() + } +} \ No newline at end of file diff --git a/rust_backend/src/market/mod.rs b/rust_backend/src/market/mod.rs new file mode 100644 index 0000000..16d1f37 --- /dev/null +++ b/rust_backend/src/market/mod.rs @@ -0,0 +1,3 @@ +mod order; + +pub use order::{Order, Orders}; \ No newline at end of file diff --git a/rust_backend/src/market/order.rs b/rust_backend/src/market/order.rs new file mode 100644 index 0000000..6dbbcf6 --- /dev/null +++ b/rust_backend/src/market/order.rs @@ -0,0 +1,83 @@ +use std::error::Error; +use std::collections::HashMap; + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::eve_service::EveService; + + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Order { + duration: u64, + is_buy_order: bool, + issued: DateTime, + location_id: u64, + min_volume: u64, + order_id: u64, + price: f64, + range: String, + system_id: u64, + type_id: u64, + volume_remain: u64, + volume_total: u64 +} + +const REGIONS: [u64; 1] = [10000033]; + +#[derive(Clone, Serialize)] +pub struct Orders { + region_orders: HashMap>, + expiry_times: HashMap>, // region_id to expiration time +} + + +impl Orders { + pub fn new() -> Self { + Orders { + region_orders: HashMap::new(), + expiry_times: HashMap::new(), + } + } + + pub fn insert(&mut self, region: u64, orders: Vec, expiry: DateTime) { + self.region_orders.insert(region, orders); + self.expiry_times.insert(region, expiry); + } + + pub fn expired_regions(&self) -> Vec { + let current_time = Utc::now(); + + println!("expiry: {:?}, time now: {:?}", &self.expiry_times, ¤t_time); + + let mut expired: Vec = self.expiry_times.iter() + .filter(|&(_region_id, &expiry_time)| expiry_time < current_time) + .map(|(®ion_id, _)| region_id) + .collect(); + + for ®ion in REGIONS.iter() { + if !self.expiry_times.contains_key(®ion) { + expired.push(region); + } + } + + + expired + + } + + pub fn update(&mut self, updated_orders: Orders) { + for (key, value) in updated_orders.region_orders { + self.region_orders.insert(key, value); + + if let Some(expiry_time) = updated_orders.expiry_times.get(&key) { + self.expiry_times.insert(key, *expiry_time); + } + } + } + + + pub fn orders(&self) -> Vec { + self.region_orders.values().flat_map(|orders| orders.clone()).collect() + } +} \ No newline at end of file diff --git a/rust_backend/src/market/trade.rs b/rust_backend/src/market/trade.rs new file mode 100644 index 0000000..362acbf --- /dev/null +++ b/rust_backend/src/market/trade.rs @@ -0,0 +1,63 @@ +use crate::order::Order; + +pub struct Trade<'a> { + from: &'a Order, + to: &'a Order, + quantity: usize, + gross_profit: f64, +} + +const TAX_RATE: f64 = 0.08 + +impl<'a> Trade<'a> { + pub fn new(from: &'a Order, to: &'a Order, quantity: usize) -> Self { + Self { from, to, quantity } + } + + pub fn create_trades(sell_orders: &'a [Order], buy_orders: &'a [Order]) -> Vec> { + let mut trades: Vec> = Vec::new(); + let mut grouped_orders: HashMap, Vec<&'a Order>)> = HashMap::new(); + + // Grouping sell orders + for order in sell_orders { + grouped_orders.entry(order.type_id) + .or_insert((Vec::new(), Vec::new())) + .0 + .push(order); + } + + // Grouping buy orders + for order in buy_orders { + grouped_orders.entry(order.type_id) + .or_insert((Vec::new(), Vec::new())) + .1 + .push(order); + } + + // Iterating over each type_id's sell and buy orders to create trades + for (_type_id, (sells, buys)) in grouped_orders { + for &sell_order in sells { + for &buy_order in buys { + let quantity = std::cmp::min(sell_order.volume_remain, buy_order.volume_remain); + let gross_profit = quantity as f64 * (buy_order.price * (1-TAX_RATE) - sell_order.price); + + if gross_profit <= 0.0 { + continue; + } + + let trade = Trade::new( + sell_order, + buy_order, + quantity, + gross_profit, + ); + + trades.push(trade); + } + } + } + + trades + } + +} diff --git a/rust_backend/src/pathfinding/graph.rs b/rust_backend/src/pathfinding/graph.rs new file mode 100644 index 0000000..0352007 --- /dev/null +++ b/rust_backend/src/pathfinding/graph.rs @@ -0,0 +1,7 @@ +use crate::pathfinding::models::{MapNode, Graph}; + +use std::collections::HashMap; + +pub fn generate_graph(stations: &Vec) -> Graph { + HashMap::new() +} \ No newline at end of file diff --git a/rust_backend/src/pathfinding/mod.rs b/rust_backend/src/pathfinding/mod.rs new file mode 100644 index 0000000..32c2831 --- /dev/null +++ b/rust_backend/src/pathfinding/mod.rs @@ -0,0 +1,15 @@ +mod models; +mod parser; +mod graph; +pub use models::{Location, Graph}; + + +use crate::pathfinding::parser::parse_stations; +use crate::pathfinding::graph::generate_graph; + +pub fn build() { + let stations = parse_stations().unwrap(); + let gates_connnections = parse_gate_connections().unwrap() + let graph = generate_graph(&stations, &gates_connnections); + +} diff --git a/rust_backend/src/pathfinding/models.rs b/rust_backend/src/pathfinding/models.rs new file mode 100644 index 0000000..4bbc580 --- /dev/null +++ b/rust_backend/src/pathfinding/models.rs @@ -0,0 +1,86 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use std::str::FromStr; +use std::collections::HashMap; + + +#[derive(Deserialize, Serialize, Clone)] +pub struct Location { + pub solar_system_id: i32, + pub station_id: Option, + pub structure_id: Option, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct RawNode { + pub itemID: u64, + pub typeID: u64, + pub groupID: u64, + #[serde(deserialize_with = "deserialize_optional_u64")] + pub solarSystemID: Option, + #[serde(deserialize_with = "deserialize_optional_u64")] + pub constellationID: Option, + #[serde(deserialize_with = "deserialize_optional_u64")] + pub regionID: Option, + #[serde(deserialize_with = "deserialize_optional_u64")] + pub orbitID: Option, + pub x: Option, + pub y: Option, + pub z: Option, + #[serde(deserialize_with = "deserialize_optional_f64")] + pub radius: Option, + pub itemName: Option, + #[serde(deserialize_with = "deserialize_optional_f64")] + pub security: Option, +} + + +fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s.as_deref() { + Some("None") | Some("") => Ok(None), // Handle "None" and empty strings + Some(str_val) => u64::from_str(str_val) + .map(Some) + .map_err(serde::de::Error::custom), + None => Ok(None), + } +} + +fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s: Option = Option::deserialize(deserializer)?; + match s.as_deref() { + Some("None") | Some("") => Ok(None), // Handle "None" and empty strings + Some(str_val) => str_val.parse::() + .map(Some) + .map_err(serde::de::Error::custom), + None => Ok(None), + } +} + +#[derive(Debug)] +pub struct MapNode { + pub item_id: u64, + pub is_station: bool, + pub solar_system_id: u64, + pub region_id: u64, + pub x: f64, + pub y: f64, + pub z: f64, + pub name: String, + pub security: f64, +} + +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct Connection { + stargate_id: u64, + destination_id: u64, +} + + + +pub type Graph = HashMap>; \ No newline at end of file diff --git a/rust_backend/src/pathfinding/parser.rs b/rust_backend/src/pathfinding/parser.rs new file mode 100644 index 0000000..955a448 --- /dev/null +++ b/rust_backend/src/pathfinding/parser.rs @@ -0,0 +1,46 @@ +use std::error::Error; +use std::fs::File; +use csv; + +use crate::pathfinding::models::{RawNode, MapNode}; + +pub fn parse_stations() -> Result, Box> { + let file_path = "../mapDenormalize.csv"; + let file = File::open(file_path)?; + + let mut rdr = csv::Reader::from_reader(file); + + let mut raw_nodes: Vec = Vec::new(); + for result in rdr.deserialize() { + match result { + Ok(node) => raw_nodes.push(node), + Err(e) => println!("Error deserializing record: {}", e), + } + } + + println!("raw nodes: {:?}", raw_nodes.len()); + + let processed_nodes: Vec = raw_nodes.into_iter() + .filter(|node| [15, 10].contains(&node.groupID)) + .map(|node| MapNode { + item_id: node.itemID, + is_station: node.groupID == 15, + solar_system_id: node.solarSystemID.unwrap_or_default(), + region_id: node.regionID.unwrap_or_default(), + x: node.x.unwrap_or_default(), + y: node.y.unwrap_or_default(), + z: node.z.unwrap_or_default(), + name: node.itemName.unwrap_or_default(), + security: node.security.unwrap_or_default(), + }) + .collect(); + + println!("processed nodes: {:?}", processed_nodes.len()); + + + Ok(processed_nodes) +} + +pub fn parse_gate_connections() -> Result, Box> { + unimplemented!(); +} \ No newline at end of file diff --git a/rust_backend/src/route.rs b/rust_backend/src/route.rs new file mode 100644 index 0000000..a2c477e --- /dev/null +++ b/rust_backend/src/route.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +struct Item { + profit: f32, + cost: f32, + cargo: f32, + quantity: usize, +} + +#[derive(PartialEq, Eq, Hash, Clone)] +struct DiscreteState { + cost: isize, + cargo: isize, +} + +type StateMap = HashMap)>; + +fn discretize(value: f32, precision: f32) -> isize { + (value / precision).round() as isize +} + +fn knapsack(items: &[Item], max_cost: f32, max_cargo: f32) -> (f32, Vec<(usize, usize)>) { + let mut best_state: StateMap = HashMap::new(); + let precision = 0.01; // Define the precision level for discretization + best_state.insert(DiscreteState { cost: 0, cargo: 0 }, (0.0, vec![])); + + for (index, item) in items.iter().enumerate() { + let mut new_state = best_state.clone(); + + for (&DiscreteState { cost: current_cost, cargo: current_cargo }, &(current_profit, ref indices)) in &best_state { + let max_quantity_cost = ((max_cost - current_cost as f32 * precision) / item.cost).floor() as usize; + let max_quantity_cargo = ((max_cargo - current_cargo as f32 * precision) / item.cargo).floor() as usize; + + let quantity = *[ + item.quantity, + max_quantity_cost, + max_quantity_cargo, + ] + .iter() + .min() + .unwrap(); + + if quantity == 0 { + continue; + } + + let total_cost = discretize(current_cost as f32 * precision + item.cost * (quantity as f32), precision); + let total_cargo = discretize(current_cargo as f32 * precision + item.cargo * (quantity as f32), precision); + let total_profit = current_profit + item.profit * (quantity as f32); + + let mut new_indices = indices.clone(); + new_indices.push((index, quantity)); + + new_state + .entry(DiscreteState { cost: total_cost, cargo: total_cargo }) + .and_modify(|(existing_profit, existing_indices)| { + if *existing_profit < total_profit { + *existing_profit = total_profit; + *existing_indices = new_indices.clone(); + } + }) + .or_insert((total_profit, new_indices)); + } + + best_state = new_state; + } + + best_state.values().max_by(|&(p1, _), &(p2, _)| p1.partial_cmp(&p2).unwrap()).unwrap_or(&(0.0, vec![])).clone() +} + +fn main() { + // Example usage with some items defined + let items = vec![ + Item { + profit: 60.0, + cost: 0.1, + cargo: 0.2, + quantity: 100, + }, + Item { + profit: 100.0, + cost: 0.2, + cargo: 0.5, + quantity: 50, + }, + Item { + profit: 100.0, + cost: 0.1, + cargo: 0.5, + quantity: 50, + }, + Item { + profit: 100.0, + cost: 0.2, + cargo: 0.3, + quantity: 50, + }, + ]; + + let max_cost = 50.0; + let max_cargo = 50.0; + + let (max_profit, item_selections) = knapsack(&items, max_cost, max_cargo); + println!("Maximum profit: {}", max_profit); + for (index, quantity) in item_selections { + println!("Item {} taken with quantity: {}", index, quantity); + } +} -- cgit v1.3