troop_catalog_loader.cpp 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #include "troop_catalog_loader.h"
  2. #include "troop_catalog.h"
  3. #include "troop_config.h"
  4. #include <QCoreApplication>
  5. #include <QDir>
  6. #include <QFile>
  7. #include <QJsonArray>
  8. #include <QJsonDocument>
  9. #include <QJsonObject>
  10. #include <QJsonValue>
  11. #include <QLoggingCategory>
  12. #include <QVariant>
  13. namespace {
  14. [[nodiscard]] auto ensure_array(const QJsonValue &value) -> QJsonArray {
  15. if (value.isArray()) {
  16. return value.toArray();
  17. }
  18. return {};
  19. }
  20. [[nodiscard]] auto ensure_object(const QJsonValue &value) -> QJsonObject {
  21. if (value.isObject()) {
  22. return value.toObject();
  23. }
  24. return {};
  25. }
  26. [[nodiscard]] auto read_float(const QJsonObject &obj, const char *key,
  27. float fallback) -> float {
  28. if (!obj.contains(key)) {
  29. return fallback;
  30. }
  31. const auto value = obj.value(key);
  32. return static_cast<float>(value.toDouble(fallback));
  33. }
  34. [[nodiscard]] auto read_int(const QJsonObject &obj, const char *key,
  35. int fallback) -> int {
  36. if (!obj.contains(key)) {
  37. return fallback;
  38. }
  39. const auto value = obj.value(key);
  40. if (value.isDouble()) {
  41. return value.toInt(fallback);
  42. }
  43. if (value.isString()) {
  44. bool ok = false;
  45. const auto str = value.toString();
  46. const int result = str.toInt(&ok);
  47. return ok ? result : fallback;
  48. }
  49. return fallback;
  50. }
  51. [[nodiscard]] auto read_bool(const QJsonObject &obj, const char *key,
  52. bool fallback) -> bool {
  53. if (!obj.contains(key)) {
  54. return fallback;
  55. }
  56. return obj.value(key).toBool(fallback);
  57. }
  58. } // namespace
  59. namespace Game::Units {
  60. static constexpr const char *k_troop_list_key = "troops";
  61. static bool g_catalog_loaded = false;
  62. static auto logger() -> QLoggingCategory & {
  63. static QLoggingCategory category("TroopCatalogLoader");
  64. return category;
  65. }
  66. auto TroopCatalogLoader::resolve_data_path(const QString &relative) -> QString {
  67. const QString direct = QDir::current().filePath(relative);
  68. if (QFile::exists(direct)) {
  69. return direct;
  70. }
  71. const QString appDir = QCoreApplication::applicationDirPath();
  72. if (!appDir.isEmpty()) {
  73. const QString fromApp = QDir(appDir).filePath(relative);
  74. if (QFile::exists(fromApp)) {
  75. return fromApp;
  76. }
  77. const QString parent = QDir(appDir).filePath("../" + relative);
  78. if (QFile::exists(parent)) {
  79. return QDir(parent).canonicalPath();
  80. }
  81. }
  82. const QString resource_path = QStringLiteral(":/") + relative;
  83. if (QFile::exists(resource_path)) {
  84. return resource_path;
  85. }
  86. return {};
  87. }
  88. auto TroopCatalogLoader::load_default_catalog() -> bool {
  89. if (g_catalog_loaded) {
  90. return true;
  91. }
  92. const QString path = resolve_data_path("assets/data/troops/base.json");
  93. if (path.isEmpty()) {
  94. qCWarning(logger()) << "Failed to locate base troop catalog at"
  95. << "assets/data/troops/base.json";
  96. return false;
  97. }
  98. if (!load_from_file(path)) {
  99. return false;
  100. }
  101. g_catalog_loaded = true;
  102. return true;
  103. }
  104. auto TroopCatalogLoader::load_from_file(const QString &path) -> bool {
  105. QFile file(path);
  106. if (!file.open(QIODevice::ReadOnly)) {
  107. qCWarning(logger()) << "Unable to open troop catalog" << path << ":"
  108. << file.errorString();
  109. return false;
  110. }
  111. const QByteArray data = file.readAll();
  112. QJsonParseError parseError;
  113. const QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
  114. if (parseError.error != QJsonParseError::NoError) {
  115. qCWarning(logger()) << "Failed to parse troop catalog" << path << ":"
  116. << parseError.errorString();
  117. return false;
  118. }
  119. const QJsonObject root = doc.object();
  120. const QJsonArray troops = ensure_array(root.value(k_troop_list_key));
  121. if (troops.isEmpty()) {
  122. qCWarning(logger()) << "Troop catalog" << path
  123. << "does not contain a 'troops' array";
  124. return false;
  125. }
  126. auto &catalog = TroopCatalog::instance();
  127. catalog.clear();
  128. for (const QJsonValue &value : troops) {
  129. const QJsonObject troop_obj = ensure_object(value);
  130. const QString troop_id = troop_obj.value("id").toString();
  131. if (troop_id.isEmpty()) {
  132. qCWarning(logger()) << "Encountered troop without id in" << path;
  133. continue;
  134. }
  135. const auto type_opt = tryParseTroopType(troop_id.toStdString());
  136. if (!type_opt.has_value()) {
  137. qCWarning(logger()) << "Unknown troop type" << troop_id << "in" << path;
  138. continue;
  139. }
  140. TroopClass troop_class{};
  141. troop_class.unit_type = *type_opt;
  142. troop_class.display_name =
  143. troop_obj.value("display_name").toString(troop_id).toStdString();
  144. const QJsonObject production = ensure_object(troop_obj.value("production"));
  145. troop_class.production.cost =
  146. read_int(production, "cost", troop_class.production.cost);
  147. troop_class.production.build_time =
  148. read_float(production, "build_time", troop_class.production.build_time);
  149. troop_class.production.priority =
  150. read_int(production, "priority", troop_class.production.priority);
  151. troop_class.production.is_melee =
  152. read_bool(production, "is_melee", troop_class.production.is_melee);
  153. const QJsonObject combat = ensure_object(troop_obj.value("combat"));
  154. troop_class.combat.health =
  155. read_int(combat, "health", troop_class.combat.health);
  156. troop_class.combat.max_health =
  157. read_int(combat, "max_health", troop_class.combat.max_health);
  158. troop_class.combat.speed =
  159. read_float(combat, "speed", troop_class.combat.speed);
  160. troop_class.combat.vision_range =
  161. read_float(combat, "vision_range", troop_class.combat.vision_range);
  162. troop_class.combat.ranged_range =
  163. read_float(combat, "ranged_range", troop_class.combat.ranged_range);
  164. troop_class.combat.ranged_damage =
  165. read_int(combat, "ranged_damage", troop_class.combat.ranged_damage);
  166. troop_class.combat.ranged_cooldown = read_float(
  167. combat, "ranged_cooldown", troop_class.combat.ranged_cooldown);
  168. troop_class.combat.melee_range =
  169. read_float(combat, "melee_range", troop_class.combat.melee_range);
  170. troop_class.combat.melee_damage =
  171. read_int(combat, "melee_damage", troop_class.combat.melee_damage);
  172. troop_class.combat.melee_cooldown =
  173. read_float(combat, "melee_cooldown", troop_class.combat.melee_cooldown);
  174. troop_class.combat.can_ranged =
  175. read_bool(combat, "can_ranged", troop_class.combat.can_ranged);
  176. troop_class.combat.can_melee =
  177. read_bool(combat, "can_melee", troop_class.combat.can_melee);
  178. troop_class.combat.max_stamina =
  179. read_float(combat, "max_stamina", troop_class.combat.max_stamina);
  180. troop_class.combat.stamina_regen_rate = read_float(
  181. combat, "stamina_regen_rate", troop_class.combat.stamina_regen_rate);
  182. troop_class.combat.stamina_depletion_rate =
  183. read_float(combat, "stamina_depletion_rate",
  184. troop_class.combat.stamina_depletion_rate);
  185. const QJsonObject visuals = ensure_object(troop_obj.value("visuals"));
  186. troop_class.visuals.render_scale =
  187. read_float(visuals, "render_scale", troop_class.visuals.render_scale);
  188. troop_class.visuals.selection_ring_size =
  189. read_float(visuals, "selection_ring_size",
  190. troop_class.visuals.selection_ring_size);
  191. troop_class.visuals.selection_ring_y_offset =
  192. read_float(visuals, "selection_ring_y_offset",
  193. troop_class.visuals.selection_ring_y_offset);
  194. troop_class.visuals.selection_ring_ground_offset =
  195. read_float(visuals, "selection_ring_ground_offset",
  196. troop_class.visuals.selection_ring_ground_offset);
  197. const QString default_renderer = QStringLiteral("troops/") + troop_id;
  198. troop_class.visuals.renderer_id =
  199. visuals.value("renderer_id").toString(default_renderer).toStdString();
  200. const QJsonObject formation = ensure_object(troop_obj.value("formation"));
  201. troop_class.individuals_per_unit = read_int(
  202. formation, "individuals_per_unit", troop_class.individuals_per_unit);
  203. troop_class.max_units_per_row =
  204. read_int(formation, "max_units_per_row", troop_class.max_units_per_row);
  205. catalog.register_class(std::move(troop_class));
  206. }
  207. TroopConfig::instance().refresh_from_catalog();
  208. return true;
  209. }
  210. } // namespace Game::Units