Skip to content

Commit 402c570

Browse files
authored
Match retail Classic character-screen and initial login flow (#481)
* Fix character screen addon packet order * Batch initial login object updates * Add retail login effect packet builders * Track character enum map evidence * Define initial world entry lifecycle * Match retail login verify-world behavior * Move social lists into retail login preamble * Add retail initial world entry hook * Match retail post-batch login effect flow * Fix pre-world login social list delivery * Preserve cinematic root failsafe across world loss * Restore login verify on admission fallback * Delay retail login effect until world presentation * Retune login effect delay to one second
1 parent 3f45817 commit 402c570

28 files changed

Lines changed: 1459 additions & 151 deletions

src/game/Object/Camera.cpp

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,17 @@ void Camera::ResetView(bool update_far_sight_field /*= true*/)
162162

163163
/**
164164
* @brief Handles the camera being added to the world.
165+
*
166+
* @param batch Shared initial-login data for the owner camera, or null for an
167+
* ordinary visibility rebuild.
165168
*/
166-
void Camera::Event_AddedToWorld()
169+
void Camera::Event_AddedToWorld(InitialWorldUpdateBatch* batch)
167170
{
168171
GridType* grid = m_source->GetViewPoint().m_grid;
169172
MANGOS_ASSERT(grid);
170173
grid->AddWorldObject(this);
171174

172-
UpdateVisibilityForOwner();
175+
UpdateVisibilityForOwnerInBatch(batch);
173176
}
174177

175178
/**
@@ -221,6 +224,15 @@ void Camera::UpdateVisibilityOf(WorldObject* target, UpdateData& data, std::set<
221224
* @brief Rebuilds visibility for the camera owner around the current source.
222225
*/
223226
void Camera::UpdateVisibilityForOwner()
227+
{
228+
UpdateVisibilityForOwnerInBatch(NULL);
229+
}
230+
231+
/**
232+
* Rebuilds owner visibility while optionally appending create blocks to the
233+
* initial self/transport batch instead of sending a second update packet.
234+
*/
235+
void Camera::UpdateVisibilityForOwnerInBatch(InitialWorldUpdateBatch* batch)
224236
{
225237
// Honor a per-viewpoint visibility distance override (e.g. the cinematic
226238
// flyover body widens the populate radius); otherwise use the map default.
@@ -230,7 +242,7 @@ void Camera::UpdateVisibilityForOwner()
230242
visibilityDistance = m_source->GetMap()->GetVisibilityDistance();
231243
}
232244

233-
MaNGOS::VisibleNotifier notifier(*this);
245+
MaNGOS::VisibleNotifier notifier(*this, batch);
234246
Cell::VisitAllObjects(m_source, notifier, visibilityDistance, false);
235247

236248
// The other side of a vessel's boundary. A deck and the shore it sails past are two

src/game/Object/Camera.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class WorldObject;
3535
class UpdateData;
3636
class WorldPacket;
3737
class Player;
38+
class InitialWorldUpdateBatch;
3839

3940
/// Camera - object-receiver. Receives broadcast packets from nearby worldobjects, object visibility changes and sends them to client
4041
class Camera
@@ -74,7 +75,7 @@ class Camera
7475

7576
private:
7677
// called when viewpoint changes visibility state
77-
void Event_AddedToWorld();
78+
void Event_AddedToWorld(InitialWorldUpdateBatch* batch);
7879
void Event_RemovedFromWorld();
7980
void Event_Moved();
8081
void Event_ViewPointVisibilityChanged();
@@ -83,6 +84,7 @@ class Camera
8384
WorldObject* m_source;
8485

8586
void UpdateForCurrentViewPoint();
87+
void UpdateVisibilityForOwnerInBatch(InitialWorldUpdateBatch* batch);
8688

8789
public:
8890
GridReference<Camera>& GetGridRef()
@@ -128,10 +130,17 @@ class ViewPoint
128130
bool hasViewers() const { return !m_cameras.empty(); }
129131

130132
// these events are called when viewpoint changes visibility state
131-
void Event_AddedToWorld(GridType* grid)
133+
void Event_AddedToWorld(GridType* grid, Player* batchOwner = NULL,
134+
InitialWorldUpdateBatch* batch = NULL)
132135
{
133136
m_grid = grid;
134-
CameraCall(&Camera::Event_AddedToWorld);
137+
// A viewpoint may have several cameras. Only the logging-in
138+
// player's own camera may consume the shared initial batch.
139+
for (CameraList::iterator itr = m_cameras.begin(); itr != m_cameras.end();)
140+
{
141+
Camera* c = *(itr++);
142+
c->Event_AddedToWorld(c->GetOwner() == batchOwner ? batch : NULL);
143+
}
135144
}
136145

137146
void Event_RemovedFromWorld()

src/game/Object/Player.cpp

Lines changed: 163 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
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"
@@ -88,6 +89,67 @@
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
*/
699761
void 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
*/
38713937
void 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-

src/game/Object/Player.h

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
#include "PetMgr.h" // held by value on Player; owns stable-slot count + temp-unsummon pet number
8888
#include "BattleGround.h"
8989
#include "DBCStores.h"
90+
#include "InitialWorldEntry.h"
9091
#include "SharedDefines.h"
9192
#include "Chat.h"
9293
#include "GMTicketMgr.h"
@@ -1189,8 +1190,11 @@ class Player : public Unit
11891190
return Where().Z() < m_lastFallZ;
11901191
}
11911192

1192-
void SendInitialPacketsBeforeAddToMap(); // Send initial packets before adding the player to the map
1193-
void SendInitialPacketsAfterAddToMap(); // Send initial packets after adding the player to the map
1193+
// A context is supplied only for the initial login lifecycle; null keeps
1194+
// ordinary teleport callers on their established packet sequence.
1195+
void SendInitialPacketsBeforeAddToMap(bool deferLoginTimeSpeed = false);
1196+
void SendInitialPacketsAfterAddToMap(InitialWorldEntryContext const* initialEntry = nullptr);
1197+
void SendLoginTimeSpeed();
11941198
void SendInstanceResetWarning(uint32 mapid, uint32 time); // Send instance reset warning
11951199

11961200
// Get the NPC if the player can interact with it
@@ -2498,7 +2502,7 @@ class Player : public Unit
24982502
void SetFFAPvP(bool state);
24992503

25002504
// Update the player's zone
2501-
void UpdateZone(uint32 newZone, uint32 newArea);
2505+
void UpdateZone(uint32 newZone, uint32 newArea, bool sendInitialWorldStates = true);
25022506

25032507
// Update the player's area
25042508
void UpdateArea(uint32 newArea);
@@ -3178,6 +3182,7 @@ class Player : public Unit
31783182
void CastItemUseSpell(Item* item, SpellCastTargets const& targets);
31793183

31803184
void SendInitWorldStates(uint32 zone);
3185+
void SendInitWorldStates(uint32 mapId, uint32 zone);
31813186
void SendUpdateWorldState(uint32 Field, uint32 Value);
31823187

31833188
// Send a direct message to the client
@@ -3551,6 +3556,11 @@ class Player : public Unit
35513556
// Set the cinematic flyover manager
35523557
void SetCinematicFlyover(std::unique_ptr<CinematicFlyover> flyover) { m_cinematicFlyover = std::move(flyover); }
35533558

3559+
// Initial-login presentation state; unrelated spell roots are not owned here.
3560+
void ScheduleLoginEffect();
3561+
void BeginLoginCinematicRoot();
3562+
void ReleaseLoginCinematicRoot();
3563+
35543564
// Forced speed changes
35553565
uint8 m_forced_speed_changes[MAX_MOVE_TYPE];
35563566

@@ -4060,6 +4070,8 @@ class Player : public Unit
40604070
// Cinematic flyover manager (optional, for first-login intro visibility)
40614071
std::unique_ptr<CinematicFlyover> m_cinematicFlyover;
40624072

4073+
LoginCinematicRootOwnership m_loginCinematicRootOwnership;
4074+
40634075
// Countdown (ms) for the periodic observer-side visibility sweep
40644076
uint32 m_visibilityObserverSweepTimer;
40654077

0 commit comments

Comments
 (0)