// 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 #include #include #include #include #include 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; using ExternWriter = std::function; // ---- 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 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& slots, double scale, const LogFn& log, const ExternWriter& ext = nullptr); } // namespace game::config