3131#include " Utilities/MathDefines.h"
3232#include " Utilities/PackedValues.h"
3333#include " Player.h"
34+ #include " LoginEffectPackets.h"
3435#include " Language.h"
3536#include " Database/DatabaseEnv.h"
3637#include " Log.h"
8889
8990#include < cmath>
9091
92+ namespace
93+ {
94+ // Spell 836 is deliberately split around the initial object batch. The
95+ // event emits START after presentation and GO on the next eligible tick.
96+ class LoginEffectEvent final : public BasicEvent
97+ {
98+ public:
99+ explicit LoginEffectEvent (Player& player) : m_player(player)
100+ {
101+ }
102+
103+ bool Execute (uint64 eTime, uint32) override
104+ {
105+ std::optional<LoginEffectPhase> phase =
106+ m_state.TakeNext (m_player.IsInWorld ());
107+ if (!phase)
108+ {
109+ return true ;
110+ }
111+
112+ WorldPacket packet = *phase == LoginEffectPhase::Start ?
113+ LoginEffectPackets::BuildStart (
114+ m_player.GetObjectGuid ().GetRawValue ()) :
115+ LoginEffectPackets::BuildGo (
116+ m_player.GetObjectGuid ().GetRawValue ());
117+ m_player.SendMessageToSet (&packet, true );
118+
119+ if (*phase == LoginEffectPhase::Start)
120+ {
121+ m_player.m_Events .AddEvent (this ,
122+ eTime + LoginEffectDelayBefore (LoginEffectPhase::Go),
123+ false );
124+ return false ;
125+ }
126+ return true ;
127+ }
128+
129+ private:
130+ Player& m_player;
131+ LoginEffectSequenceState m_state;
132+ };
133+
134+ class LoginCinematicRootTimeoutEvent final : public BasicEvent
135+ {
136+ public:
137+ explicit LoginCinematicRootTimeoutEvent (Player& player)
138+ : m_player(player)
139+ {
140+ }
141+
142+ bool Execute (uint64, uint32) override
143+ {
144+ m_player.ReleaseLoginCinematicRoot ();
145+ return true ;
146+ }
147+
148+ private:
149+ Player& m_player;
150+ };
151+ }
152+
91153#define ZONE_UPDATE_INTERVAL (1 *IN_MILLISECONDS )
92154
93155#define PLAYER_SKILL_INDEX (x ) (PLAYER_SKILL_INFO_1_1 + ((x)*3 ))
@@ -698,6 +760,10 @@ Player::~Player()
698760 */
699761void Player::CleanupsBeforeDelete ()
700762{
763+ // Event teardown destroys the timers; clear their independent root token
764+ // before any later cleanup can attempt to release it.
765+ m_loginCinematicRootOwnership.Clear ();
766+
701767 // Stop cinematic flyover if active (must happen before camera dtor)
702768 if (m_cinematicFlyover && m_cinematicFlyover->IsActive ())
703769 {
@@ -3869,10 +3935,19 @@ void Player::SendUpdateWorldState(uint32 Field, uint32 Value)
38693935 * @param zoneid The zone identifier used to select world states.
38703936 */
38713937void Player::SendInitWorldStates (uint32 zoneid)
3938+ {
3939+ SendInitWorldStates (GetMapId (), zoneid);
3940+ }
3941+
3942+ /* *
3943+ * Sends initial world states for an explicit client-visible map anchor.
3944+ * Transport passengers live on a deck map internally while the client still
3945+ * renders the world map sailed by the vessel.
3946+ */
3947+ void Player::SendInitWorldStates (uint32 mapid, uint32 zoneid)
38723948{
38733949 // data depends on zoneid/mapid...
38743950 BattleGround* bg = GetBattleGround ();
3875- uint32 mapid = GetMapId ();
38763951
38773952 DEBUG_LOG (" Sending SMSG_INIT_WORLD_STATES to Map:%u, Zone: %u" , mapid, zoneid);
38783953
@@ -5317,8 +5392,13 @@ void Player::SetComboPoints()
53175392
53185393
53195394
5320- /* Called by WorldSession::HandlePlayerLogin */
5321- void Player::SendInitialPacketsBeforeAddToMap ()
5395+ /* *
5396+ * Sends the map-independent login preamble.
5397+ *
5398+ * @param deferLoginTimeSpeed Keep time/speed for the post-admission retail
5399+ * ordering instead of sending it from this legacy position.
5400+ */
5401+ void Player::SendInitialPacketsBeforeAddToMap (bool deferLoginTimeSpeed)
53225402{
53235403 /* * This packet seems useless...
53245404 * TODO: Work out if we need SMSG_SET_REST_START */
@@ -5344,12 +5424,10 @@ void Player::SendInitialPacketsBeforeAddToMap()
53445424 /* Update player's honour information (does not send anything) */
53455425 UpdateHonor ();
53465426
5347- const float game_time = 0 .01666667f ; // Game speed
5348-
5349- data.Initialize (SMSG_LOGIN_SETTIMESPEED , 4 + 4 );
5350- data << uint32 (secsToTimeBitFields (sWorld .GetGameTime ()));
5351- data << game_time; // Float is 4 bytes here
5352- GetSession ()->SendPacket (&data);
5427+ if (!deferLoginTimeSpeed)
5428+ {
5429+ SendLoginTimeSpeed ();
5430+ }
53535431
53545432 // Set fly flag if player is on a taxi to avoid falling to the ground
53555433 if (IsTaxiFlying ())
@@ -5361,9 +5439,54 @@ void Player::SendInitialPacketsBeforeAddToMap()
53615439 SetMover (this );
53625440}
53635441
5364- /* *
5365- * @brief Sends map-dependent initialization packets after the player is added to the world.
5366- */
5442+ /* * Isolates SMSG_LOGIN_SETTIMESPEED so entry ordering can defer it unchanged. */
5443+ void Player::SendLoginTimeSpeed ()
5444+ {
5445+ WorldPacket data (SMSG_LOGIN_SETTIMESPEED , 8 );
5446+ data << uint32 (secsToTimeBitFields (sWorld .GetGameTime ()));
5447+ data << float (0 .01666667f );
5448+ GetSession ()->SendPacket (&data);
5449+ }
5450+
5451+ /* * Queues the visible START/GO half after the initial object batch is sent. */
5452+ void Player::ScheduleLoginEffect ()
5453+ {
5454+ m_Events.AddEvent (new LoginEffectEvent (*this ),
5455+ m_Events.CalculateTime (
5456+ LoginEffectDelayBefore (LoginEffectPhase::Start)));
5457+ }
5458+
5459+ void Player::BeginLoginCinematicRoot ()
5460+ {
5461+ if (!m_loginCinematicRootOwnership.Claim ())
5462+ {
5463+ return ;
5464+ }
5465+
5466+ // Normal cinematic completion releases first; this timer is the bounded
5467+ // failsafe for clients that never send completion.
5468+ m_Events.AddEvent (new LoginCinematicRootTimeoutEvent (*this ),
5469+ m_Events.CalculateTime (LOGIN_CINEMATIC_ROOT_TIMEOUT_MS ));
5470+ SetRoot (true );
5471+ }
5472+
5473+ void Player::ReleaseLoginCinematicRoot ()
5474+ {
5475+ // Retain the token while out of world; consuming it there would lose the
5476+ // only later opportunity to send the matching unroot.
5477+ if (!m_loginCinematicRootOwnership.ReleaseOnce (IsInWorld ()))
5478+ {
5479+ return ;
5480+ }
5481+
5482+ if (!HasAuraType (SPELL_AURA_MOD_STUN ) &&
5483+ !HasAuraType (SPELL_AURA_MOD_ROOT ))
5484+ {
5485+ // This path owns only the cinematic root; active aura roots win.
5486+ SetRoot (false );
5487+ }
5488+ }
5489+
53675490/* *
53685491 * @brief Where this player is, for the questions the WORLD answers: a graveyard, an area
53695492 * trigger, anything looked up against terrain the client shipped.
@@ -5511,15 +5634,36 @@ void Player::UpdateLiftMinions()
55115634 CONTROLLED_PET | CONTROLLED_MINIPET | CONTROLLED_GUARDIANS );
55125635}
55135636
5514- void Player::SendInitialPacketsAfterAddToMap ()
5637+ /* *
5638+ * Sends map-dependent initialization after committed world entry.
5639+ *
5640+ * A non-null context means the entry hook already sent world states and
5641+ * time/speed before the object batch. Null preserves the legacy teleport path.
5642+ */
5643+ void Player::SendInitialPacketsAfterAddToMap (InitialWorldEntryContext const * initialEntry)
55155644{
5516- /* Update players zone */
5517- uint32 newzone, newarea;
5518- GetTerrain ()->GetZoneAndAreaId (newzone, newarea, Where ().X (), Where ().Y (), Where ().Z ());
5519- UpdateZone (newzone, newarea); // This calls SendInitWorldStates
5645+ if (initialEntry)
5646+ {
5647+ UpdateZone (initialEntry->zoneId , initialEntry->areaId ,
5648+ !initialEntry->initialWorldStatesSent );
5649+ if (initialEntry->cinematicStarted )
5650+ {
5651+ BeginLoginCinematicRoot ();
5652+ }
5653+ // CAST_FAILED was part of the pre-batch hook; START/GO are intentionally
5654+ // deferred until the client has received its initial object world.
5655+ ScheduleLoginEffect ();
5656+ }
5657+ else
5658+ {
5659+ /* Update players zone */
5660+ uint32 newzone, newarea;
5661+ GetTerrain ()->GetZoneAndAreaId (newzone, newarea, Where ().X (), Where ().Y (), Where ().Z ());
5662+ UpdateZone (newzone, newarea); // This calls SendInitWorldStates
55205663
5521- /* Login effect spell */
5522- CastSpell (this , 836 , true ); // LOGINEFFECT
5664+ /* Login effect spell */
5665+ CastSpell (this , 836 , true ); // LOGINEFFECT
5666+ }
55235667
55245668 /* * Sets aura effects that need to be sent after the player is added to the map
55255669 * We use SendMessageToSet so that it's sent to everyone, including the player
@@ -6936,5 +7080,3 @@ void Player::KnockBackFrom(Unit* target, float horizontalSpeed, float verticalSp
69367080 float angle = this == target ? Where ().Facing () + M_PI_F : target->Where ().BearingTo (this ->Where ());
69377081 GetSession ()->SendKnockBack (angle, horizontalSpeed, verticalSpeed);
69387082}
6939-
6940-
0 commit comments