mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 03:23:51 +00:00
feat(pipeline): WPHM player movement-to-animation map (127th open format)
Novel replacement for the implicit movementState->animation
binding that vanilla WoW baked into per-race M2 model files.
Each WPHM entry binds one (raceId, genderId, movementState)
tuple to a base M2 animation sequence id, an optional variant
sequence (drunk-walk, wounded-run), and a blend transition
duration in milliseconds.
8-state machine: Idle / Walk / Run / Swim / Fly / Sit / Mount /
Death. Three presets each emit the full 16 bindings (M+F):
--gen-phm-human 16 bindings with drunk-walk variant on Walk
--gen-phm-orc 16 bindings with AttackRun variant on Run
for war-stance flavor
--gen-phm-undead 16 bindings with canonical shambling variant
(anim 38) on Run for low-health renderer
override + slower swim transition (undead are
awkward in water)
Validator catches: id required, raceId 1..10, genderId 0..1,
movementState 0..7, no duplicate mapIds, no duplicate
(race,gender,state) triples (renderer dispatch ambiguity),
baseAnimId=0 forbidden on non-Idle states (model would freeze
when entering that state). Warns on variantAnimId==baseAnimId
(no-op overhead) and transitionMs > 2000 (would feel like
animation hang).
Format count 126 -> 127. CLI flag count 1337 -> 1344.
This commit is contained in:
parent
e652f8595d
commit
949a6e0182
10 changed files with 770 additions and 0 deletions
|
|
@ -715,6 +715,7 @@ set(WOWEE_SOURCES
|
|||
src/pipeline/wowee_global_channels.cpp
|
||||
src/pipeline/wowee_addon_manifest.cpp
|
||||
src/pipeline/wowee_spell_pack.cpp
|
||||
src/pipeline/wowee_player_movement_anim.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/dbc_layout.cpp
|
||||
|
||||
|
|
@ -1593,6 +1594,7 @@ add_executable(wowee_editor
|
|||
tools/editor/cli_global_channels_catalog.cpp
|
||||
tools/editor/cli_addon_manifest_catalog.cpp
|
||||
tools/editor/cli_spell_pack_catalog.cpp
|
||||
tools/editor/cli_player_movement_anim_catalog.cpp
|
||||
tools/editor/cli_catalog_pluck.cpp
|
||||
tools/editor/cli_catalog_find.cpp
|
||||
tools/editor/cli_catalog_by_name.cpp
|
||||
|
|
@ -1790,6 +1792,7 @@ add_executable(wowee_editor
|
|||
src/pipeline/wowee_global_channels.cpp
|
||||
src/pipeline/wowee_addon_manifest.cpp
|
||||
src/pipeline/wowee_spell_pack.cpp
|
||||
src/pipeline/wowee_player_movement_anim.cpp
|
||||
src/pipeline/custom_zone_discovery.cpp
|
||||
src/pipeline/terrain_mesh.cpp
|
||||
|
||||
|
|
|
|||
125
include/pipeline/wowee_player_movement_anim.hpp
Normal file
125
include/pipeline/wowee_player_movement_anim.hpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
// Wowee Open Player Movement-to-Animation Map (.wphm)
|
||||
// — novel replacement for the implicit
|
||||
// movementState->animation mapping vanilla WoW baked
|
||||
// into per-race M2 model files. Each WPHM entry binds
|
||||
// one (raceId, genderId, movementState) tuple to a
|
||||
// base animation sequence ID + an optional variant
|
||||
// (e.g. wounded run, drunk walk) and a transition
|
||||
// blend duration.
|
||||
//
|
||||
// Cross-references with previously-added formats:
|
||||
// WCDB: raceId references the playable-race
|
||||
// catalog (currently 1..10 in vanilla:
|
||||
// Human=1, Orc=2 ... Troll=8, Goblin=9,
|
||||
// BloodElf=10).
|
||||
// None to M2 binary directly — animation index
|
||||
// numbers come from the standard WoW M2 animation
|
||||
// table (Stand=0, Walk=4, Run=5, Death=1...).
|
||||
//
|
||||
// Binary layout (little-endian):
|
||||
// magic[4] = "WPHM"
|
||||
// version (uint32) = current 1
|
||||
// nameLen + name (catalog label)
|
||||
// entryCount (uint32)
|
||||
// entries (each):
|
||||
// mapId (uint32) — surrogate primary key
|
||||
// for cross-format
|
||||
// --catalog-find lookups
|
||||
// raceId (uint8) — 1..10 vanilla race
|
||||
// genderId (uint8) — 0=male, 1=female
|
||||
// movementState (uint8) — 0=Idle / 1=Walk / 2=Run
|
||||
// / 3=Swim / 4=Fly /
|
||||
// 5=Sit / 6=Mount /
|
||||
// 7=Death
|
||||
// pad0 (uint8)
|
||||
// baseAnimId (uint32) — M2 anim sequence id
|
||||
// variantAnimId (uint32) — alternate sequence
|
||||
// (0 = no variant)
|
||||
// transitionMs (uint16) — blend duration to enter
|
||||
// pad1 (uint16)
|
||||
struct WoweePlayerMovementAnim {
|
||||
enum MovementState : uint8_t {
|
||||
StateIdle = 0,
|
||||
StateWalk = 1,
|
||||
StateRun = 2,
|
||||
StateSwim = 3,
|
||||
StateFly = 4,
|
||||
StateSit = 5,
|
||||
StateMount = 6,
|
||||
StateDeath = 7,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t mapId = 0;
|
||||
uint8_t raceId = 0;
|
||||
uint8_t genderId = 0;
|
||||
uint8_t movementState = StateIdle;
|
||||
uint8_t pad0 = 0;
|
||||
uint32_t baseAnimId = 0;
|
||||
uint32_t variantAnimId = 0;
|
||||
uint16_t transitionMs = 0;
|
||||
uint16_t pad1 = 0;
|
||||
};
|
||||
|
||||
std::string name;
|
||||
std::vector<Entry> entries;
|
||||
|
||||
bool isValid() const { return !entries.empty(); }
|
||||
|
||||
const Entry* findById(uint32_t mapId) const;
|
||||
|
||||
// Returns the binding for a specific (race, gender,
|
||||
// state) — the canonical lookup the renderer uses
|
||||
// each frame to decide which animation sequence to
|
||||
// play.
|
||||
const Entry* find(uint8_t raceId, uint8_t genderId,
|
||||
uint8_t state) const;
|
||||
|
||||
// Returns all bindings for a race+gender combo.
|
||||
// Used by character-preview UIs to show the full
|
||||
// state machine for one model.
|
||||
std::vector<const Entry*> findByRaceGender(uint8_t raceId,
|
||||
uint8_t genderId) const;
|
||||
};
|
||||
|
||||
class WoweePlayerMovementAnimLoader {
|
||||
public:
|
||||
static bool save(const WoweePlayerMovementAnim& cat,
|
||||
const std::string& basePath);
|
||||
static WoweePlayerMovementAnim load(const std::string& basePath);
|
||||
static bool exists(const std::string& basePath);
|
||||
|
||||
// Preset emitters used by --gen-phm* variants.
|
||||
//
|
||||
// makeHumanMovement — full 8-state machine for
|
||||
// Human male + female =
|
||||
// 16 entries. Walk-while-
|
||||
// drunk variant for State
|
||||
// Walk only.
|
||||
// makeOrcMovement — same shape for Orc male
|
||||
// + female. Orc Run uses
|
||||
// a more aggressive
|
||||
// variant for war-stance
|
||||
// flavor.
|
||||
// makeUndeadMovement — Undead male + female,
|
||||
// with canonical
|
||||
// "shambling-when-wounded"
|
||||
// variantAnimId on Run
|
||||
// state (low-health
|
||||
// renderer override).
|
||||
static WoweePlayerMovementAnim makeHumanMovement(const std::string& catalogName);
|
||||
static WoweePlayerMovementAnim makeOrcMovement(const std::string& catalogName);
|
||||
static WoweePlayerMovementAnim makeUndeadMovement(const std::string& catalogName);
|
||||
};
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
295
src/pipeline/wowee_player_movement_anim.cpp
Normal file
295
src/pipeline/wowee_player_movement_anim.cpp
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
#include "pipeline/wowee_player_movement_anim.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
||||
namespace wowee {
|
||||
namespace pipeline {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kMagic[4] = {'W', 'P', 'H', 'M'};
|
||||
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;
|
||||
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) != ".wphm") {
|
||||
base += ".wphm";
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const WoweePlayerMovementAnim::Entry*
|
||||
WoweePlayerMovementAnim::findById(uint32_t mapId) const {
|
||||
for (const auto& e : entries)
|
||||
if (e.mapId == mapId) return &e;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const WoweePlayerMovementAnim::Entry*
|
||||
WoweePlayerMovementAnim::find(uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
uint8_t state) const {
|
||||
for (const auto& e : entries) {
|
||||
if (e.raceId == raceId &&
|
||||
e.genderId == genderId &&
|
||||
e.movementState == state) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const WoweePlayerMovementAnim::Entry*>
|
||||
WoweePlayerMovementAnim::findByRaceGender(uint8_t raceId,
|
||||
uint8_t genderId) const {
|
||||
std::vector<const Entry*> out;
|
||||
for (const auto& e : entries) {
|
||||
if (e.raceId == raceId && e.genderId == genderId)
|
||||
out.push_back(&e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweePlayerMovementAnimLoader::save(
|
||||
const WoweePlayerMovementAnim& 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.mapId);
|
||||
writePOD(os, e.raceId);
|
||||
writePOD(os, e.genderId);
|
||||
writePOD(os, e.movementState);
|
||||
writePOD(os, e.pad0);
|
||||
writePOD(os, e.baseAnimId);
|
||||
writePOD(os, e.variantAnimId);
|
||||
writePOD(os, e.transitionMs);
|
||||
writePOD(os, e.pad1);
|
||||
}
|
||||
return os.good();
|
||||
}
|
||||
|
||||
WoweePlayerMovementAnim WoweePlayerMovementAnimLoader::load(
|
||||
const std::string& basePath) {
|
||||
WoweePlayerMovementAnim 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.mapId) ||
|
||||
!readPOD(is, e.raceId) ||
|
||||
!readPOD(is, e.genderId) ||
|
||||
!readPOD(is, e.movementState) ||
|
||||
!readPOD(is, e.pad0) ||
|
||||
!readPOD(is, e.baseAnimId) ||
|
||||
!readPOD(is, e.variantAnimId) ||
|
||||
!readPOD(is, e.transitionMs) ||
|
||||
!readPOD(is, e.pad1)) {
|
||||
out.entries.clear(); return out;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool WoweePlayerMovementAnimLoader::exists(
|
||||
const std::string& basePath) {
|
||||
std::ifstream is(normalizePath(basePath), std::ios::binary);
|
||||
return is.good();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Helper to build an 8-state machine for one
|
||||
// (raceId, genderId) pair. Canonical M2 anim ids:
|
||||
// Stand=0, Death=1, Walk=4, Run=5, Swim=12,
|
||||
// Fly=68 (vanilla had no flying except taxi),
|
||||
// Sit=20, Mount=91.
|
||||
struct StateRow {
|
||||
uint8_t state;
|
||||
uint32_t baseAnim;
|
||||
uint32_t variantAnim;
|
||||
uint16_t transitionMs;
|
||||
};
|
||||
|
||||
WoweePlayerMovementAnim::Entry makeEntry(uint32_t mapId,
|
||||
uint8_t race,
|
||||
uint8_t gender,
|
||||
const StateRow& r) {
|
||||
WoweePlayerMovementAnim::Entry e;
|
||||
e.mapId = mapId;
|
||||
e.raceId = race;
|
||||
e.genderId = gender;
|
||||
e.movementState = r.state;
|
||||
e.baseAnimId = r.baseAnim;
|
||||
e.variantAnimId = r.variantAnim;
|
||||
e.transitionMs = r.transitionMs;
|
||||
return e;
|
||||
}
|
||||
|
||||
void appendRaceGender(WoweePlayerMovementAnim& c,
|
||||
uint32_t baseId, uint8_t race,
|
||||
uint8_t gender,
|
||||
const std::vector<StateRow>& rows) {
|
||||
using P = WoweePlayerMovementAnim;
|
||||
for (size_t i = 0; i < rows.size(); ++i) {
|
||||
c.entries.push_back(makeEntry(
|
||||
baseId + static_cast<uint32_t>(i),
|
||||
race, gender, rows[i]));
|
||||
}
|
||||
(void)P::StateIdle;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WoweePlayerMovementAnim
|
||||
WoweePlayerMovementAnimLoader::makeHumanMovement(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweePlayerMovementAnim;
|
||||
WoweePlayerMovementAnim c;
|
||||
c.name = catalogName;
|
||||
// Human Male: variantAnim on Walk = drunk-walk
|
||||
// sequence (anim id 39 in the canonical M2 table).
|
||||
// Other states have no variant.
|
||||
appendRaceGender(c, 1000, 1 /* Human */, 0 /* M */, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 39, 250},
|
||||
{P::StateRun, 5, 0, 200},
|
||||
{P::StateSwim, 12, 0, 350},
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
// Human Female: identical state shape but anim
|
||||
// base ids differ (M2 sex models have separate
|
||||
// anim tables) — using same numeric ids here as
|
||||
// placeholder; in production these would be the
|
||||
// female-model-specific anim indices.
|
||||
appendRaceGender(c, 1100, 1, 1, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 39, 250},
|
||||
{P::StateRun, 5, 0, 200},
|
||||
{P::StateSwim, 12, 0, 350},
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweePlayerMovementAnim
|
||||
WoweePlayerMovementAnimLoader::makeOrcMovement(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweePlayerMovementAnim;
|
||||
WoweePlayerMovementAnim c;
|
||||
c.name = catalogName;
|
||||
// Orc Run uses a more aggressive variant (anim 17
|
||||
// = AttackRun) for war-stance flavor.
|
||||
appendRaceGender(c, 2000, 2 /* Orc */, 0, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 0, 250},
|
||||
{P::StateRun, 5, 17, 200},
|
||||
{P::StateSwim, 12, 0, 350},
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
appendRaceGender(c, 2100, 2, 1, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 0, 250},
|
||||
{P::StateRun, 5, 17, 200},
|
||||
{P::StateSwim, 12, 0, 350},
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
return c;
|
||||
}
|
||||
|
||||
WoweePlayerMovementAnim
|
||||
WoweePlayerMovementAnimLoader::makeUndeadMovement(
|
||||
const std::string& catalogName) {
|
||||
using P = WoweePlayerMovementAnim;
|
||||
WoweePlayerMovementAnim c;
|
||||
c.name = catalogName;
|
||||
// Undead Run uses a "shambling" variant anim (38)
|
||||
// as the wounded-low-health renderer override.
|
||||
// Walk uses a stiffer cadence variant (40).
|
||||
appendRaceGender(c, 5000, 5 /* Undead */, 0, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 40, 250},
|
||||
{P::StateRun, 5, 38, 200},
|
||||
{P::StateSwim, 12, 0, 400}, // slower blend
|
||||
// — undead aren't
|
||||
// graceful in
|
||||
// water
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
appendRaceGender(c, 5100, 5, 1, {
|
||||
{P::StateIdle, 0, 0, 200},
|
||||
{P::StateWalk, 4, 40, 250},
|
||||
{P::StateRun, 5, 38, 200},
|
||||
{P::StateSwim, 12, 0, 400},
|
||||
{P::StateFly, 68, 0, 400},
|
||||
{P::StateSit, 20, 0, 300},
|
||||
{P::StateMount,91, 0, 400},
|
||||
{P::StateDeath, 1, 0, 100},
|
||||
});
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace pipeline
|
||||
} // namespace wowee
|
||||
|
|
@ -389,6 +389,8 @@ const char* const kArgRequired[] = {
|
|||
"--gen-spk-warrior", "--gen-spk-mage", "--gen-spk-rogue",
|
||||
"--info-wspk", "--validate-wspk",
|
||||
"--export-wspk-json", "--import-wspk-json",
|
||||
"--gen-phm-human", "--gen-phm-orc", "--gen-phm-undead",
|
||||
"--info-wphm", "--validate-wphm",
|
||||
"--gen-weather-temperate", "--gen-weather-arctic",
|
||||
"--gen-weather-desert", "--gen-weather-stormy",
|
||||
"--gen-zone-atmosphere",
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@
|
|||
#include "cli_global_channels_catalog.hpp"
|
||||
#include "cli_addon_manifest_catalog.hpp"
|
||||
#include "cli_spell_pack_catalog.hpp"
|
||||
#include "cli_player_movement_anim_catalog.hpp"
|
||||
#include "cli_catalog_pluck.hpp"
|
||||
#include "cli_catalog_find.hpp"
|
||||
#include "cli_catalog_by_name.hpp"
|
||||
|
|
@ -387,6 +388,7 @@ constexpr DispatchFn kDispatchTable[] = {
|
|||
handleGlobalChannelsCatalog,
|
||||
handleAddonManifestCatalog,
|
||||
handleSpellPackCatalog,
|
||||
handlePlayerMovementAnimCatalog,
|
||||
handleCatalogPluck,
|
||||
handleCatalogFind,
|
||||
handleCatalogByName,
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ constexpr FormatMagicEntry kFormats[] = {
|
|||
{{'W','G','C','H'}, ".wgch", "chat", "--info-wgch", "Global chat channel catalog"},
|
||||
{{'W','M','O','D'}, ".wmod", "addons", "--info-wmod", "Addon manifest catalog"},
|
||||
{{'W','S','P','K'}, ".wspk", "spells", "--info-wspk", "Spell pack catalog"},
|
||||
{{'W','P','H','M'}, ".wphm", "anim", "--info-wphm", "Player movement-to-animation map"},
|
||||
{{'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"},
|
||||
|
|
|
|||
|
|
@ -2531,6 +2531,16 @@ void printUsage(const char* argv0) {
|
|||
std::printf(" Export binary .wspk to a human-editable JSON sidecar (defaults to <base>.wspk.json; emits spellIds as JSON int array preserving display order)\n");
|
||||
std::printf(" --import-wspk-json <json-path> [out-base]\n");
|
||||
std::printf(" Import a .wspk.json sidecar back into binary .wspk (spellIds array preserves order — round-trips per-tab spell lists byte-identical)\n");
|
||||
std::printf(" --gen-phm-human <wphm-base> [name]\n");
|
||||
std::printf(" Emit .wphm Human Male+Female 8-state machine (16 entries) with drunk-walk variant on Walk state\n");
|
||||
std::printf(" --gen-phm-orc <wphm-base> [name]\n");
|
||||
std::printf(" Emit .wphm Orc Male+Female 8-state machine (16 entries) with AttackRun variant on Run for war-stance flavor\n");
|
||||
std::printf(" --gen-phm-undead <wphm-base> [name]\n");
|
||||
std::printf(" Emit .wphm Undead Male+Female with shambling variant on Run (low-health renderer override) and slower swim transition (undead are awkward in water)\n");
|
||||
std::printf(" --info-wphm <wphm-base> [--json]\n");
|
||||
std::printf(" Print WPHM bindings (id / race / gender / movementState / baseAnimId / variantAnimId / transitionMs)\n");
|
||||
std::printf(" --validate-wphm <wphm-base> [--json]\n");
|
||||
std::printf(" Static checks: id required, raceId 1..10, genderId 0..1, movementState 0..7, no duplicate mapIds, no duplicate (race,gender,state) triples (renderer dispatch ambiguity), baseAnimId=0 forbidden on non-Idle states (would freeze model); warns on variantAnimId==baseAnimId no-op and transitionMs > 2000 animation hang\n");
|
||||
std::printf(" --catalog-pluck <wXXX-file> <id> [--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 <directory> <id> [--magic <WXXX>] [--json]\n");
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ constexpr FormatRow kFormats[] = {
|
|||
{"WGCH", ".wgch", "chat", "ChatChannels.dbc + zone-default joins","Global chat channel catalog (access policy + zone auto-join)"},
|
||||
{"WMOD", ".wmod", "addons", "per-addon TOC text + load-order rules","Addon manifest catalog (deps + cycle detection)"},
|
||||
{"WSPK", ".wspk", "spells", "SkillLineAbility + per-spec tab order","Spell pack catalog (per-class spellbook tab layout)"},
|
||||
{"WPHM", ".wphm", "anim", "implicit M2 movementState->anim map","Player movement-to-animation map (per race/gender/state)"},
|
||||
|
||||
// Additional pipeline catalogs without the alternating
|
||||
// gen/info/validate CLI surface (loaded by the engine
|
||||
|
|
|
|||
319
tools/editor/cli_player_movement_anim_catalog.cpp
Normal file
319
tools/editor/cli_player_movement_anim_catalog.cpp
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
#include "cli_player_movement_anim_catalog.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_box_emitter.hpp"
|
||||
|
||||
#include "pipeline/wowee_player_movement_anim.hpp"
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string stripWphmExt(std::string base) {
|
||||
stripExt(base, ".wphm");
|
||||
return base;
|
||||
}
|
||||
|
||||
const char* movementStateName(uint8_t s) {
|
||||
using P = wowee::pipeline::WoweePlayerMovementAnim;
|
||||
switch (s) {
|
||||
case P::StateIdle: return "idle";
|
||||
case P::StateWalk: return "walk";
|
||||
case P::StateRun: return "run";
|
||||
case P::StateSwim: return "swim";
|
||||
case P::StateFly: return "fly";
|
||||
case P::StateSit: return "sit";
|
||||
case P::StateMount: return "mount";
|
||||
case P::StateDeath: return "death";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char* raceIdName(uint8_t r) {
|
||||
// Vanilla 1.12 ChrRaces ids — display only.
|
||||
switch (r) {
|
||||
case 1: return "Human";
|
||||
case 2: return "Orc";
|
||||
case 3: return "Dwarf";
|
||||
case 4: return "NightElf";
|
||||
case 5: return "Undead";
|
||||
case 6: return "Tauren";
|
||||
case 7: return "Gnome";
|
||||
case 8: return "Troll";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
const char* genderName(uint8_t g) {
|
||||
return g == 0 ? "M" : (g == 1 ? "F" : "?");
|
||||
}
|
||||
|
||||
bool saveOrError(const wowee::pipeline::WoweePlayerMovementAnim& c,
|
||||
const std::string& base, const char* cmd) {
|
||||
if (!wowee::pipeline::WoweePlayerMovementAnimLoader::save(c, base)) {
|
||||
std::fprintf(stderr, "%s: failed to save %s.wphm\n",
|
||||
cmd, base.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void printGenSummary(const wowee::pipeline::WoweePlayerMovementAnim& c,
|
||||
const std::string& base) {
|
||||
std::printf("Wrote %s.wphm\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" bindings: %zu\n", c.entries.size());
|
||||
}
|
||||
|
||||
int handleGenHuman(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "HumanMovementAnim";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWphmExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerMovementAnimLoader::
|
||||
makeHumanMovement(name);
|
||||
if (!saveOrError(c, base, "gen-phm-human")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenOrc(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "OrcMovementAnim";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWphmExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerMovementAnimLoader::
|
||||
makeOrcMovement(name);
|
||||
if (!saveOrError(c, base, "gen-phm-orc")) return 1;
|
||||
printGenSummary(c, base);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleGenUndead(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
std::string name = "UndeadMovementAnim";
|
||||
if (parseOptArg(i, argc, argv)) name = argv[++i];
|
||||
base = stripWphmExt(base);
|
||||
auto c = wowee::pipeline::WoweePlayerMovementAnimLoader::
|
||||
makeUndeadMovement(name);
|
||||
if (!saveOrError(c, base, "gen-phm-undead")) 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 = stripWphmExt(base);
|
||||
if (!wowee::pipeline::WoweePlayerMovementAnimLoader::exists(base)) {
|
||||
std::fprintf(stderr, "WPHM not found: %s.wphm\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweePlayerMovementAnimLoader::load(base);
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wphm"] = base + ".wphm";
|
||||
j["name"] = c.name;
|
||||
j["count"] = c.entries.size();
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (const auto& e : c.entries) {
|
||||
arr.push_back({
|
||||
{"mapId", e.mapId},
|
||||
{"raceId", e.raceId},
|
||||
{"raceName", raceIdName(e.raceId)},
|
||||
{"genderId", e.genderId},
|
||||
{"genderName", genderName(e.genderId)},
|
||||
{"movementState", e.movementState},
|
||||
{"movementStateName",
|
||||
movementStateName(e.movementState)},
|
||||
{"baseAnimId", e.baseAnimId},
|
||||
{"variantAnimId", e.variantAnimId},
|
||||
{"transitionMs", e.transitionMs},
|
||||
});
|
||||
}
|
||||
j["entries"] = arr;
|
||||
std::printf("%s\n", j.dump(2).c_str());
|
||||
return 0;
|
||||
}
|
||||
std::printf("WPHM: %s.wphm\n", base.c_str());
|
||||
std::printf(" catalog : %s\n", c.name.c_str());
|
||||
std::printf(" bindings: %zu\n", c.entries.size());
|
||||
if (c.entries.empty()) return 0;
|
||||
std::printf(" id race g state baseAnim variant blendMs\n");
|
||||
for (const auto& e : c.entries) {
|
||||
std::printf(" %4u %-8s %s %-7s %8u %7u %7u\n",
|
||||
e.mapId,
|
||||
raceIdName(e.raceId),
|
||||
genderName(e.genderId),
|
||||
movementStateName(e.movementState),
|
||||
e.baseAnimId, e.variantAnimId,
|
||||
e.transitionMs);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int handleValidate(int& i, int argc, char** argv) {
|
||||
std::string base = argv[++i];
|
||||
bool jsonOut = consumeJsonFlag(i, argc, argv);
|
||||
base = stripWphmExt(base);
|
||||
if (!wowee::pipeline::WoweePlayerMovementAnimLoader::exists(base)) {
|
||||
std::fprintf(stderr,
|
||||
"validate-wphm: WPHM not found: %s.wphm\n",
|
||||
base.c_str());
|
||||
return 1;
|
||||
}
|
||||
auto c = wowee::pipeline::WoweePlayerMovementAnimLoader::load(base);
|
||||
std::vector<std::string> errors;
|
||||
std::vector<std::string> warnings;
|
||||
if (c.entries.empty()) {
|
||||
warnings.push_back("catalog has zero entries");
|
||||
}
|
||||
std::set<uint32_t> idsSeen;
|
||||
using KeyTriple = std::tuple<uint8_t, uint8_t, uint8_t>;
|
||||
std::set<KeyTriple> tripleSeen;
|
||||
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.mapId) +
|
||||
" " + raceIdName(e.raceId) +
|
||||
" " + genderName(e.genderId) +
|
||||
" " + movementStateName(e.movementState) +
|
||||
")";
|
||||
if (e.mapId == 0)
|
||||
errors.push_back(ctx + ": mapId is 0");
|
||||
if (e.raceId == 0 || e.raceId > 10) {
|
||||
errors.push_back(ctx + ": raceId " +
|
||||
std::to_string(e.raceId) +
|
||||
" out of vanilla range (1..10)");
|
||||
}
|
||||
if (e.genderId > 1) {
|
||||
errors.push_back(ctx + ": genderId " +
|
||||
std::to_string(e.genderId) +
|
||||
" out of range (must be 0=male or 1=female)");
|
||||
}
|
||||
if (e.movementState > 7) {
|
||||
errors.push_back(ctx + ": movementState " +
|
||||
std::to_string(e.movementState) +
|
||||
" out of range (0..7)");
|
||||
}
|
||||
// baseAnimId 0 IS valid (Stand is anim id 0
|
||||
// in the M2 table) BUT only for the Idle
|
||||
// state. For any other state, base 0 means
|
||||
// "no animation bound" which would freeze the
|
||||
// model in the previous frame.
|
||||
if (e.baseAnimId == 0 &&
|
||||
e.movementState != wowee::pipeline::
|
||||
WoweePlayerMovementAnim::StateIdle) {
|
||||
errors.push_back(ctx +
|
||||
": baseAnimId 0 on non-Idle state — model "
|
||||
"would freeze when entering this state");
|
||||
}
|
||||
// (race, gender, state) MUST be unique — the
|
||||
// renderer dispatches by this triple. Two
|
||||
// bindings would non-deterministically tie.
|
||||
KeyTriple key{e.raceId, e.genderId, e.movementState};
|
||||
if (!tripleSeen.insert(key).second) {
|
||||
errors.push_back(ctx +
|
||||
": duplicate (raceId, genderId, "
|
||||
"movementState) triple — renderer "
|
||||
"dispatch ambiguous");
|
||||
}
|
||||
if (!idsSeen.insert(e.mapId).second) {
|
||||
errors.push_back(ctx + ": duplicate mapId");
|
||||
}
|
||||
// Self-variant: variantAnimId pointing to the
|
||||
// same id as baseAnimId is meaningless overhead
|
||||
// — it's still a valid setup but the variant
|
||||
// would be a no-op.
|
||||
if (e.variantAnimId != 0 &&
|
||||
e.variantAnimId == e.baseAnimId) {
|
||||
warnings.push_back(ctx +
|
||||
": variantAnimId equals baseAnimId — the "
|
||||
"variant would visually equal the base "
|
||||
"(no-op overhead)");
|
||||
}
|
||||
// Excessive transition: more than 2s of blend
|
||||
// would feel like an animation hang to the
|
||||
// player.
|
||||
if (e.transitionMs > 2000) {
|
||||
warnings.push_back(ctx +
|
||||
": transitionMs=" +
|
||||
std::to_string(e.transitionMs) +
|
||||
" exceeds 2000ms — would feel like an "
|
||||
"animation hang");
|
||||
}
|
||||
}
|
||||
bool ok = errors.empty();
|
||||
if (jsonOut) {
|
||||
nlohmann::json j;
|
||||
j["wphm"] = base + ".wphm";
|
||||
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-wphm: %s.wphm\n", base.c_str());
|
||||
if (ok && warnings.empty()) {
|
||||
std::printf(" OK — %zu bindings, all mapIds unique, "
|
||||
"(race,gender,state) triple unique, "
|
||||
"raceId 1..10, genderId 0..1, state "
|
||||
"0..7, no non-Idle baseAnim==0\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 handlePlayerMovementAnimCatalog(int& i, int argc, char** argv,
|
||||
int& outRc) {
|
||||
if (std::strcmp(argv[i], "--gen-phm-human") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenHuman(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-phm-orc") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenOrc(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--gen-phm-undead") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleGenUndead(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--info-wphm") == 0 && i + 1 < argc) {
|
||||
outRc = handleInfo(i, argc, argv); return true;
|
||||
}
|
||||
if (std::strcmp(argv[i], "--validate-wphm") == 0 &&
|
||||
i + 1 < argc) {
|
||||
outRc = handleValidate(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
12
tools/editor/cli_player_movement_anim_catalog.hpp
Normal file
12
tools/editor/cli_player_movement_anim_catalog.hpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
bool handlePlayerMovementAnimCatalog(int& i, int argc, char** argv,
|
||||
int& outRc);
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue