From d537d7163ecb5e635af5f745fcb7f854ff9ca412 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 9 May 2026 14:10:13 -0700 Subject: [PATCH] feat(pipeline): add Wowee Open Weather (.wow) zone schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8th open-format addition to the Wowee pipeline. Replaces WoW's WeatherTypes.dbc / WeatherEffect logic with a single binary file holding a list of weather states for one zone, each tagged with intensity bounds, a probability weight, and duration bounds. The renderer / runtime samples one entry at a time using weighted-random selection, drives it for a uniform-random duration in [min, max] sec, then re-rolls. • Types: Clear / Rain / Snow / Storm / Sandstorm / Fog / Blizzard (extensible enum). • Binary format: magic "WOWA", version 1, name, N entries each storing (typeId, minIntensity, maxIntensity, weight, minDurationSec, maxDurationSec). CLI: • --info-wow [--json] — inspect a WOW • --gen-weather-temperate — clear + rain + fog (forest) • --gen-weather-arctic — snow + blizzard + fog (tundra) • --gen-weather-desert — clear + sandstorm (dunes) • --gen-weather-stormy — rain + storm + occasional clear The 8th open format complementing the rest: M2 → WOM | WMO → WOB | WMO collision → WOC | ADT → WOT DBC → JsonDBC | BLP → PNG | Light.dbc → WOL | WeatherTypes.dbc → WOW Smoke-tested all 4 presets + JSON output. Each preset reads back identically with the expected entry count and weight distribution. --- CMakeLists.txt | 2 + include/pipeline/wowee_weather.hpp | 82 +++++++++++++++ src/pipeline/wowee_weather.cpp | 155 +++++++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_help.cpp | 10 ++ tools/editor/cli_world_info.cpp | 127 +++++++++++++++++++++++ 6 files changed, 378 insertions(+) create mode 100644 include/pipeline/wowee_weather.hpp create mode 100644 src/pipeline/wowee_weather.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f5bbb6e..75881d99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -595,6 +595,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_building.cpp src/pipeline/wowee_collision.cpp src/pipeline/wowee_light.cpp + src/pipeline/wowee_weather.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1410,6 +1411,7 @@ add_executable(wowee_editor src/pipeline/wowee_building.cpp src/pipeline/wowee_collision.cpp src/pipeline/wowee_light.cpp + src/pipeline/wowee_weather.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_weather.hpp b/include/pipeline/wowee_weather.hpp new file mode 100644 index 00000000..1a479688 --- /dev/null +++ b/include/pipeline/wowee_weather.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Weather format (.wow) — novel replacement for WoW's +// WeatherTypes.dbc / WeatherEffect logic. A WOW file holds a list +// of weather states for one zone (clear / rain / snow / fog / etc.) +// each tagged with intensity bounds, probability weight, and +// duration bounds. The renderer / game runtime samples one entry +// at a time using weighted-random selection, drives it for a +// uniform-random duration in [minDurationSec, maxDurationSec], +// then re-rolls. +// +// Binary layout (little-endian): +// magic[4] = "WOWA" +// version (uint32) = current 1 +// nameLen (uint32) + name bytes +// entryCount (uint32) +// entries (each): +// weatherTypeId (uint32) +// minIntensity (float) +// maxIntensity (float) +// weight (float) -- probability share in selection +// minDurationSec (uint32) +// maxDurationSec (uint32) +struct WoweeWeather { + enum Type : uint32_t { + Clear = 0, + Rain = 1, + Snow = 2, + Storm = 3, // rain + lightning + Sandstorm = 4, + Fog = 5, + Blizzard = 6, + }; + + struct Entry { + uint32_t weatherTypeId = Clear; + float minIntensity = 0.0f; // 0..1 + float maxIntensity = 1.0f; + float weight = 1.0f; // selection probability share + uint32_t minDurationSec = 60; + uint32_t maxDurationSec = 600; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + // Total weight across all entries — handy for normalizing + // selection probabilities at the call site. + float totalWeight() const; + + static const char* typeName(uint32_t typeId); +}; + +class WoweeWeatherLoader { +public: + static bool save(const WoweeWeather& weather, + const std::string& basePath); + static WoweeWeather load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-weather variants. + // makeTemperate — clear-dominant + occasional rain + fog + // makeArctic — snow-dominant + blizzard + fog + // makeDesert — clear-dominant + sandstorm + // makeStormy — heavy rain + storm + occasional clear + static WoweeWeather makeTemperate(const std::string& zoneName); + static WoweeWeather makeArctic(const std::string& zoneName); + static WoweeWeather makeDesert(const std::string& zoneName); + static WoweeWeather makeStormy(const std::string& zoneName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_weather.cpp b/src/pipeline/wowee_weather.cpp new file mode 100644 index 00000000..2af619c7 --- /dev/null +++ b/src/pipeline/wowee_weather.cpp @@ -0,0 +1,155 @@ +#include "pipeline/wowee_weather.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'O', 'W', 'A'}; +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)); +} + +std::string normalizePath(std::string base) { + if (base.size() < 4 || base.substr(base.size() - 4) != ".wow") { + base += ".wow"; + } + return base; +} + +} // namespace + +float WoweeWeather::totalWeight() const { + float t = 0.0f; + for (const auto& e : entries) t += e.weight; + return t; +} + +const char* WoweeWeather::typeName(uint32_t typeId) { + switch (typeId) { + case Clear: return "clear"; + case Rain: return "rain"; + case Snow: return "snow"; + case Storm: return "storm"; + case Sandstorm: return "sandstorm"; + case Fog: return "fog"; + case Blizzard: return "blizzard"; + default: return "unknown"; + } +} + +bool WoweeWeatherLoader::save(const WoweeWeather& w, + const std::string& basePath) { + std::ofstream os(normalizePath(basePath), std::ios::binary); + if (!os) return false; + os.write(kMagic, 4); + writePOD(os, kVersion); + uint32_t nameLen = static_cast(w.name.size()); + writePOD(os, nameLen); + if (nameLen > 0) os.write(w.name.data(), nameLen); + uint32_t entryCount = static_cast(w.entries.size()); + writePOD(os, entryCount); + for (const auto& e : w.entries) { + writePOD(os, e.weatherTypeId); + writePOD(os, e.minIntensity); + writePOD(os, e.maxIntensity); + writePOD(os, e.weight); + writePOD(os, e.minDurationSec); + writePOD(os, e.maxDurationSec); + } + return os.good(); +} + +WoweeWeather WoweeWeatherLoader::load(const std::string& basePath) { + WoweeWeather 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; + uint32_t nameLen = 0; + if (!readPOD(is, nameLen)) return out; + if (nameLen > 0) { + out.name.resize(nameLen); + is.read(out.name.data(), nameLen); + if (is.gcount() != static_cast(nameLen)) { + out.name.clear(); + return out; + } + } + uint32_t entryCount = 0; + if (!readPOD(is, entryCount)) return out; + out.entries.resize(entryCount); + for (auto& e : out.entries) { + if (!readPOD(is, e.weatherTypeId) || + !readPOD(is, e.minIntensity) || + !readPOD(is, e.maxIntensity) || + !readPOD(is, e.weight) || + !readPOD(is, e.minDurationSec) || + !readPOD(is, e.maxDurationSec)) { + out.entries.clear(); + return out; + } + } + return out; +} + +bool WoweeWeatherLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeWeather WoweeWeatherLoader::makeTemperate(const std::string& zoneName) { + WoweeWeather w; + w.name = zoneName; + w.entries.push_back({WoweeWeather::Clear, 0.0f, 0.0f, 6.0f, 300, 1800}); + w.entries.push_back({WoweeWeather::Rain, 0.3f, 0.7f, 2.0f, 120, 900}); + w.entries.push_back({WoweeWeather::Fog, 0.4f, 0.8f, 1.0f, 180, 600}); + return w; +} + +WoweeWeather WoweeWeatherLoader::makeArctic(const std::string& zoneName) { + WoweeWeather w; + w.name = zoneName; + w.entries.push_back({WoweeWeather::Snow, 0.3f, 0.7f, 5.0f, 300, 1800}); + w.entries.push_back({WoweeWeather::Blizzard, 0.7f, 1.0f, 2.0f, 120, 600}); + w.entries.push_back({WoweeWeather::Fog, 0.5f, 0.9f, 2.0f, 180, 900}); + w.entries.push_back({WoweeWeather::Clear, 0.0f, 0.0f, 1.0f, 180, 600}); + return w; +} + +WoweeWeather WoweeWeatherLoader::makeDesert(const std::string& zoneName) { + WoweeWeather w; + w.name = zoneName; + w.entries.push_back({WoweeWeather::Clear, 0.0f, 0.0f, 8.0f, 600, 2400}); + w.entries.push_back({WoweeWeather::Sandstorm, 0.5f, 0.9f, 2.0f, 120, 600}); + return w; +} + +WoweeWeather WoweeWeatherLoader::makeStormy(const std::string& zoneName) { + WoweeWeather w; + w.name = zoneName; + w.entries.push_back({WoweeWeather::Rain, 0.5f, 0.9f, 5.0f, 300, 1200}); + w.entries.push_back({WoweeWeather::Storm, 0.6f, 1.0f, 3.0f, 180, 600}); + w.entries.push_back({WoweeWeather::Fog, 0.4f, 0.7f, 1.0f, 120, 300}); + w.entries.push_back({WoweeWeather::Clear, 0.0f, 0.0f, 1.0f, 60, 240}); + return w; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 568aae5b..c64393d1 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -17,6 +17,8 @@ const char* const kArgRequired[] = { "--info-wob", "--info-wob-stats", "--info-woc", "--info-wot", "--info-wol", "--info-wol-at", "--validate-wol", "--gen-light", "--gen-light-cave", "--gen-light-dungeon", "--gen-light-night", + "--info-wow", "--gen-weather-temperate", "--gen-weather-arctic", + "--gen-weather-desert", "--gen-weather-stormy", "--info-creatures", "--info-objects", "--info-quests", "--info-extract", "--info-extract-tree", "--info-extract-budget", "--list-missing-sidecars", diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index c295deb7..1c6a484a 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -791,6 +791,16 @@ void printUsage(const char* argv0) { std::printf(" Emit a single-keyframe .wol with warm torchlit ambient + medium fog (dungeon / crypt interior)\n"); std::printf(" --gen-light-night [zoneName]\n"); std::printf(" Emit a single-keyframe .wol with moonlit directional + far fog (always-night zone / shadow realm)\n"); + std::printf(" --info-wow [--json]\n"); + std::printf(" Print WOW weather entries (zone + per-state type / intensity / weight / duration) and exit\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"); + std::printf(" Emit .wow weather schedule: snow-dominant + blizzard + fog (tundra / glacier)\n"); + std::printf(" --gen-weather-desert [zoneName]\n"); + std::printf(" Emit .wow weather schedule: clear-dominant + sandstorm (dunes / wasteland)\n"); + std::printf(" --gen-weather-stormy [zoneName]\n"); + std::printf(" Emit .wow weather schedule: heavy rain + storm + occasional clear (coastal / monsoon)\n"); std::printf(" --info-wot [--json]\n"); std::printf(" Print WOT/WHM terrain metadata (tile, chunks, height range) and exit\n"); std::printf(" --info-extract [--json]\n"); diff --git a/tools/editor/cli_world_info.cpp b/tools/editor/cli_world_info.cpp index fbc250ab..f3141a3e 100644 --- a/tools/editor/cli_world_info.cpp +++ b/tools/editor/cli_world_info.cpp @@ -4,6 +4,7 @@ #include "pipeline/wowee_building.hpp" #include "pipeline/wowee_collision.hpp" #include "pipeline/wowee_light.hpp" +#include "pipeline/wowee_weather.hpp" #include "pipeline/wowee_terrain_loader.hpp" #include "pipeline/adt_loader.hpp" #include @@ -581,6 +582,117 @@ int handleGenLightNight(int& i, int argc, char** argv) { "moonlit directional + far fog"); } +int handleInfoWow(int& i, int argc, char** argv) { + // Inspect a Wowee Open Weather (.wow) file: zone name + + // per-entry weather type + intensity bounds + selection + // weight + duration bounds. + std::string base = argv[++i]; + bool jsonOut = (i + 1 < argc && + std::strcmp(argv[i + 1], "--json") == 0); + if (jsonOut) ++i; + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wow") + base = base.substr(0, base.size() - 4); + if (!wowee::pipeline::WoweeWeatherLoader::exists(base)) { + std::fprintf(stderr, "WOW not found: %s.wow\n", base.c_str()); + return 1; + } + auto wow = wowee::pipeline::WoweeWeatherLoader::load(base); + if (!wow.isValid()) { + std::fprintf(stderr, "WOW parse failed: %s.wow\n", base.c_str()); + return 1; + } + if (jsonOut) { + nlohmann::json j; + j["wow"] = base + ".wow"; + j["name"] = wow.name; + j["entryCount"] = wow.entries.size(); + j["totalWeight"] = wow.totalWeight(); + nlohmann::json es = nlohmann::json::array(); + for (const auto& e : wow.entries) { + es.push_back({ + {"type", wowee::pipeline::WoweeWeather::typeName( + e.weatherTypeId)}, + {"typeId", e.weatherTypeId}, + {"minIntensity", e.minIntensity}, + {"maxIntensity", e.maxIntensity}, + {"weight", e.weight}, + {"minDurationSec", e.minDurationSec}, + {"maxDurationSec", e.maxDurationSec}, + }); + } + j["entries"] = es; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WOW: %s.wow\n", base.c_str()); + std::printf(" zone : %s\n", wow.name.c_str()); + std::printf(" entries : %zu (totalWeight=%.2f)\n", + wow.entries.size(), wow.totalWeight()); + for (std::size_t k = 0; k < wow.entries.size(); ++k) { + const auto& e = wow.entries[k]; + std::printf(" [%zu] %-9s intensity %.2f..%.2f weight %.2f " + "duration %u..%u s\n", + k, + wowee::pipeline::WoweeWeather::typeName(e.weatherTypeId), + e.minIntensity, e.maxIntensity, e.weight, + e.minDurationSec, e.maxDurationSec); + } + return 0; +} + +int emitWeatherPreset(const std::string& cmdName, + int& i, int argc, char** argv, + wowee::pipeline::WoweeWeather (*maker)(const std::string&), + const char* presetDescription) { + std::string base = argv[++i]; + std::string zoneName = "Default"; + if (i + 1 < argc && argv[i + 1][0] != '-') { + zoneName = argv[++i]; + } + if (base.size() >= 4 && base.substr(base.size() - 4) == ".wow") { + base = base.substr(0, base.size() - 4); + } + auto wow = maker(zoneName); + if (!wowee::pipeline::WoweeWeatherLoader::save(wow, base)) { + std::fprintf(stderr, "%s: failed to save %s.wow\n", + cmdName.c_str(), base.c_str()); + return 1; + } + std::printf("Wrote %s.wow\n", base.c_str()); + std::printf(" zone : %s\n", zoneName.c_str()); + std::printf(" preset : %s (%zu entries)\n", + presetDescription, wow.entries.size()); + return 0; +} + +int handleGenWeatherTemperate(int& i, int argc, char** argv) { + return emitWeatherPreset( + "gen-weather-temperate", i, argc, argv, + wowee::pipeline::WoweeWeatherLoader::makeTemperate, + "clear-dominant + occasional rain + fog"); +} + +int handleGenWeatherArctic(int& i, int argc, char** argv) { + return emitWeatherPreset( + "gen-weather-arctic", i, argc, argv, + wowee::pipeline::WoweeWeatherLoader::makeArctic, + "snow-dominant + blizzard + fog"); +} + +int handleGenWeatherDesert(int& i, int argc, char** argv) { + return emitWeatherPreset( + "gen-weather-desert", i, argc, argv, + wowee::pipeline::WoweeWeatherLoader::makeDesert, + "clear-dominant + sandstorm"); +} + +int handleGenWeatherStormy(int& i, int argc, char** argv) { + return emitWeatherPreset( + "gen-weather-stormy", i, argc, argv, + wowee::pipeline::WoweeWeatherLoader::makeStormy, + "heavy rain + storm + occasional clear"); +} + } // namespace bool handleWorldInfo(int& i, int argc, char** argv, int& outRc) { @@ -617,6 +729,21 @@ bool handleWorldInfo(int& i, int argc, char** argv, int& outRc) { if (std::strcmp(argv[i], "--gen-light-night") == 0 && i + 1 < argc) { outRc = handleGenLightNight(i, argc, argv); return true; } + if (std::strcmp(argv[i], "--info-wow") == 0 && i + 1 < argc) { + outRc = handleInfoWow(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-weather-temperate") == 0 && i + 1 < argc) { + outRc = handleGenWeatherTemperate(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-weather-arctic") == 0 && i + 1 < argc) { + outRc = handleGenWeatherArctic(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-weather-desert") == 0 && i + 1 < argc) { + outRc = handleGenWeatherDesert(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-weather-stormy") == 0 && i + 1 < argc) { + outRc = handleGenWeatherStormy(i, argc, argv); return true; + } return false; }