#include "cli_items_mutate.hpp" #include #include #include #include #include #include #include #include #include namespace wowee { namespace editor { namespace cli { namespace { int handleSetItem(int& i, int argc, char** argv) { // Edit fields on an existing item in place. Lookup is by // id by default; '#N' for index lookup. Only specified // flags are changed; everything else is preserved // verbatim — including any extra fields added by hand. // // Supported flags: --name, --quality, --displayId, // --itemLevel, --stackable. Each takes one positional // argument that follows the flag. std::string zoneDir = argv[++i]; std::string lookup = argv[++i]; namespace fs = std::filesystem; std::string path = zoneDir + "/items.json"; if (!fs::exists(path)) { std::fprintf(stderr, "set-item: %s has no items.json\n", zoneDir.c_str()); return 1; } nlohmann::json doc; try { std::ifstream in(path); in >> doc; } catch (...) { std::fprintf(stderr, "set-item: %s is not valid JSON\n", path.c_str()); return 1; } if (!doc.contains("items") || !doc["items"].is_array()) { std::fprintf(stderr, "set-item: %s has no 'items' array\n", path.c_str()); return 1; } auto& items = doc["items"]; int foundIdx = -1; if (!lookup.empty() && lookup[0] == '#') { try { int idx = std::stoi(lookup.substr(1)); if (idx >= 0 && static_cast(idx) < items.size()) foundIdx = idx; } catch (...) {} } else { uint32_t targetId = 0; try { targetId = static_cast(std::stoul(lookup)); } catch (...) { std::fprintf(stderr, "set-item: lookup '%s' is not a number\n", lookup.c_str()); return 1; } for (size_t k = 0; k < items.size(); ++k) { if (items[k].contains("id") && items[k]["id"].is_number_unsigned() && items[k]["id"].get() == targetId) { foundIdx = static_cast(k); break; } } } if (foundIdx < 0) { std::fprintf(stderr, "set-item: no match for '%s' in %s\n", lookup.c_str(), path.c_str()); return 1; } auto& it = items[foundIdx]; std::vector changes; // Walk the remaining args looking for known --field value // pairs. Anything unrecognized is reported and aborts so // typos don't silently no-op. while (i + 2 < argc) { std::string flag = argv[i + 1]; std::string val = argv[i + 2]; if (flag.size() < 2 || flag[0] != '-' || flag[1] != '-') break; if (flag == "--name") { it["name"] = val; changes.push_back("name=" + val); } else if (flag == "--quality") { try { uint32_t q = static_cast(std::stoul(val)); if (q > 6) { std::fprintf(stderr, "set-item: quality %u out of range (0..6)\n", q); return 1; } it["quality"] = q; changes.push_back("quality=" + val); } catch (...) { std::fprintf(stderr, "set-item: --quality needs a number\n"); return 1; } } else if (flag == "--displayId") { try { it["displayId"] = static_cast(std::stoul(val)); changes.push_back("displayId=" + val); } catch (...) { std::fprintf(stderr, "set-item: --displayId needs a number\n"); return 1; } } else if (flag == "--itemLevel") { try { it["itemLevel"] = static_cast(std::stoul(val)); changes.push_back("itemLevel=" + val); } catch (...) { std::fprintf(stderr, "set-item: --itemLevel needs a number\n"); return 1; } } else if (flag == "--stackable") { try { uint32_t s = static_cast(std::stoul(val)); if (s == 0 || s > 1000) { std::fprintf(stderr, "set-item: stackable %u out of range (1..1000)\n", s); return 1; } it["stackable"] = s; changes.push_back("stackable=" + val); } catch (...) { std::fprintf(stderr, "set-item: --stackable needs a number\n"); return 1; } } else { std::fprintf(stderr, "set-item: unknown flag '%s' (typo?)\n", flag.c_str()); return 1; } i += 2; } if (changes.empty()) { std::fprintf(stderr, "set-item: no field flags supplied — nothing to change\n"); return 1; } std::ofstream out(path); if (!out) { std::fprintf(stderr, "set-item: failed to write %s\n", path.c_str()); return 1; } out << doc.dump(2); out.close(); std::printf("Updated item %d in %s:\n", foundIdx, path.c_str()); for (const auto& c : changes) { std::printf(" %s\n", c.c_str()); } return 0; } int handleCopyZoneItems(int& i, int argc, char** argv) { // 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 usedIds; for (const auto& it : dst["items"]) { if (it.contains("id") && it["id"].is_number_unsigned()) { usedIds.insert(it["id"].get()); } } 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(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; } int handleCloneItem(int& i, int argc, char** argv) { // Duplicate the item at given 0-based index. Auto-assigns // the smallest unused positive id; optional // overrides the cloned name (without it the new entry // gets " (copy)" appended). std::string zoneDir = argv[++i]; int idx = -1; try { idx = std::stoi(argv[++i]); } catch (...) { std::fprintf(stderr, "clone-item: index must be an integer\n"); return 1; } std::string newName; if (i + 1 < argc && argv[i + 1][0] != '-') newName = argv[++i]; namespace fs = std::filesystem; std::string path = zoneDir + "/items.json"; if (!fs::exists(path)) { std::fprintf(stderr, "clone-item: %s has no items.json\n", zoneDir.c_str()); return 1; } nlohmann::json doc; try { std::ifstream in(path); in >> doc; } catch (...) { std::fprintf(stderr, "clone-item: %s is not valid JSON\n", path.c_str()); return 1; } if (!doc.contains("items") || !doc["items"].is_array()) { std::fprintf(stderr, "clone-item: %s has no 'items' array\n", path.c_str()); return 1; } auto& items = doc["items"]; if (idx < 0 || static_cast(idx) >= items.size()) { std::fprintf(stderr, "clone-item: index %d out of range (have %zu)\n", idx, items.size()); return 1; } // Pick the next free id. std::set used; for (const auto& it : items) { if (it.contains("id") && it["id"].is_number_unsigned()) { used.insert(it["id"].get()); } } uint32_t newId = 1; while (used.count(newId)) ++newId; nlohmann::json clone = items[idx]; clone["id"] = newId; if (!newName.empty()) { clone["name"] = newName; } else { std::string oldName = clone.value("name", std::string("(unnamed)")); clone["name"] = oldName + " (copy)"; } items.push_back(clone); std::ofstream out(path); if (!out) { std::fprintf(stderr, "clone-item: failed to write %s\n", path.c_str()); return 1; } out << doc.dump(2); out.close(); std::printf("Cloned item idx %d to '%s' (id=%u) in %s (now %zu total)\n", idx, clone["name"].get().c_str(), newId, path.c_str(), items.size()); return 0; } } // namespace bool handleItemsMutate(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--set-item") == 0 && i + 2 < argc) { outRc = handleSetItem(i, argc, argv); return true; } if (std::strcmp(argv[i], "--copy-zone-items") == 0 && i + 2 < argc) { outRc = handleCopyZoneItems(i, argc, argv); return true; } if (std::strcmp(argv[i], "--clone-item") == 0 && i + 2 < argc) { outRc = handleCloneItem(i, argc, argv); return true; } return false; } } // namespace cli } // namespace editor } // namespace wowee