diff --git a/CMakeLists.txt b/CMakeLists.txt index 66c82681..b928b609 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -632,6 +632,7 @@ set(WOWEE_SOURCES src/pipeline/wowee_glyphs.cpp src/pipeline/wowee_vehicles.cpp src/pipeline/wowee_holidays.cpp + src/pipeline/wowee_liquids.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/dbc_layout.cpp @@ -1410,6 +1411,7 @@ add_executable(wowee_editor tools/editor/cli_glyphs_catalog.cpp tools/editor/cli_vehicles_catalog.cpp tools/editor/cli_holidays_catalog.cpp + tools/editor/cli_liquids_catalog.cpp tools/editor/cli_quest_objective.cpp tools/editor/cli_quest_reward.cpp tools/editor/cli_clone.cpp @@ -1520,6 +1522,7 @@ add_executable(wowee_editor src/pipeline/wowee_glyphs.cpp src/pipeline/wowee_vehicles.cpp src/pipeline/wowee_holidays.cpp + src/pipeline/wowee_liquids.cpp src/pipeline/custom_zone_discovery.cpp src/pipeline/terrain_mesh.cpp diff --git a/include/pipeline/wowee_liquids.hpp b/include/pipeline/wowee_liquids.hpp new file mode 100644 index 00000000..90e5a946 --- /dev/null +++ b/include/pipeline/wowee_liquids.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +// Wowee Open Liquid Type catalog (.wliq) — novel replacement +// for Blizzard's LiquidType.dbc plus the AzerothCore-style +// terrain liquid descriptor data. The 45th open format added +// to the editor. +// +// Defines liquid materials used by terrain (MCNK liquid +// layers), WMO interior pools, and procedurally-generated +// fluid bodies in custom zones. Each liquid pairs a render +// material (shader + texture array + flow vectors) with +// gameplay data (entry damage spell, swim immunity, ambient +// audio). Used by both the renderer (to draw the surface) +// and the physics tick (to apply DoTs / breath timer). +// +// Cross-references with previously-added formats: +// WLIQ.entry.ambientSoundId → WSND.entry.soundId +// (loop while submerged) +// WLIQ.entry.splashSoundId → WSND.entry.soundId +// (entry / exit one-shot) +// WLIQ.entry.damageSpellId → WSPL.spellId +// (DoT applied while in liquid) +// +// Binary layout (little-endian): +// magic[4] = "WLIQ" +// version (uint32) = current 1 +// nameLen + name (catalog label) +// entryCount (uint32) +// entries (each): +// liquidId (uint32) +// nameLen + name +// descLen + description +// shaderLen + shaderPath +// matLen + materialPath +// liquidKind (uint8) / fogColorR (uint8) / +// fogColorG (uint8) / fogColorB (uint8) +// fogDensity (float) +// ambientSoundId (uint32) +// splashSoundId (uint32) +// damageSpellId (uint32) +// damagePerSecond (uint32) +// minimapColor (uint32) — RGBA packed +// flowDirection (float) — radians +// flowSpeed (float) +// viscosity (float) — 0=water, 1=thick slime +struct WoweeLiquid { + enum LiquidKind : uint8_t { + Water = 0, // standard fresh / sea water + Magma = 1, // lava — DoT applied + Slime = 2, // green slime (Naxx, Sludge Fields) + OceanSalt = 3, // salt water — separate audio + FelFire = 4, // green fel-burn — magical DoT + HolyLight = 5, // shimmering light — heal-over-time + TarOil = 6, // dark tar — slow movement + AcidBog = 7, // greenish acid — armor damage + FrozenWater = 8, // walkable surface (Wintergrasp ice) + UnderworldGoo = 9, // shadowfang / void liquid + }; + + struct Entry { + uint32_t liquidId = 0; + std::string name; + std::string description; + std::string shaderPath; + std::string materialPath; + uint8_t liquidKind = Water; + uint8_t fogColorR = 0, fogColorG = 0, fogColorB = 0; + float fogDensity = 0.0f; // 0=clear, 1=opaque + uint32_t ambientSoundId = 0; // WSND cross-ref + uint32_t splashSoundId = 0; // WSND cross-ref + uint32_t damageSpellId = 0; // WSPL cross-ref + uint32_t damagePerSecond = 0; // raw HP if no spell + uint32_t minimapColor = 0; // RGBA packed + float flowDirection = 0.0f; // radians + float flowSpeed = 0.0f; // units / sec + float viscosity = 0.0f; // 0=water, 1=slime + }; + + std::string name; + std::vector entries; + + bool isValid() const { return !entries.empty(); } + + const Entry* findById(uint32_t liquidId) const; + + static const char* liquidKindName(uint8_t k); +}; + +class WoweeLiquidLoader { +public: + static bool save(const WoweeLiquid& cat, + const std::string& basePath); + static WoweeLiquid load(const std::string& basePath); + static bool exists(const std::string& basePath); + + // Preset emitters used by --gen-liquids* variants. + // + // makeStarter — 3 stock liquids (Water / Magma / + // Slime) covering the canonical fluid + // triad in classic terrain. + // makeMagical — 4 magical liquids (Fel Fire / Holy + // Light / Underworld Goo / Cosmic + // Plasma) for set-piece zones. + // makeHazardous — 3 high-damage liquids (Naxx Slime / + // Acid Bog / Fel Lava) with damage + // spells cross-ref WSPL. + static WoweeLiquid makeStarter(const std::string& catalogName); + static WoweeLiquid makeMagical(const std::string& catalogName); + static WoweeLiquid makeHazardous(const std::string& catalogName); +}; + +} // namespace pipeline +} // namespace wowee diff --git a/src/pipeline/wowee_liquids.cpp b/src/pipeline/wowee_liquids.cpp new file mode 100644 index 00000000..257bfc32 --- /dev/null +++ b/src/pipeline/wowee_liquids.cpp @@ -0,0 +1,290 @@ +#include "pipeline/wowee_liquids.hpp" + +#include +#include +#include + +namespace wowee { +namespace pipeline { + +namespace { + +constexpr char kMagic[4] = {'W', 'L', 'I', 'Q'}; +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) != ".wliq") { + base += ".wliq"; + } + 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 WoweeLiquid::Entry* +WoweeLiquid::findById(uint32_t liquidId) const { + for (const auto& e : entries) if (e.liquidId == liquidId) return &e; + return nullptr; +} + +const char* WoweeLiquid::liquidKindName(uint8_t k) { + switch (k) { + case Water: return "water"; + case Magma: return "magma"; + case Slime: return "slime"; + case OceanSalt: return "ocean"; + case FelFire: return "fel-fire"; + case HolyLight: return "holy-light"; + case TarOil: return "tar"; + case AcidBog: return "acid"; + case FrozenWater: return "frozen"; + case UnderworldGoo: return "underworld"; + default: return "unknown"; + } +} + +bool WoweeLiquidLoader::save(const WoweeLiquid& 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.liquidId); + writeStr(os, e.name); + writeStr(os, e.description); + writeStr(os, e.shaderPath); + writeStr(os, e.materialPath); + writePOD(os, e.liquidKind); + writePOD(os, e.fogColorR); + writePOD(os, e.fogColorG); + writePOD(os, e.fogColorB); + writePOD(os, e.fogDensity); + writePOD(os, e.ambientSoundId); + writePOD(os, e.splashSoundId); + writePOD(os, e.damageSpellId); + writePOD(os, e.damagePerSecond); + writePOD(os, e.minimapColor); + writePOD(os, e.flowDirection); + writePOD(os, e.flowSpeed); + writePOD(os, e.viscosity); + } + return os.good(); +} + +WoweeLiquid WoweeLiquidLoader::load(const std::string& basePath) { + WoweeLiquid 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.liquidId)) { + out.entries.clear(); return out; + } + if (!readStr(is, e.name) || !readStr(is, e.description) || + !readStr(is, e.shaderPath) || + !readStr(is, e.materialPath)) { + out.entries.clear(); return out; + } + if (!readPOD(is, e.liquidKind) || + !readPOD(is, e.fogColorR) || + !readPOD(is, e.fogColorG) || + !readPOD(is, e.fogColorB) || + !readPOD(is, e.fogDensity) || + !readPOD(is, e.ambientSoundId) || + !readPOD(is, e.splashSoundId) || + !readPOD(is, e.damageSpellId) || + !readPOD(is, e.damagePerSecond) || + !readPOD(is, e.minimapColor) || + !readPOD(is, e.flowDirection) || + !readPOD(is, e.flowSpeed) || + !readPOD(is, e.viscosity)) { + out.entries.clear(); return out; + } + } + return out; +} + +bool WoweeLiquidLoader::exists(const std::string& basePath) { + std::ifstream is(normalizePath(basePath), std::ios::binary); + return is.good(); +} + +WoweeLiquid WoweeLiquidLoader::makeStarter(const std::string& catalogName) { + WoweeLiquid c; + c.name = catalogName; + { + WoweeLiquid::Entry e; + e.liquidId = 1; e.name = "Fresh Water"; + e.description = "Standard rivers, lakes, and ponds."; + e.shaderPath = "shaders/water_basic.frag"; + e.materialPath = "textures/liquid/water_array.dds"; + e.liquidKind = WoweeLiquid::Water; + e.fogColorR = 80; e.fogColorG = 130; e.fogColorB = 180; + e.fogDensity = 0.04f; + e.minimapColor = packRgba(80, 130, 180); + e.flowSpeed = 0.5f; + e.ambientSoundId = 10; // hypothetical WSND id + e.splashSoundId = 11; + c.entries.push_back(e); + } + { + WoweeLiquid::Entry e; + e.liquidId = 2; e.name = "Lava"; + e.description = "Burning magma — applies fire DoT to " + "anyone who enters."; + e.shaderPath = "shaders/water_emissive.frag"; + e.materialPath = "textures/liquid/lava_array.dds"; + e.liquidKind = WoweeLiquid::Magma; + e.fogColorR = 180; e.fogColorG = 60; e.fogColorB = 10; + e.fogDensity = 0.8f; + e.minimapColor = packRgba(220, 70, 0); + e.viscosity = 0.5f; + e.flowSpeed = 0.05f; + e.damagePerSecond = 500; + e.damageSpellId = 24858; // canonical lava DoT + e.ambientSoundId = 12; + e.splashSoundId = 13; + c.entries.push_back(e); + } + { + WoweeLiquid::Entry e; + e.liquidId = 3; e.name = "Sludge Slime"; + e.description = "Toxic green slime found in dungeons."; + e.shaderPath = "shaders/water_toxic.frag"; + e.materialPath = "textures/liquid/slime_array.dds"; + e.liquidKind = WoweeLiquid::Slime; + e.fogColorR = 60; e.fogColorG = 130; e.fogColorB = 30; + e.fogDensity = 0.5f; + e.minimapColor = packRgba(60, 140, 30); + e.viscosity = 0.7f; + e.flowSpeed = 0.1f; + e.damagePerSecond = 50; + e.ambientSoundId = 14; + e.splashSoundId = 15; + c.entries.push_back(e); + } + return c; +} + +WoweeLiquid WoweeLiquidLoader::makeMagical(const std::string& catalogName) { + WoweeLiquid c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint8_t r, uint8_t g, uint8_t b, + float density, float visc, uint32_t spellId, + const char* desc) { + WoweeLiquid::Entry e; + e.liquidId = id; e.name = name; e.description = desc; + e.shaderPath = "shaders/water_magical.frag"; + e.materialPath = std::string("textures/liquid/") + + name + "_array.dds"; + e.liquidKind = kind; + e.fogColorR = r; e.fogColorG = g; e.fogColorB = b; + e.fogDensity = density; + e.minimapColor = packRgba(r, g, b); + e.viscosity = visc; + e.damageSpellId = spellId; + c.entries.push_back(e); + }; + add(100, "FelFire", WoweeLiquid::FelFire, + 80, 200, 30, 0.7f, 0.4f, 22682, + "Demonic green fel — burns even fire-immune creatures."); + add(101, "HolyLight", WoweeLiquid::HolyLight, + 240, 230, 180, 0.3f, 0.0f, 0, + "Pool of liquid Light — heals players who enter."); + add(102, "Underworld", WoweeLiquid::UnderworldGoo, + 70, 30, 100, 0.9f, 0.8f, 27654, + "Shadow-tainted void liquid — drains mana on contact."); + add(103, "Cosmic", WoweeLiquid::HolyLight, + 100, 80, 200, 0.4f, 0.0f, 0, + "Naaru-touched water — randomly grants buffs."); + return c; +} + +WoweeLiquid WoweeLiquidLoader::makeHazardous(const std::string& catalogName) { + WoweeLiquid c; + c.name = catalogName; + auto add = [&](uint32_t id, const char* name, uint8_t kind, + uint32_t spellId, uint32_t dps, + uint8_t r, uint8_t g, uint8_t b, + const char* desc) { + WoweeLiquid::Entry e; + e.liquidId = id; e.name = name; e.description = desc; + e.shaderPath = "shaders/water_hazardous.frag"; + e.materialPath = std::string("textures/liquid/") + + name + "_array.dds"; + e.liquidKind = kind; + e.damageSpellId = spellId; + e.damagePerSecond = dps; + e.fogColorR = r; e.fogColorG = g; e.fogColorB = b; + e.fogDensity = 0.85f; + e.viscosity = 0.6f; + e.minimapColor = packRgba(r, g, b); + c.entries.push_back(e); + }; + add(200, "NaxxSlime", WoweeLiquid::Slime, + 28157, 1500, 100, 200, 60, + "Naxxramas-grade plague slime — lethal to non-tanks."); + add(201, "AcidBog", WoweeLiquid::AcidBog, + 29213, 300, 90, 160, 40, + "Greenish acid — destroys armor durability over time."); + add(202, "FelLava", WoweeLiquid::Magma, + 30122, 2000, 130, 220, 30, + "Fel-corrupted lava — applies a stacking burn debuff."); + return c; +} + +} // namespace pipeline +} // namespace wowee diff --git a/tools/editor/cli_arg_required.cpp b/tools/editor/cli_arg_required.cpp index 5ee42e0b..a17d1d5b 100644 --- a/tools/editor/cli_arg_required.cpp +++ b/tools/editor/cli_arg_required.cpp @@ -131,6 +131,8 @@ const char* const kArgRequired[] = { "--gen-holidays", "--gen-holidays-weekly", "--gen-holidays-special", "--info-whol", "--validate-whol", "--export-whol-json", "--import-whol-json", + "--gen-liquids", "--gen-liquids-magical", "--gen-liquids-hazardous", + "--info-wliq", "--validate-wliq", "--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 0f62b541..ee2eddd2 100644 --- a/tools/editor/cli_dispatch.cpp +++ b/tools/editor/cli_dispatch.cpp @@ -72,6 +72,7 @@ #include "cli_glyphs_catalog.hpp" #include "cli_vehicles_catalog.hpp" #include "cli_holidays_catalog.hpp" +#include "cli_liquids_catalog.hpp" #include "cli_quest_objective.hpp" #include "cli_quest_reward.hpp" #include "cli_clone.hpp" @@ -185,6 +186,7 @@ constexpr DispatchFn kDispatchTable[] = { handleGlyphsCatalog, handleVehiclesCatalog, handleHolidaysCatalog, + handleLiquidsCatalog, handleQuestObjective, handleQuestReward, handleClone, diff --git a/tools/editor/cli_help.cpp b/tools/editor/cli_help.cpp index c51268e6..5ebc8b78 100644 --- a/tools/editor/cli_help.cpp +++ b/tools/editor/cli_help.cpp @@ -1333,6 +1333,16 @@ void printUsage(const char* argv0) { std::printf(" Export binary .whol to a human-editable JSON sidecar (defaults to .whol.json)\n"); std::printf(" --import-whol-json [out-base]\n"); std::printf(" Import a .whol.json sidecar back into binary .whol (accepts holidayKind/recurrence int OR name string)\n"); + std::printf(" --gen-liquids [name]\n"); + std::printf(" Emit .wliq starter: 3 stock liquids (Fresh Water / Lava / Sludge Slime) with shaders+sounds+DPS\n"); + std::printf(" --gen-liquids-magical [name]\n"); + std::printf(" Emit .wliq 4 magical liquids (Fel Fire / Holy Light / Underworld / Cosmic) with damage spell IDs\n"); + std::printf(" --gen-liquids-hazardous [name]\n"); + std::printf(" Emit .wliq 3 high-damage liquids (Naxx Slime / Acid Bog / Fel Lava) with WSPL cross-refs\n"); + std::printf(" --info-wliq [--json]\n"); + std::printf(" Print WLIQ entries (id / kind / fog RGB / density / viscosity / DPS / spell+sound IDs / name)\n"); + std::printf(" --validate-wliq [--json]\n"); + std::printf(" Static checks: id>0+unique, name+shader+material not empty, kind 0..9, fog/visc 0..1, hazardous-no-damage warning\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"); diff --git a/tools/editor/cli_liquids_catalog.cpp b/tools/editor/cli_liquids_catalog.cpp new file mode 100644 index 00000000..f4c62f06 --- /dev/null +++ b/tools/editor/cli_liquids_catalog.cpp @@ -0,0 +1,261 @@ +#include "cli_liquids_catalog.hpp" +#include "cli_arg_parse.hpp" +#include "cli_box_emitter.hpp" + +#include "pipeline/wowee_liquids.hpp" +#include + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace editor { +namespace cli { + +namespace { + +std::string stripWliqExt(std::string base) { + stripExt(base, ".wliq"); + return base; +} + +bool saveOrError(const wowee::pipeline::WoweeLiquid& c, + const std::string& base, const char* cmd) { + if (!wowee::pipeline::WoweeLiquidLoader::save(c, base)) { + std::fprintf(stderr, "%s: failed to save %s.wliq\n", + cmd, base.c_str()); + return false; + } + return true; +} + +void printGenSummary(const wowee::pipeline::WoweeLiquid& c, + const std::string& base) { + std::printf("Wrote %s.wliq\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" liquids : %zu\n", c.entries.size()); +} + +int handleGenStarter(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "StarterLiquids"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWliqExt(base); + auto c = wowee::pipeline::WoweeLiquidLoader::makeStarter(name); + if (!saveOrError(c, base, "gen-liquids")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenMagical(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "MagicalLiquids"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWliqExt(base); + auto c = wowee::pipeline::WoweeLiquidLoader::makeMagical(name); + if (!saveOrError(c, base, "gen-liquids-magical")) return 1; + printGenSummary(c, base); + return 0; +} + +int handleGenHazardous(int& i, int argc, char** argv) { + std::string base = argv[++i]; + std::string name = "HazardousLiquids"; + if (parseOptArg(i, argc, argv)) name = argv[++i]; + base = stripWliqExt(base); + auto c = wowee::pipeline::WoweeLiquidLoader::makeHazardous(name); + if (!saveOrError(c, base, "gen-liquids-hazardous")) 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 = stripWliqExt(base); + if (!wowee::pipeline::WoweeLiquidLoader::exists(base)) { + std::fprintf(stderr, "WLIQ not found: %s.wliq\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLiquidLoader::load(base); + if (jsonOut) { + nlohmann::json j; + j["wliq"] = base + ".wliq"; + j["name"] = c.name; + j["count"] = c.entries.size(); + nlohmann::json arr = nlohmann::json::array(); + for (const auto& e : c.entries) { + arr.push_back({ + {"liquidId", e.liquidId}, + {"name", e.name}, + {"description", e.description}, + {"shaderPath", e.shaderPath}, + {"materialPath", e.materialPath}, + {"liquidKind", e.liquidKind}, + {"liquidKindName", wowee::pipeline::WoweeLiquid::liquidKindName(e.liquidKind)}, + {"fogColorR", e.fogColorR}, + {"fogColorG", e.fogColorG}, + {"fogColorB", e.fogColorB}, + {"fogDensity", e.fogDensity}, + {"ambientSoundId", e.ambientSoundId}, + {"splashSoundId", e.splashSoundId}, + {"damageSpellId", e.damageSpellId}, + {"damagePerSecond", e.damagePerSecond}, + {"minimapColor", e.minimapColor}, + {"flowDirection", e.flowDirection}, + {"flowSpeed", e.flowSpeed}, + {"viscosity", e.viscosity}, + }); + } + j["entries"] = arr; + std::printf("%s\n", j.dump(2).c_str()); + return 0; + } + std::printf("WLIQ: %s.wliq\n", base.c_str()); + std::printf(" catalog : %s\n", c.name.c_str()); + std::printf(" liquids : %zu\n", c.entries.size()); + if (c.entries.empty()) return 0; + std::printf(" id kind fog RGB dens visc dps spell ambient splash name\n"); + for (const auto& e : c.entries) { + std::printf(" %4u %-10s %3u/%3u/%3u %4.2f %4.2f %5u %5u %5u %5u %s\n", + e.liquidId, + wowee::pipeline::WoweeLiquid::liquidKindName(e.liquidKind), + e.fogColorR, e.fogColorG, e.fogColorB, + e.fogDensity, e.viscosity, + e.damagePerSecond, e.damageSpellId, + e.ambientSoundId, e.splashSoundId, + 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 = stripWliqExt(base); + if (!wowee::pipeline::WoweeLiquidLoader::exists(base)) { + std::fprintf(stderr, + "validate-wliq: WLIQ not found: %s.wliq\n", base.c_str()); + return 1; + } + auto c = wowee::pipeline::WoweeLiquidLoader::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.liquidId); + if (!e.name.empty()) ctx += " " + e.name; + ctx += ")"; + if (e.liquidId == 0) + errors.push_back(ctx + ": liquidId is 0"); + if (e.name.empty()) + errors.push_back(ctx + ": name is empty"); + if (e.shaderPath.empty()) + errors.push_back(ctx + ": shaderPath is empty"); + if (e.materialPath.empty()) + errors.push_back(ctx + ": materialPath is empty"); + if (e.liquidKind > wowee::pipeline::WoweeLiquid::UnderworldGoo) { + errors.push_back(ctx + ": liquidKind " + + std::to_string(e.liquidKind) + " not in 0..9"); + } + if (e.fogDensity < 0.0f || e.fogDensity > 1.0f) { + errors.push_back(ctx + ": fogDensity " + + std::to_string(e.fogDensity) + " not in 0..1"); + } + if (e.viscosity < 0.0f || e.viscosity > 1.0f) { + errors.push_back(ctx + ": viscosity " + + std::to_string(e.viscosity) + " not in 0..1"); + } + // Magma / Slime / FelFire / AcidBog liquids without + // any damage source are mechanically harmless — flag + // as a warning so the caller can confirm intent. + bool hazardous = + e.liquidKind == wowee::pipeline::WoweeLiquid::Magma || + e.liquidKind == wowee::pipeline::WoweeLiquid::Slime || + e.liquidKind == wowee::pipeline::WoweeLiquid::FelFire || + e.liquidKind == wowee::pipeline::WoweeLiquid::AcidBog; + if (hazardous && e.damageSpellId == 0 && + e.damagePerSecond == 0) { + warnings.push_back(ctx + + ": hazardous liquid kind but no damageSpellId / " + "damagePerSecond (won't hurt anything)"); + } + // Water and OceanSalt with non-zero damage is unusual + // — could be intentional acid water but worth checking. + if ((e.liquidKind == wowee::pipeline::WoweeLiquid::Water || + e.liquidKind == wowee::pipeline::WoweeLiquid::OceanSalt) && + e.damagePerSecond > 0) { + warnings.push_back(ctx + + ": Water/OceanSalt with damagePerSecond>0 " + "(unusual — verify intent)"); + } + for (uint32_t prev : idsSeen) { + if (prev == e.liquidId) { + errors.push_back(ctx + ": duplicate liquidId"); + break; + } + } + idsSeen.push_back(e.liquidId); + } + bool ok = errors.empty(); + if (jsonOut) { + nlohmann::json j; + j["wliq"] = base + ".wliq"; + 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-wliq: %s.wliq\n", base.c_str()); + if (ok && warnings.empty()) { + std::printf(" OK — %zu liquids, all liquidIds 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 handleLiquidsCatalog(int& i, int argc, char** argv, int& outRc) { + if (std::strcmp(argv[i], "--gen-liquids") == 0 && i + 1 < argc) { + outRc = handleGenStarter(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-liquids-magical") == 0 && i + 1 < argc) { + outRc = handleGenMagical(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--gen-liquids-hazardous") == 0 && i + 1 < argc) { + outRc = handleGenHazardous(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--info-wliq") == 0 && i + 1 < argc) { + outRc = handleInfo(i, argc, argv); return true; + } + if (std::strcmp(argv[i], "--validate-wliq") == 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_liquids_catalog.hpp b/tools/editor/cli_liquids_catalog.hpp new file mode 100644 index 00000000..56b3bace --- /dev/null +++ b/tools/editor/cli_liquids_catalog.hpp @@ -0,0 +1,11 @@ +#pragma once + +namespace wowee { +namespace editor { +namespace cli { + +bool handleLiquidsCatalog(int& i, int argc, char** argv, int& outRc); + +} // namespace cli +} // namespace editor +} // namespace wowee