feat(editor): add --copy-zone-items for cross-zone item reuse

Copies items from one zone to another. Two modes:

Default (replace): destination items.json becomes a verbatim copy
of the source. Useful when zoneB is a fresh copy of zoneA's loot
table.

--merge: appends source items to existing destination items, but
preserves the destination's existing IDs by re-id'ing source
entries on collision. The destination's items take precedence; the
source's entries get fresh IDs picked from the smallest unused
positive integer.

Verified: merge with id-1 collision (dest has Boot id=1; source has
Sword id=1, Helm id=2) → Sword becomes id=2, then Helm becomes id=3
because 2 was just claimed; "re-ided: 2 (id collisions)" reported.
Replace mode wholesale-overwrites as expected. Brings command count
to 223.
This commit is contained in:
Kelsi 2026-05-07 07:12:55 -07:00
parent a3253eebc6
commit 05645b2a79

View file

@ -568,6 +568,8 @@ static void printUsage(const char* argv0) {
std::printf(" Edit fields on an existing item in place; only specified flags are changed\n");
std::printf(" --remove-item <zoneDir> <index>\n");
std::printf(" Remove item at given 0-based index from <zoneDir>/items.json\n");
std::printf(" --copy-zone-items <fromZoneDir> <toZoneDir> [--merge]\n");
std::printf(" Copy items from one zone to another (default replaces; --merge appends with re-id)\n");
std::printf(" --clone-item <zoneDir> <index> [newName]\n");
std::printf(" Duplicate the item at index, assign next free id (and optional name override)\n");
std::printf(" --validate-items <zoneDir>\n");
@ -1000,6 +1002,7 @@ int main(int argc, char* argv[]) {
"--info-project-items",
"--clone-object",
"--remove-creature", "--remove-object", "--remove-quest", "--remove-item",
"--copy-zone-items",
"--copy-zone", "--rename-zone", "--remove-zone",
"--clear-zone-content", "--strip-zone", "--strip-project",
"--repair-zone", "--repair-project",
@ -13549,6 +13552,104 @@ int main(int argc, char* argv[]) {
removedName.c_str(), removedId,
path.c_str(), items.size());
return 0;
} else if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) {
// Copy items from one zone to another. Default mode
// replaces the destination items.json wholesale; --merge
// appends each source item to the existing destination
// list, re-id'ing on collision so the destination's
// existing IDs are preserved and the source's new
// entries get fresh ones.
std::string fromZone = argv[++i];
std::string toZone = argv[++i];
bool mergeMode = false;
if (i + 1 < argc && std::strcmp(argv[i + 1], "--merge") == 0) {
mergeMode = true; i++;
}
namespace fs = std::filesystem;
std::string srcPath = fromZone + "/items.json";
if (!fs::exists(srcPath)) {
std::fprintf(stderr,
"copy-zone-items: %s has no items.json\n", fromZone.c_str());
return 1;
}
if (!fs::exists(toZone) || !fs::is_directory(toZone)) {
std::fprintf(stderr,
"copy-zone-items: dest %s is not a directory\n",
toZone.c_str());
return 1;
}
nlohmann::json src;
try {
std::ifstream in(srcPath);
in >> src;
} catch (...) {
std::fprintf(stderr,
"copy-zone-items: %s is not valid JSON\n", srcPath.c_str());
return 1;
}
if (!src.contains("items") || !src["items"].is_array()) {
std::fprintf(stderr,
"copy-zone-items: %s has no 'items' array\n",
srcPath.c_str());
return 1;
}
std::string dstPath = toZone + "/items.json";
nlohmann::json dst = nlohmann::json::object({{"items",
nlohmann::json::array()}});
int copied = 0, reIded = 0;
if (mergeMode && fs::exists(dstPath)) {
try {
std::ifstream in(dstPath);
in >> dst;
} catch (...) {}
if (!dst.contains("items") || !dst["items"].is_array()) {
dst["items"] = nlohmann::json::array();
}
std::set<uint32_t> usedIds;
for (const auto& it : dst["items"]) {
if (it.contains("id") && it["id"].is_number_unsigned()) {
usedIds.insert(it["id"].get<uint32_t>());
}
}
for (const auto& it : src["items"]) {
nlohmann::json newItem = it;
uint32_t srcId = it.value("id", 0u);
if (srcId == 0 || usedIds.count(srcId)) {
// Pick the next free id.
uint32_t fresh = 1;
while (usedIds.count(fresh)) ++fresh;
newItem["id"] = fresh;
usedIds.insert(fresh);
if (srcId != 0) reIded++;
} else {
usedIds.insert(srcId);
}
dst["items"].push_back(newItem);
copied++;
}
} else {
// Replace mode: destination becomes a verbatim copy of
// the source items array.
dst["items"] = src["items"];
copied = static_cast<int>(src["items"].size());
}
std::ofstream out(dstPath);
if (!out) {
std::fprintf(stderr,
"copy-zone-items: failed to write %s\n", dstPath.c_str());
return 1;
}
out << dst.dump(2);
out.close();
std::printf("Copied %d item(s) from %s to %s\n",
copied, fromZone.c_str(), toZone.c_str());
std::printf(" mode : %s\n",
mergeMode ? "merge (append + re-id)" : "replace");
std::printf(" dst total : %zu\n", dst["items"].size());
if (reIded > 0) {
std::printf(" re-ided : %d (id collisions)\n", reIded);
}
return 0;
} else if (std::strcmp(argv[i], "--clone-item") == 0 && i + 2 < argc) {
// Duplicate the item at given 0-based index. Auto-assigns
// the smallest unused positive id; optional <newName>