flatland-client-lib 0.2.118

Flatland3 remote game client library (TCP session, bots, game state)
Documentation
//! Per-character craft UI prefs (favorites + recent) — local JSON under `~/.flatland3/`.

use std::collections::HashMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

const RECENT_CAP: usize = 12;

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct CraftCharacterPrefs {
    #[serde(default)]
    pub favorites: Vec<String>,
    #[serde(default)]
    pub recent: Vec<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
struct CraftPrefsFile {
    #[serde(default)]
    pub by_character: HashMap<String, CraftCharacterPrefs>,
}

fn prefs_path() -> anyhow::Result<PathBuf> {
    let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("home directory not found"))?;
    Ok(home.join(".flatland3").join("craft-prefs.json"))
}

fn load_file() -> CraftPrefsFile {
    let Ok(path) = prefs_path() else {
        return CraftPrefsFile::default();
    };
    let Ok(bytes) = std::fs::read(&path) else {
        return CraftPrefsFile::default();
    };
    serde_json::from_slice(&bytes).unwrap_or_default()
}

fn save_file(file: &CraftPrefsFile) -> anyhow::Result<()> {
    let path = prefs_path()?;
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(path, serde_json::to_vec_pretty(file)?)?;
    Ok(())
}

pub fn load_for_character(character_id: &str) -> CraftCharacterPrefs {
    if character_id.is_empty() {
        return CraftCharacterPrefs::default();
    }
    load_file()
        .by_character
        .get(character_id)
        .cloned()
        .unwrap_or_default()
}

pub fn save_for_character(character_id: &str, prefs: &CraftCharacterPrefs) {
    if character_id.is_empty() {
        return;
    }
    let mut file = load_file();
    file.by_character
        .insert(character_id.to_string(), prefs.clone());
    let _ = save_file(&file);
}

impl CraftCharacterPrefs {
    pub fn is_favorite(&self, blueprint_id: &str) -> bool {
        self.favorites.iter().any(|id| id == blueprint_id)
    }

    pub fn toggle_favorite(&mut self, blueprint_id: &str) -> bool {
        if let Some(pos) = self.favorites.iter().position(|id| id == blueprint_id) {
            self.favorites.remove(pos);
            false
        } else {
            self.favorites.push(blueprint_id.to_string());
            true
        }
    }

    pub fn record_crafted(&mut self, blueprint_id: &str) {
        self.recent.retain(|id| id != blueprint_id);
        self.recent.insert(0, blueprint_id.to_string());
        if self.recent.len() > RECENT_CAP {
            self.recent.truncate(RECENT_CAP);
        }
    }
}