flatland-client-lib 0.2.22

Flatland3 remote game client library (TCP session, bots, game state)
Documentation
//! Client keybind settings (`assets/config/client-settings.yaml`).

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientSettings {
    #[serde(default = "default_version")]
    pub version: String,
    #[serde(default)]
    pub keys: ClientKeyBindings,
}

fn default_version() -> String {
    "1".into()
}

impl Default for ClientSettings {
    fn default() -> Self {
        Self {
            version: default_version(),
            keys: ClientKeyBindings::default(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientKeyBindings {
    #[serde(default = "default_target_t1")]
    pub target_t1: String,
    #[serde(default = "default_target_t2")]
    pub target_t2: String,
    #[serde(default = "default_auto_t1")]
    pub auto_t1: String,
    #[serde(default = "default_auto_t2")]
    pub auto_t2: String,
    #[serde(default = "default_loadout")]
    pub loadout_menu: String,
    #[serde(default = "default_rotation_editor")]
    pub rotation_editor: String,
    #[serde(default = "default_dodge")]
    pub dodge: String,
    #[serde(default = "default_lunge")]
    pub lunge: String,
    #[serde(default = "default_block")]
    pub block: String,
    #[serde(default = "default_clear_t1")]
    pub clear_target_t1: String,
    #[serde(default = "default_clear_t2")]
    pub clear_target_t2: String,
    /// Legacy client-local hotbar ability ids (unused for play casting — server persists hotbar).
    #[serde(default = "default_hotbar")]
    pub hotbar: Vec<Option<String>>,
}

fn default_target_t1() -> String {
    "Tab".into()
}
fn default_target_t2() -> String {
    "Shift+Tab".into()
}
fn default_auto_t1() -> String {
    "r".into()
}
fn default_auto_t2() -> String {
    "Shift+r".into()
}
fn default_loadout() -> String {
    "l".into()
}
fn default_rotation_editor() -> String {
    "o".into()
}
fn default_dodge() -> String {
    "c".into()
}
fn default_lunge() -> String {
    "Shift+Space".into()
}
fn default_block() -> String {
    "q".into()
}
fn default_clear_t1() -> String {
    "0".into()
}
fn default_clear_t2() -> String {
    "Shift+0".into()
}
fn default_hotbar() -> Vec<Option<String>> {
    Vec::new()
}

impl Default for ClientKeyBindings {
    fn default() -> Self {
        Self {
            target_t1: default_target_t1(),
            target_t2: default_target_t2(),
            auto_t1: default_auto_t1(),
            auto_t2: default_auto_t2(),
            loadout_menu: default_loadout(),
            rotation_editor: default_rotation_editor(),
            dodge: default_dodge(),
            lunge: default_lunge(),
            block: default_block(),
            clear_target_t1: default_clear_t1(),
            clear_target_t2: default_clear_t2(),
            hotbar: default_hotbar(),
        }
    }
}

impl ClientKeyBindings {
    /// Legacy lookup for `keys.hotbar` — unused for play (server-persisted hotbar on GameState).
    pub fn hotbar_ability(&self, slot_1_to_9: u8) -> Option<&str> {
        if !(1..=9).contains(&slot_1_to_9) {
            return None;
        }
        self.hotbar
            .get((slot_1_to_9 - 1) as usize)
            .and_then(|a| a.as_deref())
            .filter(|id| !id.is_empty())
    }
}

fn find_settings_path() -> PathBuf {
    if let Ok(p) = std::env::var("FLATLAND_CLIENT_SETTINGS") {
        let path = PathBuf::from(p);
        if path.is_file() {
            return path;
        }
    }
    if let Some(rev) = crate::assets::synced_rev_dir() {
        let path = rev.join("config/client-settings.yaml");
        if path.is_file() {
            return path;
        }
    }
    if let Ok(home) = std::env::var("HOME") {
        let path = PathBuf::from(home).join(".flatland3/assets/current/config/client-settings.yaml");
        if path.is_file() {
            return path;
        }
    }
    let mut dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    for _ in 0..8 {
        let candidate = dir.join("assets/config/client-settings.yaml");
        if candidate.is_file() {
            return candidate;
        }
        if !dir.pop() {
            break;
        }
    }
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    for ancestor in manifest.ancestors() {
        let candidate = ancestor.join("assets/config/client-settings.yaml");
        if candidate.is_file() {
            return candidate;
        }
    }
    PathBuf::from("assets/config/client-settings.yaml")
}

pub fn load_client_settings() -> ClientSettings {
    let path = find_settings_path();
    let Ok(raw) = std::fs::read_to_string(&path) else {
        return ClientSettings::default();
    };
    serde_yaml::from_str(&raw).unwrap_or_default()
}