mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-08 01:53:52 +00:00
feat(editor): add --check-zone-content for content data-quality validation
Sanity-checks creature/object/quest fields for plausible values.
Where --check-zone-refs catches dangling references, this catches
data-quality issues that pass technical validation but break
gameplay:
wowee_editor --check-zone-content custom_zones/MyZone
Zone content: custom_zones/MyZone
creature warnings: 1
object warnings : 0
quest warnings : 2
FAILED — 3 total warning(s):
- creature[0] 'Wolf' has displayId=0 (will render invisibly)
- quest[0] 'Hunt' has no objectives (uncompletable)
- quest[0] 'Hunt' has no reward at all
Per-type checks:
Creatures:
- empty name
- 0 health (dies on spawn)
- level 0
- minDamage > maxDamage (broken combat math)
- non-positive or non-finite scale
- displayId=0 (invisible at runtime)
Objects:
- empty path
- non-positive or non-finite scale
- non-finite position
Quests:
- empty title
- no objectives (player can never complete)
- no reward at all (XP=0, items=[], coins all 0)
- requiredLevel=0
Both --check-zone-refs (link integrity) and --check-zone-content
(data quality) needed — a quest can have valid NPC IDs (refs OK)
AND no objectives (content broken). Run both before --pack-wcp.
Verified end-to-end: zone with displayId=0 creature + objective-
less + rewardless quest reports 3 warnings; after fixing all three,
PASSED.
This commit is contained in:
parent
c9e8ad9930
commit
dadefab64e
1 changed files with 133 additions and 1 deletions
|
|
@ -434,6 +434,8 @@ static void printUsage(const char* argv0) {
|
|||
std::printf(" Markdown dep table for a zone (with on-disk presence column)\n");
|
||||
std::printf(" --check-zone-refs <zoneDir> [--json]\n");
|
||||
std::printf(" Verify every referenced model/quest NPC actually exists; exit 1 on missing refs\n");
|
||||
std::printf(" --check-zone-content <zoneDir> [--json]\n");
|
||||
std::printf(" Sanity-check creature/object/quest fields for plausible values\n");
|
||||
std::printf(" --for-each-zone <projectDir> -- <cmd...>\n");
|
||||
std::printf(" Run <cmd...> for every zone in <projectDir>; '{}' in cmd is replaced with the zone path\n");
|
||||
std::printf(" --scaffold-zone <name> [tx ty] Create a blank zone in custom_zones/<name>/ and exit\n");
|
||||
|
|
@ -687,7 +689,8 @@ int main(int argc, char* argv[]) {
|
|||
"--export-zone-csv", "--export-zone-html", "--export-project-html",
|
||||
"--scaffold-zone", "--add-tile", "--remove-tile", "--list-tiles",
|
||||
"--for-each-zone", "--zone-stats", "--info-tilemap",
|
||||
"--list-zone-deps", "--check-zone-refs", "--export-zone-deps-md",
|
||||
"--list-zone-deps", "--check-zone-refs", "--check-zone-content",
|
||||
"--export-zone-deps-md",
|
||||
"--add-creature", "--add-object", "--add-quest",
|
||||
"--add-quest-objective", "--add-quest-reward-item", "--set-quest-reward",
|
||||
"--remove-quest-objective", "--clone-quest", "--clone-creature",
|
||||
|
|
@ -10517,6 +10520,135 @@ int main(int argc, char* argv[]) {
|
|||
std::printf(" FAILED — %d issue(s):\n", totalErrors);
|
||||
for (const auto& e : errors) std::printf(" - %s\n", e.c_str());
|
||||
return 1;
|
||||
} else if (std::strcmp(argv[i], "--check-zone-content") == 0 && i + 1 < argc) {
|
||||
// Sanity-check creature/object/quest fields for plausible
|
||||
// values. --check-zone-refs catches dangling references;
|
||||
// this catches data-quality issues like creatures with 0 HP,
|
||||
// objects with negative scale, quests with no objectives.
|
||||
// Both are needed — a quest can have valid NPC IDs (refs OK)
|
||||
// AND no objectives (content broken).
|
||||
std::string zoneDir = argv[++i];
|
||||
bool jsonOut = (i + 1 < argc &&
|
||||
std::strcmp(argv[i + 1], "--json") == 0);
|
||||
if (jsonOut) i++;
|
||||
namespace fs = std::filesystem;
|
||||
if (!fs::exists(zoneDir + "/zone.json")) {
|
||||
std::fprintf(stderr,
|
||||
"check-zone-content: %s has no zone.json\n", zoneDir.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::vector<std::string> warnings;
|
||||
int creatureWarn = 0, objectWarn = 0, questWarn = 0;
|
||||
// Creatures
|
||||
wowee::editor::NpcSpawner sp;
|
||||
if (sp.loadFromFile(zoneDir + "/creatures.json")) {
|
||||
for (size_t k = 0; k < sp.spawnCount(); ++k) {
|
||||
const auto& s = sp.getSpawns()[k];
|
||||
if (s.name.empty()) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] has empty name");
|
||||
creatureWarn++;
|
||||
}
|
||||
if (s.health == 0) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
||||
s.name + "' has 0 health");
|
||||
creatureWarn++;
|
||||
}
|
||||
if (s.level == 0) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
||||
s.name + "' has level 0");
|
||||
creatureWarn++;
|
||||
}
|
||||
if (s.minDamage > s.maxDamage) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
||||
s.name + "' has minDamage > maxDamage");
|
||||
creatureWarn++;
|
||||
}
|
||||
if (s.scale <= 0.0f || !std::isfinite(s.scale)) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
||||
s.name + "' has non-positive or non-finite scale");
|
||||
creatureWarn++;
|
||||
}
|
||||
if (s.displayId == 0) {
|
||||
warnings.push_back("creature[" + std::to_string(k) + "] '" +
|
||||
s.name + "' has displayId=0 (will render invisibly)");
|
||||
creatureWarn++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Objects
|
||||
wowee::editor::ObjectPlacer op;
|
||||
if (op.loadFromFile(zoneDir + "/objects.json")) {
|
||||
for (size_t k = 0; k < op.getObjects().size(); ++k) {
|
||||
const auto& o = op.getObjects()[k];
|
||||
if (o.path.empty()) {
|
||||
warnings.push_back("object[" + std::to_string(k) + "] has empty path");
|
||||
objectWarn++;
|
||||
}
|
||||
if (o.scale <= 0.0f || !std::isfinite(o.scale)) {
|
||||
warnings.push_back("object[" + std::to_string(k) +
|
||||
"] has non-positive or non-finite scale");
|
||||
objectWarn++;
|
||||
}
|
||||
if (!std::isfinite(o.position.x) ||
|
||||
!std::isfinite(o.position.y) ||
|
||||
!std::isfinite(o.position.z)) {
|
||||
warnings.push_back("object[" + std::to_string(k) +
|
||||
"] has non-finite position");
|
||||
objectWarn++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Quests
|
||||
wowee::editor::QuestEditor qe;
|
||||
if (qe.loadFromFile(zoneDir + "/quests.json")) {
|
||||
for (size_t k = 0; k < qe.questCount(); ++k) {
|
||||
const auto& q = qe.getQuests()[k];
|
||||
if (q.title.empty()) {
|
||||
warnings.push_back("quest[" + std::to_string(k) + "] has empty title");
|
||||
questWarn++;
|
||||
}
|
||||
if (q.objectives.empty()) {
|
||||
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
||||
q.title + "' has no objectives (uncompletable)");
|
||||
questWarn++;
|
||||
}
|
||||
if (q.reward.xp == 0 && q.reward.itemRewards.empty() &&
|
||||
q.reward.gold == 0 && q.reward.silver == 0 && q.reward.copper == 0) {
|
||||
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
||||
q.title + "' has no reward at all");
|
||||
questWarn++;
|
||||
}
|
||||
if (q.requiredLevel == 0) {
|
||||
warnings.push_back("quest[" + std::to_string(k) + "] '" +
|
||||
q.title + "' has requiredLevel=0");
|
||||
questWarn++;
|
||||
}
|
||||
}
|
||||
}
|
||||
int total = creatureWarn + objectWarn + questWarn;
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["zone"] = zoneDir;
|
||||
j["creatureWarnings"] = creatureWarn;
|
||||
j["objectWarnings"] = objectWarn;
|
||||
j["questWarnings"] = questWarn;
|
||||
j["totalWarnings"] = total;
|
||||
j["warnings"] = warnings;
|
||||
j["passed"] = (total == 0);
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return total == 0 ? 0 : 1;
|
||||
}
|
||||
std::printf("Zone content: %s\n", zoneDir.c_str());
|
||||
std::printf(" creature warnings: %d\n", creatureWarn);
|
||||
std::printf(" object warnings : %d\n", objectWarn);
|
||||
std::printf(" quest warnings : %d\n", questWarn);
|
||||
if (total == 0) {
|
||||
std::printf(" PASSED\n");
|
||||
return 0;
|
||||
}
|
||||
std::printf(" FAILED — %d total warning(s):\n", total);
|
||||
for (const auto& w : warnings) std::printf(" - %s\n", w.c_str());
|
||||
return 1;
|
||||
} else if (std::strcmp(argv[i], "--for-each-zone") == 0 && i + 1 < argc) {
|
||||
// Batch runner: enumerates zones in <projectDir> and runs the
|
||||
// command after '--' for each one. '{}' in the command is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue