#include "game/data.hpp" #include "engine/script.hpp" #include #include #include // sol2's string-literal table keys trip a GCC -Warray-bounds false positive // under inlining/optimisation. The access is correct; silence the noise here. #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic ignored "-Warray-bounds" #endif namespace fs = std::filesystem; namespace { constexpr float DEG2RAD = 3.14159265358979323846f / 180.0f; MaterialDef read_material(const sol::table& m) { MaterialDef d{}; d.name = m.get_or("name", std::string("unnamed")); d.stiffness = m.get_or("stiffness", 600.0f); d.damping = m.get_or("damping", 12.0f); d.mass = m.get_or("mass", 0.25f); d.max_compression = m.get_or("max_compression", 0.90f); d.max_expansion = m.get_or("max_expansion", 1.10f); d.angle_threshold = m.get_or("angle_threshold", 30.0f) * DEG2RAD; // data is degrees d.min_length = m.get_or("min_length", 0.4f); d.max_length = m.get_or("max_length", 3.0f); d.max_link_length = m.get_or("max_link_length", 6.0f); d.hit_points = m.get_or("hit_points", 150.0f); d.flammable = m.get_or("flammable", true); d.burn_rate = m.get_or("burn_rate", 15.0f); d.spread_time = m.get_or("spread_time", 2.0f); d.blocks_projectiles = m.get_or("blocks_projectiles", true); d.blocks_beam = m.get_or("blocks_beam", true); d.tension_only = m.get_or("tension_only", false); d.half_width = m.get_or("half_width", 0.15f); // Defaults match the flat costs struts used before they became per-material. d.cost_metal = m.get_or("cost_metal", 5.0f); d.cost_energy = m.get_or("cost_energy", 25.0f); d.texture = m.get_or("texture", std::string("")); d.texture_forts = m.get_or("texture_forts", std::string("")); sol::optional col = m["color"]; d.r = col ? col->get_or(1, 0.8f) : 0.8f; d.g = col ? col->get_or(2, 0.5f) : 0.5f; d.b = col ? col->get_or(3, 0.2f) : 0.2f; d.a = col ? col->get_or(4, 1.0f) : 1.0f; return d; } } // namespace std::vector load_materials(ScriptEngine& script, const std::string& data_dir) { std::vector out; // 1. Base data. if (!script.run_file(data_dir + "/materials.lua")) return out; // 2. Mods, ordered by (priority asc, path). Each mod.lua sets Priority; // each materials.lua mutates the shared global Materials table. std::error_code ec; std::string mods_dir = data_dir + "/mods"; std::vector> mods; if (fs::is_directory(mods_dir, ec)) { for (const auto& e : fs::directory_iterator(mods_dir, ec)) { if (!e.is_directory()) continue; std::string mp = e.path().string(); if (!fs::exists(mp + "/materials.lua")) continue; int priority = 5; if (fs::exists(mp + "/mod.lua") && script.run_file(mp + "/mod.lua")) { sol::optional p = script.lua()["Priority"]; if (p) priority = *p; } mods.emplace_back(priority, mp); } std::sort(mods.begin(), mods.end()); for (const auto& [prio, mp] : mods) { printf("Mod: applying %s (priority %d)\n", mp.c_str(), prio); script.run_file(mp + "/materials.lua"); } } // 3. Read the final Materials array (1-based Lua array part, in order). sol::optional mats = script.lua()["Materials"]; if (!mats) { fprintf(stderr, "load_materials: no global 'Materials' table\n"); return out; } for (std::size_t i = 1; ; i++) { sol::object o = (*mats)[i]; if (o.get_type() != sol::type::table) break; out.push_back(read_material(o.as())); } printf("Loaded %zu materials from Lua\n", out.size()); return out; } namespace { // Run / then every mod's (priority, name order). void run_layered(ScriptEngine& script, const std::string& data_dir, const std::string& file) { if (!script.run_file(data_dir + "/" + file)) return; std::error_code ec; std::string mods_dir = data_dir + "/mods"; std::vector> mods; if (fs::is_directory(mods_dir, ec)) { for (const auto& e : fs::directory_iterator(mods_dir, ec)) { if (!e.is_directory()) continue; std::string mp = e.path().string(); if (!fs::exists(mp + "/" + file)) continue; int priority = 5; if (fs::exists(mp + "/mod.lua") && script.run_file(mp + "/mod.lua")) { sol::optional p = script.lua()["Priority"]; if (p) priority = *p; } mods.emplace_back(priority, mp); } std::sort(mods.begin(), mods.end()); for (const auto& [prio, mp] : mods) script.run_file(mp + "/" + file); } } WeaponDef read_weapon(const sol::table& w) { WeaponDef d{}; d.name = w.get_or("name", std::string("unnamed")); d.muzzle_speed = w.get_or("muzzle_speed", 35.0f); d.reload = w.get_or("reload", 2.0f); d.splash_damage = w.get_or("splash_damage", 200.0f); d.splash_radius = w.get_or("splash_radius", 3.0f); d.knockback = w.get_or("knockback", 60.0f); d.projectile_radius = w.get_or("projectile_radius", 0.15f); d.fire_chance = w.get_or("fire_chance", 0.0f); d.min_fire_angle = w.get_or("min_fire_angle", -20.0f) * DEG2RAD; d.max_fire_angle = w.get_or("max_fire_angle", 30.0f) * DEG2RAD; d.is_beam = w.get_or("is_beam", false); d.beam_range = w.get_or("beam_range", 60.0f); d.beam_dps = w.get_or("beam_dps", 120.0f); d.beam_duration = w.get_or("beam_duration", 1.5f); d.energy_cost = w.get_or("energy_cost", 0.0f); d.recoil = w.get_or("recoil", 0.0f); d.texture = w.get_or("texture", std::string("")); d.projectile_texture = w.get_or("projectile_texture", std::string("")); d.texture_forts = w.get_or("texture_forts", std::string("")); d.projectile_texture_forts = w.get_or("projectile_texture_forts", std::string("")); return d; } } // namespace std::vector load_weapons(ScriptEngine& script, const std::string& data_dir) { std::vector out; run_layered(script, data_dir, "weapons.lua"); sol::optional ws = script.lua()["Weapons"]; if (!ws) { fprintf(stderr, "load_weapons: no global 'Weapons' table\n"); return out; } for (std::size_t i = 1; ; i++) { sol::object o = (*ws)[i]; if (o.get_type() != sol::type::table) break; out.push_back(read_weapon(o.as())); } printf("Loaded %zu weapons from Lua\n", out.size()); return out; } void build_map(const MapDef& m, BuildGraph& graph) { graph.nodes.clear(); graph.edges.clear(); // Clearing the vectors does NOT touch the private adjacency and edge-key // caches, so rebuild them now. Skipping this leaves the PREVIOUS map's edge // keys in the set and add_edge then refuses the new map's beams as // duplicates, quietly producing a fort that collapses on load. graph.rebuild(); // Set the ground BEFORE welding: node_at asks is_on_ground() to decide which // endpoints are pinned foundations. graph.ground_level = m.ground_level; auto mat_index = [&](const std::string& nm) -> int { for (int i = 0; i < (int)graph.materials.size(); i++) if (graph.materials[i].name == nm) return i; return 0; }; auto node_at = [&](float x, float y) -> int { int n = graph.find_nearest_node(x, y, BuildGraph::SNAP_RADIUS); return (n >= 0) ? n : graph.add_node(x, y, graph.is_on_ground(y)); }; int built = 0; for (const auto& b : m.beams) { int na = node_at(b.x1, b.y1), nb = node_at(b.x2, b.y2); if (graph.add_edge(na, nb, mat_index(b.material)) >= 0) { built++; continue; } // A dropped beam silently weakens the fort, so never fail quietly: // endpoints closer than SNAP_RADIUS collapse onto one node, and a // repeated node pair is refused as a duplicate. fprintf(stderr, "map '%s': beam (%g,%g)-(%g,%g) [%s] rejected -> nodes %d,%d%s\n", m.name.c_str(), b.x1, b.y1, b.x2, b.y2, b.material.c_str(), na, nb, na == nb ? " (endpoints snapped together)" : " (duplicate)"); } for (auto& e : graph.edges) e.settle = 0.0f; // prebuilt = already cured graph.rebuild(); printf("Map '%s': %d prebuilt struts, ground %.1f\n", m.name.c_str(), built, m.ground_level); } namespace { DeviceDef read_device(const sol::table& d) { DeviceDef v{}; v.name = d.get_or("name", std::string("device")); v.hp = d.get_or("hp", 100.0f); v.energy_rate = d.get_or("energy_rate", 0.0f); v.metal_rate = d.get_or("metal_rate", 0.0f); v.energy_storage = d.get_or("energy_storage", 0.0f); v.metal_storage = d.get_or("metal_storage", 0.0f); v.needs_deposit = d.get_or("needs_deposit", false); v.wind = d.get_or("wind", false); v.wind_max_rate = d.get_or("wind_max_rate", 15.0f); v.wind_max_height = d.get_or("wind_max_height", 20.0f); v.is_core = d.get_or("is_core", false); v.half_w = d.get_or("half_w", 0.6f); v.half_h = d.get_or("half_h", 0.6f); v.cost_metal = d.get_or("cost_metal", 0.0f); v.cost_energy = d.get_or("cost_energy", 0.0f); v.texture = d.get_or("texture", std::string("")); v.texture_forts = d.get_or("texture_forts", std::string("")); sol::optional col = d["color"]; v.r = col ? col->get_or(1, 0.8f) : 0.8f; v.g = col ? col->get_or(2, 0.8f) : 0.8f; v.b = col ? col->get_or(3, 0.8f) : 0.8f; return v; } } // namespace std::vector load_devices(ScriptEngine& script, const std::string& data_dir) { std::vector out; run_layered(script, data_dir, "devices.lua"); sol::optional ds = script.lua()["DeviceDefs"]; if (!ds) { fprintf(stderr, "load_devices: no 'DeviceDefs' table\n"); return out; } for (std::size_t i = 1; ; i++) { sol::object o = (*ds)[i]; if (o.get_type() != sol::type::table) break; out.push_back(read_device(o.as())); } printf("Loaded %zu devices from Lua\n", out.size()); return out; } namespace { MapDef read_map(const sol::table& M) { MapDef m; m.name = M.get_or("name", std::string("unnamed")); m.description = M.get_or("description", std::string("")); m.ground_level = M.get_or("ground_level", 1.0f); if (sol::optional dep = M["deposits"]) { for (std::size_t i = 1; ; i++) { sol::object o = (*dep)[i]; if (o.get_type() != sol::type::table) break; sol::table p = o.as(); m.deposits.emplace_back(p.get_or(1, 0.0f), p.get_or(2, 0.0f)); } } if (sol::optional dv = M["devices"]) { for (std::size_t i = 1; ; i++) { sol::object o = (*dv)[i]; if (o.get_type() != sol::type::table) break; sol::table d = o.as(); m.devices.push_back({ d.get_or("type", std::string("reactor")), d.get_or("x", 0.0f), d.get_or("y", 0.0f), d.get_or("team", 0) }); } } if (sol::optional wp = M["weapons"]) { for (std::size_t i = 1; ; i++) { sol::object o = (*wp)[i]; if (o.get_type() != sol::type::table) break; sol::table w = o.as(); m.weapons.push_back({ w.get_or("type", std::string("cannon")), w.get_or("x", 0.0f), w.get_or("y", 0.0f) }); } } // structures[] -> beams[] {x1,y1,x2,y2,material}, flattened: which fort a // beam belonged to does not matter once it is welded into the graph. if (sol::optional st = M["structures"]) { for (std::size_t si = 1; ; si++) { sol::object so = (*st)[si]; if (so.get_type() != sol::type::table) break; sol::table S = so.as(); sol::optional beams = S["beams"]; if (!beams) continue; for (std::size_t bi = 1; ; bi++) { sol::object bo = (*beams)[bi]; if (bo.get_type() != sol::type::table) break; sol::table b = bo.as(); m.beams.push_back({ b.get_or(1, 0.0f), b.get_or(2, 0.0f), b.get_or(3, 0.0f), b.get_or(4, 0.0f), b.get_or(5, std::string("wood")) }); } } } return m; } } // namespace std::vector load_maps(ScriptEngine& script, const std::string& data_dir) { std::vector out; run_layered(script, data_dir, "maps.lua"); sol::optional maps = script.lua()["Maps"]; if (!maps) { fprintf(stderr, "load_maps: no global 'Maps' table\n"); return out; } for (std::size_t i = 1; ; i++) { sol::object o = (*maps)[i]; if (o.get_type() != sol::type::table) break; out.push_back(read_map(o.as())); } printf("Loaded %zu maps\n", out.size()); return out; }