main.cpp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. #include "../../game/map/campaign_loader.h"
  2. #include "../../game/map/mission_loader.h"
  3. #include <QCoreApplication>
  4. #include <QDebug>
  5. #include <QDir>
  6. #include <QFile>
  7. #include <QFileInfo>
  8. #include <QJsonDocument>
  9. #include <QJsonObject>
  10. #include <iostream>
  11. #include <set>
  12. namespace {
  13. struct ValidationResult {
  14. bool success = true;
  15. std::vector<QString> errors;
  16. std::vector<QString> warnings;
  17. void addError(const QString &error) {
  18. success = false;
  19. errors.push_back(error);
  20. }
  21. void addWarning(const QString &warning) { warnings.push_back(warning); }
  22. };
  23. auto validateMissionFile(const QString &file_path) -> ValidationResult {
  24. ValidationResult result;
  25. QFileInfo file_info(file_path);
  26. if (!file_info.exists()) {
  27. result.addError(QString("Mission file not found: %1").arg(file_path));
  28. return result;
  29. }
  30. Game::Mission::MissionDefinition mission;
  31. QString error_msg;
  32. if (!Game::Mission::MissionLoader::loadFromJsonFile(file_path, mission,
  33. &error_msg)) {
  34. result.addError(QString("Failed to parse mission %1: %2")
  35. .arg(file_path)
  36. .arg(error_msg));
  37. return result;
  38. }
  39. if (mission.id.isEmpty()) {
  40. result.addError(QString("Mission %1: missing 'id' field").arg(file_path));
  41. }
  42. if (mission.title.isEmpty()) {
  43. result.addError(
  44. QString("Mission %1: missing 'title' field").arg(file_path));
  45. }
  46. if (mission.map_path.isEmpty()) {
  47. result.addError(
  48. QString("Mission %1: missing 'map_path' field").arg(file_path));
  49. } else {
  50. QString map_path = mission.map_path;
  51. if (map_path.startsWith(":/")) {
  52. map_path = map_path.mid(2);
  53. }
  54. QString abs_map_path =
  55. QDir::currentPath() + "/" + map_path.replace("assets/", "");
  56. bool map_found = false;
  57. QStringList search_paths = {abs_map_path,
  58. QDir::currentPath() + "/assets/maps/" +
  59. QFileInfo(map_path).fileName(),
  60. mission.map_path};
  61. for (const auto &search_path : search_paths) {
  62. if (QFile::exists(search_path)) {
  63. map_found = true;
  64. break;
  65. }
  66. }
  67. if (!map_found) {
  68. result.addWarning(QString("Mission %1: referenced map '%2' not found "
  69. "(this may be OK if it's a Qt resource)")
  70. .arg(file_path)
  71. .arg(mission.map_path));
  72. }
  73. }
  74. if (mission.player_setup.nation.isEmpty()) {
  75. result.addWarning(
  76. QString("Mission %1: player_setup missing 'nation'").arg(file_path));
  77. }
  78. if (mission.victory_conditions.empty()) {
  79. result.addError(
  80. QString("Mission %1: no victory conditions defined").arg(file_path));
  81. }
  82. if (mission.defeat_conditions.empty()) {
  83. result.addWarning(
  84. QString("Mission %1: no defeat conditions defined").arg(file_path));
  85. }
  86. return result;
  87. }
  88. auto validateCampaignFile(const QString &file_path,
  89. const std::set<QString> &available_missions)
  90. -> ValidationResult {
  91. ValidationResult result;
  92. QFileInfo file_info(file_path);
  93. if (!file_info.exists()) {
  94. result.addError(QString("Campaign file not found: %1").arg(file_path));
  95. return result;
  96. }
  97. Game::Campaign::CampaignDefinition campaign;
  98. QString error_msg;
  99. if (!Game::Campaign::CampaignLoader::loadFromJsonFile(file_path, campaign,
  100. &error_msg)) {
  101. result.addError(QString("Failed to parse campaign %1: %2")
  102. .arg(file_path)
  103. .arg(error_msg));
  104. return result;
  105. }
  106. if (campaign.id.isEmpty()) {
  107. result.addError(QString("Campaign %1: missing 'id' field").arg(file_path));
  108. }
  109. if (campaign.title.isEmpty()) {
  110. result.addError(
  111. QString("Campaign %1: missing 'title' field").arg(file_path));
  112. }
  113. if (campaign.missions.empty()) {
  114. result.addError(QString("Campaign %1: no missions defined").arg(file_path));
  115. return result;
  116. }
  117. std::set<int> order_indices;
  118. for (const auto &mission : campaign.missions) {
  119. if (order_indices.count(mission.order_index) > 0) {
  120. result.addError(QString("Campaign %1: duplicate order_index %2")
  121. .arg(file_path)
  122. .arg(mission.order_index));
  123. }
  124. order_indices.insert(mission.order_index);
  125. if (!available_missions.count(mission.mission_id)) {
  126. result.addError(QString("Campaign %1: references unknown mission '%2'")
  127. .arg(file_path)
  128. .arg(mission.mission_id));
  129. }
  130. }
  131. if (!order_indices.empty()) {
  132. const int min_index = *order_indices.begin();
  133. const int max_index = *order_indices.rbegin();
  134. const int expected_count = max_index - min_index + 1;
  135. if (static_cast<int>(order_indices.size()) != expected_count) {
  136. result.addError(
  137. QString("Campaign %1: order_index values are not contiguous")
  138. .arg(file_path));
  139. }
  140. if (min_index != 0 && min_index != 1) {
  141. result.addWarning(
  142. QString("Campaign %1: order_index starts at %2 (expected 0 or 1)")
  143. .arg(file_path)
  144. .arg(min_index));
  145. }
  146. }
  147. return result;
  148. }
  149. void printResults(const ValidationResult &result, const QString &file_name) {
  150. if (!result.warnings.empty()) {
  151. for (const auto &warning : result.warnings) {
  152. std::cout << "[WARNING] " << warning.toStdString() << std::endl;
  153. }
  154. }
  155. if (!result.errors.empty()) {
  156. for (const auto &error : result.errors) {
  157. std::cerr << "[ERROR] " << error.toStdString() << std::endl;
  158. }
  159. }
  160. if (result.success && result.warnings.empty()) {
  161. std::cout << "[OK] " << file_name.toStdString() << std::endl;
  162. }
  163. }
  164. } // namespace
  165. auto main(int argc, char *argv[]) -> int {
  166. QCoreApplication app(argc, argv);
  167. if (argc < 2) {
  168. std::cerr << "Usage: content_validator <assets_directory>" << std::endl;
  169. std::cerr << " Validates all mission and campaign JSON files in the "
  170. "assets directory"
  171. << std::endl;
  172. return 1;
  173. }
  174. const QString assets_dir = argv[1];
  175. const QDir base_dir(assets_dir);
  176. if (!base_dir.exists()) {
  177. std::cerr << "Error: Assets directory not found: "
  178. << assets_dir.toStdString() << std::endl;
  179. return 1;
  180. }
  181. std::cout << "Validating content in: " << assets_dir.toStdString()
  182. << std::endl;
  183. std::cout << "========================================" << std::endl;
  184. bool all_valid = true;
  185. std::set<QString> mission_ids;
  186. const QDir missions_dir = base_dir.filePath("missions");
  187. if (missions_dir.exists()) {
  188. const QStringList mission_files =
  189. missions_dir.entryList(QStringList() << "*.json", QDir::Files);
  190. std::cout << "\nValidating " << mission_files.size() << " mission(s)..."
  191. << std::endl;
  192. for (const auto &mission_file : mission_files) {
  193. const QString mission_path = missions_dir.filePath(mission_file);
  194. const ValidationResult result = validateMissionFile(mission_path);
  195. printResults(result, QString("missions/") + mission_file);
  196. if (result.success) {
  197. Game::Mission::MissionDefinition mission;
  198. if (Game::Mission::MissionLoader::loadFromJsonFile(mission_path,
  199. mission)) {
  200. mission_ids.insert(mission.id);
  201. }
  202. } else {
  203. all_valid = false;
  204. }
  205. }
  206. } else {
  207. std::cout << "\nNo missions directory found (this is OK)" << std::endl;
  208. }
  209. const QDir campaigns_dir = base_dir.filePath("campaigns");
  210. if (campaigns_dir.exists()) {
  211. const QStringList campaign_files =
  212. campaigns_dir.entryList(QStringList() << "*.json", QDir::Files);
  213. std::cout << "\nValidating " << campaign_files.size() << " campaign(s)..."
  214. << std::endl;
  215. for (const auto &campaign_file : campaign_files) {
  216. const QString campaign_path = campaigns_dir.filePath(campaign_file);
  217. const ValidationResult result =
  218. validateCampaignFile(campaign_path, mission_ids);
  219. printResults(result, QString("campaigns/") + campaign_file);
  220. if (!result.success) {
  221. all_valid = false;
  222. }
  223. }
  224. } else {
  225. std::cout << "\nNo campaigns directory found (this is OK)" << std::endl;
  226. }
  227. std::cout << "\n========================================" << std::endl;
  228. if (all_valid) {
  229. std::cout << "✓ All content validation passed!" << std::endl;
  230. return 0;
  231. }
  232. std::cerr << "✗ Content validation failed!" << std::endl;
  233. return 1;
  234. }