fix state gate races and robust spline

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-10 23:30:55 +03:00
parent 6ba0edc2fb
commit 535cc20afe
2 changed files with 88 additions and 24 deletions

View file

@ -73,17 +73,24 @@ EntityController::EntityController(GameHandler& owner)
: owner_(owner) { initTypeHandlers(); }
void EntityController::registerOpcodes(DispatchTable& table) {
// World object updates
table[Opcode::SMSG_UPDATE_OBJECT] = [this](network::Packet& packet) {
// World object updates — accept during ENTERING_WORLD too so that entity
// packets arriving before SMSG_LOGIN_VERIFY_WORLD are parsed and queued
// rather than silently dropped (the budget system processes them later once
// the state transitions to IN_WORLD).
auto inWorldOrEntering = [this]() {
auto s = owner_.getState();
return s == WorldState::IN_WORLD || s == WorldState::ENTERING_WORLD;
};
table[Opcode::SMSG_UPDATE_OBJECT] = [this, inWorldOrEntering](network::Packet& packet) {
LOG_DEBUG("Received SMSG_UPDATE_OBJECT, state=", static_cast<int>(owner_.getState()), " size=", packet.getSize());
if (owner_.getState() == WorldState::IN_WORLD) handleUpdateObject(packet);
if (inWorldOrEntering()) handleUpdateObject(packet);
};
table[Opcode::SMSG_COMPRESSED_UPDATE_OBJECT] = [this](network::Packet& packet) {
table[Opcode::SMSG_COMPRESSED_UPDATE_OBJECT] = [this, inWorldOrEntering](network::Packet& packet) {
LOG_DEBUG("Received SMSG_COMPRESSED_UPDATE_OBJECT, state=", static_cast<int>(owner_.getState()), " size=", packet.getSize());
if (owner_.getState() == WorldState::IN_WORLD) handleCompressedUpdateObject(packet);
if (inWorldOrEntering()) handleCompressedUpdateObject(packet);
};
table[Opcode::SMSG_DESTROY_OBJECT] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleDestroyObject(packet);
table[Opcode::SMSG_DESTROY_OBJECT] = [this, inWorldOrEntering](network::Packet& packet) {
if (inWorldOrEntering()) handleDestroyObject(packet);
};
// Entity queries

View file

@ -1029,25 +1029,37 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Save position before WotLK spline header for fallback
size_t beforeSplineHeader = packet.getReadPos();
// AzerothCore MoveSplineFlag constants:
// CATMULLROM = 0x00080000 — uncompressed Catmull-Rom interpolation
// CYCLIC = 0x00100000 — cyclic path
// ENTER_CYCLE = 0x00200000 — entering cyclic path
// ANIMATION = 0x00400000 — animation spline with animType+effectStart
// PARABOLIC = 0x00000008 — vertical_acceleration+effectStartTime
constexpr uint32_t SF_PARABOLIC = 0x00000008;
constexpr uint32_t SF_CATMULLROM = 0x00080000;
constexpr uint32_t SF_CYCLIC = 0x00100000;
constexpr uint32_t SF_ENTER_CYCLE = 0x00200000;
constexpr uint32_t SF_ANIMATION = 0x00400000;
constexpr uint32_t SF_UNCOMPRESSED_MASK = SF_CATMULLROM | SF_CYCLIC | SF_ENTER_CYCLE;
// Try 1: WotLK format (durationMod+durationModNext+[ANIMATION]+vertAccel+effectStart+points)
// Some servers (ChromieCraft) always write vertAccel+effectStart unconditionally.
bool splineParsed = false;
if (bytesAvailable(8)) {
/*float durationMod =*/ packet.readFloat();
/*float durationModNext =*/ packet.readFloat();
bool wotlkOk = true;
if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION
if (splineFlags & SF_ANIMATION) {
if (!bytesAvailable(5)) { wotlkOk = false; }
else { packet.readUInt8(); packet.readUInt32(); }
}
// AzerothCore/ChromieCraft always writes verticalAcceleration(float)
// + effectStartTime(uint32) unconditionally -- NOT gated by PARABOLIC flag.
// Unconditional vertAccel+effectStart (ChromieCraft/some AzerothCore builds)
if (wotlkOk) {
if (!bytesAvailable(8)) { wotlkOk = false; }
else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); }
}
if (wotlkOk) {
// WotLK: compressed unless CYCLIC(0x80000) or ENTER_CYCLE(0x2000) set
bool useCompressed = (splineFlags & (0x00080000 | 0x00002000)) == 0;
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed");
@ -1055,29 +1067,72 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
}
}
if (!splineParsed) {
// WotLK compressed+uncompressed both failed. Try without the parabolic
// fields (some cores don't send vertAccel+effectStart unconditionally).
// Try 2: ANIMATION present but vertAccel+effectStart gated by PARABOLIC
// (standard AzerothCore: only writes vertAccel+effectStart when PARABOLIC is set)
if (!splineParsed && (splineFlags & SF_ANIMATION)) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
bool ok = true;
if (!bytesAvailable(5)) { ok = false; }
else { packet.readUInt8(); packet.readUInt32(); } // animType + effectStart
if (ok && (splineFlags & SF_PARABOLIC)) {
if (!bytesAvailable(8)) { ok = false; }
else { packet.readFloat(); packet.readUInt32(); }
}
if (ok) {
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-anim-conditional");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-anim-conditional-uncomp");
}
}
}
}
// Try 3: No ANIMATION — vertAccel+effectStart only when PARABOLIC set
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
bool ok = true;
if (splineFlags & SF_PARABOLIC) {
if (!bytesAvailable(8)) { ok = false; }
else { packet.readFloat(); packet.readUInt32(); }
}
if (ok) {
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-parabolic-gated");
if (!splineParsed) {
splineParsed = tryParseSplinePoints(false, "wotlk-parabolic-gated-uncomp");
}
}
}
}
// Try 4: No header at all — just durationMod+durationModNext then points
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
if (bytesAvailable(8)) {
packet.readFloat(); // durationMod
packet.readFloat(); // durationModNext
// Skip parabolic fields — try points directly
splineParsed = tryParseSplinePoints(false, "wotlk-no-parabolic");
if (!splineParsed) {
bool useComp = (splineFlags & (0x00080000 | 0x00002000)) == 0;
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useComp, "wotlk-no-parabolic-compressed");
}
}
}
// Try 3: bare points (no WotLK header at all — some spline types skip everything)
// Try 5: bare points (no WotLK header at all — some spline types skip everything)
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
splineParsed = tryParseSplinePoints(false, "bare-uncompressed");
if (!splineParsed) {
packet.setReadPos(beforeSplineHeader);
bool useComp = (splineFlags & (0x00080000 | 0x00002000)) == 0;
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
splineParsed = tryParseSplinePoints(useComp, "bare-compressed");
}
}
@ -1090,8 +1145,8 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
d[di] = packet.readUInt32();
packet.setReadPos(beforeSplineHeader);
LOG_WARNING("WotLK spline parse failed for guid=0x", std::hex, block.guid, std::dec,
" splineFlags=0x", splineFlags,
" remaining=", std::dec, packet.getRemainingSize(),
" splineFlags=0x", std::hex, splineFlags, std::dec,
" remaining=", packet.getRemainingSize(),
" header=[0x", std::hex, d[0], " 0x", d[1], " 0x", d[2],
" 0x", d[3], " 0x", d[4], "]", std::dec);
return false;
@ -1424,10 +1479,12 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
UpdateBlock block;
if (!parseUpdateBlock(packet, block)) {
static int parseBlockErrors = 0;
if (++parseBlockErrors <= 5) {
const uint32_t lostBlocks = data.blockCount - i;
if (++parseBlockErrors <= 10) {
LOG_ERROR("Failed to parse update block ", i + 1, " of ", data.blockCount,
" (", i, " blocks parsed successfully before failure)");
if (parseBlockErrors == 5)
" (", i, " blocks parsed, ", lostBlocks, " blocks LOST",
", remaining=", packet.getRemainingSize(), " bytes)");
if (parseBlockErrors == 10)
LOG_ERROR("(suppressing further update block parse errors)");
}
// Cannot reliably re-sync to the next block after a parse failure,