Sign in Sign up
kretrod/lodestone Public
Branches
master
228 lines (209 loc) · 10.2 KB Raw
//! Saving and loading the menu's settings.
//!
//! A plain text file, one `key = value` per line, so it can be read and edited
//! without the client. Module names come from the same registry the menu and
//! the keybinds use, so a module cannot appear in one and be missing here.

use crate::state::{BindTarget, ModuleId, Shared};

fn path() -> std::path::PathBuf {
    crate::client_dir().join("lodestone-config.txt")
}

pub fn save(shared: &Shared) -> Result<String, String> {
    let mut out = String::from("# lodestone settings\n");

    for m in ModuleId::ALL {
        out.push_str(&format!("{} = {}\n", m.key(), m.get(&shared.cfg)));
    }

    let c = &shared.cfg;
    let mut num = |k: &str, v: f32| out.push_str(&format!("{k} = {v}\n"));
    num("fly_speed", c.movement.fly_speed);
    num("fly_step", c.movement.fly_step);
    num("fly_step_interval", c.movement.fly_step_interval);
    num("speed_value", c.movement.speed_value);
    num("jetpack_power", c.movement.jetpack_power);
    num("step_height", c.movement.step_height);
    num("jump_multiplier", c.movement.jump_multiplier);
    num("spider_power", c.movement.spider_power);
    num("freecam_speed", c.movement.freecam_speed);
    num("aura_range", c.combat.aura_range);
    num("aura_cps", c.combat.aura_cps);
    num("aim_fov", c.combat.aim_fov);
    num("aim_speed", c.combat.aim_speed);
    num("trigger_delay", c.combat.trigger_delay);
    num("reach_distance", c.combat.reach_distance);
    num("click_cps", c.combat.click_cps);
    num("click_jitter", c.combat.click_jitter);
    num("place_delay", c.building.place_delay);
    num("kb_horizontal", c.combat.kb_horizontal);
    num("kb_vertical", c.combat.kb_vertical);
    num("esp_distance", c.esp.distance);
    num("fov_value", c.visuals.fov_value);
    num("ui_scale", c.ui_scale);

    out.push_str(&format!("fly_mode = {}\n", c.movement.fly_mode.label()));
    out.push_str(&format!("speed_mode = {}\n", c.movement.speed_mode.label()));
    out.push_str(&format!("aim_mode = {}\n", c.combat.aim_mode.label()));
    out.push_str(&format!("aura_players = {}\n", c.combat.aura_players));
    out.push_str(&format!("aura_mobs = {}\n", c.combat.aura_mobs));
    out.push_str(&format!("aura_animals = {}\n", c.combat.aura_animals));

    // Customizable UI state that is not a module toggle or a numeric setting.
    out.push_str(&format!("menu_key = {}\n", c.menu_key));
    out.push_str(&format!("accent = {}\n", fmt_color(c.accent)));
    out.push_str(&format!("totem_mode = {}\n", c.combat.totem_mode.label()));
    // Written directly rather than through `num`: that closure holds a mutable
    // borrow of `out`, so reaching for it again down here would overlap with
    // the plain pushes above.
    out.push_str(&format!("crystal_range = {}\n", c.combat.crystal_range));
    out.push_str(&format!("crystal_delay = {}\n", c.combat.crystal_delay));
    out.push_str(&format!("esp_box_style = {}\n", c.esp.box_style.label()));
    out.push_str(&format!("esp_box_fill = {}\n", c.esp.box_fill));
    out.push_str(&format!("color_player = {}\n", fmt_color(c.esp.color_player)));
    out.push_str(&format!("color_mob = {}\n", fmt_color(c.esp.color_mob)));
    out.push_str(&format!("color_animal = {}\n", fmt_color(c.esp.color_animal)));
    out.push_str(&format!("color_item = {}\n", fmt_color(c.esp.color_item)));
    out.push_str(&format!("color_threat = {}\n", fmt_color(c.esp.color_threat)));
    out.push_str(&format!("color_invis = {}\n", fmt_color(c.esp.color_invis)));

    for b in &shared.binds {
        let name = match b.target {
            BindTarget::Module(m) => m.key(),
            BindTarget::Panic => "panic",
            // Never stored in the bind list — it lives in cfg.menu_key.
            BindTarget::MenuToggle => continue,
        };
        out.push_str(&format!("bind.{name} = {}\n", b.key));
    }

    let path = path();
    std::fs::write(&path, out).map_err(|e| e.to_string())?;
    Ok(path.display().to_string())
}

/// Load at startup, when there is no `Shared` borrow to hand.
/// Returns false when there is simply no config yet.
pub fn load_into_shared() -> Result<bool, String> {
    if !path().exists() {
        return Ok(false);
    }
    crate::state::with(|s| load(s))
        .unwrap_or(Err("state lock poisoned".into()))
        .map(|_| true)
}

pub fn load(shared: &mut Shared) -> Result<(), String> {
    let text = std::fs::read_to_string(path()).map_err(|e| e.to_string())?;
    shared.binds.clear();

    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let (key, value) = (key.trim(), value.trim());

        if let Some(name) = key.strip_prefix("bind.") {
            if let Ok(vk) = value.parse::<u32>() {
                let target = if name == "panic" {
                    Some(BindTarget::Panic)
                } else {
                    ModuleId::from_key(name).map(BindTarget::Module)
                };
                if let Some(target) = target {
                    shared.binds.push(crate::state::Bind { key: vk, target });
                }
            }
            continue;
        }

        if let Some(m) = ModuleId::from_key(key) {
            m.set(&mut shared.cfg, value == "true");
            continue;
        }

        apply_setting(shared, key, value);
    }

    // Anything the window-capture switch turned on has to be re-applied, since
    // it is Windows state rather than ours.
    crate::capture::apply(shared.cfg.misc.hide_from_capture);
    Ok(())
}

/// Format a colour as "r,g,b,a" with four decimals each.
fn fmt_color(c: [f32; 4]) -> String {
    format!("{:.4},{:.4},{:.4},{:.4}", c[0], c[1], c[2], c[3])
}

/// Parse "r,g,b,a" back into a colour.
fn parse_color(s: &str) -> Option<[f32; 4]> {
    let mut it = s.split(',').map(|p| p.trim().parse::<f32>());
    let r = it.next()?.ok()?;
    let g = it.next()?.ok()?;
    let b = it.next()?.ok()?;
    let a = it.next().and_then(|v| v.ok()).unwrap_or(1.0);
    Some([r, g, b, a])
}

fn apply_setting(shared: &mut Shared, key: &str, value: &str) {
    use crate::state::{AimMode, BoxStyle, FlyMode, SpeedMode, TotemMode};
    let c = &mut shared.cfg;
    let f = || value.parse::<f32>().ok();
    let b = value == "true";
    match key {
        "menu_key" => c.menu_key = value.parse::<u32>().unwrap_or(c.menu_key),
        "accent" => c.accent = parse_color(value).unwrap_or(c.accent),
        "crystal_range" => c.combat.crystal_range = f().unwrap_or(c.combat.crystal_range),
        "crystal_delay" => c.combat.crystal_delay = f().unwrap_or(c.combat.crystal_delay),
        "totem_mode" => {
            if let Some(m) = TotemMode::ALL.into_iter().find(|m| m.label() == value) {
                c.combat.totem_mode = m;
            }
        }
        "esp_box_fill" => c.esp.box_fill = b,
        "esp_box_style" => {
            if let Some(m) = BoxStyle::ALL.into_iter().find(|m| m.label() == value) {
                c.esp.box_style = m;
            }
        }
        "color_player" => c.esp.color_player = parse_color(value).unwrap_or(c.esp.color_player),
        "color_mob" => c.esp.color_mob = parse_color(value).unwrap_or(c.esp.color_mob),
        "color_animal" => c.esp.color_animal = parse_color(value).unwrap_or(c.esp.color_animal),
        "color_item" => c.esp.color_item = parse_color(value).unwrap_or(c.esp.color_item),
        "color_threat" => c.esp.color_threat = parse_color(value).unwrap_or(c.esp.color_threat),
        "color_invis" => c.esp.color_invis = parse_color(value).unwrap_or(c.esp.color_invis),
        "fly_speed" => c.movement.fly_speed = f().unwrap_or(c.movement.fly_speed),
        "fly_step" => c.movement.fly_step = f().unwrap_or(c.movement.fly_step),
        "fly_step_interval" => {
            c.movement.fly_step_interval = f().unwrap_or(c.movement.fly_step_interval)
        }
        "speed_value" => c.movement.speed_value = f().unwrap_or(c.movement.speed_value),
        "jetpack_power" => c.movement.jetpack_power = f().unwrap_or(c.movement.jetpack_power),
        "step_height" => c.movement.step_height = f().unwrap_or(c.movement.step_height),
        "jump_multiplier" => {
            c.movement.jump_multiplier = f().unwrap_or(c.movement.jump_multiplier)
        }
        "spider_power" => c.movement.spider_power = f().unwrap_or(c.movement.spider_power),
        "freecam_speed" => c.movement.freecam_speed = f().unwrap_or(c.movement.freecam_speed),
        "aura_range" => c.combat.aura_range = f().unwrap_or(c.combat.aura_range),
        "aura_cps" => c.combat.aura_cps = f().unwrap_or(c.combat.aura_cps),
        "aim_fov" => c.combat.aim_fov = f().unwrap_or(c.combat.aim_fov),
        "aim_speed" => c.combat.aim_speed = f().unwrap_or(c.combat.aim_speed),
        "trigger_delay" => c.combat.trigger_delay = f().unwrap_or(c.combat.trigger_delay),
        "reach_distance" => c.combat.reach_distance = f().unwrap_or(c.combat.reach_distance),
        "click_cps" => c.combat.click_cps = f().unwrap_or(c.combat.click_cps),
        "click_jitter" => c.combat.click_jitter = f().unwrap_or(c.combat.click_jitter),
        "place_delay" => c.building.place_delay = f().unwrap_or(c.building.place_delay),
        "kb_horizontal" => c.combat.kb_horizontal = f().unwrap_or(c.combat.kb_horizontal),
        "kb_vertical" => c.combat.kb_vertical = f().unwrap_or(c.combat.kb_vertical),
        "esp_distance" => c.esp.distance = f().unwrap_or(c.esp.distance),
        "fov_value" => c.visuals.fov_value = f().unwrap_or(c.visuals.fov_value),
        "ui_scale" => c.ui_scale = f().unwrap_or(c.ui_scale),
        "aura_players" => c.combat.aura_players = b,
        "aura_mobs" => c.combat.aura_mobs = b,
        "aura_animals" => c.combat.aura_animals = b,
        "fly_mode" => {
            if let Some(m) = FlyMode::ALL.into_iter().find(|m| m.label() == value) {
                c.movement.fly_mode = m;
            }
        }
        "speed_mode" => {
            if let Some(m) = SpeedMode::ALL.into_iter().find(|m| m.label() == value) {
                c.movement.speed_mode = m;
            }
        }
        "aim_mode" => {
            if let Some(m) = AimMode::ALL.into_iter().find(|m| m.label() == value) {
                c.combat.aim_mode = m;
            }
        }
        _ => {}
    }
}