mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 11:33:52 +00:00
Novel open replacement for AzerothCore-style scattered
creature_template / gameobject SQL spawn tables PLUS the
ADT MDDF / MODF doodad-placement chunks. The 11th open
format, and the first that covers the live world-content
side (atmosphere + sounds + spawns now form the runtime
"what fills this zone" picture).
A WSPN file holds all spawn points for a zone in a single
table, with kind discriminating creature vs game object
vs static doodad. The same format powers:
• server runtime — knows what NPCs / objects to spawn
• editor — draws spawn markers
• renderer — reads the doodad subset directly to
draw static props without going
through a server roundtrip
Format:
• magic "WSPN", version 1, little-endian
• per entry: kind / entryId / position(3f) / rotation(3f)
/ scale / flags / respawnSec / factionId /
questIdRequired / wanderRadius / label
Flags packed: disabled (0x01), event-only (0x02),
quest-phased (0x04). Reserved bits for future per-entry
encoding extensions.
API: WoweeSpawnsLoader::save / load / exists; presets
makeStarter (1 each kind), makeCamp (4-bandit ring +
chest + 2 tents), makeVillage (6 NPCs + 2 signs + 4
corner trees).
CLI added (5 flags, 473 documented total now):
--gen-spawns / --gen-spawns-camp / --gen-spawns-village
--info-wspn / --validate-wspn
Validator catches: out-of-range kind, NaN/inf coords,
non-positive scale, doodad with non-zero respawn (static
prop misuse), creature with respawn=0 (won't respawn after
kill), entryId=0 (orphan reference).
All 3 presets save / load / re-validate clean. Doodad and
game-object entries explicitly set wanderRadius=0 so the
generated catalogs are noise-free.
286 lines
8.7 KiB
C++
286 lines
8.7 KiB
C++
#include "pipeline/wowee_spawns.hpp"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
|
|
namespace wowee {
|
|
namespace pipeline {
|
|
|
|
namespace {
|
|
|
|
constexpr char kMagic[4] = {'W', 'S', 'P', 'N'};
|
|
constexpr uint32_t kVersion = 1;
|
|
|
|
template <typename T>
|
|
void writePOD(std::ofstream& os, const T& v) {
|
|
os.write(reinterpret_cast<const char*>(&v), sizeof(T));
|
|
}
|
|
|
|
template <typename T>
|
|
bool readPOD(std::ifstream& is, T& v) {
|
|
is.read(reinterpret_cast<char*>(&v), sizeof(T));
|
|
return is.gcount() == static_cast<std::streamsize>(sizeof(T));
|
|
}
|
|
|
|
void writeStr(std::ofstream& os, const std::string& s) {
|
|
uint32_t n = static_cast<uint32_t>(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; // 1 MiB sanity cap
|
|
s.resize(n);
|
|
if (n > 0) {
|
|
is.read(s.data(), n);
|
|
if (is.gcount() != static_cast<std::streamsize>(n)) {
|
|
s.clear();
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::string normalizePath(std::string base) {
|
|
if (base.size() < 5 || base.substr(base.size() - 5) != ".wspn") {
|
|
base += ".wspn";
|
|
}
|
|
return base;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
uint32_t WoweeSpawns::countByKind(uint8_t k) const {
|
|
uint32_t n = 0;
|
|
for (const auto& e : entries) if (e.kind == k) ++n;
|
|
return n;
|
|
}
|
|
|
|
const char* WoweeSpawns::kindName(uint8_t k) {
|
|
switch (k) {
|
|
case Creature: return "creature";
|
|
case GameObject: return "object";
|
|
case Doodad: return "doodad";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
bool WoweeSpawnsLoader::save(const WoweeSpawns& 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<uint32_t>(cat.entries.size());
|
|
writePOD(os, entryCount);
|
|
for (const auto& e : cat.entries) {
|
|
writePOD(os, e.kind);
|
|
uint8_t pad[3] = {0, 0, 0};
|
|
os.write(reinterpret_cast<const char*>(pad), 3);
|
|
writePOD(os, e.entryId);
|
|
writePOD(os, e.position.x);
|
|
writePOD(os, e.position.y);
|
|
writePOD(os, e.position.z);
|
|
writePOD(os, e.rotation.x);
|
|
writePOD(os, e.rotation.y);
|
|
writePOD(os, e.rotation.z);
|
|
writePOD(os, e.scale);
|
|
writePOD(os, e.flags);
|
|
writePOD(os, e.respawnSec);
|
|
writePOD(os, e.factionId);
|
|
writePOD(os, e.questIdRequired);
|
|
writePOD(os, e.wanderRadius);
|
|
writeStr(os, e.label);
|
|
}
|
|
return os.good();
|
|
}
|
|
|
|
WoweeSpawns WoweeSpawnsLoader::load(const std::string& basePath) {
|
|
WoweeSpawns 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; // 1M cap
|
|
out.entries.resize(entryCount);
|
|
for (auto& e : out.entries) {
|
|
if (!readPOD(is, e.kind)) { out.entries.clear(); return out; }
|
|
uint8_t pad[3];
|
|
is.read(reinterpret_cast<char*>(pad), 3);
|
|
if (is.gcount() != 3) { out.entries.clear(); return out; }
|
|
if (!readPOD(is, e.entryId)) { out.entries.clear(); return out; }
|
|
if (!readPOD(is, e.position.x) ||
|
|
!readPOD(is, e.position.y) ||
|
|
!readPOD(is, e.position.z) ||
|
|
!readPOD(is, e.rotation.x) ||
|
|
!readPOD(is, e.rotation.y) ||
|
|
!readPOD(is, e.rotation.z) ||
|
|
!readPOD(is, e.scale) ||
|
|
!readPOD(is, e.flags) ||
|
|
!readPOD(is, e.respawnSec) ||
|
|
!readPOD(is, e.factionId) ||
|
|
!readPOD(is, e.questIdRequired) ||
|
|
!readPOD(is, e.wanderRadius)) {
|
|
out.entries.clear();
|
|
return out;
|
|
}
|
|
if (!readStr(is, e.label)) { out.entries.clear(); return out; }
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool WoweeSpawnsLoader::exists(const std::string& basePath) {
|
|
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
|
return is.good();
|
|
}
|
|
|
|
WoweeSpawns WoweeSpawnsLoader::makeStarter(const std::string& catalogName) {
|
|
WoweeSpawns c;
|
|
c.name = catalogName;
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Creature;
|
|
e.entryId = 1; e.position = {0, 0, 0};
|
|
e.factionId = 35; e.respawnSec = 300; e.wanderRadius = 5.0f;
|
|
e.label = "starter creature";
|
|
c.entries.push_back(e);
|
|
}
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::GameObject;
|
|
e.entryId = 2; e.position = {3, 0, 0};
|
|
e.respawnSec = 600;
|
|
e.wanderRadius = 0.0f; // game objects don't wander
|
|
e.label = "starter chest";
|
|
c.entries.push_back(e);
|
|
}
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Doodad;
|
|
e.entryId = 100; e.position = {-3, 0, 0};
|
|
e.respawnSec = 0;
|
|
e.wanderRadius = 0.0f; // doodads don't wander
|
|
e.label = "starter tree";
|
|
c.entries.push_back(e);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
WoweeSpawns WoweeSpawnsLoader::makeCamp(const std::string& catalogName) {
|
|
WoweeSpawns c;
|
|
c.name = catalogName;
|
|
// 4 bandits spaced around a wander ring, all sharing
|
|
// the same template entry id and the same wander radius.
|
|
const float ringR = 4.0f;
|
|
for (int k = 0; k < 4; ++k) {
|
|
float a = (k / 4.0f) * 6.2831853f;
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Creature;
|
|
e.entryId = 1000;
|
|
e.position = {std::cos(a) * ringR, 0.0f, std::sin(a) * ringR};
|
|
e.rotation = {0.0f, a + 3.14159265f, 0.0f}; // facing inward
|
|
e.factionId = 14; // hostile
|
|
e.respawnSec = 240;
|
|
e.wanderRadius = 3.0f;
|
|
e.label = std::string("bandit ") + std::to_string(k + 1);
|
|
c.entries.push_back(e);
|
|
}
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::GameObject;
|
|
e.entryId = 2000;
|
|
e.position = {0, 0, 0};
|
|
e.respawnSec = 1800;
|
|
e.wanderRadius = 0.0f;
|
|
e.label = "bandit chest";
|
|
c.entries.push_back(e);
|
|
}
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Doodad;
|
|
e.entryId = 3000;
|
|
e.position = {2.0f, 0.0f, 2.0f};
|
|
e.respawnSec = 0;
|
|
e.wanderRadius = 0.0f;
|
|
e.label = "tent A";
|
|
c.entries.push_back(e);
|
|
}
|
|
{
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Doodad;
|
|
e.entryId = 3000;
|
|
e.position = {-2.0f, 0.0f, -2.0f};
|
|
e.respawnSec = 0;
|
|
e.wanderRadius = 0.0f;
|
|
e.label = "tent B";
|
|
c.entries.push_back(e);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
WoweeSpawns WoweeSpawnsLoader::makeVillage(const std::string& catalogName) {
|
|
WoweeSpawns c;
|
|
c.name = catalogName;
|
|
// 6 friendly NPCs (different roles) spread over a ~30 m
|
|
// square plus 2 signpost game objects + 4 tree doodads.
|
|
struct Npc { float x; float z; uint32_t id; const char* label; };
|
|
Npc npcs[6] = {
|
|
{ 0.0f, 0.0f, 4001, "innkeeper" },
|
|
{ 12.0f, -5.0f, 4002, "smith" },
|
|
{-10.0f, 8.0f, 4003, "alchemist" },
|
|
{ 6.0f, 10.0f, 4004, "scribe" },
|
|
{ -8.0f, -7.0f, 4005, "guard captain" },
|
|
{ 15.0f, 3.0f, 4006, "stable master" },
|
|
};
|
|
for (const auto& n : npcs) {
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Creature;
|
|
e.entryId = n.id;
|
|
e.position = {n.x, 0.0f, n.z};
|
|
e.factionId = 35; // friendly
|
|
e.respawnSec = 600;
|
|
e.wanderRadius = 1.0f;
|
|
e.label = n.label;
|
|
c.entries.push_back(e);
|
|
}
|
|
for (int k = 0; k < 2; ++k) {
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::GameObject;
|
|
e.entryId = 5000 + k;
|
|
e.position = {k == 0 ? -15.0f : 15.0f, 0.0f, 0.0f};
|
|
e.respawnSec = 0;
|
|
e.wanderRadius = 0.0f;
|
|
e.label = (k == 0 ? "north sign" : "south sign");
|
|
c.entries.push_back(e);
|
|
}
|
|
struct Tree { float x; float z; };
|
|
Tree trees[4] = {
|
|
{ -18.0f, -12.0f }, { 18.0f, -12.0f },
|
|
{ -18.0f, 12.0f }, { 18.0f, 12.0f },
|
|
};
|
|
for (const auto& t : trees) {
|
|
WoweeSpawns::Entry e;
|
|
e.kind = WoweeSpawns::Doodad;
|
|
e.entryId = 6000;
|
|
e.position = {t.x, 0.0f, t.z};
|
|
e.respawnSec = 0;
|
|
e.wanderRadius = 0.0f;
|
|
e.label = "village tree";
|
|
c.entries.push_back(e);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
} // namespace pipeline
|
|
} // namespace wowee
|