diff --git a/CMakeLists.txt b/CMakeLists.txt index 60a3851c..4e1f25b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1323,6 +1323,7 @@ add_executable(wowee_editor tools/editor/cli_extract_info.cpp tools/editor/cli_export.cpp tools/editor/cli_bake.cpp + tools/editor/cli_migrate.cpp tools/editor/editor_app.cpp tools/editor/editor_camera.cpp tools/editor/editor_viewport.cpp diff --git a/tools/editor/cli_migrate.cpp b/tools/editor/cli_migrate.cpp new file mode 100644 index 00000000..0c7bf679 --- /dev/null +++ b/tools/editor/cli_migrate.cpp @@ -0,0 +1,333 @@ +#include "cli_migrate.hpp" + +#include "pipeline/wowee_model.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +int handleMigrateWom(int& i, int argc, char** argv) { + // Upgrade an older WOM (v1=static, v2=animated) to WOM3 by + // adding a default single-batch entry that covers the whole + // mesh. WOM3 is a strict superset; tooling that consumes + // batches (--info-batches, --export-glb per-primitive split, + // material-aware renderers) becomes useful on previously- + // batchless content. The save() function picks WOM3 magic + // automatically once batches.size() > 0. + std::string base = argv[++i]; + std::string outBase; + if (i + 1 < argc && argv[i + 1][0] != '-') outBase = argv[++i]; + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") + base = base.substr(0, base.size() - 4); + if (!wowee::pipeline::WoweeModelLoader::exists(base)) { + std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); + return 1; + } + if (outBase.empty()) outBase = base; + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + if (!wom.isValid()) { + std::fprintf(stderr, "migrate-wom: %s.wom has no geometry\n", base.c_str()); + return 1; + } + int oldVersion = wom.version; + int batchesAdded = 0; + if (wom.batches.empty()) { + // Single batch covering the entire index range with the + // first texture (or 0 if no textures exist). Opaque + // blend mode + no flags — safe defaults that match how + // the renderer was treating the whole mesh implicitly. + wowee::pipeline::WoweeModel::Batch b; + b.indexStart = 0; + b.indexCount = static_cast(wom.indices.size()); + b.textureIndex = wom.texturePaths.empty() ? 0 : 0; + b.blendMode = 0; + b.flags = 0; + wom.batches.push_back(b); + batchesAdded = 1; + } + // version field is recomputed inside save() based on + // hasBatches/hasAnimation, so we don't need to set it here. + if (!wowee::pipeline::WoweeModelLoader::save(wom, outBase)) { + std::fprintf(stderr, "migrate-wom: failed to write %s.wom\n", + outBase.c_str()); + return 1; + } + // Re-load to verify the new version flag landed correctly. + auto check = wowee::pipeline::WoweeModelLoader::load(outBase); + std::printf("Migrated %s.wom -> %s.wom\n", base.c_str(), outBase.c_str()); + std::printf(" version: %d -> %u batches: %zu -> %zu (added %d)\n", + oldVersion, check.version, + size_t(0), check.batches.size(), batchesAdded); + if (batchesAdded == 0) { + std::printf(" (already had batches; no schema change)\n"); + } + return 0; +} + +int handleMigrateZone(int& i, int argc, char** argv) { + // Batch-runs --migrate-wom in-place on every .wom under + // a zone directory. Idempotent (already-migrated files + // become no-ops). Useful when wowee_editor adds a new + // WOM3-only feature and you want to upgrade legacy zones + // in one shot. + std::string zoneDir = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(zoneDir) || !fs::is_directory(zoneDir)) { + std::fprintf(stderr, + "migrate-zone: %s is not a directory\n", zoneDir.c_str()); + return 1; + } + int scanned = 0, upgraded = 0, alreadyV3 = 0, failed = 0; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + std::string ext = e.path().extension().string(); + if (ext != ".wom") continue; + scanned++; + std::string base = e.path().string(); + if (base.size() >= 4) base = base.substr(0, base.size() - 4); + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + if (!wom.isValid()) { failed++; continue; } + if (!wom.batches.empty()) { alreadyV3++; continue; } + wowee::pipeline::WoweeModel::Batch b; + b.indexStart = 0; + b.indexCount = static_cast(wom.indices.size()); + b.textureIndex = 0; + b.blendMode = 0; + b.flags = 0; + wom.batches.push_back(b); + if (wowee::pipeline::WoweeModelLoader::save(wom, base)) { + upgraded++; + std::printf(" upgraded: %s.wom\n", base.c_str()); + } else { + failed++; + std::fprintf(stderr, " FAILED: %s.wom\n", base.c_str()); + } + } + std::printf("\nmigrate-zone: %s\n", zoneDir.c_str()); + std::printf(" scanned : %d WOM file(s)\n", scanned); + std::printf(" upgraded : %d (added single-batch entry)\n", upgraded); + std::printf(" already v3: %d (no change needed)\n", alreadyV3); + if (failed > 0) { + std::printf(" FAILED : %d (see stderr)\n", failed); + } + return failed == 0 ? 0 : 1; +} + +int handleMigrateProject(int& i, int argc, char** argv) { + // Project-level wrapper around --migrate-zone. Walks every + // zone in and upgrades legacy WOMs in-place. + // Idempotent — already-migrated files become no-ops, safe to + // run repeatedly. + (void)argc; + std::string projectDir = argv[++i]; + namespace fs = std::filesystem; + if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { + std::fprintf(stderr, + "migrate-project: %s is not a directory\n", + projectDir.c_str()); + return 1; + } + std::vector zones; + for (const auto& entry : fs::directory_iterator(projectDir)) { + if (!entry.is_directory()) continue; + if (!fs::exists(entry.path() / "zone.json")) continue; + zones.push_back(entry.path().string()); + } + std::sort(zones.begin(), zones.end()); + int totalScanned = 0, totalUpgraded = 0, totalAlreadyV3 = 0, totalFailed = 0; + // Per-zone breakdown for the summary table. + struct ZRow { std::string name; int scanned, upgraded, alreadyV3, failed; }; + std::vector rows; + for (const auto& zoneDir : zones) { + ZRow r{fs::path(zoneDir).filename().string(), 0, 0, 0, 0}; + std::error_code ec; + for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { + if (!e.is_regular_file()) continue; + if (e.path().extension() != ".wom") continue; + r.scanned++; + std::string base = e.path().string(); + if (base.size() >= 4) base = base.substr(0, base.size() - 4); + auto wom = wowee::pipeline::WoweeModelLoader::load(base); + if (!wom.isValid()) { r.failed++; continue; } + if (!wom.batches.empty()) { r.alreadyV3++; continue; } + wowee::pipeline::WoweeModel::Batch b; + b.indexStart = 0; + b.indexCount = static_cast(wom.indices.size()); + b.textureIndex = 0; + b.blendMode = 0; + b.flags = 0; + wom.batches.push_back(b); + if (wowee::pipeline::WoweeModelLoader::save(wom, base)) { + r.upgraded++; + } else { + r.failed++; + } + } + totalScanned += r.scanned; + totalUpgraded += r.upgraded; + totalAlreadyV3 += r.alreadyV3; + totalFailed += r.failed; + rows.push_back(r); + } + std::printf("migrate-project: %s\n", projectDir.c_str()); + std::printf(" zones : %zu\n", zones.size()); + std::printf(" totals : %d scanned, %d upgraded, %d already-v3, %d failed\n", + totalScanned, totalUpgraded, totalAlreadyV3, totalFailed); + if (!rows.empty()) { + std::printf("\n zone scan upgrade v3 failed\n"); + for (const auto& r : rows) { + std::printf(" %-26s %4d %5d %3d %5d\n", + r.name.substr(0, 26).c_str(), + r.scanned, r.upgraded, r.alreadyV3, r.failed); + } + } + return totalFailed == 0 ? 0 : 1; +} + +int handleMigrateJsondbc(int& i, int argc, char** argv) { + // Auto-fix common schema problems in JSON DBC sidecars so they + // pass --validate-jsondbc cleanly. Designed for upgrading + // sidecars produced by older asset_extract versions or from + // third-party tools that omit fields the runtime now expects: + // - missing 'format' tag → add 'wowee-dbc-json-1.0' + // - missing 'source' field → derive from filename + // - missing 'fieldCount' → infer from first row + // - recordCount mismatch → recompute from actual records[] + // Wrong-width rows are not silently fixed (data loss risk); + // they're surfaced as warnings so the user can decide. + std::string path = argv[++i]; + std::string outPath; + if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; + if (outPath.empty()) outPath = path; // in-place + std::ifstream in(path); + if (!in) { + std::fprintf(stderr, + "migrate-jsondbc: cannot open %s\n", path.c_str()); + return 1; + } + nlohmann::json doc; + try { in >> doc; } + catch (const std::exception& e) { + std::fprintf(stderr, + "migrate-jsondbc: bad JSON in %s (%s)\n", + path.c_str(), e.what()); + return 1; + } + in.close(); + if (!doc.is_object()) { + std::fprintf(stderr, + "migrate-jsondbc: top-level value is not an object\n"); + return 1; + } + int fixes = 0; + if (!doc.contains("format") || !doc["format"].is_string()) { + doc["format"] = "wowee-dbc-json-1.0"; + fixes++; + std::printf(" added: format = 'wowee-dbc-json-1.0'\n"); + } else if (doc["format"] != "wowee-dbc-json-1.0") { + std::printf(" retained existing format: '%s' (not changed)\n", + doc["format"].get().c_str()); + } + if (!doc.contains("source") || !doc["source"].is_string() || + doc["source"].get().empty()) { + // Derive from input path's stem + .dbc — best-effort + // matching the convention asset_extract uses. + std::string stem = std::filesystem::path(path).stem().string(); + doc["source"] = stem + ".dbc"; + fixes++; + std::printf(" added: source = '%s'\n", + doc["source"].get().c_str()); + } + // recordCount + fieldCount are non-negotiable for re-import. + if (!doc.contains("records") || !doc["records"].is_array()) { + std::fprintf(stderr, + "migrate-jsondbc: 'records' missing or not an array — cannot fix\n"); + return 1; + } + const auto& records = doc["records"]; + uint32_t actualCount = static_cast(records.size()); + uint32_t headerCount = doc.value("recordCount", 0u); + if (headerCount != actualCount) { + doc["recordCount"] = actualCount; + fixes++; + std::printf(" fixed: recordCount %u -> %u (matches actual)\n", + headerCount, actualCount); + } + // Infer fieldCount from first row if missing. + if (!doc.contains("fieldCount") || + !doc["fieldCount"].is_number_integer()) { + if (!records.empty() && records[0].is_array()) { + uint32_t inferred = static_cast(records[0].size()); + doc["fieldCount"] = inferred; + fixes++; + std::printf(" inferred: fieldCount = %u (from first row)\n", + inferred); + } + } + // Surface wrong-width rows as warnings (no auto-fix). + uint32_t fc = doc.value("fieldCount", 0u); + int badRows = 0; + for (size_t r = 0; r < records.size(); ++r) { + if (records[r].is_array() && records[r].size() != fc) { + if (++badRows <= 3) { + std::printf(" WARN: row %zu has %zu cells, expected %u\n", + r, records[r].size(), fc); + } + } + } + if (badRows > 3) { + std::printf(" WARN: ... and %d more wrong-width rows\n", + badRows - 3); + } + std::ofstream out(outPath); + if (!out) { + std::fprintf(stderr, + "migrate-jsondbc: cannot write %s\n", outPath.c_str()); + return 1; + } + out << doc.dump(2) << "\n"; + out.close(); + std::printf("Migrated %s -> %s\n", path.c_str(), outPath.c_str()); + std::printf(" fixes applied: %d\n", fixes); + if (badRows > 0) { + std::printf(" warnings : %d wrong-width rows (NOT auto-fixed)\n", + badRows); + } + return 0; +} + +} // namespace + +bool handleMigrate(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--migrate-wom") == 0 && i + 1 < argc) { + outRc = handleMigrateWom(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--migrate-zone") == 0 && i + 1 < argc) { + outRc = handleMigrateZone(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--migrate-project") == 0 && i + 1 < argc) { + outRc = handleMigrateProject(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--migrate-jsondbc") == 0 && i + 1 < argc) { + outRc = handleMigrateJsondbc(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_migrate.hpp b/tools/editor/cli_migrate.hpp new file mode 100644 index 00000000..949e3c52 --- /dev/null +++ b/tools/editor/cli_migrate.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +// Dispatch the schema-migration handlers — these upgrade older +// on-disk asset versions in-place so newer tooling stops rejecting +// or silently misreading them. All migrators are idempotent: +// already-modern files become no-ops. +// --migrate-wom single WOM v1/v2 → WOM3 (single-batch) +// --migrate-zone every WOM in a zone directory +// --migrate-project every zone in a project directory +// --migrate-jsondbc JSON DBC sidecar schema fixes +// +// Returns true if matched; outRc holds the exit code. +bool handleMigrate(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index 85180c35..ad04080a 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -24,6 +24,7 @@ #include "cli_extract_info.hpp" #include "cli_export.hpp" #include "cli_bake.hpp" +#include "cli_migrate.hpp" #include "content_pack.hpp" #include "npc_spawner.hpp" #include "object_placer.hpp" @@ -423,6 +424,9 @@ int main(int argc, char* argv[]) { if (wowee::editor::cli::handleBake(i, argc, argv, outRc)) { return outRc; } + if (wowee::editor::cli::handleMigrate(i, argc, argv, outRc)) { + return outRc; + } } if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) { dataPath = argv[++i]; @@ -10951,292 +10955,6 @@ int main(int argc, char* argv[]) { img.width, img.height, img.data.size()); return 0; } - if (std::strcmp(argv[i], "--migrate-wom") == 0 && i + 1 < argc) { - // Upgrade an older WOM (v1=static, v2=animated) to WOM3 by - // adding a default single-batch entry that covers the whole - // mesh. WOM3 is a strict superset; tooling that consumes - // batches (--info-batches, --export-glb per-primitive split, - // material-aware renderers) becomes useful on previously- - // batchless content. The save() function picks WOM3 magic - // automatically once batches.size() > 0. - std::string base = argv[++i]; - std::string outBase; - if (i + 1 < argc && argv[i + 1][0] != '-') outBase = argv[++i]; - if (base.size() >= 4 && base.substr(base.size() - 4) == ".wom") - base = base.substr(0, base.size() - 4); - if (!wowee::pipeline::WoweeModelLoader::exists(base)) { - std::fprintf(stderr, "WOM not found: %s.wom\n", base.c_str()); - return 1; - } - if (outBase.empty()) outBase = base; - auto wom = wowee::pipeline::WoweeModelLoader::load(base); - if (!wom.isValid()) { - std::fprintf(stderr, "migrate-wom: %s.wom has no geometry\n", base.c_str()); - return 1; - } - int oldVersion = wom.version; - int batchesAdded = 0; - if (wom.batches.empty()) { - // Single batch covering the entire index range with the - // first texture (or 0 if no textures exist). Opaque - // blend mode + no flags — safe defaults that match how - // the renderer was treating the whole mesh implicitly. - wowee::pipeline::WoweeModel::Batch b; - b.indexStart = 0; - b.indexCount = static_cast(wom.indices.size()); - b.textureIndex = wom.texturePaths.empty() ? 0 : 0; - b.blendMode = 0; - b.flags = 0; - wom.batches.push_back(b); - batchesAdded = 1; - } - // version field is recomputed inside save() based on - // hasBatches/hasAnimation, so we don't need to set it here. - if (!wowee::pipeline::WoweeModelLoader::save(wom, outBase)) { - std::fprintf(stderr, "migrate-wom: failed to write %s.wom\n", - outBase.c_str()); - return 1; - } - // Re-load to verify the new version flag landed correctly. - auto check = wowee::pipeline::WoweeModelLoader::load(outBase); - std::printf("Migrated %s.wom -> %s.wom\n", base.c_str(), outBase.c_str()); - std::printf(" version: %d -> %u batches: %zu -> %zu (added %d)\n", - oldVersion, check.version, - size_t(0), check.batches.size(), batchesAdded); - if (batchesAdded == 0) { - std::printf(" (already had batches; no schema change)\n"); - } - return 0; - } - if (std::strcmp(argv[i], "--migrate-zone") == 0 && i + 1 < argc) { - // Batch-runs --migrate-wom in-place on every .wom under - // a zone directory. Idempotent (already-migrated files - // become no-ops). Useful when wowee_editor adds a new - // WOM3-only feature and you want to upgrade legacy zones - // in one shot. - std::string zoneDir = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(zoneDir) || !fs::is_directory(zoneDir)) { - std::fprintf(stderr, - "migrate-zone: %s is not a directory\n", zoneDir.c_str()); - return 1; - } - int scanned = 0, upgraded = 0, alreadyV3 = 0, failed = 0; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file()) continue; - std::string ext = e.path().extension().string(); - if (ext != ".wom") continue; - scanned++; - std::string base = e.path().string(); - if (base.size() >= 4) base = base.substr(0, base.size() - 4); - auto wom = wowee::pipeline::WoweeModelLoader::load(base); - if (!wom.isValid()) { failed++; continue; } - if (!wom.batches.empty()) { alreadyV3++; continue; } - wowee::pipeline::WoweeModel::Batch b; - b.indexStart = 0; - b.indexCount = static_cast(wom.indices.size()); - b.textureIndex = 0; - b.blendMode = 0; - b.flags = 0; - wom.batches.push_back(b); - if (wowee::pipeline::WoweeModelLoader::save(wom, base)) { - upgraded++; - std::printf(" upgraded: %s.wom\n", base.c_str()); - } else { - failed++; - std::fprintf(stderr, " FAILED: %s.wom\n", base.c_str()); - } - } - std::printf("\nmigrate-zone: %s\n", zoneDir.c_str()); - std::printf(" scanned : %d WOM file(s)\n", scanned); - std::printf(" upgraded : %d (added single-batch entry)\n", upgraded); - std::printf(" already v3: %d (no change needed)\n", alreadyV3); - if (failed > 0) { - std::printf(" FAILED : %d (see stderr)\n", failed); - } - return failed == 0 ? 0 : 1; - } - if (std::strcmp(argv[i], "--migrate-project") == 0 && i + 1 < argc) { - // Project-level wrapper around --migrate-zone. Walks every - // zone in and upgrades legacy WOMs in-place. - // Idempotent — already-migrated files become no-ops, safe to - // run repeatedly. - std::string projectDir = argv[++i]; - namespace fs = std::filesystem; - if (!fs::exists(projectDir) || !fs::is_directory(projectDir)) { - std::fprintf(stderr, - "migrate-project: %s is not a directory\n", - projectDir.c_str()); - return 1; - } - std::vector zones; - for (const auto& entry : fs::directory_iterator(projectDir)) { - if (!entry.is_directory()) continue; - if (!fs::exists(entry.path() / "zone.json")) continue; - zones.push_back(entry.path().string()); - } - std::sort(zones.begin(), zones.end()); - int totalScanned = 0, totalUpgraded = 0, totalAlreadyV3 = 0, totalFailed = 0; - // Per-zone breakdown for the summary table. - struct ZRow { std::string name; int scanned, upgraded, alreadyV3, failed; }; - std::vector rows; - for (const auto& zoneDir : zones) { - ZRow r{fs::path(zoneDir).filename().string(), 0, 0, 0, 0}; - std::error_code ec; - for (const auto& e : fs::recursive_directory_iterator(zoneDir, ec)) { - if (!e.is_regular_file()) continue; - if (e.path().extension() != ".wom") continue; - r.scanned++; - std::string base = e.path().string(); - if (base.size() >= 4) base = base.substr(0, base.size() - 4); - auto wom = wowee::pipeline::WoweeModelLoader::load(base); - if (!wom.isValid()) { r.failed++; continue; } - if (!wom.batches.empty()) { r.alreadyV3++; continue; } - wowee::pipeline::WoweeModel::Batch b; - b.indexStart = 0; - b.indexCount = static_cast(wom.indices.size()); - b.textureIndex = 0; - b.blendMode = 0; - b.flags = 0; - wom.batches.push_back(b); - if (wowee::pipeline::WoweeModelLoader::save(wom, base)) { - r.upgraded++; - } else { - r.failed++; - } - } - totalScanned += r.scanned; - totalUpgraded += r.upgraded; - totalAlreadyV3 += r.alreadyV3; - totalFailed += r.failed; - rows.push_back(r); - } - std::printf("migrate-project: %s\n", projectDir.c_str()); - std::printf(" zones : %zu\n", zones.size()); - std::printf(" totals : %d scanned, %d upgraded, %d already-v3, %d failed\n", - totalScanned, totalUpgraded, totalAlreadyV3, totalFailed); - if (!rows.empty()) { - std::printf("\n zone scan upgrade v3 failed\n"); - for (const auto& r : rows) { - std::printf(" %-26s %4d %5d %3d %5d\n", - r.name.substr(0, 26).c_str(), - r.scanned, r.upgraded, r.alreadyV3, r.failed); - } - } - return totalFailed == 0 ? 0 : 1; - } - if (std::strcmp(argv[i], "--migrate-jsondbc") == 0 && i + 1 < argc) { - // Auto-fix common schema problems in JSON DBC sidecars so they - // pass --validate-jsondbc cleanly. Designed for upgrading - // sidecars produced by older asset_extract versions or from - // third-party tools that omit fields the runtime now expects: - // - missing 'format' tag → add 'wowee-dbc-json-1.0' - // - missing 'source' field → derive from filename - // - missing 'fieldCount' → infer from first row - // - recordCount mismatch → recompute from actual records[] - // Wrong-width rows are not silently fixed (data loss risk); - // they're surfaced as warnings so the user can decide. - std::string path = argv[++i]; - std::string outPath; - if (i + 1 < argc && argv[i + 1][0] != '-') outPath = argv[++i]; - if (outPath.empty()) outPath = path; // in-place - std::ifstream in(path); - if (!in) { - std::fprintf(stderr, - "migrate-jsondbc: cannot open %s\n", path.c_str()); - return 1; - } - nlohmann::json doc; - try { in >> doc; } - catch (const std::exception& e) { - std::fprintf(stderr, - "migrate-jsondbc: bad JSON in %s (%s)\n", - path.c_str(), e.what()); - return 1; - } - in.close(); - if (!doc.is_object()) { - std::fprintf(stderr, - "migrate-jsondbc: top-level value is not an object\n"); - return 1; - } - int fixes = 0; - if (!doc.contains("format") || !doc["format"].is_string()) { - doc["format"] = "wowee-dbc-json-1.0"; - fixes++; - std::printf(" added: format = 'wowee-dbc-json-1.0'\n"); - } else if (doc["format"] != "wowee-dbc-json-1.0") { - std::printf(" retained existing format: '%s' (not changed)\n", - doc["format"].get().c_str()); - } - if (!doc.contains("source") || !doc["source"].is_string() || - doc["source"].get().empty()) { - // Derive from input path's stem + .dbc — best-effort - // matching the convention asset_extract uses. - std::string stem = std::filesystem::path(path).stem().string(); - doc["source"] = stem + ".dbc"; - fixes++; - std::printf(" added: source = '%s'\n", - doc["source"].get().c_str()); - } - // recordCount + fieldCount are non-negotiable for re-import. - if (!doc.contains("records") || !doc["records"].is_array()) { - std::fprintf(stderr, - "migrate-jsondbc: 'records' missing or not an array — cannot fix\n"); - return 1; - } - const auto& records = doc["records"]; - uint32_t actualCount = static_cast(records.size()); - uint32_t headerCount = doc.value("recordCount", 0u); - if (headerCount != actualCount) { - doc["recordCount"] = actualCount; - fixes++; - std::printf(" fixed: recordCount %u -> %u (matches actual)\n", - headerCount, actualCount); - } - // Infer fieldCount from first row if missing. - if (!doc.contains("fieldCount") || - !doc["fieldCount"].is_number_integer()) { - if (!records.empty() && records[0].is_array()) { - uint32_t inferred = static_cast(records[0].size()); - doc["fieldCount"] = inferred; - fixes++; - std::printf(" inferred: fieldCount = %u (from first row)\n", - inferred); - } - } - // Surface wrong-width rows as warnings (no auto-fix). - uint32_t fc = doc.value("fieldCount", 0u); - int badRows = 0; - for (size_t r = 0; r < records.size(); ++r) { - if (records[r].is_array() && records[r].size() != fc) { - if (++badRows <= 3) { - std::printf(" WARN: row %zu has %zu cells, expected %u\n", - r, records[r].size(), fc); - } - } - } - if (badRows > 3) { - std::printf(" WARN: ... and %d more wrong-width rows\n", - badRows - 3); - } - std::ofstream out(outPath); - if (!out) { - std::fprintf(stderr, - "migrate-jsondbc: cannot write %s\n", outPath.c_str()); - return 1; - } - out << doc.dump(2) << "\n"; - out.close(); - std::printf("Migrated %s -> %s\n", path.c_str(), outPath.c_str()); - std::printf(" fixes applied: %d\n", fixes); - if (badRows > 0) { - std::printf(" warnings : %d wrong-width rows (NOT auto-fixed)\n", - badRows); - } - return 0; - } } if (dataPath.empty()) {