From b3e34e0edfa4b7428f08e1448200f4abbb88d4ca Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 13:41:45 -0700 Subject: [PATCH] feat(editor): add --validate-jsondbc for strict JSON DBC schema check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --info-jsondbc only verifies recordCount matches the actual records[] array length. This goes deeper, validating the full sidecar schema that --convert-json-dbc consumes: wowee_editor --validate-jsondbc db/Spell.json Checks: - top-level value is a JSON object - 'format' field exists, is a string, equals 'wowee-dbc-json-1.0' - 'source' field present (so re-import knows the DBC slot) - recordCount + fieldCount are non-negative integers - 'records' is an array; recordCount matches actual length - each record is an array exactly fieldCount cells wide - each cell is string|number|bool|null (no nested objects/arrays) Errors capped (3 per category) with '... and N more' tail so a 1000-row file with consistent breakage doesn't drown the report. Exit 1 on any error so CI can gate. Verified on a hand-rolled good JSON (passes clean) and a bad one with: wrong format tag, missing source, wrong-width row, and an object cell — all 4 issues reported with precise positions and exit 1. Format-validator lineup is now complete: Open binary: WOM / WOB / WOC / WHM / GLB Open text: JSON DBC Every shippable open format has a CLI validator that gates on schema/structure errors. --- tools/editor/main.cpp | 149 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/tools/editor/main.cpp b/tools/editor/main.cpp index f170e3a8..15f91c18 100644 --- a/tools/editor/main.cpp +++ b/tools/editor/main.cpp @@ -500,6 +500,8 @@ static void printUsage(const char* argv0) { std::printf(" Recursively run all per-format validators on every file\n"); std::printf(" --validate-glb [--json]\n"); std::printf(" Verify a glTF 2.0 binary's structure (magic, chunks, JSON, accessors)\n"); + std::printf(" --validate-jsondbc [--json]\n"); + std::printf(" Verify a JSON DBC sidecar's full schema (per-cell types, row width, format tag)\n"); std::printf(" --info-glb [--json]\n"); std::printf(" Print glTF 2.0 binary metadata (chunks, mesh/primitive counts, accessors)\n"); std::printf(" --zone-summary [--json]\n"); @@ -602,6 +604,7 @@ int main(int argc, char* argv[]) { "--unpack-wcp", "--pack-wcp", "--validate", "--validate-wom", "--validate-wob", "--validate-woc", "--validate-whm", "--validate-all", "--validate-glb", "--info-glb", + "--validate-jsondbc", "--zone-summary", "--export-zone-summary-md", "--export-quest-graph", "--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles", @@ -3681,6 +3684,152 @@ int main(int argc, char* argv[]) { std::printf(" FAILED — %d error(s):\n", errorCount); for (const auto& e : errors) std::printf(" - %s\n", e.c_str()); return isValidate ? 1 : 0; + } else if (std::strcmp(argv[i], "--validate-jsondbc") == 0 && i + 1 < argc) { + // Strict schema validator for JSON DBC sidecars. --info-jsondbc + // checks that header recordCount matches the actual records[] + // length; this goes deeper: + // - format tag is the wowee 1.0 string + // - source field present (so re-import knows which DBC slot) + // - recordCount + fieldCount are non-negative integers + // - records is an array + // - each record is an array exactly fieldCount long + // - each cell is string|number|bool|null (no objects/arrays) + // Catches the kind of corruption that load() might silently + // tolerate (missing fields default to 0/empty), letting the + // editor's runtime DBC loader downstream-fail in confusing + // ways. + std::string path = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) i++; + std::ifstream in(path); + if (!in) { + std::fprintf(stderr, + "validate-jsondbc: cannot open %s\n", path.c_str()); + return 1; + } + nlohmann::json doc; + std::vector errors; + try { + in >> doc; + } catch (const std::exception& e) { + errors.push_back(std::string("JSON parse error: ") + e.what()); + } + std::string format, source; + uint32_t recordCount = 0, fieldCount = 0; + uint32_t actualRecs = 0; + int badRowWidths = 0, badCellTypes = 0; + if (errors.empty()) { + if (!doc.is_object()) { + errors.push_back("top-level value is not a JSON object"); + } else { + if (!doc.contains("format")) { + errors.push_back("missing 'format' field"); + } else if (!doc["format"].is_string()) { + errors.push_back("'format' field is not a string"); + } else { + format = doc["format"].get(); + if (format != "wowee-dbc-json-1.0") { + errors.push_back("'format' is '" + format + + "', expected 'wowee-dbc-json-1.0'"); + } + } + if (!doc.contains("source")) { + errors.push_back("missing 'source' field (re-import needs it)"); + } else { + source = doc.value("source", std::string{}); + } + if (!doc.contains("recordCount") || + !doc["recordCount"].is_number_integer()) { + errors.push_back("'recordCount' missing or not an integer"); + } else { + recordCount = doc["recordCount"].get(); + } + if (!doc.contains("fieldCount") || + !doc["fieldCount"].is_number_integer()) { + errors.push_back("'fieldCount' missing or not an integer"); + } else { + fieldCount = doc["fieldCount"].get(); + } + if (!doc.contains("records") || !doc["records"].is_array()) { + errors.push_back("'records' missing or not an array"); + } else { + const auto& records = doc["records"]; + actualRecs = static_cast(records.size()); + if (actualRecs != recordCount) { + errors.push_back("recordCount " + std::to_string(recordCount) + + " != actual records " + + std::to_string(actualRecs)); + } + for (size_t r = 0; r < records.size(); ++r) { + const auto& row = records[r]; + if (!row.is_array()) { + errors.push_back("record[" + std::to_string(r) + + "] is not an array"); + continue; + } + if (row.size() != fieldCount) { + badRowWidths++; + if (badRowWidths <= 3) { + errors.push_back("record[" + std::to_string(r) + + "] has " + std::to_string(row.size()) + + " cells, expected " + + std::to_string(fieldCount)); + } + } + for (size_t c = 0; c < row.size(); ++c) { + const auto& cell = row[c]; + bool ok = cell.is_string() || cell.is_number() || + cell.is_boolean() || cell.is_null(); + if (!ok) { + badCellTypes++; + if (badCellTypes <= 3) { + errors.push_back("record[" + std::to_string(r) + + "][" + std::to_string(c) + + "] has invalid type (objects/arrays not allowed)"); + } + } + } + } + if (badRowWidths > 3) { + errors.push_back("... and " + std::to_string(badRowWidths - 3) + + " more rows with wrong cell count"); + } + if (badCellTypes > 3) { + errors.push_back("... and " + std::to_string(badCellTypes - 3) + + " more cells with invalid types"); + } + } + } + } + int errorCount = static_cast(errors.size()); + if (jsonOut) { + nlohmann::json j; + j["jsondbc"] = path; + j["format"] = format; + j["source"] = source; + j["recordCount"] = recordCount; + j["fieldCount"] = fieldCount; + j["actualRecords"] = actualRecs; + j["errorCount"] = errorCount; + j["errors"] = errors; + j["passed"] = errors.empty(); + std::printf("%s\n", j.dump(2).c_str()); + return errors.empty() ? 0 : 1; + } + std::printf("JSON DBC: %s\n", path.c_str()); + std::printf(" format : %s\n", format.empty() ? "?" : format.c_str()); + std::printf(" source : %s\n", source.empty() ? "?" : source.c_str()); + std::printf(" records : %u (header) / %u (actual)\n", + recordCount, actualRecs); + std::printf(" fields : %u\n", fieldCount); + if (errors.empty()) { + std::printf(" PASSED\n"); + return 0; + } + std::printf(" FAILED — %d error(s):\n", errorCount); + for (const auto& e : errors) std::printf(" - %s\n", e.c_str()); + return 1; } else if (std::strcmp(argv[i], "--export-obj") == 0 && i + 1 < argc) { // Convert WOM (our open M2 replacement) to Wavefront OBJ — a // universally supported text format that opens directly in