GCC Code Coverage Report


Directory: ./
File: src/merger/main.cpp
Date: 2026-04-01 15:09:43
Exec Total Coverage
Lines: 19 118 16.1%
Functions: 1 5 20.0%
Branches: 41 335 12.2%

Line Branch Exec Source
1 #include <algorithm>
2 #include <string>
3 #include <fstream>
4 #include <vector>
5 #include <filesystem>
6 #include <fmt/base.h>
7 #include <fmt/format.h>
8 #include <spdlog/spdlog.h>
9 #include <CLI/CLI.hpp>
10 #include <nlohmann/json.hpp>
11
12 #if defined(__GNUC__) && !defined(__clang__)
13 // Disable a false positive warning which is a bug in the compiler's static analysis
14 #pragma GCC diagnostic push
15 #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
16 #endif
17 #include <regex>
18
19 #include <internal_use_only/config.hpp>
20 #include "merger.hpp"
21
22 /**
23 * @brief Command line options for the mesh merger application
24 *
25 * This structure holds all the command line parameters that can be passed
26 * to the application to control its behavior.
27 */
28 struct ProgramOptions {
29 std::filesystem::path input_folder; ///< Input directory with cliff/ramp obj meshes
30 std::filesystem::path output_folder; ///< Output folder for merged meshes and JSON files
31 std::string filename_pattern; ///< Regex template for mesh filenames
32 std::string geoset_name; ///< Input geoset name
33 std::optional<float> scale_factor; ///< Optional mesh scale factor
34 bool is_append = false; ///< Append keys to existing JSON file
35 bool is_ramp = false; ///< Process ramp geoset instead of cliff
36 bool is_verbose = false; ///< Enable verbose output
37 };
38
39 /**
40 * @brief Add a group entry to the JSON document
41 *
42 * Adds a mapping between a group index and its corresponding geokey
43 * to the JSON document.
44 *
45 * @param doc Reference to the JSON document to update
46 * @param group_idx Index of the group
47 * @param geo_key Geometry key for the group
48 */
49 static inline void add_json_group(
50 nlohmann::ordered_json& doc,
51 size_t group_idx,
52 uint32_t geo_key)
53 {
54 doc[std::to_string(group_idx)] = std::to_string(geo_key);
55 }
56
57 /**
58 * @brief Apply transformations to a mesh
59 *
60 * @param mesh_data Reference to the mesh data to transform
61 * @param popt Program options containing transformation parameters
62 */
63 static void transform_mesh(assmpq::merger::MeshData& mesh_data, const ProgramOptions& popt)
64 {
65 transform_mesh_to_base_xz(mesh_data);
66
67 if (popt.scale_factor.has_value()) {
68 scale_mesh(mesh_data, popt.scale_factor.value());
69 }
70 }
71
72 /**
73 * @brief Process a mesh and add it to the appropriate group
74 *
75 * Processes either a ramp or cliff mesh, splitting ramp meshes into two parts
76 * if needed, and adds the mesh(es) to the mesh group with appropriate geometry keys.
77 *
78 * @param all_mesh_group Reference to the collection of all mesh groups
79 * @param mesh_data Reference to the mesh data to process
80 * @param json_groups Reference to the JSON groups document to update
81 * @param extracted_geo_name The extracted geometry name from the file
82 * @param popt Program options controlling processing behavior
83 */
84 static void process_mesh(
85 assmpq::merger::MeshGroups& all_mesh_group,
86 assmpq::merger::MeshData& mesh_data,
87 nlohmann::ordered_json& json_groups,
88 const std::string& extracted_geo_name,
89 const ProgramOptions& popt)
90 {
91 if (popt.is_ramp) { // process ramp meshes
92 assmpq::merger::MeshData mesh_data0;
93 assmpq::merger::MeshData mesh_data1;
94
95 if (split_ramp_mesh(mesh_data, mesh_data0, mesh_data1)) {
96 // first group
97 all_mesh_group.push_back(mesh_data0);
98 const uint32_t geo_key0 = assmpq::merger::get_ramp_key_from_geo_name(extracted_geo_name, 0);
99 add_json_group(json_groups, all_mesh_group.size() - 1, geo_key0);
100
101 // second group
102 all_mesh_group.push_back(mesh_data1);
103 const uint32_t geo_key1 = assmpq::merger::get_ramp_key_from_geo_name(extracted_geo_name, 1);
104 add_json_group(json_groups, all_mesh_group.size() - 1, geo_key1);
105 } else {
106 spdlog::error("Failed to split ramp geo: {}", extracted_geo_name);
107 return;
108 }
109 } else { // process cliff mesh
110 const uint32_t geo_key = assmpq::merger::get_cliff_key_from_geo_name(extracted_geo_name);
111
112 all_mesh_group.push_back(mesh_data);
113 add_json_group(json_groups, all_mesh_group.size() - 1, geo_key);
114
115 if (popt.is_verbose) {
116 spdlog::info("Append cliff mesh {} (key:{}, vertices:{}, normals:{}, uvs:{}, triangles:{}, geo:{})\n",
117 all_mesh_group.size(), geo_key,
118 mesh_data.vertices.size(), mesh_data.normals.size(), mesh_data.uvs.size(),
119 mesh_data.faces.size(), extracted_geo_name);
120 }
121 }
122 }
123
124 2 auto main(int argc, char* argv[])-> int
125 try {
126 2 const auto app_description = fmt::format(
127 "{} version {} merger\n"
128 "* Processes cliff or ramp meshes from OBJ files, applies transformations,\n"
129 "* splits ramp meshes, and generates a merged multigroup mesh with corresponding\n"
130 "* JSON metadata files containing geometry keys.\n",
131 2 assets_mpq_importer::cmake::project_name, assets_mpq_importer::cmake::project_version);
132
133
2/4
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
6 CLI::App app { app_description };
134 2 ProgramOptions popt;
135
136
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
✓ Branch 13 taken 2 times.
✗ Branch 14 not taken.
10 app.set_version_flag("-v,--version", app_description);
137
138
3/6
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
16 app.add_option("-i,--input", popt.input_folder, "Input directory with cliff/ramp obj meshes.")
139 6 ->required()
140
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
6 ->check(CLI::ExistingDirectory);
141
142
3/6
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 7 taken 2 times.
✗ Branch 8 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
16 app.add_option("-o,--output", popt.output_folder, "Output folder.")
143
3/6
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
✓ Branch 11 taken 2 times.
✗ Branch 12 not taken.
✓ Branch 14 taken 2 times.
✗ Branch 15 not taken.
10 ->check(CLI::ExistingDirectory);
144
145
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_option("-p,--pattern", popt.filename_pattern, "Regex template for mesh filenames. "
146 "Used to obtain a filename key part: CityCliffsBABC0.mdx -> BABC0.");
147
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_option("-n,--name", popt.geoset_name, "Output geoset name.");
148
149
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_option("-s,--scale", popt.scale_factor, "Mesh scale factor.");
150
151
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_flag("-a,--append", popt.is_append, "Append keys to existing JSON file.");
152
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_flag("-r,--ramp", popt.is_ramp, "The input folder contains ramp geoset.");
153
3/6
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 9 taken 2 times.
✗ Branch 10 not taken.
✓ Branch 12 taken 2 times.
✗ Branch 13 not taken.
10 app.add_flag("--verbose", popt.is_verbose, "Enable verbose output.");
154
155
3/6
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✓ Branch 15 taken 2 times.
✗ Branch 16 not taken.
2 CLI11_PARSE(app, argc, argv);
156
157 nlohmann::ordered_json output_json_doc;
158
159 const std::string output_json_filename = std::format("{}_keys.json", popt.geoset_name);
160
161 spdlog::info("Process folder: {}", popt.input_folder.string());
162
163 auto output_json_path = popt.output_folder / output_json_filename;
164
165 if (popt.is_append) { // read an existing JSON file
166 std::ifstream input_json_file(output_json_path);
167 if (!input_json_file.is_open()) {
168 spdlog::error("Error: Could not open JSON file: {}", output_json_path.string());
169 return 1;
170 }
171
172 try {
173 input_json_file >> output_json_doc;
174 } catch (const nlohmann::json::exception& e) {
175 spdlog::error("JSON parse error: {}", e.what());
176 return 1;
177 }
178 }
179
180 // pattern example: CityCliffsBABC0.mdx: "CityCliffs([a-zA-Z0-9]{5})\." -> BABC0
181 const std::regex match_regex(popt.filename_pattern, std::regex::icase);
182 std::smatch matches;
183
184 // sort mesh file names
185 std::vector<std::filesystem::directory_entry> sorted_entries;
186 std::ranges::copy(std::filesystem::directory_iterator(popt.input_folder), std::back_inserter(sorted_entries));
187 std::ranges::sort(sorted_entries,
188 [](const std::filesystem::directory_entry& first, const std::filesystem::directory_entry& second) {
189 return first.path().filename() < second.path().filename();
190 });
191
192 nlohmann::ordered_json json_groups = {};
193 assmpq::merger::MeshGroups all_mesh_group;
194
195 // loop for all meshes
196 for (const auto& entry : sorted_entries) {
197 std::string current_file_name = entry.path().string();
198
199 // check if the name matches the geoset pattern and substr the geo name
200 if (!std::regex_search(current_file_name, matches, match_regex)) {
201 spdlog::error("The pattern does not match the file: {}", current_file_name);
202 break;
203 }
204
205 if (matches.size() > 2) {
206 spdlog::error("Too much pattern matches for the file {}", current_file_name);
207 return 1;
208 }
209
210 std::string extracted_geo_name = matches[1].str();
211 assmpq::merger::MeshGroups mesh_group;
212
213 if (popt.is_verbose) {
214 spdlog::info("-> Loading {}", entry.path().string());
215 spdlog::info(" Group name: {}", extracted_geo_name);
216 }
217
218 if (!load_model(entry.path().string(), mesh_group)) {
219 spdlog::error("Loading error {}", entry.path().string());
220 return 1;
221 }
222
223 if (mesh_group.size() > 1) {
224 spdlog::error("Too much shapes for cliff/ramp mesh: {}", current_file_name);
225 return 1;
226 }
227
228 assmpq::merger::MeshData mesh_data = mesh_group.front();
229 mesh_data.name = extracted_geo_name;
230
231 // Apply all transforamtions to mesh
232 transform_mesh(mesh_data, popt);
233
234 // Split ramps and generate geo keys
235 process_mesh(all_mesh_group, mesh_data, json_groups, extracted_geo_name, popt);
236 }
237
238 const std::string mesh_type = popt.is_ramp ? "ramps" : "cliffs";
239 const std::string typed_geoset_name = std::format("{}_{}", popt.geoset_name, mesh_type);
240 const std::string output_mesh_filename = std::format("{}.obj", typed_geoset_name);
241
242 output_json_doc[typed_geoset_name]["count"] = std::to_string(all_mesh_group.size());
243 output_json_doc[typed_geoset_name]["mesh"] = output_mesh_filename;
244 output_json_doc[typed_geoset_name]["type"] = mesh_type;
245 output_json_doc[typed_geoset_name]["groups"] = json_groups;
246
247 const auto output_mesh_path = popt.output_folder / output_mesh_filename;
248 spdlog::info("-> Saving {} mesh file: {}", mesh_type, output_mesh_path.string());
249 spdlog::info(" Shapes count: {}", all_mesh_group.size());
250
251 if (!save_model(output_mesh_path.string(), all_mesh_group)) {
252 spdlog::error("Could not save mesh: {}", output_mesh_path.string());
253 return 1;
254 }
255
256 spdlog::info("-> Saving json file: {}", output_json_path.string());
257 std::ofstream output_json_file(output_json_path);
258 if (!output_json_file.is_open()) {
259 spdlog::error("Failed to open output JSON file");
260 return 1;
261 }
262
263 output_json_file << std::setw(4) << output_json_doc;
264
265
3/34
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✗ Branch 38 not taken.
✗ Branch 39 not taken.
✗ Branch 41 not taken.
✓ Branch 42 taken 2 times.
✗ Branch 45 not taken.
✓ Branch 46 taken 2 times.
✗ Branch 48 not taken.
✓ Branch 49 taken 2 times.
✗ Branch 68 not taken.
✗ Branch 69 not taken.
6 } catch (const std::exception &e) {
266 spdlog::error("Unhandled exception in main: {}", e.what());
267 }
268
269 #if defined(__GNUC__) && !defined(__clang__)
270 #pragma GCC diagnostic pop
271 #endif
272