//! Garage management operations for the Arma 3 server extension. //! //! Provides Arma 3 extension commands for vehicle storage, retrieval, and updates. use arma_rs::{CallContext, Group}; use forge_models::Vehicle; use forge_repositories::{InMemoryGarageHotRepository, RedisGarageRepository}; use forge_services::{GarageHotStateService, GarageService}; use std::collections::HashMap; use std::sync::LazyLock; use crate::adapters::ExtensionRedisClient; use crate::enqueue_persistence_task; use crate::helpers::resolve_uid; use crate::log::log; /// Global garage service instance. static GARAGE_SERVICE: LazyLock>> = LazyLock::new(|| { let redis_client = ExtensionRedisClient::new(); let repository = RedisGarageRepository::new(redis_client); GarageService::new(repository) }); static HOT_GARAGE_SERVICE: LazyLock< GarageHotStateService, InMemoryGarageHotRepository>, > = LazyLock::new(|| { let redis_client = ExtensionRedisClient::new(); let repository = RedisGarageRepository::new(redis_client); let hot_repository = InMemoryGarageHotRepository::new(); GarageHotStateService::new(repository, hot_repository) }); /// Creates the Arma 3 command group for garage operations. /// /// Registers commands: `create`, `get`, `add`, `update`, `remove`, `delete`, `exists`. pub fn group() -> Group { Group::new() .command("create", create_garage) .command("get", get_garage) .command("add", add_vehicle) .command("update", update_garage) .command("patch", patch_vehicle) .command("remove", remove_vehicle) .command("delete", delete_garage) .command("exists", garage_exists) .group( "hot", Group::new() .command("init", init_hot_garage) .command("get", get_hot_garage) .command("override", override_hot_garage) .command("save", save_hot_garage) .command("remove", remove_hot_garage) .command("add", add_hot_vehicle) .command("remove_vehicle", remove_hot_vehicle), ) } fn serialize_hot_vehicles(garage: forge_models::garage::Garage) -> String { match serde_json::to_string(&garage.vehicles) { Ok(json) => json, Err(error) => format!("Error: Failed to serialize hot garage: {}", error), } } pub(crate) fn init_hot_garage(call_context: CallContext, key: String) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; match HOT_GARAGE_SERVICE.init_garage(resolved_uid) { Ok(garage) => serialize_hot_vehicles(garage), Err(error) => format!("Error: {}", error), } } pub(crate) fn get_hot_garage(call_context: CallContext, key: String) -> String { init_hot_garage(call_context, key) } pub(crate) fn override_hot_garage( call_context: CallContext, key: String, json_data: String, ) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; let vehicles: HashMap = match serde_json::from_str(&json_data) { Ok(data) => data, Err(error) => return format!("Error: Invalid JSON data: {}", error), }; match HOT_GARAGE_SERVICE.override_garage(resolved_uid, vehicles) { Ok(garage) => serialize_hot_vehicles(garage), Err(error) => format!("Error: {}", error), } } pub(crate) fn save_hot_garage(call_context: CallContext, key: String) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; match HOT_GARAGE_SERVICE.get_garage(resolved_uid.clone()) { Ok(garage) => { enqueue_persistence_task("garage", move || { HOT_GARAGE_SERVICE.save_garage(resolved_uid).map(|_| ()) }); serialize_hot_vehicles(garage) } Err(error) => format!("Error: {}", error), } } pub(crate) fn remove_hot_garage(call_context: CallContext, key: String) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; match HOT_GARAGE_SERVICE.remove_garage(resolved_uid) { Ok(_) => "OK".to_string(), Err(error) => format!("Error: {}", error), } } pub(crate) fn add_hot_vehicle(call_context: CallContext, key: String, json_data: String) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; let data: serde_json::Value = match serde_json::from_str(&json_data) { Ok(d) => d, Err(error) => return format!("Error: Invalid JSON data: {}", error), }; let classname = match data.get("classname").and_then(|v| v.as_str()) { Some(c) => c.to_string(), None => return "Error: Missing or invalid classname".to_string(), }; let fuel = match data.get("fuel").and_then(|v| v.as_f64()) { Some(f) => f, None => return "Error: Missing or invalid fuel".to_string(), }; let damage = match data.get("damage").and_then(|v| v.as_f64()) { Some(d) => d, None => return "Error: Missing or invalid damage".to_string(), }; let hit_points_json = match data.get("hit_points") { Some(hp) => match serde_json::to_string(hp) { Ok(s) => s, Err(error) => return format!("Error: Failed to serialize hit_points: {}", error), }, None => return "Error: Missing hit_points".to_string(), }; match HOT_GARAGE_SERVICE.add_vehicle(resolved_uid, classname, fuel, damage, hit_points_json) { Ok(garage) => serialize_hot_vehicles(garage), Err(error) => format!("Error: {}", error), } } pub(crate) fn remove_hot_vehicle( call_context: CallContext, key: String, json_data: String, ) -> String { let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => uid, None => return format!("Error: Failed to resolve UID for key: {}", key), }; let data: serde_json::Value = match serde_json::from_str(&json_data) { Ok(d) => d, Err(error) => return format!("Error: Invalid JSON data: {}", error), }; let plate = match data.get("plate").and_then(|v| v.as_str()) { Some(p) => p.to_string(), None => return "Error: Missing or invalid plate".to_string(), }; match HOT_GARAGE_SERVICE.remove_vehicle(resolved_uid, plate) { Ok(garage) => serialize_hot_vehicles(garage), Err(error) => format!("Error: {}", error), } } /// Creates a new empty garage for a player. /// /// Parameters: key pub fn create_garage(call_context: CallContext, key: String) -> String { log( "garage", "DEBUG", &format!("Creating garage for key: {}", key), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.create_garage(resolved_uid.clone()) { Ok(empty_garage) => { log( "garage", "INFO", &format!("Successfully created garage for: {}", resolved_uid), ); match serde_json::to_string(&empty_garage.vehicles) { Ok(json) => json, Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to create garage '{}': {}", resolved_uid, e), ); error_msg } } } /// Retrieves a player's garage by key/UID. /// /// Returns JSON object with garage data including all vehicles. pub fn get_garage(call_context: CallContext, key: String) -> String { log( "garage", "DEBUG", &format!("Getting garage for key: {}", key), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.get_garage(resolved_uid.clone()) { Ok(garage) => { log( "garage", "INFO", &format!( "Successfully retrieved garage with {} vehicles", garage.vehicles.len() ), ); match serde_json::to_string(&garage.vehicles) { Ok(json) => { log( "garage", "DEBUG", &format!("Serialized garage to JSON: {}", json), ); json } Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to get garage '{}': {}", resolved_uid, e), ); error_msg } } } /// Adds a new vehicle to a player's garage. /// /// Parameters: key, json_data pub fn add_vehicle(call_context: CallContext, key: String, json_data: String) -> String { log( "garage", "DEBUG", &format!("Adding vehicle for key: {} with data: {}", key, json_data), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; // Parse JSON data let data: serde_json::Value = match serde_json::from_str(&json_data) { Ok(d) => d, Err(e) => { let error_msg = format!("Error: Invalid JSON data: {}", e); log("garage", "ERROR", &error_msg); return error_msg; } }; // Extract fields let classname = match data.get("classname").and_then(|v| v.as_str()) { Some(c) => c.to_string(), None => { let error_msg = "Error: Missing or invalid classname".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; let fuel = match data.get("fuel").and_then(|v| v.as_f64()) { Some(f) => f, None => { let error_msg = "Error: Missing or invalid fuel".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; let damage = match data.get("damage").and_then(|v| v.as_f64()) { Some(d) => d, None => { let error_msg = "Error: Missing or invalid damage".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; let hit_points_json = match data.get("hit_points") { Some(hp) => match serde_json::to_string(hp) { Ok(s) => s, Err(e) => { let error_msg = format!("Error: Failed to serialize hit_points: {}", e); log("garage", "ERROR", &error_msg); return error_msg; } }, None => { let error_msg = "Error: Missing hit_points".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.add_vehicle( resolved_uid.clone(), classname, fuel, damage, hit_points_json, ) { Ok(garage) => { log( "garage", "INFO", &format!("Successfully added vehicle to garage: {}", resolved_uid), ); match serde_json::to_string(&garage.vehicles) { Ok(json) => json, Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to add vehicle '{}': {}", resolved_uid, e), ); error_msg } } } /// Updates the entire garage state. /// /// Parameters: key, json_data (Map of vehicles) pub fn update_garage(call_context: CallContext, key: String, json_data: String) -> String { log( "garage", "DEBUG", &format!("Updating garage for key: {} with data: {}", key, json_data), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; // Parse JSON data let vehicles: HashMap = match serde_json::from_str(&json_data) { Ok(d) => d, Err(e) => { let error_msg = format!("Error: Invalid JSON data: {}", e); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.update_garage(resolved_uid.clone(), vehicles) { Ok(garage) => { log( "garage", "INFO", &format!("Successfully updated garage for: {}", resolved_uid), ); match serde_json::to_string(&garage.vehicles) { Ok(json) => json, Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to update garage '{}': {}", resolved_uid, e), ); error_msg } } } /// Patches a specific vehicle in the garage. /// /// Parameters: key, json_data (Map with plate and optional fields) pub fn patch_vehicle(call_context: CallContext, key: String, json_data: String) -> String { log( "garage", "DEBUG", &format!("Patching vehicle for key: {} with data: {}", key, json_data), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; let data: serde_json::Value = match serde_json::from_str(&json_data) { Ok(d) => d, Err(e) => { let error_msg = format!("Error: Invalid JSON data: {}", e); log("garage", "ERROR", &error_msg); return error_msg; } }; let plate = match data.get("plate").and_then(|v| v.as_str()) { Some(s) => s.to_string(), None => { let error_msg = "Error: Missing plate".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; let fuel = data.get("fuel").and_then(|v| v.as_f64()); let damage = data.get("damage").and_then(|v| v.as_f64()); let hit_points_json = data .get("hit_points") .and_then(|v| serde_json::to_string(v).ok()); match GARAGE_SERVICE.patch_vehicle(resolved_uid.clone(), plate, damage, fuel, hit_points_json) { Ok(garage) => { log( "garage", "INFO", &format!("Successfully patched vehicle for: {}", resolved_uid), ); match serde_json::to_string(&garage.vehicles) { Ok(json) => json, Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to patch vehicle '{}': {}", resolved_uid, e), ); error_msg } } } /// Removes a vehicle from the garage. /// /// Parameters: key, json_data pub fn remove_vehicle(call_context: CallContext, key: String, json_data: String) -> String { log( "garage", "DEBUG", &format!( "Removing vehicle from garage for key: {} with data: {}", key, json_data ), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; // Parse JSON data let data: serde_json::Value = match serde_json::from_str(&json_data) { Ok(d) => d, Err(e) => { let error_msg = format!("Error: Invalid JSON data: {}", e); log("garage", "ERROR", &error_msg); return error_msg; } }; // Extract plate let plate = match data.get("plate").and_then(|v| v.as_str()) { Some(p) => p.to_string(), None => { let error_msg = "Error: Missing or invalid plate".to_string(); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.remove_vehicle(resolved_uid.clone(), plate) { Ok(garage) => { log( "garage", "INFO", &format!("Successfully removed vehicle from garage: {}", resolved_uid), ); match serde_json::to_string(&garage.vehicles) { Ok(json) => json, Err(e) => { let error_msg = format!("Error: Failed to serialize garage: {}", e); log("garage", "ERROR", &error_msg); error_msg } } } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to remove vehicle '{}': {}", resolved_uid, e), ); error_msg } } } /// Permanently deletes a player's garage. pub fn delete_garage(call_context: CallContext, key: String) -> String { log( "garage", "DEBUG", &format!("Deleting garage for key: {}", key), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { let error_msg = format!("Error: Failed to resolve UID for key: {}", key); log("garage", "ERROR", &error_msg); return error_msg; } }; match GARAGE_SERVICE.delete_garage(resolved_uid.clone()) { Ok(_) => { log( "garage", "INFO", &format!("Successfully deleted garage: {}", resolved_uid), ); "OK".to_string() } Err(e) => { let error_msg = format!("Error: {}", e); log( "garage", "ERROR", &format!("Failed to delete garage '{}': {}", resolved_uid, e), ); error_msg } } } /// Checks if a player has a garage. pub fn garage_exists(call_context: CallContext, key: String) -> String { log( "garage", "DEBUG", &format!("Checking garage existence for key: {}", key), ); let resolved_uid = match resolve_uid(&key, &call_context) { Some(uid) => { log("garage", "DEBUG", &format!("Resolved UID: {}", uid)); uid } None => { log( "garage", "ERROR", &format!("Failed to resolve UID for key: {}", key), ); return "false".to_string(); } }; match GARAGE_SERVICE.garage_exists(resolved_uid.clone()) { Ok(exists) => { log( "garage", "DEBUG", &format!("Garage '{}' exists: {}", resolved_uid, exists), ); exists.to_string() } Err(e) => { log( "garage", "ERROR", &format!("Failed to check if garage '{}' exists: {}", resolved_uid, e), ); "false".to_string() } } }