From 0016b0d597183cf6803af8a6d3254d3c0a972fb2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 10 May 2026 02:51:23 -0700 Subject: [PATCH] =?UTF-8?q?feat(editor):=20add=20WSKP=20(Sky=20Parameters)?= =?UTF-8?q?=20=E2=80=94=20119th=20open=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Novel replacement for the LightParams.dbc + Light.dbc pair vanilla WoW used to drive the per-zone diurnal sky cycle. Each entry binds one (mapId, areaId, timeOfDayHour) triplet to its sky-rendering parameters: sky-dome zenith and horizon colors, sun angle and color, fog start/end distances, cloud-layer opacity, and cloud drift speed in tenths-mph. The renderer interpolates between adjacent keyframes when the in-game clock crosses an hour boundary, so a 4-keyframe set (Dawn/Noon/Dusk/Midnight) produces the full diurnal cycle through linear interpolation. Servers can author finer-grained keyframes (e.g. every 3 hours) for smoother transitions. Three preset emitters demonstrating the catalog's range: makeStormwindDay (4 standard temperate keyframes from lavender dawn through bright noon to deep blue-black midnight), makeNorthrendArctic (4 cold steel-blue keyframes with high-density ice fog peaking at the midnight blizzard whiteout — minimum 30yd visibility), makeOutlandHellfire (3 keyframes — no midnight, since Outland's permanent gravitational anomaly from the Twisting Nether keeps the sky lit; iconic crimson + orange palette throughout). Validator's most novel checks: per-(mapId, areaId, timeOfDayHour) triple uniqueness — two keyframes at the same hour for the same area would render in unstable order during diurnal interpolation. Plus fogStartYards >= fogEndYards (inverted falloff) error, sunAngleDeg outside [0,360] warning (renderer wraps modulo but suggests authoring confusion). Format count 118 -> 119. CLI flag count 1255 -> 1260. --- CMakeLists.txt | 3 + include/pipeline/wowee_sky_params.hpp | 121 +++++++++ src/pipeline/wowee_sky_params.cpp | 347 ++++++++++++++++++++++++ tools/editor/cli_arg_required.cpp | 2 + tools/editor/cli_dispatch.cpp | 2 + tools/editor/cli_format_table.cpp | 1 + tools/editor/cli_help.cpp | 10 + tools/editor/cli_list_formats.cpp | 1 + tools/editor/cli_sky_params_catalog.cpp | 272 +++++++++++++++++++ tools/editor/cli_sky_params_catalog.hpp | 12 + 10 files changed, 771 insertions(+) create mode 100644 include/pipeline/wowee_sky_params.hpp create mode 100644 src/pipeline/wowee_sky_params.cpp create mode 100644 tools/editor/cli_sky_params_catalog.cpp create mode 100644 tools/editor/cli_sky_params_catalog.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 31a1162c..83d9dd3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -707,6 +707,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_word_filters.cpp src/pipeline/wowee_raid_markers.cpp src/pipeline/wowee_loot_modes.cpp + src/pipeline/wowee_sky_params.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1577,6 +1578,7 @@ add_executable(wowee_editor tools/editor/cli_word_filters_catalog.cpp tools/editor/cli_raid_markers_catalog.cpp tools/editor/cli_loot_modes_catalog.cpp + tools/editor/cli_sky_params_catalog.cpp tools/editor/cli_catalog_pluck.cpp tools/editor/cli_catalog_find.cpp tools/editor/cli_catalog_by_name.cpp @@ -1766,6 +1768,7 @@ add_executable(wowee_editor src/pipeline/wowee_word_filters.cpp src/pipeline/wowee_raid_markers.cpp src/pipeline/wowee_loot_modes.cpp + src/pipeline/wowee_sky_params.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_sky_params.hpp b/include/pipeline/wowee_sky_params.hpp new file mode 100644 index 00000000..65de1fe6 --- /dev/null +++ b/include/pipeline/wowee_sky_params.hpp @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Sky Parameters catalog (.wskp) — novel +// replacement for the LightParams.dbc + Light.dbc pair +// that vanilla WoW used to drive the per-zone diurnal +// sky cycle: sky-dome zenith and horizon colors, sun +// angle and color, fog falloff distances, cloud layer +// opacity and drift speed. Each entry binds one +// (mapId, areaId, timeOfDayHour) triplet to its sky +// rendering parameters; the renderer interpolates +// between adjacent entries when the in-game clock +// crosses an hour boundary. +// +// Cross-references with previously-added formats: +// WMS: mapId references the WMS map catalog; +// areaId references the WMS sub-area entry. +// WOLA: WSKP supersedes WOLA's outdoor-light catalog +// where the latter exists; WOLA remains for +// backward-compat with engine code that hasn't +// migrated yet. +// +// Binary layout (little-endian): +// magic[4] = "WSKP" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// skyId (uint32) +// nameLen + name +// descLen + description +// mapId (uint32) / areaId (uint32) +// timeOfDayHour (uint8) — 0..23 keyframe hour +// pad0 (uint8) / pad1 (uint8) / pad2 (uint8) +// zenithColor (uint32) — RGBA top-of-sky +// horizonColor (uint32) — RGBA at horizon +// sunColor (uint32) — RGBA sun disc tint +// sunAngleDeg (float) — 0..360 azimuth +// fogStartYards (float) — distance fog begins +// fogEndYards (float) — distance fog opaque +// cloudOpacity (uint8) — 0..255 cloud layer +// alpha +// cloudSpeedX10 (uint8) — wind speed in +// tenths-mph (0..255 +// = 0..25.5 mph drift +// rate for the cloud +// layer) +// pad3 (uint8) / pad4 (uint8) +// iconColorRGBA (uint32) +struct WoweeSkyParams { + struct Entry { + uint32_t skyId = 0; + std::string name; + std::string description; + uint32_t mapId = 0; + uint32_t areaId = 0; + uint8_t timeOfDayHour = 12; + uint8_t pad0 = 0; + uint8_t pad1 = 0; + uint8_t pad2 = 0; + uint32_t zenithColor = 0xFF000000u; + uint32_t horizonColor = 0xFF000000u; + uint32_t sunColor = 0xFFFFFFFFu; + float sunAngleDeg = 0.0f; + float fogStartYards = 100.0f; + float fogEndYards = 500.0f; + uint8_t cloudOpacity = 128; + uint8_t cloudSpeedX10 = 30; // 3.0 mph + uint8_t pad3 = 0; + uint8_t pad4 = 0; + uint32_t iconColorRGBA = 0xFFFFFFFFu; + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t skyId) const; + + // Returns all entries for one (map, area) sorted by + // hour. Used by the sky renderer to build the + // diurnal interpolation curve at zone load time. + std::vector findByArea(uint32_t mapId, + uint32_t areaId) const; +}; + +class WoweeSkyParamsLoader { +public: + static bool save(const WoweeSkyParams& cat, + const std::string& basePath); + static WoweeSkyParams load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-skp* variants. + // + // makeStormwindDay — 4 keyframes for Stormwind's + // diurnal cycle (Dawn 6AM / + // Noon 12 / Dusk 18 / + // Midnight 0). + // makeNorthrendArctic — 4 cold steel-blue keyframes + // for Northrend zones (Dawn / + // Noon / Dusk / Midnight). + // makeOutlandHellfire — 3 keyframes for Outland's + // iconic red/orange skies + // (Dawn / Noon / Sunset; no + // midnight keyframe — Outland + // is permanently bright). + static WoweeSkyParams makeStormwindDay(const std::string& catalogName); + static WoweeSkyParams makeNorthrendArctic(const std::string& catalogName); + static WoweeSkyParams makeOutlandHellfire(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_sky_params.cpp b/src/pipeline/wowee_sky_params.cpp new file mode 100644 index 00000000..1744001d --- /dev/null +++ b/src/pipeline/wowee_sky_params.cpp @@ -0,0 +1,347 @@ +#include "pipeline/wowee_sky_params.hpp" + +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'S', 'K', 'P'}; +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) != ".wskp") { + base += ".wskp"; + } + return base; +} + +uint32_t packRgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 0xFF) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(g) << 8) | + static_cast(r); +} + +} // namespace + +const WoweeSkyParams::Entry* +WoweeSkyParams::findById(uint32_t skyId) const { + for (const auto& e : entries) + if (e.skyId == skyId) return &e; + return nullptr; +} + +std::vector +WoweeSkyParams::findByArea(uint32_t mapId, + uint32_t areaId) const { + std::vector out; + for (const auto& e : entries) { + if (e.mapId == mapId && e.areaId == areaId) + out.push_back(&e); + } + std::sort(out.begin(), out.end(), + [](const Entry* a, const Entry* b) { + return a->timeOfDayHour < b->timeOfDayHour; + }); + return out; +} + +bool WoweeSkyParamsLoader::save(const WoweeSkyParams& 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.skyId); + writeStr(os, e.name); + writeStr(os, e.description); + writePOD(os, e.mapId); + writePOD(os, e.areaId); + writePOD(os, e.timeOfDayHour); + writePOD(os, e.pad0); + writePOD(os, e.pad1); + writePOD(os, e.pad2); + writePOD(os, e.zenithColor); + writePOD(os, e.horizonColor); + writePOD(os, e.sunColor); + writePOD(os, e.sunAngleDeg); + writePOD(os, e.fogStartYards); + writePOD(os, e.fogEndYards); + writePOD(os, e.cloudOpacity); + writePOD(os, e.cloudSpeedX10); + writePOD(os, e.pad3); + writePOD(os, e.pad4); + writePOD(os, e.iconColorRGBA); + } + return os.good(); +} + +WoweeSkyParams WoweeSkyParamsLoader::load( + const std::string& basePath) { + WoweeSkyParams 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.skyId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.mapId) || + !readPOD(is, e.areaId) || + !readPOD(is, e.timeOfDayHour) || + !readPOD(is, e.pad0) || + !readPOD(is, e.pad1) || + !readPOD(is, e.pad2) || + !readPOD(is, e.zenithColor) || + !readPOD(is, e.horizonColor) || + !readPOD(is, e.sunColor) || + !readPOD(is, e.sunAngleDeg) || + !readPOD(is, e.fogStartYards) || + !readPOD(is, e.fogEndYards) || + !readPOD(is, e.cloudOpacity) || + !readPOD(is, e.cloudSpeedX10) || + !readPOD(is, e.pad3) || + !readPOD(is, e.pad4) || + !readPOD(is, e.iconColorRGBA)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeSkyParamsLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeSkyParams WoweeSkyParamsLoader::makeStormwindDay( + const std::string& catalogName) { + using S = WoweeSkyParams; + WoweeSkyParams c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t hour, + uint32_t zenith, uint32_t horizon, + uint32_t sun, float sunAngle, + float fogS, float fogE, + uint8_t cloudO, uint8_t cloudSp, + const char* desc) { + S::Entry e; + e.skyId = id; e.name = name; e.description = desc; + e.mapId = 0; // Eastern Kingdoms + e.areaId = 1519; // Stormwind City + e.timeOfDayHour = hour; + e.zenithColor = zenith; + e.horizonColor = horizon; + e.sunColor = sun; + e.sunAngleDeg = sunAngle; + e.fogStartYards = fogS; + e.fogEndYards = fogE; + e.cloudOpacity = cloudO; + e.cloudSpeedX10 = cloudSp; + e.iconColorRGBA = packRgba(140, 200, 255); // sky blue + c.entries.push_back(e); + }; + add(1, "StormwindDawn", 6, + packRgba( 80, 100, 180), // dawn lavender + packRgba(220, 160, 100), // peach horizon + packRgba(255, 220, 180), // warm sun + 90.0f, 80.0f, 600.0f, 100, 25, + "Stormwind 6AM — sun at horizon (90deg " + "azimuth). Lavender zenith fading to peach. " + "Light morning fog at 80yd."); + add(2, "StormwindNoon", 12, + packRgba( 60, 130, 220), // bright blue + packRgba(180, 210, 240), // pale horizon + packRgba(255, 250, 230), // bright sun + 180.0f, 200.0f, 800.0f, 80, 30, + "Stormwind noon — sun overhead (180deg). " + "Bright cyan-blue zenith, faint cloud layer."); + add(3, "StormwindDusk", 18, + packRgba(140, 100, 180), // dusk purple + packRgba(240, 140, 60), // orange horizon + packRgba(255, 180, 100), // sunset sun + 270.0f, 70.0f, 500.0f, 120, 35, + "Stormwind 6PM — sun setting at western " + "horizon. Purple zenith fading to orange. " + "Slightly heavier cloud layer."); + add(4, "StormwindMidnight", 0, + packRgba( 10, 20, 60), // deep blue-black + packRgba( 30, 40, 80), // navy horizon + packRgba(180, 180, 220), // moon + 180.0f, 60.0f, 400.0f, 60, 20, + "Stormwind midnight — moon at zenith. " + "Deep blue-black sky, short fog distance for " + "intimate night feel."); + return c; +} + +WoweeSkyParams WoweeSkyParamsLoader::makeNorthrendArctic( + const std::string& catalogName) { + using S = WoweeSkyParams; + WoweeSkyParams c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t hour, + uint32_t zenith, uint32_t horizon, + uint32_t sun, float sunAngle, + float fogS, float fogE, + uint8_t cloudO, uint8_t cloudSp, + const char* desc) { + S::Entry e; + e.skyId = id; e.name = name; e.description = desc; + e.mapId = 571; // Northrend + e.areaId = 4395; // Dalaran (placeholder + // for arctic zone) + e.timeOfDayHour = hour; + e.zenithColor = zenith; + e.horizonColor = horizon; + e.sunColor = sun; + e.sunAngleDeg = sunAngle; + e.fogStartYards = fogS; + e.fogEndYards = fogE; + e.cloudOpacity = cloudO; + e.cloudSpeedX10 = cloudSp; + e.iconColorRGBA = packRgba(180, 220, 240); // arctic ice + c.entries.push_back(e); + }; + add(100, "ArcticDawn", 6, + packRgba(140, 180, 220), + packRgba(220, 200, 220), + packRgba(220, 230, 240), + 90.0f, 50.0f, 350.0f, 200, 70, + "Arctic 6AM — pale steel-blue with weak peachy " + "horizon. Dense ice-fog at 50yd. Strong wind " + "(7mph cloud drift)."); + add(101, "ArcticNoon", 12, + packRgba(160, 200, 240), + packRgba(200, 220, 240), + packRgba(255, 255, 240), + 180.0f, 100.0f, 500.0f, 180, 60, + "Arctic noon — bright but flat steel-blue sky. " + "Snow glare from sun."); + add(102, "ArcticDusk", 18, + packRgba(100, 130, 180), + packRgba(180, 140, 160), + packRgba(220, 200, 200), + 270.0f, 40.0f, 300.0f, 220, 80, + "Arctic 6PM — pale violet zenith with washed-" + "rose horizon. Maximum fog density (300yd)."); + add(103, "ArcticMidnight", 0, + packRgba( 20, 30, 60), + packRgba( 40, 60, 80), + packRgba(200, 220, 240), + 180.0f, 30.0f, 250.0f, 240, 50, + "Arctic midnight — near-pitch dark with cold " + "moon. Minimum fog visibility (30yd start) — " + "blizzard-style whiteout."); + return c; +} + +WoweeSkyParams WoweeSkyParamsLoader::makeOutlandHellfire( + const std::string& catalogName) { + using S = WoweeSkyParams; + WoweeSkyParams c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t hour, + uint32_t zenith, uint32_t horizon, + uint32_t sun, float sunAngle, + float fogS, float fogE, + uint8_t cloudO, uint8_t cloudSp, + const char* desc) { + S::Entry e; + e.skyId = id; e.name = name; e.description = desc; + e.mapId = 530; // Outland + e.areaId = 3483; // Hellfire Peninsula + e.timeOfDayHour = hour; + e.zenithColor = zenith; + e.horizonColor = horizon; + e.sunColor = sun; + e.sunAngleDeg = sunAngle; + e.fogStartYards = fogS; + e.fogEndYards = fogE; + e.cloudOpacity = cloudO; + e.cloudSpeedX10 = cloudSp; + e.iconColorRGBA = packRgba(220, 80, 60); // hellfire red + c.entries.push_back(e); + }; + add(200, "OutlandDawn", 6, + packRgba(180, 60, 60), // crimson zenith + packRgba(240, 120, 60), // orange horizon + packRgba(255, 200, 100), + 90.0f, 250.0f, 1200.0f, 120, 40, + "Hellfire 6AM — sky is permanently smoke-tinged. " + "Crimson zenith with orange horizon. Long sight " + "distance (1200yd) — Outland is sparse."); + add(201, "OutlandNoon", 12, + packRgba(200, 100, 80), + packRgba(220, 160, 100), + packRgba(255, 230, 180), + 180.0f, 300.0f, 1500.0f, 100, 35, + "Hellfire noon — peak orange sky, bright sun " + "with maximum visibility."); + add(202, "OutlandSunset", 18, + packRgba(220, 80, 60), // scarlet + packRgba(240, 100, 40), // deep orange + packRgba(255, 160, 100), + 270.0f, 200.0f, 1000.0f, 140, 45, + "Hellfire sunset — most dramatic time, sky " + "fully scarlet. No midnight keyframe — Outland " + "is permanently lit by the gravitational anomaly " + "of the Twisting Nether visible at zenith."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 10ac804f..7eba701c 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -365,6 +365,8 @@ const char* const kArgRequired[] = { "--gen-lma", "--gen-lma-raid", "--gen-lma-afk", "--info-wlma", "--validate-wlma", "--export-wlma-json", "--import-wlma-json", + "--gen-skp", "--gen-skp-arctic", "--gen-skp-hellfire", + "--info-wskp", "--validate-wskp", "--gen-weather-temperate", "--gen-weather-arctic", "--gen-weather-desert", "--gen-weather-stormy", "--gen-zone-atmosphere", diff --git a/tools/editor/cli_dispatch.cpp b/tools/editor/cli_dispatch.cpp index 54311afd..c04054ee 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -163,6 +163,7 @@ #include "cli_word_filters_catalog.hpp" #include "cli_raid_markers_catalog.hpp" #include "cli_loot_modes_catalog.hpp" +#include "cli_sky_params_catalog.hpp" #include "cli_catalog_pluck.hpp" #include "cli_catalog_find.hpp" #include "cli_catalog_by_name.hpp" @@ -371,6 +372,7 @@ constexpr DispatchFn kDispatchTable[] = { handleWordFiltersCatalog, handleRaidMarkersCatalog, handleLootModesCatalog, + handleSkyParamsCatalog, handleCatalogPluck, handleCatalogFind, handleCatalogByName, diff --git a/tools/editor/cli_format_table.cpp b/tools/editor/cli_format_table.cpp index cde4f5c3..d310f7e7 100644 --- a/tools/editor/cli_format_table.cpp +++ b/tools/editor/cli_format_table.cpp @@ -121,6 +121,7 @@ constexpr FormatMagicEntry kFormats[] = { {{'W','W','F','L'}, ".wwfl", "social", "--info-wwfl", "Word filter catalog"}, {{'W','M','A','R'}, ".wmar", "ui", "--info-wmar", "Raid marker catalog"}, {{'W','L','M','A'}, ".wlma", "loot", "--info-wlma", "Loot mode policy catalog"}, + {{'W','S','K','P'}, ".wskp", "world", "--info-wskp", "Sky parameters catalog"}, {{'W','F','A','C'}, ".wfac", "factions", nullptr, "Faction catalog"}, {{'W','L','C','K'}, ".wlck", "locks", nullptr, "Lock catalog"}, {{'W','S','K','L'}, ".wskl", "skills", nullptr, "Skill catalog"}, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index 7139be01..ec5a85bb 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -2419,6 +2419,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .wlma to a human-editable JSON sidecar (defaults to .wlma.json; emits both modeKind and timeoutFallbackKind as int + name string; thresholdQuality also gets a derived qualityName string)\n"); std::printf(" --import-wlma-json [out-base]\n"); std::printf(" Import a .wlma.json sidecar back into binary .wlma (modeKind / timeoutFallbackKind int OR \"freeforall\"/\"roundrobin\"/\"masterloot\"/\"needbeforegreed\"/\"personal\"/\"disenchant\"; masterLooterRequired accepts bool OR int)\n"); + std::printf(" --gen-skp [name]\n"); + std::printf(" Emit .wskp 4 Stormwind diurnal sky keyframes (Dawn 6AM lavender / Noon 12PM bright blue / Dusk 6PM purple-orange / Midnight 12AM deep blue-black)\n"); + std::printf(" --gen-skp-arctic [name]\n"); + std::printf(" Emit .wskp 4 Northrend arctic sky keyframes (cold steel-blue palette across the diurnal cycle with high-density ice fog)\n"); + std::printf(" --gen-skp-hellfire [name]\n"); + std::printf(" Emit .wskp 3 Outland Hellfire sky keyframes (permanent crimson/orange palette — Outland's iconic skies; no midnight keyframe since the Twisting Nether keeps the sky permanently lit)\n"); + std::printf(" --info-wskp [--json]\n"); + std::printf(" Print WSKP entries (id / map / area / hour / sun angle / fog start..end yards / cloud opacity %% / wind mph / name)\n"); + std::printf(" --validate-wskp [--json]\n"); + std::printf(" Static checks: id+name required, timeOfDayHour 0..23, fogStartYards < fogEndYards (else inverted/zero falloff), no negative fog distances, no duplicate skyIds, no two keyframes at same (mapId, areaId, timeOfDayHour) triple (diurnal interpolation tie); warns on sunAngleDeg outside [0,360]\n"); std::printf(" --catalog-pluck [--json]\n"); std::printf(" Extract one entry by id from any registered catalog format. Auto-detects magic, dispatches to the per-format --info-* handler internally, then prints just the matching entry. Primary-key field is auto-detected (first *Id field, or first numeric)\n"); std::printf(" --catalog-find [--magic ] [--json]\n"); diff --git a/tools/editor/cli_list_formats.cpp b/tools/editor/cli_list_formats.cpp index 6d4ec2b3..98ba4227 100644 --- a/tools/editor/cli_list_formats.cpp +++ b/tools/editor/cli_list_formats.cpp @@ -143,6 +143,7 @@ constexpr FormatRow kFormats[] = { {"WWFL", ".wwfl", "social", "chat preprocessor bad-word matcher", "Word filter catalog (spam/RMT/all-caps/URL)"}, {"WMAR", ".wmar", "ui", "raid-target icon set (8 fixed)", "Raid marker catalog (8 raid + map pins + party roles)"}, {"WLMA", ".wlma", "loot", "GroupLoot CMSG_LOOT_METHOD policy", "Loot mode policy catalog (FFA / RR / Master / NBG / Personal)"}, + {"WSKP", ".wskp", "world", "LightParams.dbc + Light.dbc diurnal","Sky parameters catalog (per-zone keyframes)"}, // Additional pipeline catalogs without the alternating // gen/info/validate CLI surface (loaded by the engine diff --git a/tools/editor/cli_sky_params_catalog.cpp b/tools/editor/cli_sky_params_catalog.cpp new file mode 100644 index 00000000..4122b0ac --- /dev/null +++ b/tools/editor/cli_sky_params_catalog.cpp @@ -0,0 +1,272 @@ +#include "cli_sky_params_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_sky_params.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWskpExt(std::string base) { + stripExt(base, ".wskp"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeSkyParams& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeSkyParamsLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wskp\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeSkyParams& c, + const std::string& base) { + std::printf("Wrote %s.wskp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" keyframes: %zu\n", c.entries.size()); +} + +int handleGenStormwind(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StormwindSkyDay"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWskpExt(base); + auto c = wowee::pipeline::WoweeSkyParamsLoader::makeStormwindDay(name); + if (!saveOrError(c, base, "gen-skp")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenArctic(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "NorthrendArcticSky"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWskpExt(base); + auto c = wowee::pipeline::WoweeSkyParamsLoader::makeNorthrendArctic(name); + if (!saveOrError(c, base, "gen-skp-arctic")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHellfire(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "OutlandHellfireSky"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWskpExt(base); + auto c = wowee::pipeline::WoweeSkyParamsLoader::makeOutlandHellfire(name); + if (!saveOrError(c, base, "gen-skp-hellfire")) 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 = stripWskpExt(base); + if (!wowee::pipeline::WoweeSkyParamsLoader::exists(base)) { + std::fprintf(stderr, "WSKP not found: %s.wskp\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSkyParamsLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wskp"] = base + ".wskp"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"skyId", e.skyId}, + {"name", e.name}, + {"description", e.description}, + {"mapId", e.mapId}, + {"areaId", e.areaId}, + {"timeOfDayHour", e.timeOfDayHour}, + {"zenithColor", e.zenithColor}, + {"horizonColor", e.horizonColor}, + {"sunColor", e.sunColor}, + {"sunAngleDeg", e.sunAngleDeg}, + {"fogStartYards", e.fogStartYards}, + {"fogEndYards", e.fogEndYards}, + {"cloudOpacity", e.cloudOpacity}, + {"cloudSpeedX10", e.cloudSpeedX10}, + {"cloudSpeedMph", e.cloudSpeedX10 / 10.0}, + {"iconColorRGBA", e.iconColorRGBA}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WSKP: %s.wskp\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" keyframes: %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id map area hr sunAngle fog(s..e)yd cloud%% windMph name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %4u %4u %2u %5.1f %4.0f..%4.0f %3u%% %4.1f %s\n", + e.skyId, e.mapId, e.areaId, + e.timeOfDayHour, e.sunAngleDeg, + e.fogStartYards, e.fogEndYards, + (e.cloudOpacity * 100) / 255, + e.cloudSpeedX10 / 10.0, + 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 = stripWskpExt(base); + if (!wowee::pipeline::WoweeSkyParamsLoader::exists(base)) { + std::fprintf(stderr, + "validate-wskp: WSKP not found: %s.wskp\n", + base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeSkyParamsLoader::load(base); + std::vector errors; + std::vector warnings; + if (c.entries.empty()) { + warnings.push_back("catalog has zero entries"); + } + std::set idsSeen; + // Per-(mapId, areaId, timeOfDayHour) triple + // uniqueness — two keyframes at same hour for the + // same area would render in unstable order during + // diurnal interpolation. + std::set tripleSeen; + auto tripleKey = [](uint32_t mapId, uint32_t areaId, + uint8_t hour) { + return (static_cast(mapId) << 40) | + (static_cast(areaId) << 8) | + hour; + }; + 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.skyId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.skyId == 0) + errors.push_back(ctx + ": skyId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.timeOfDayHour > 23) { + errors.push_back(ctx + ": timeOfDayHour " + + std::to_string(e.timeOfDayHour) + + " > 23 — must be 0..23"); + } + if (e.sunAngleDeg < 0.0f || e.sunAngleDeg > 360.0f) { + warnings.push_back(ctx + ": sunAngleDeg " + + std::to_string(e.sunAngleDeg) + + " outside [0, 360] — renderer wraps " + "modulo but values outside the canonical " + "range suggest authoring confusion"); + } + if (e.fogStartYards >= e.fogEndYards) { + errors.push_back(ctx + ": fogStartYards " + + std::to_string(e.fogStartYards) + + " >= fogEndYards " + + std::to_string(e.fogEndYards) + + " — fog falloff would be inverted or " + "zero-thickness"); + } + if (e.fogStartYards < 0.0f || e.fogEndYards < 0.0f) { + errors.push_back(ctx + + ": negative fog distance — fog distances " + "must be non-negative"); + } + // Triple uniqueness: same area + same hour + // duplication would tie at runtime. + uint64_t key = tripleKey(e.mapId, e.areaId, + e.timeOfDayHour); + if (!tripleSeen.insert(key).second) { + errors.push_back(ctx + + ": (mapId=" + std::to_string(e.mapId) + + ", areaId=" + std::to_string(e.areaId) + + ", hour=" + + std::to_string(e.timeOfDayHour) + + ") triple already bound by another sky " + "entry — diurnal interpolation would tie " + "non-deterministically"); + } + if (!idsSeen.insert(e.skyId).second) { + errors.push_back(ctx + ": duplicate skyId"); + } + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wskp"] = base + ".wskp"; + 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-wskp: %s.wskp\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu keyframes, all skyIds + " + "(map,area,hour) triples 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 handleSkyParamsCatalog(int& i, int argc, char** argv, + int& outRc) { + if (std::strcmp(argv[i], "--gen-skp") == 0 && i + 1 < argc) { + outRc = handleGenStormwind(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-skp-arctic") == 0 && + i + 1 < argc) { + outRc = handleGenArctic(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-skp-hellfire") == 0 && + i + 1 < argc) { + outRc = handleGenHellfire(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wskp") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wskp") == 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_sky_params_catalog.hpp b/tools/editor/cli_sky_params_catalog.hpp new file mode 100644 index 00000000..273c931d --- /dev/null +++ b/tools/editor/cli_sky_params_catalog.hpp @@ -0,0 +1,12 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleSkyParamsCatalog(int& i, int argc, char** argv, + int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee