diff --git a/CMakeLists.txt b/CMakeLists.txt index e4ab7295..4976981d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -626,6 +626,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_guilds.cpp src/pipeline/wowee_conditions.cpp src/pipeline/wowee_pets.cpp + src/pipeline/wowee_auction.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1398,6 +1399,7 @@ add_executable(wowee_editor tools/editor/cli_guilds_catalog.cpp tools/editor/cli_conditions_catalog.cpp tools/editor/cli_pets_catalog.cpp + tools/editor/cli_auction_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1502,6 +1504,7 @@ add_executable(wowee_editor src/pipeline/wowee_guilds.cpp src/pipeline/wowee_conditions.cpp src/pipeline/wowee_pets.cpp + src/pipeline/wowee_auction.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_auction.hpp b/include/pipeline/wowee_auction.hpp new file mode 100644 index 00000000..893636f1 --- /dev/null +++ b/include/pipeline/wowee_auction.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Auction House catalog (.wauc) — novel +// replacement for Blizzard's AuctionHouse.dbc + the +// AzerothCore-style auctionhouse / auctionhousebot SQL +// tables. The 39th open format added to the editor. +// +// Defines per-house rules for the auction system: +// faction access, deposit rate (basis points of buyout +// price), house cut on successful sale, three listing +// duration tiers with per-tier deposit multipliers, +// disallowed item-class bitmask, and the auctioneer NPC +// who acts as the in-world interface. +// +// Cross-references with previously-added formats: +// WAUC.entry.auctioneerNpcId → WCRT.entry.creatureId +// (Auctioneer-flagged NPC) +// WAUC.entry.disallowedClassMask +// bitmask of WIT.Class values +// that may not be auctioned at +// this house (Quest items, +// Containers, etc.) +// +// Binary layout (little-endian): +// magic[4] = "WAUC" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// houseId (uint32) +// auctioneerNpcId (uint32) +// nameLen + name +// factionAccess (uint8) / pad[3] +// baseDepositRateBp (uint32) -- basis points (out of 10000) +// houseCutRateBp (uint32) +// maxBidCopper (uint32) -- 0 = unlimited +// shortHours (uint16) / mediumHours (uint16) +// longHours (uint16) / pad[2] +// shortMultBp (uint32) / mediumMultBp (uint32) +// longMultBp (uint32) +// disallowedClassMask (uint32) +struct WoweeAuction { + enum FactionAccess : uint8_t { + Alliance = 0, + Horde = 1, + Neutral = 2, // goblin AHs in classic — both factions can use + Both = 3, // private / shared instance house + }; + + static constexpr uint16_t kBpDenominator = 10000; // basis-point base + + struct Entry { + uint32_t houseId = 0; + uint32_t auctioneerNpcId = 0; + std::string name; + uint8_t factionAccess = Alliance; + uint32_t baseDepositRateBp = 1500; // 15% per short tier default + uint32_t houseCutRateBp = 500; // 5% house cut + uint32_t maxBidCopper = 0; // unlimited by default + uint16_t shortHours = 12; + uint16_t mediumHours = 24; + uint16_t longHours = 48; + uint32_t shortMultBp = 10000; // 1x base + uint32_t mediumMultBp = 20000; // 2x base + uint32_t longMultBp = 40000; // 4x base + uint32_t disallowedClassMask = 0; // 0 = nothing disallowed + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t houseId) const; + + static const char* factionAccessName(uint8_t f); +}; + +class WoweeAuctionLoader { +public: + static bool save(const WoweeAuction& cat, + const std::string& basePath); + static WoweeAuction load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-auction* variants. + // + // makeStarter — 1 neutral house with stock 12h/24h/48h + // duration tiers and 5% cut. + // makeFactionPair — Alliance + Horde + Neutral (3 + // houses) with the canonical + // neutral 15% cut vs faction 5% cut + // asymmetry. + // makeRestricted — 1 house that disallows Quest + + // Container item classes (auction + // house templates with tighter rules). + static WoweeAuction makeStarter(const std::string& catalogName); + static WoweeAuction makeFactionPair(const std::string& catalogName); + static WoweeAuction makeRestricted(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_auction.cpp b/src/pipeline/wowee_auction.cpp new file mode 100644 index 00000000..84f6cbe5 --- /dev/null +++ b/src/pipeline/wowee_auction.cpp @@ -0,0 +1,210 @@ +#include "pipeline/wowee_auction.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'A', 'U', 'C'}; +constexpr uint32_t kVersion = 1; + +template +void writePOD(std::ofstream& os, const T& v) { + os.write(reinterpret_cast(&v), sizeof(T)); +} + +template +bool readPOD(std::ifstream& is, T& v) { + is.read(reinterpret_cast(&v), sizeof(T)); + return is.gcount() == static_cast(sizeof(T)); +} + +void writeStr(std::ofstream& os, const std::string& s) { + uint32_t n = static_cast(s.size()); + writePOD(os, n); + if (n > 0) os.write(s.data(), n); +} + +bool readStr(std::ifstream& is, std::string& s) { + uint32_t n = 0; + if (!readPOD(is, n)) return false; + if (n > (1u << 20)) return false; + s.resize(n); + if (n > 0) { + is.read(s.data(), n); + if (is.gcount() != static_cast(n)) { + s.clear(); + return false; + } + } + return true; +} + +std::string normalizePath(std::string base) { + if (base.size() < 5 || base.substr(base.size() - 5) != ".wauc") { + base += ".wauc"; + } + return base; +} + +} // namespace + +const WoweeAuction::Entry* WoweeAuction::findById(uint32_t houseId) const { + for (const auto& e : entries) if (e.houseId == houseId) return &e; + return nullptr; +} + +const char* WoweeAuction::factionAccessName(uint8_t f) { + switch (f) { + case Alliance: return "alliance"; + case Horde: return "horde"; + case Neutral: return "neutral"; + case Both: return "both"; + default: return "unknown"; + } +} + +bool WoweeAuctionLoader::save(const WoweeAuction& cat, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + writeStr(os, cat.name); + uint32_t entryCount = static_cast(cat.entries.size()); + writePOD(os, entryCount); + for (const auto& e : cat.entries) { + writePOD(os, e.houseId); + writePOD(os, e.auctioneerNpcId); + writeStr(os, e.name); + writePOD(os, e.factionAccess); + uint8_t pad3[3] = {0, 0, 0}; + os.write(reinterpret_cast(pad3), 3); + writePOD(os, e.baseDepositRateBp); + writePOD(os, e.houseCutRateBp); + writePOD(os, e.maxBidCopper); + writePOD(os, e.shortHours); + writePOD(os, e.mediumHours); + writePOD(os, e.longHours); + uint8_t pad2[2] = {0, 0}; + os.write(reinterpret_cast(pad2), 2); + writePOD(os, e.shortMultBp); + writePOD(os, e.mediumMultBp); + writePOD(os, e.longMultBp); + writePOD(os, e.disallowedClassMask); + } + return os.good(); +} + +WoweeAuction WoweeAuctionLoader::load(const std::string& basePath) { + WoweeAuction out; + std::ifstream is(normalizePath(basePath), std::ios::binary); + if (!is) return out; + char magic[4]; + is.read(magic, 4); + if (std::memcmp(magic, kMagic, 4) != 0) return out; + uint32_t version = 0; + if (!readPOD(is, version) || version != kVersion) return out; + if (!readStr(is, out.name)) return out; + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + if (entryCount > (1u << 20)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.houseId) || + !readPOD(is, e.auctioneerNpcId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.factionAccess)) { + out.entries.clear(); return out; + } + uint8_t pad3[3]; + is.read(reinterpret_cast(pad3), 3); + if (is.gcount() != 3) { out.entries.clear(); return out; } + if (!readPOD(is, e.baseDepositRateBp) || + !readPOD(is, e.houseCutRateBp) || + !readPOD(is, e.maxBidCopper) || + !readPOD(is, e.shortHours) || + !readPOD(is, e.mediumHours) || + !readPOD(is, e.longHours)) { + out.entries.clear(); return out; + } + uint8_t pad2[2]; + is.read(reinterpret_cast(pad2), 2); + if (is.gcount() != 2) { out.entries.clear(); return out; } + if (!readPOD(is, e.shortMultBp) || + !readPOD(is, e.mediumMultBp) || + !readPOD(is, e.longMultBp) || + !readPOD(is, e.disallowedClassMask)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeAuctionLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeAuction WoweeAuctionLoader::makeStarter(const std::string& catalogName) { + WoweeAuction c; + c.name = catalogName; + { + WoweeAuction::Entry e; + e.houseId = 1; e.name = "Starter House"; + e.factionAccess = WoweeAuction::Neutral; + c.entries.push_back(e); + } + return c; +} + +WoweeAuction WoweeAuctionLoader::makeFactionPair(const std::string& catalogName) { + WoweeAuction c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t fac, + uint32_t cutBp, uint32_t auctioneerNpc) { + WoweeAuction::Entry e; + e.houseId = id; e.name = name; + e.factionAccess = fac; + e.houseCutRateBp = cutBp; + e.auctioneerNpcId = auctioneerNpc; + c.entries.push_back(e); + }; + // Faction houses charge 5%; neutral charges the canonical + // 15% premium for cross-faction trade. + add(1, "Stormwind", WoweeAuction::Alliance, 500, 8719); + add(2, "Orgrimmar", WoweeAuction::Horde, 500, 8718); + add(3, "Booty Bay", WoweeAuction::Neutral, 1500, 2622); + return c; +} + +WoweeAuction WoweeAuctionLoader::makeRestricted(const std::string& catalogName) { + WoweeAuction c; + c.name = catalogName; + { + WoweeAuction::Entry e; + e.houseId = 100; e.name = "Restricted House"; + e.factionAccess = WoweeAuction::Both; + // Disallow Quest items (class 12) and Containers (class 1) + // and Keys (class 13) — bitmask combination. + e.disallowedClassMask = (1u << 1) | (1u << 12) | (1u << 13); + // Tighter durations + lower max bid for testing. + e.shortHours = 2; + e.mediumHours = 6; + e.longHours = 12; + e.maxBidCopper = 10000000; // 1000g cap + c.entries.push_back(e); + } + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 43c9ab02..82142b0e 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -112,6 +112,8 @@ const char* const kArgRequired[] = { "--export-wpcd-json", "--import-wpcd-json", "--gen-pets", "--gen-pets-hunter", "--gen-pets-warlock", "--info-wpet", "--validate-wpet", + "--gen-auction", "--gen-auction-pair", "--gen-auction-restricted", + "--info-wauc", "--validate-wauc", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_auction_catalog.cpp b/tools/editor/cli_auction_catalog.cpp new file mode 100644 index 00000000..aefc2371 --- /dev/null +++ b/tools/editor/cli_auction_catalog.cpp @@ -0,0 +1,239 @@ +#include "cli_auction_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_auction.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWaucExt(std::string base) { + stripExt(base, ".wauc"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeAuction& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeAuctionLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wauc\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeAuction& c, + const std::string& base) { + std::printf("Wrote %s.wauc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" houses : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterAuction"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWaucExt(base); + auto c = wowee::pipeline::WoweeAuctionLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-auction")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenPair(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "FactionPairAuction"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWaucExt(base); + auto c = wowee::pipeline::WoweeAuctionLoader::makeFactionPair(name); + if (!saveOrError(c, base, "gen-auction-pair")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenRestricted(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "RestrictedAuction"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWaucExt(base); + auto c = wowee::pipeline::WoweeAuctionLoader::makeRestricted(name); + if (!saveOrError(c, base, "gen-auction-restricted")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleInfo(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWaucExt(base); + if (!wowee::pipeline::WoweeAuctionLoader::exists(base)) { + std::fprintf(stderr, "WAUC not found: %s.wauc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAuctionLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wauc"] = base + ".wauc"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"houseId", e.houseId}, + {"auctioneerNpcId", e.auctioneerNpcId}, + {"name", e.name}, + {"factionAccess", e.factionAccess}, + {"factionAccessName", wowee::pipeline::WoweeAuction::factionAccessName(e.factionAccess)}, + {"baseDepositRateBp", e.baseDepositRateBp}, + {"houseCutRateBp", e.houseCutRateBp}, + {"maxBidCopper", e.maxBidCopper}, + {"shortHours", e.shortHours}, + {"mediumHours", e.mediumHours}, + {"longHours", e.longHours}, + {"shortMultBp", e.shortMultBp}, + {"mediumMultBp", e.mediumMultBp}, + {"longMultBp", e.longMultBp}, + {"disallowedClassMask", e.disallowedClassMask}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WAUC: %s.wauc\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" houses : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id faction deposit%% cut%% durations(h) disallow npc\n"); + for (const auto& e : c.entries) { + float depPct = e.baseDepositRateBp / 100.0f; + float cutPct = e.houseCutRateBp / 100.0f; + std::printf(" %4u %-8s %5.2f %4.2f %3u/%3u/%3u 0x%-6x %5u %s\n", + e.houseId, + wowee::pipeline::WoweeAuction::factionAccessName(e.factionAccess), + depPct, cutPct, + e.shortHours, e.mediumHours, e.longHours, + e.disallowedClassMask, e.auctioneerNpcId, + e.name.c_str()); + } + return 0; +} + +int handleValidate(int& i, int argc, char** argv) { + std::string base = argv[++i]; + bool jsonOut = consumeJsonFlag(i, argc, argv); + base = stripWaucExt(base); + if (!wowee::pipeline::WoweeAuctionLoader::exists(base)) { + std::fprintf(stderr, + "validate-wauc: WAUC not found: %s.wauc\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeAuctionLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::vector idsSeen; + for (size_t k = 0; k < c.entries.size(); ++k) { + const auto& e = c.entries[k]; + std::string ctx = "entry " + std::to_string(k) + + " (id=" + std::to_string(e.houseId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.houseId == 0) errors.push_back(ctx + ": houseId is 0"); + if (e.name.empty()) errors.push_back(ctx + ": name is empty"); + if (e.factionAccess > wowee::pipeline::WoweeAuction::Both) { + errors.push_back(ctx + ": factionAccess " + + std::to_string(e.factionAccess) + " not in 0..3"); + } + if (e.shortHours == 0 || e.mediumHours == 0 || e.longHours == 0) { + errors.push_back(ctx + ": duration tier is 0 (no listing time)"); + } + if (e.shortHours > e.mediumHours || + e.mediumHours > e.longHours) { + errors.push_back(ctx + + ": durations must satisfy short <= medium <= long"); + } + // Cut rate > 50% is suspicious; rates > 100% mean the + // seller pays the house more than the buyer paid. + if (e.houseCutRateBp > 5000) { + warnings.push_back(ctx + + ": houseCutRateBp > 5000 (>50% cut — verify intentional)"); + } + if (e.houseCutRateBp >= wowee::pipeline::WoweeAuction::kBpDenominator) { + errors.push_back(ctx + + ": houseCutRateBp >= 10000 (>=100% cut — seller loses money)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.houseId) { + errors.push_back(ctx + ": duplicate houseId"); + break; + } + } + idsSeen.push_back(e.houseId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wauc"] = base + ".wauc"; + j["ok"] = ok; + j["errors"] = errors; + j["warnings"] = warnings; + std::printf("%s\n", j.dump(2).c_str()); + return ok ? 0 : 1; + } + std::printf("validate-wauc: %s.wauc\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu houses, all houseIds unique\n", + c.entries.size()); + return 0; + } + if (!warnings.empty()) { + std::printf(" warnings (%zu):\n", warnings.size()); + for (const auto& w : warnings) + std::printf(" - %s\n", w.c_str()); + } + if (!errors.empty()) { + std::printf(" ERRORS (%zu):\n", errors.size()); + for (const auto& e : errors) + std::printf(" - %s\n", e.c_str()); + } + return ok ? 0 : 1; +} + +} // namespace + +bool handleAuctionCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-auction") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-auction-pair") == 0 && i + 1 < argc) { + outRc = handleGenPair(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-auction-restricted") == 0 && i + 1 < argc) { + outRc = handleGenRestricted(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wauc") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wauc") == 0 && i + 1 < argc) { + outRc = handleValidate(i, argc, argv); return true; + } + return false; +} + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_auction_catalog.hpp b/tools/editor/cli_auction_catalog.hpp new file mode 100644 index 00000000..c11213d1 --- /dev/null +++ b/tools/editor/cli_auction_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleAuctionCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index cd7bb82f..c1ac4916 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -66,6 +66,7 @@ #include "cli_guilds_catalog.hpp" #include "cli_conditions_catalog.hpp" #include "cli_pets_catalog.hpp" +#include "cli_auction_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -173,6 +174,7 @@ constexpr DispatchFn kDispatchTable[] = { handleGuildsCatalog, handleConditionsCatalog, handlePetsCatalog, + handleAuctionCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 578d8191..a5f71c4d 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1245,6 +1245,16 @@ void printUsage(const char* argv0) { std::printf(" Print WPET families (id / petType / atkSpd / dmg+arm mult / diet) + minions (id / summon / creatureId)\n"); std::printf(" --validate-wpet [--json]\n"); std::printf(" Static checks: ids>0+unique, name not empty, petType 0..2, atkSpeed>0, minion needs summon+creatureId\n"); + std::printf(" --gen-auction [name]\n"); + std::printf(" Emit .wauc starter: 1 neutral house with stock 12h/24h/48h tiers and 5%% house cut\n"); + std::printf(" --gen-auction-pair [name]\n"); + std::printf(" Emit .wauc 3 houses (Stormwind / Orgrimmar / Booty Bay) — faction 5%% cut vs neutral 15%% asymmetry\n"); + std::printf(" --gen-auction-restricted [name]\n"); + std::printf(" Emit .wauc 1 house with disallowedClassMask (no Containers/Quest/Keys) + tighter durations + bid cap\n"); + std::printf(" --info-wauc [--json]\n"); + std::printf(" Print WAUC houses (id / faction / deposit + cut percent / 3 duration tiers / disallow mask / npc)\n"); + std::printf(" --validate-wauc [--json]\n"); + std::printf(" Static checks: id>0+unique, faction 0..3, durations short<=medium<=long, cut < 100%%\n"); std::printf(" --gen-weather-temperate [zoneName]\n"); std::printf(" Emit .wow weather schedule: clear-dominant + occasional rain + fog (forest / grassland)\n"); std::printf(" --gen-weather-arctic [zoneName]\n");