sots-engine/src/game/config/config_loader.h

108 lines
5.5 KiB
C++

// game::config -- the flat "KEY value" constant tables (Data/globals.txt,
// Data/Strategy/StrategyVars.txt, Data/Species.txt, Data/encounters.txt, Data/Combat/*.txt).
//
// Model of the original loader (findings: loader-prototypes.md section M1):
//
// * Every constant is a static "slot" registered at image start-up with a key, a storage
// word and one typed parser. The *slot* decides the type; the file never does.
// * The file is read with mars::text::parse_flat_kv, which follows the engine's own loop:
// the Mars::Script pull tokenizer, KEY value pairs, `NAME {` blocks skipped whole, a
// lone `}` ignored, and a final pair whose value touches end-of-file dropped.
// * Keys match case-insensitively (ASCII). A slot is consumed on first sight, so the
// first occurrence wins; a repeat -- or an unknown key -- is logged and ignored. Slots
// the file never names keep whatever the image default was.
//
// Slot kinds and their storage (widths are what the hook declares as side-effect regions):
// Int int32 "%d"
// Float float "%f"
// FloatScaled float "%f", then multiplied by a process constant (pi/180 as a double)
// Colour float[4] "%d %d %d %d", each defaulted to 255, /255, clamped to 0..1
// Vec3 float[3] "%f %f %f" (components not matched stay unchanged)
// Rect int32[4] "%d %d %d %d" (idem)
// String an engine std::string (24 bytes): the text itself. This module cannot build
// the engine's string object, so String slots are written through the
// caller's ExternWriter (the shim hands them to the game's own parser).
// Unknown a parser this code does not model; never written, declared 4 bytes wide.
#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
#include <vector>
namespace game::config {
enum class Kind : std::uint8_t { Int, Float, FloatScaled, Colour, Vec3, Rect, String, Unknown };
constexpr std::size_t width(Kind k) {
switch (k) {
case Kind::Colour: return 16;
case Kind::Vec3: return 12;
case Kind::Rect: return 16;
case Kind::String: return 24;
default: return 4;
}
}
const char* kind_name(Kind k); // "int" | "float" | "fscaled" | "colour" | "vec3" | "rect" | "string" | "unknown"
// One registered constant: where the value lives and how its text is typed.
struct Slot {
std::string key; // as registered (matched case-insensitively)
Kind kind = Kind::Int;
void* storage = nullptr; // width(kind) bytes
void* native = nullptr; // opaque handle for the ExternWriter (the shim keeps the game parser here)
bool consumed = false; // set by apply(): first occurrence wins
};
struct Stats {
unsigned applied = 0; // pairs written to a slot
unsigned duplicate = 0; // repeats of an already consumed key (ignored)
unsigned unknown = 0; // keys with no slot (ignored)
unsigned missing = 0; // slots the file never named
};
using LogFn = std::function<void(const std::string& line)>;
using ExternWriter = std::function<void(Slot& slot, std::string_view value)>;
// ---- typed value parsers -------------------------------------------------------------------
//
// scan_int / scan_float follow C `sscanf` "%d" / "%f": leading whitespace skipped, an optional
// sign, then the longest valid prefix; they return false and leave `out` alone when no digit
// is found. Integers wrap modulo 2^32 like the CRT's accumulator; floats are converted with
// one rounding from the decimal text. The *_at forms continue from `pos` (for "%d %d ..").
bool scan_int(std::string_view text, std::int32_t& out);
bool scan_float(std::string_view text, float& out);
bool scan_int_at(std::string_view text, std::size_t& pos, std::int32_t& out);
bool scan_float_at(std::string_view text, std::size_t& pos, float& out);
// "%d %d %d %d" with every component defaulted to 255; each is divided by 255 (double
// arithmetic, then stored as float) and clamped to [0, 1]. Always writes all four.
void parse_colour(std::string_view text, float rgba[4]);
// "%f %f %f" / "%d %d %d %d": components are written left to right until one fails to scan;
// returns how many were written.
int parse_vec3(std::string_view text, float xyz[3]);
int parse_rect(std::string_view text, std::int32_t rect[4]);
// The scaled-float product as the original computes it (double product, stored as float).
float scale_float(float v, double scale);
// ---- the pairs ------------------------------------------------------------------------------------
struct Pair {
std::string key, value;
};
// Every top-level KEY value pair in file order (duplicates included), per the engine rules.
std::vector<Pair> pairs(std::string_view text);
// Writes the value text into the slot according to its kind. `scale` feeds FloatScaled;
// String slots go through `ext` (nothing happens without one).
void write_slot(Slot& slot, std::string_view value, double scale, const ExternWriter& ext);
// The whole loader for one file: `text` is the file's bytes, `slots` the constants registered
// for that file. Consumed slots are flagged; diagnostics mirror the original's wording:
// "[file] KEY not recognized or is multiply defined." "[file] KEY expected but not found."
// `file` is only used in the log lines.
Stats apply(std::string_view file, std::string_view text, std::vector<Slot>& slots, double scale,
const LogFn& log, const ExternWriter& ext = nullptr);
} // namespace game::config