nation_loader.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. #include "nation_loader.h"
  2. #include "../units/building_type.h"
  3. #include "../units/troop_catalog.h"
  4. #include "../units/troop_type.h"
  5. #include "nation_id.h"
  6. #include "nation_registry.h"
  7. #include <QCoreApplication>
  8. #include <QDir>
  9. #include <QDirIterator>
  10. #include <QFile>
  11. #include <QJsonArray>
  12. #include <QJsonDocument>
  13. #include <QJsonObject>
  14. #include <QLoggingCategory>
  15. namespace {
  16. using Game::Systems::FormationType;
  17. using Game::Systems::Nation;
  18. using Game::Systems::NationLoader;
  19. using Game::Systems::NationTroopVariant;
  20. using Game::Systems::TroopType;
  21. using Game::Units::TroopCatalog;
  22. using Game::Units::TroopClass;
  23. [[nodiscard]] auto ensure_object(const QJsonValue &value) -> QJsonObject {
  24. if (value.isObject()) {
  25. return value.toObject();
  26. }
  27. return {};
  28. }
  29. [[nodiscard]] auto ensure_array(const QJsonValue &value) -> QJsonArray {
  30. if (value.isArray()) {
  31. return value.toArray();
  32. }
  33. return {};
  34. }
  35. [[nodiscard]] auto read_string(const QJsonObject &obj, const char *key,
  36. const QString &fallback) -> QString {
  37. const auto value = obj.value(key);
  38. if (value.isString()) {
  39. return value.toString();
  40. }
  41. return fallback;
  42. }
  43. [[nodiscard]] auto read_float_opt(const QJsonObject &obj,
  44. const char *key) -> std::optional<float> {
  45. if (!obj.contains(key)) {
  46. return std::nullopt;
  47. }
  48. const auto value = obj.value(key);
  49. return static_cast<float>(value.toDouble());
  50. }
  51. [[nodiscard]] auto read_int_opt(const QJsonObject &obj,
  52. const char *key) -> std::optional<int> {
  53. if (!obj.contains(key)) {
  54. return std::nullopt;
  55. }
  56. const auto value = obj.value(key);
  57. if (value.isDouble()) {
  58. return value.toInt();
  59. }
  60. if (value.isString()) {
  61. bool ok = false;
  62. const auto str = value.toString();
  63. const int parsed = str.toInt(&ok);
  64. if (ok) {
  65. return parsed;
  66. }
  67. }
  68. return std::nullopt;
  69. }
  70. [[nodiscard]] auto read_bool(const QJsonObject &obj, const char *key,
  71. bool fallback) -> bool {
  72. if (!obj.contains(key)) {
  73. return fallback;
  74. }
  75. return obj.value(key).toBool(fallback);
  76. }
  77. [[nodiscard]] auto read_bool_opt(const QJsonObject &obj,
  78. const char *key) -> std::optional<bool> {
  79. if (!obj.contains(key)) {
  80. return std::nullopt;
  81. }
  82. return obj.value(key).toBool();
  83. }
  84. [[nodiscard]] auto
  85. parse_formation_type(const QString &value) -> std::optional<FormationType> {
  86. const QString lowered = value.trimmed().toLower();
  87. if (lowered == QStringLiteral("roman")) {
  88. return FormationType::Roman;
  89. }
  90. if (lowered == QStringLiteral("barbarian")) {
  91. return FormationType::Barbarian;
  92. }
  93. if (lowered == QStringLiteral("carthage")) {
  94. return FormationType::Carthage;
  95. }
  96. return std::nullopt;
  97. }
  98. [[nodiscard]] auto logger() -> QLoggingCategory & {
  99. static QLoggingCategory category("NationLoader");
  100. return category;
  101. }
  102. static constexpr const char *k_nation_troops_key = "troops";
  103. static auto nation_loader_logger() -> QLoggingCategory & { return logger(); }
  104. [[nodiscard]] auto build_troop_entry(const QJsonObject &obj,
  105. Nation &nation) -> bool {
  106. const QString troop_id = obj.value("id").toString();
  107. if (troop_id.isEmpty()) {
  108. qCWarning(logger()) << "Encountered troop without id in nation"
  109. << nation_id_to_qstring(nation.id);
  110. return false;
  111. }
  112. const auto type_opt = Game::Units::tryParseTroopType(troop_id.toStdString());
  113. if (!type_opt.has_value()) {
  114. qCWarning(logger()) << "Unknown troop type" << troop_id << "for nation"
  115. << nation_id_to_qstring(nation.id);
  116. return false;
  117. }
  118. const Game::Units::TroopType troop_type = *type_opt;
  119. const TroopClass &base_class =
  120. TroopCatalog::instance().get_class_or_fallback(troop_type);
  121. TroopType entry{};
  122. entry.unit_type = troop_type;
  123. entry.display_name =
  124. read_string(obj, "display_name",
  125. QString::fromStdString(base_class.display_name))
  126. .toStdString();
  127. entry.is_melee = read_bool(ensure_object(obj.value("production")), "is_melee",
  128. base_class.production.is_melee);
  129. const QJsonObject production = ensure_object(obj.value("production"));
  130. entry.cost = production.value("cost").toInt(base_class.production.cost);
  131. entry.build_time =
  132. static_cast<float>(production.value("build_time")
  133. .toDouble(base_class.production.build_time));
  134. entry.priority =
  135. production.value("priority").toInt(base_class.production.priority);
  136. nation.available_troops.push_back(entry);
  137. NationTroopVariant variant{};
  138. variant.unit_type = troop_type;
  139. bool has_variant = false;
  140. const QJsonObject combat = ensure_object(obj.value("combat"));
  141. if (auto value = read_int_opt(combat, "health")) {
  142. variant.health = value;
  143. has_variant = true;
  144. }
  145. if (auto value = read_int_opt(combat, "max_health")) {
  146. variant.max_health = value;
  147. has_variant = true;
  148. }
  149. if (auto value = read_float_opt(combat, "speed")) {
  150. variant.speed = value;
  151. has_variant = true;
  152. }
  153. if (auto value = read_float_opt(combat, "vision_range")) {
  154. variant.vision_range = value;
  155. has_variant = true;
  156. }
  157. if (auto value = read_int_opt(combat, "ranged_damage")) {
  158. variant.attack_damage = value;
  159. has_variant = true;
  160. }
  161. if (auto value = read_float_opt(combat, "ranged_range")) {
  162. variant.attack_range = value;
  163. has_variant = true;
  164. }
  165. if (auto value = read_float_opt(combat, "ranged_cooldown")) {
  166. variant.attack_cooldown = value;
  167. has_variant = true;
  168. }
  169. if (auto value = read_int_opt(combat, "melee_damage")) {
  170. variant.melee_damage = value;
  171. has_variant = true;
  172. }
  173. if (auto value = read_float_opt(combat, "melee_range")) {
  174. variant.melee_range = value;
  175. has_variant = true;
  176. }
  177. if (auto value = read_float_opt(combat, "melee_cooldown")) {
  178. variant.melee_cooldown = value;
  179. has_variant = true;
  180. }
  181. if (auto value = read_bool_opt(combat, "can_ranged")) {
  182. variant.can_ranged = value;
  183. has_variant = true;
  184. }
  185. if (auto value = read_bool_opt(combat, "can_melee")) {
  186. variant.can_melee = value;
  187. has_variant = true;
  188. }
  189. if (auto value = read_float_opt(combat, "max_stamina")) {
  190. variant.max_stamina = value;
  191. has_variant = true;
  192. }
  193. if (auto value = read_float_opt(combat, "stamina_regen_rate")) {
  194. variant.stamina_regen_rate = value;
  195. has_variant = true;
  196. }
  197. if (auto value = read_float_opt(combat, "stamina_depletion_rate")) {
  198. variant.stamina_depletion_rate = value;
  199. has_variant = true;
  200. }
  201. const QJsonObject visuals = ensure_object(obj.value("visuals"));
  202. if (auto value = read_float_opt(visuals, "selection_ring_size")) {
  203. variant.selection_ring_size = value;
  204. has_variant = true;
  205. }
  206. if (auto value = read_float_opt(visuals, "selection_ring_y_offset")) {
  207. variant.selection_ring_y_offset = value;
  208. has_variant = true;
  209. }
  210. if (auto value = read_float_opt(visuals, "selection_ring_ground_offset")) {
  211. variant.selection_ring_ground_offset = value;
  212. has_variant = true;
  213. }
  214. if (visuals.contains("renderer_id")) {
  215. variant.renderer_id = visuals.value("renderer_id").toString().toStdString();
  216. has_variant = true;
  217. }
  218. if (auto value = read_float_opt(visuals, "render_scale")) {
  219. variant.render_scale = value;
  220. has_variant = true;
  221. }
  222. const QJsonObject formation = ensure_object(obj.value("formation"));
  223. if (auto value = read_int_opt(formation, "individuals_per_unit")) {
  224. variant.individuals_per_unit = value;
  225. has_variant = true;
  226. }
  227. if (auto value = read_int_opt(formation, "max_units_per_row")) {
  228. variant.max_units_per_row = value;
  229. has_variant = true;
  230. }
  231. if (auto formation_override =
  232. parse_formation_type(obj.value("formation_type").toString())) {
  233. variant.formation_type = formation_override;
  234. has_variant = true;
  235. }
  236. if (has_variant) {
  237. nation.troop_variants[troop_type] = std::move(variant);
  238. }
  239. return true;
  240. }
  241. } // namespace
  242. namespace Game::Systems {
  243. auto NationLoader::resolve_data_path(const QString &relative) -> QString {
  244. const QString direct = QDir::current().filePath(relative);
  245. if (QFile::exists(direct)) {
  246. return direct;
  247. }
  248. const QString app_dir = QCoreApplication::applicationDirPath();
  249. if (!app_dir.isEmpty()) {
  250. const QString from_app = QDir(app_dir).filePath(relative);
  251. if (QFile::exists(from_app)) {
  252. return from_app;
  253. }
  254. const QString parent = QDir(app_dir).filePath("../" + relative);
  255. if (QFile::exists(parent)) {
  256. return QDir(parent).canonicalPath();
  257. }
  258. }
  259. const QString resource_path = QStringLiteral(":/") + relative;
  260. if (QFile::exists(resource_path)) {
  261. return resource_path;
  262. }
  263. return {};
  264. }
  265. auto NationLoader::load_default_nations() -> std::vector<Nation> {
  266. const QString dir = resolve_data_path("assets/data/nations");
  267. if (dir.isEmpty()) {
  268. qCWarning(nation_loader_logger())
  269. << "Failed to locate assets/data/nations directory";
  270. return {};
  271. }
  272. return load_from_directory(dir);
  273. }
  274. auto NationLoader::load_from_directory(const QString &directory)
  275. -> std::vector<Nation> {
  276. std::vector<Nation> nations;
  277. QDir dir(directory);
  278. if (!dir.exists()) {
  279. qCWarning(nation_loader_logger())
  280. << "Nation directory does not exist" << directory;
  281. return nations;
  282. }
  283. QDirIterator it(directory, QStringList{QStringLiteral("*.json")},
  284. QDir::Files | QDir::Readable);
  285. while (it.hasNext()) {
  286. const QString file_path = it.next();
  287. if (auto nation = load_from_file(file_path)) {
  288. nations.push_back(std::move(*nation));
  289. }
  290. }
  291. return nations;
  292. }
  293. auto NationLoader::load_from_file(const QString &path)
  294. -> std::optional<Nation> {
  295. QFile file(path);
  296. if (!file.open(QIODevice::ReadOnly)) {
  297. qCWarning(nation_loader_logger()) << "Unable to open nation definition"
  298. << path << ":" << file.errorString();
  299. return std::nullopt;
  300. }
  301. const QByteArray data = file.readAll();
  302. QJsonParseError parse_error;
  303. const QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error);
  304. if (parse_error.error != QJsonParseError::NoError) {
  305. qCWarning(nation_loader_logger())
  306. << "Failed to parse nation" << path << ":" << parse_error.errorString();
  307. return std::nullopt;
  308. }
  309. const QJsonObject root = doc.object();
  310. Nation nation{};
  311. const QString id_str = root.value("id").toString();
  312. if (id_str.isEmpty()) {
  313. qCWarning(nation_loader_logger())
  314. << "Nation file" << path << "is missing 'id'";
  315. return std::nullopt;
  316. }
  317. auto parsed_id = Game::Systems::nation_id_from_string(id_str.toStdString());
  318. if (!parsed_id) {
  319. qCWarning(nation_loader_logger())
  320. << "Nation file" << path << "has unknown nation id:" << id_str;
  321. return std::nullopt;
  322. }
  323. nation.id = *parsed_id;
  324. nation.display_name =
  325. root.value("display_name").toString(id_str).toStdString();
  326. const QString building_str =
  327. root.value("primary_building").toString(QStringLiteral("barracks"));
  328. auto parsed_building =
  329. Game::Units::buildingTypeFromString(building_str.toStdString());
  330. nation.primary_building =
  331. parsed_building.value_or(Game::Units::BuildingType::Barracks);
  332. if (auto formation =
  333. parse_formation_type(root.value("formation_type").toString())) {
  334. nation.formation_type = *formation;
  335. }
  336. const QJsonArray troops = ensure_array(root.value(k_nation_troops_key));
  337. for (const auto &value : troops) {
  338. const QJsonObject troop_obj = ensure_object(value);
  339. if (!build_troop_entry(troop_obj, nation)) {
  340. qCWarning(nation_loader_logger())
  341. << "Failed to load troop entry in nation" << path;
  342. }
  343. }
  344. return nation;
  345. }
  346. } // namespace Game::Systems