Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
231 lines (209 loc) · 8.8 KB Raw
// Settings on disk.
//
// Deliberately the same plain `key = value` format and the same filename as the
// Rust client, so a config written by either build means the same thing to the
// other. Unknown keys are ignored rather than rejected, which is what lets one
// build carry settings the other does not have yet.
//
// The key lists live in one place and are walked by both the writer and the
// reader, so a setting cannot be saved under one name and loaded under another.
#pragma once

// <fstream> and <sstream> are deliberately avoided. Including either one drags
// the whole iostream machinery — and its static initialisers — into the DLL,
// which cost about 1.8 MB of binary for what amounts to two file operations.
// std::fopen does the same job at no measurable size.
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>

#include "log.hpp"
#include "state.hpp"

namespace lodestone::config {

namespace {

inline std::string path() { return client_dir() + "lodestone-config.txt"; }

/// Every numeric setting, by the name it is stored under. One list, used to
/// both write and read.
template <typename F>
void for_each_number(state::Config& c, F fn) {
    fn("fly_speed", c.movement.fly_speed);
    fn("fly_step", c.movement.fly_step);
    fn("fly_step_interval", c.movement.fly_step_interval);
    fn("fly_dip_interval", c.movement.fly_dip_interval);
    fn("speed_value", c.movement.speed_value);
    fn("jetpack_power", c.movement.jetpack_power);
    fn("step_height", c.movement.step_height);
    fn("jump_multiplier", c.movement.jump_multiplier);
    fn("spider_power", c.movement.spider_power);
    fn("freecam_speed", c.movement.freecam_speed);
    fn("timer_speed", c.movement.timer_speed);
    fn("aura_range", c.combat.aura_range);
    fn("aura_cps", c.combat.aura_cps);
    fn("aim_fov", c.combat.aim_fov);
    fn("aim_speed", c.combat.aim_speed);
    fn("trigger_delay", c.combat.trigger_delay);
    fn("reach_distance", c.combat.reach_distance);
    fn("click_cps", c.combat.click_cps);
    fn("click_jitter", c.combat.click_jitter);
    fn("crit_hop", c.combat.crit_hop);
    fn("hitbox_expand", c.combat.hitbox_expand);
    fn("backtrack_ms", c.combat.backtrack_ms);
    fn("kb_horizontal", c.combat.kb_horizontal);
    fn("kb_vertical", c.combat.kb_vertical);
    fn("dodge_range", c.combat.dodge_range);
    fn("dodge_speed", c.combat.dodge_speed);
    fn("place_delay", c.building.place_delay);
    fn("block_reach_dist", c.building.block_reach_dist);
    fn("esp_distance", c.esp.distance);
    fn("block_radius", c.esp.block_radius);
    fn("base_chunk_radius", c.esp.base_chunk_radius);
    fn("view_distance_chunks", c.visuals.view_distance_chunks);
    fn("fast_chunks_rate", c.visuals.fast_chunks_rate);
    fn("fov_value", c.visuals.fov_value);
    fn("ui_scale", c.ui_scale);
}

/// Every colour, likewise.
template <typename F>
void for_each_colour(state::Config& c, F fn) {
    fn("accent", c.accent);
    fn("color_player", c.esp.color_player);
    fn("color_mob", c.esp.color_mob);
    fn("color_animal", c.esp.color_animal);
    fn("color_item", c.esp.color_item);
    fn("color_threat", c.esp.color_threat);
    fn("color_invis", c.esp.color_invis);
}

std::string trim(const std::string& s) {
    size_t a = s.find_first_not_of(" \t\r\n");
    if (a == std::string::npos) return {};
    size_t b = s.find_last_not_of(" \t\r\n");
    return s.substr(a, b - a + 1);
}

std::string fmt_colour(const state::Colour& c) {
    char buf[96];
    std::snprintf(buf, sizeof(buf), "%.4f,%.4f,%.4f,%.4f", c[0], c[1], c[2], c[3]);
    return buf;
}

bool parse_colour(const std::string& v, state::Colour* out) {
    int i = 0;
    state::Colour c{0, 0, 0, 1};
    for (size_t start = 0; i < 4 && start <= v.size();) {
        size_t comma = v.find(',', start);
        size_t len = (comma == std::string::npos) ? v.size() - start : comma - start;
        c[static_cast<size_t>(i)] = std::strtof(trim(v.substr(start, len)).c_str(), nullptr);
        ++i;
        if (comma == std::string::npos) break;
        start = comma + 1;
    }
    // Alpha is optional: a three-part colour keeps the opaque default.
    if (i < 3) return false;
    *out = c;
    return true;
}

}  // namespace

/// Write the settings out. Returns the path, or an empty string on failure.
inline std::string save(const state::Shared& s) {
    // The table walkers want a mutable config; nothing here actually writes to
    // it, so a copy keeps the signature honest without touching the original.
    state::Config copy = s.cfg;

    std::string out = "# lodestone settings\n";
    char line[256];
    for (const auto& m : state::modules()) {
        std::snprintf(line, sizeof(line), "%s = %s\n", m.key, m.flag(copy) ? "true" : "false");
        out += line;
    }
    for_each_number(copy, [&](const char* name, float& v) {
        std::snprintf(line, sizeof(line), "%s = %g\n", name, static_cast<double>(v));
        out += line;
    });
    for_each_colour(copy, [&](const char* name, state::Colour& c) {
        out += std::string(name) + " = " + fmt_colour(c) + "\n";
    });
    std::snprintf(line, sizeof(line), "menu_key = %u\n", static_cast<unsigned>(copy.menu_key));
    out += line;
    std::snprintf(line, sizeof(line), "totem_mode = %d\n",
                  copy.combat.totem_mode == state::TotemMode::Legit ? 0 : 1);
    out += line;
    for (const auto& b : s.binds) {
        if (b.target.kind != state::BindKind::Module) continue;
        if (b.target.module >= state::modules().size()) continue;
        std::snprintf(line, sizeof(line), "bind.%s = %u\n",
                      state::modules()[b.target.module].key, static_cast<unsigned>(b.key));
        out += line;
    }

    std::string file = path();
    std::FILE* f = std::fopen(file.c_str(), "wb");
    if (!f) return {};
    size_t wrote = std::fwrite(out.data(), 1, out.size(), f);
    // The close is what flushes, so a short write only shows up here.
    bool ok = (std::fclose(f) == 0) && wrote == out.size();
    return ok ? file : std::string{};
}

/// Read the settings back. Missing file is not an error — it just means there
/// is nothing saved yet.
inline bool load(state::Shared& s) {
    std::FILE* f = std::fopen(path().c_str(), "rb");
    if (!f) return false;
    std::string text;
    char chunk[4096];
    size_t got;
    while ((got = std::fread(chunk, 1, sizeof(chunk), f)) > 0) text.append(chunk, got);
    std::fclose(f);

    s.binds.clear();
    // A line at a time. The bound is `pos <= size` rather than "until no more
    // newlines" so a final line with no trailing newline still counts.
    for (size_t pos = 0; pos <= text.size();) {
        size_t nl = text.find('\n', pos);
        size_t len = (nl == std::string::npos) ? text.size() - pos : nl - pos;
        std::string raw = text.substr(pos, len);
        pos = (nl == std::string::npos) ? text.size() + 1 : nl + 1;
        std::string line = trim(raw);
        if (line.empty() || line[0] == '#') continue;
        size_t eq = line.find('=');
        if (eq == std::string::npos) continue;
        std::string key = trim(line.substr(0, eq));
        std::string value = trim(line.substr(eq + 1));

        // A keybind, named by the module it belongs to.
        if (key.rfind("bind.", 0) == 0) {
            std::string name = key.substr(5);
            const auto& table = state::modules();
            for (size_t i = 0; i < table.size(); ++i) {
                if (name != table[i].key) continue;
                s.binds.push_back(
                    state::Bind{static_cast<uint32_t>(std::strtoul(value.c_str(), nullptr, 10)),
                                state::BindTarget{state::BindKind::Module, i}});
                break;
            }
            continue;
        }

        bool handled = false;
        for (const auto& m : state::modules()) {
            if (key != m.key) continue;
            m.flag(s.cfg) = (value == "true");
            handled = true;
            break;
        }
        if (handled) continue;

        for_each_number(s.cfg, [&](const char* name, float& v) {
            if (!handled && key == name) {
                v = std::strtof(value.c_str(), nullptr);
                handled = true;
            }
        });
        if (handled) continue;

        for_each_colour(s.cfg, [&](const char* name, state::Colour& c) {
            if (!handled && key == name) {
                handled = parse_colour(value, &c);
            }
        });
        if (handled) continue;

        if (key == "menu_key") {
            s.cfg.menu_key = static_cast<uint32_t>(std::strtoul(value.c_str(), nullptr, 10));
        } else if (key == "totem_mode") {
            s.cfg.combat.totem_mode = std::strtol(value.c_str(), nullptr, 10) == 0
                                          ? state::TotemMode::Legit
                                          : state::TotemMode::Instant;
        }
    }
    return true;
}

}  // namespace lodestone::config