World-enter queries wait for the player's create, as 1.12.1 sends them (#801)
At login benilla sent `CMSG_SET_ACTIVE_MOVER`, `CMSG_QUERY_TIME`, `MSG_QUERY_NEXT_MAIL_TIME` and `CMSG_BATTLEFIELD_STATUS` right after `CMSG_PLAYER_LOGIN`, before the server had seated the player. cmangos drops them; after a worldport or `/reload` the time and mail queries never went again. In 1.12.1 all four follow the local player's own create (`0x5dea50`): `SetActiveMover` (`0x6006e0`), then the world-enter cascade (`0x5deb60 call 0x4908c0`), whose inits send `0x1CE`, `0x284` and `0x2D3` in that order. A cross-map worldport re-creates the player and takes the same route; `/reload` re-enters the cascade alone (`0x490168`). The four now ride the self create, in that order, and `/reload` re-sends the three. Tests cover login, the create, a worldport, the schedule order and the reload; a live trace shows the order at login, after `.tele` and on `ReloadUI()`. Fixes #789 Fixes #100 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Elias Miettinen <eliaspmiettinen@gmail.com> Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Elias Miettinen
Sam
parent
bb84f1f1fb
commit
ffdc206ac2
@@ -1120,7 +1120,7 @@ pub(crate) mod schedule_tests {
|
||||
("ui_bank/mod.rs", "feed_bank", Because::PlayerRoundTrip,
|
||||
"`BankErrors` answers a `BuyBankSlot` click; the OPENED edge rides `VmMemo`s"),
|
||||
("ui_battlefield.rs", "feed_battlefield", Because::SelfHealing,
|
||||
"`reset_on_world_enter` runs before it, clears the session and re-sends `BattlefieldStatusRequest`"),
|
||||
"`reset_on_world_enter` runs before it at each world-enter cascade, clears the session and re-sends `BattlefieldStatusRequest`"),
|
||||
("ui_binder.rs", "feed_binder", Because::PlayerRoundTrip,
|
||||
"`SMSG_BINDER_CONFIRM` only answers the innkeeper's gossip line"),
|
||||
("ui_char.rs", "feed_char", Because::MemoLatched,
|
||||
|
||||
@@ -29,6 +29,7 @@ mod session;
|
||||
mod world;
|
||||
|
||||
pub(crate) use apply::apply_net_updates;
|
||||
pub(crate) use apply::enter_world_on_self_create;
|
||||
use apply::tag_self_player;
|
||||
pub(crate) use handlers::NetHandlerApp;
|
||||
|
||||
@@ -120,6 +121,7 @@ impl Plugin for NetPlugin {
|
||||
.add_message::<CharActionResultMessage>()
|
||||
.add_message::<CharacterLoginFailedMessage>()
|
||||
.add_message::<EnteredWorldMessage>()
|
||||
.add_message::<WorldEnterCascadeMessage>()
|
||||
.add_message::<CinematicTriggeredMessage>()
|
||||
.add_message::<ServerSaidMessage>()
|
||||
.add_message::<LoggedOutMessage>()
|
||||
@@ -132,6 +134,7 @@ impl Plugin for NetPlugin {
|
||||
(
|
||||
apply_net_updates,
|
||||
tag_self_player,
|
||||
enter_world_on_self_create,
|
||||
sample_splines,
|
||||
// Swim state from the water at the feet (the wire never carries it for
|
||||
// creatures), before the clamp so a swimmer is exempt the same frame.
|
||||
@@ -151,8 +154,14 @@ impl Plugin for NetPlugin {
|
||||
// `apply_net_updates` and before `drive_display_facing`, so the `"npc"` token is
|
||||
// current when a window's show handler reads it.
|
||||
)
|
||||
// Not part of the movement chain above: one send on the world-enter message.
|
||||
.add_systems(Update, send_query_time.in_set(WorldStage::Net))
|
||||
// Not part of the movement chain above: the cascade's time query, after the self create
|
||||
// has claimed the mover.
|
||||
.add_systems(
|
||||
Update,
|
||||
send_query_time
|
||||
.in_set(WorldStage::Net)
|
||||
.after(enter_world_on_self_create),
|
||||
)
|
||||
.add_systems(Update, population_pulse.in_set(WorldStage::Net));
|
||||
}
|
||||
}
|
||||
@@ -553,19 +562,22 @@ impl ServerWallClock {
|
||||
/// `[0xbb749c]`, armed `now + 0xe10` at `0x4def11`).
|
||||
const RESYNC_AFTER: Duration = Duration::from_secs(3600);
|
||||
|
||||
/// Ask for the server's wall clock on entering the world (login, worldport, instance transfer)
|
||||
/// and hourly after, which tracks a server re-clocked under us.
|
||||
fn send_query_time(
|
||||
mut entered: MessageReader<EnteredWorldMessage>,
|
||||
/// Ask for the server's wall clock at each world-enter cascade (the quest-log init `0x4de430`,
|
||||
/// called at `0x4909a1`, sends it at `0x4de45b`) and hourly after, which tracks a server
|
||||
/// re-clocked under us.
|
||||
pub(crate) fn send_query_time(
|
||||
mut cascades: MessageReader<WorldEnterCascadeMessage>,
|
||||
commands: Res<NetCommands>,
|
||||
clock: Res<ServerWallClock>,
|
||||
status: Res<NetStatus>,
|
||||
mut asked_at: Local<Option<Instant>>,
|
||||
mut seated: Local<bool>,
|
||||
) {
|
||||
let entering = entered.read().next().is_some();
|
||||
// Only while connected; the world-enter send covers a reconnect.
|
||||
let due =
|
||||
status.connected && clock.stale() && asked_at.is_none_or(|t| t.elapsed() >= RESYNC_AFTER);
|
||||
let entering = cascades.read().next().is_some();
|
||||
// `connected` rises at `CMSG_PLAYER_LOGIN`, before the server seats us, so the resync waits
|
||||
// for the first cascade; a logout or a disconnect clears `connected` and so the wait re-arms.
|
||||
*seated = status.connected && (*seated || entering);
|
||||
let due = *seated && clock.stale() && asked_at.is_none_or(|t| t.elapsed() >= RESYNC_AFTER);
|
||||
if entering || due {
|
||||
*asked_at = Some(Instant::now());
|
||||
let _ = commands.0.send(ClientCommand::QueryTime);
|
||||
@@ -812,8 +824,8 @@ pub(crate) enum ClientCommand {
|
||||
guid: u64,
|
||||
lag_ms: u32,
|
||||
},
|
||||
/// `CMSG_SET_ACTIVE_MOVER`: at login and on possession; the server drops `MSG_MOVE_*` for an
|
||||
/// unconfirmed mover.
|
||||
/// `CMSG_SET_ACTIVE_MOVER`: at our own player's create and on possession; the server drops
|
||||
/// `MSG_MOVE_*` for an unconfirmed mover.
|
||||
SetActiveMover {
|
||||
guid: u64,
|
||||
},
|
||||
@@ -2004,6 +2016,12 @@ pub(crate) struct EnteredWorldMessage {
|
||||
pub(crate) tutorial_flags: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// The reference's world-enter cascade (`0x4908c0`) ran: our own player's create, at login and
|
||||
/// every cross-map worldport ([`enter_world_on_self_create`]), or a `/reload`. Its sends wait for
|
||||
/// this, not [`EnteredWorldMessage`], which fires before the server has seated the player.
|
||||
#[derive(Message)]
|
||||
pub(crate) struct WorldEnterCascadeMessage;
|
||||
|
||||
/// `SMSG_ADDON_INFO`: the addons the server hid from the Lua index space, or `None` if it did
|
||||
/// not answer. A resource, since it must exist before the world-entry UI load runs any addon;
|
||||
/// rewritten on every login.
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
//! The per-frame wire→ECS bridge systems: [`apply_net_updates`] drains the inbound
|
||||
//! [`SessionEvent`] channel through the handler table, and [`tag_self_player`] marks our own
|
||||
//! streamed entity. Nothing is applied here: every packet's handler lives with its subsystem.
|
||||
//! [`SessionEvent`] channel through the handler table, [`tag_self_player`] marks our own
|
||||
//! streamed entity, and [`enter_world_on_self_create`] runs what its create starts. Nothing is
|
||||
//! applied here: every packet's handler lives with its subsystem.
|
||||
|
||||
use benilla_protocol::SessionEvent;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::{Guid, NetEvents, SelfGuid, SelfPlayer};
|
||||
use super::{
|
||||
ClientCommand, Guid, NetCommands, NetEvents, SelfGuid, SelfPlayer, WorldEnterCascadeMessage,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod seam_tests;
|
||||
#[cfg(test)]
|
||||
mod world_enter_tests;
|
||||
|
||||
// ── The per-frame bridge systems ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -38,3 +43,26 @@ pub(super) fn tag_self_player(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Our own player's create, the reference's `0x5dea50` (reached `0x465dbc` → `0x5debe0` →
|
||||
/// `0x5dec7b`): `SetActiveMover` (`0x6006e0`, sending `CMSG_SET_ACTIVE_MOVER` at `0x6007ae`), then
|
||||
/// the world-enter cascade (`0x5deb60 call 0x4908c0`). `SMSG_LOGIN_VERIFY_WORLD` and
|
||||
/// `SMSG_NEW_WORLD` purge the streamed world (`session::worldport`), so a login, a reconnect and
|
||||
/// every cross-map worldport create us afresh and tag us again; a same-map teleport creates
|
||||
/// nothing. The server has seated the player by then, so nothing sent here is dropped.
|
||||
pub(crate) fn enter_world_on_self_create(
|
||||
created: Query<&Guid, Added<SelfPlayer>>,
|
||||
net: Res<NetCommands>,
|
||||
mut cascades: MessageWriter<WorldEnterCascadeMessage>,
|
||||
) {
|
||||
for guid in &created {
|
||||
if benilla_assets::trace::enabled() {
|
||||
benilla_assets::trace::line(
|
||||
"mvr",
|
||||
&format!("SET_ACTIVE_MOVER guid={:#x} (self create)", guid.0),
|
||||
);
|
||||
}
|
||||
let _ = net.0.send(ClientCommand::SetActiveMover { guid: guid.0 });
|
||||
cascades.write(WorldEnterCascadeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//! The world-enter sends wait for our own player's create, as the reference's do (`0x5dea50`):
|
||||
//! none goes out on the login edge alone, which fires before the server seats the player; the
|
||||
//! create sends `CMSG_SET_ACTIVE_MOVER`, then the cascade's `CMSG_QUERY_TIME`,
|
||||
//! `MSG_QUERY_NEXT_MAIL_TIME` and `CMSG_BATTLEFIELD_STATUS`; and a worldport's fresh create sends
|
||||
//! all four again.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use crossbeam_channel::Receiver;
|
||||
|
||||
use crate::net::{
|
||||
ClientCommand, EnteredWorldMessage, Guid, NetCommands, NetStatus, SelfGuid, ServerWallClock,
|
||||
WorldEnterCascadeMessage,
|
||||
};
|
||||
|
||||
const ME: u64 = 0x0000_0000_0000_0042;
|
||||
|
||||
/// The self-create edge and the cascade's three senders, in the order the client schedules them
|
||||
/// (`the_client_schedules_the_sends_in_the_reference_order` pins that), logged in as [`ME`].
|
||||
fn app() -> (App, Receiver<ClientCommand>) {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let mut app = App::new();
|
||||
app.insert_resource(NetCommands(tx))
|
||||
.insert_resource(SelfGuid(Some(ME)))
|
||||
.insert_resource(NetStatus {
|
||||
connected: true,
|
||||
last_reason: None,
|
||||
})
|
||||
.init_resource::<ServerWallClock>()
|
||||
.init_resource::<crate::ui_mail::MailPending>()
|
||||
.init_resource::<crate::ui_battlefield::Battlefield>()
|
||||
.add_message::<EnteredWorldMessage>()
|
||||
.add_message::<WorldEnterCascadeMessage>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
super::tag_self_player,
|
||||
super::enter_world_on_self_create,
|
||||
crate::net::send_query_time,
|
||||
crate::ui_mail::send_query_next_mail_time_on_enter,
|
||||
crate::ui_battlefield::reset_on_world_enter,
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
(app, rx)
|
||||
}
|
||||
|
||||
/// The login edge `connected` writes at `CMSG_PLAYER_LOGIN`.
|
||||
fn log_in(app: &mut App) {
|
||||
app.world_mut().write_message(EnteredWorldMessage {
|
||||
billing_time_rested: 0,
|
||||
tutorial_flags: None,
|
||||
});
|
||||
}
|
||||
|
||||
/// What `object_create` spawns for our own create block.
|
||||
fn self_create(app: &mut App) -> Entity {
|
||||
app.world_mut().spawn(Guid(ME)).id()
|
||||
}
|
||||
|
||||
fn sent(rx: &Receiver<ClientCommand>) -> Vec<String> {
|
||||
rx.try_iter()
|
||||
.map(|c| match c {
|
||||
ClientCommand::SetActiveMover { guid } => format!("SetActiveMover {guid:#x}"),
|
||||
other => format!("{other:?}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn the_four() -> Vec<String> {
|
||||
vec![
|
||||
format!("SetActiveMover {ME:#x}"),
|
||||
"QueryTime".into(),
|
||||
"QueryNextMailTime".into(),
|
||||
"BattlefieldStatusRequest".into(),
|
||||
]
|
||||
}
|
||||
|
||||
/// cmangos drops each of the four as "the player has not logged in yet" when it beats the load,
|
||||
/// so the login edge sends none, and the hourly clock resync (the clock never answered, so stale)
|
||||
/// waits for the create too.
|
||||
#[test]
|
||||
fn the_login_edge_alone_sends_nothing() {
|
||||
let (mut app, rx) = app();
|
||||
log_in(&mut app);
|
||||
for _ in 0..3 {
|
||||
app.update();
|
||||
}
|
||||
assert_eq!(sent(&rx), Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_self_create_sends_the_four_in_the_reference_order() {
|
||||
let (mut app, rx) = app();
|
||||
log_in(&mut app);
|
||||
app.update();
|
||||
self_create(&mut app);
|
||||
app.update();
|
||||
assert_eq!(sent(&rx), the_four());
|
||||
// The same body on later frames is no new create.
|
||||
app.update();
|
||||
app.update();
|
||||
assert_eq!(sent(&rx), Vec::<String>::new());
|
||||
}
|
||||
|
||||
/// A cross-map worldport purges the streamed world and the server creates us afresh
|
||||
/// (`0x401bc0` rebuilds the object manager; the create takes the login's route).
|
||||
#[test]
|
||||
fn a_worldport_create_sends_the_four_again() {
|
||||
let (mut app, rx) = app();
|
||||
log_in(&mut app);
|
||||
let first = self_create(&mut app);
|
||||
app.update();
|
||||
assert_eq!(sent(&rx), the_four());
|
||||
|
||||
app.world_mut().despawn(first);
|
||||
app.update();
|
||||
assert_eq!(sent(&rx), Vec::<String>::new(), "nothing between the two");
|
||||
self_create(&mut app);
|
||||
app.update();
|
||||
assert_eq!(sent(&rx), the_four());
|
||||
}
|
||||
|
||||
/// The production schedule: the self-create edge, then the time, mail and battlefield sends.
|
||||
#[test]
|
||||
fn the_client_schedules_the_sends_in_the_reference_order() {
|
||||
use crate::test_support::runs_before;
|
||||
let mut app = crate::game_plugins::schedule_tests::headless_client();
|
||||
assert!(runs_before(
|
||||
&mut app,
|
||||
super::tag_self_player,
|
||||
super::enter_world_on_self_create
|
||||
));
|
||||
assert!(runs_before(
|
||||
&mut app,
|
||||
super::enter_world_on_self_create,
|
||||
crate::net::send_query_time
|
||||
));
|
||||
assert!(runs_before(
|
||||
&mut app,
|
||||
crate::net::send_query_time,
|
||||
crate::ui_mail::send_query_next_mail_time_on_enter
|
||||
));
|
||||
assert!(runs_before(
|
||||
&mut app,
|
||||
crate::ui_mail::send_query_next_mail_time_on_enter,
|
||||
crate::ui_battlefield::reset_on_world_enter
|
||||
));
|
||||
}
|
||||
@@ -607,8 +607,9 @@ fn run(
|
||||
.find(|c| c.guid == guid)
|
||||
.map(|c| c.name.clone())
|
||||
.unwrap_or_default();
|
||||
// No `CMSG_SET_ACTIVE_MOVER` here: it waits for our own player's create, as the
|
||||
// reference's does (`super::enter_world_on_self_create`); a server may drop it before then.
|
||||
session.player_login(refuse_once(guid))?;
|
||||
session.set_active_mover(guid)?;
|
||||
|
||||
let billing_time_rested = session.billing_time_rested();
|
||||
let tutorial_flags = session.take_tutorial_flags();
|
||||
|
||||
@@ -16,7 +16,7 @@ use benilla_protocol::messages::{BattlefieldList, BattlefieldStatus};
|
||||
use benilla_ui::script::{BattlefieldListView, BattlefieldMapInfo, BattlefieldQueueSlot, UiScript};
|
||||
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{ClientCommand, EnteredWorldMessage, NetCommands};
|
||||
use crate::net::{ClientCommand, NetCommands, WorldEnterCascadeMessage};
|
||||
use crate::player::Player;
|
||||
use crate::ui_dialog_verbs::BattlefieldQueue;
|
||||
use crate::ui_party::GroupState;
|
||||
@@ -307,15 +307,16 @@ fn drain_battlefield(
|
||||
}
|
||||
}
|
||||
|
||||
/// World enter (`0x4a9db0`): the list, the selection and the anchor clear, the queue slots stay,
|
||||
/// and the bodyless `CMSG_BATTLEFIELD_STATUS` goes out, answered slot by slot.
|
||||
fn reset_on_world_enter(
|
||||
mut entered: MessageReader<EnteredWorldMessage>,
|
||||
/// The world-enter cascade's battlefield init (`0x4909fb` → `0x4a9db0`), after the mail query: the
|
||||
/// list, the selection and the anchor clear, the queue slots stay, and the bodyless
|
||||
/// `CMSG_BATTLEFIELD_STATUS` goes out, answered slot by slot.
|
||||
pub(crate) fn reset_on_world_enter(
|
||||
mut cascades: MessageReader<WorldEnterCascadeMessage>,
|
||||
mut state: ResMut<Battlefield>,
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
commands: Res<NetCommands>,
|
||||
) {
|
||||
if entered.read().next().is_none() {
|
||||
if cascades.read().next().is_none() {
|
||||
return;
|
||||
}
|
||||
state.clear_session();
|
||||
@@ -362,6 +363,7 @@ impl Plugin for BattlefieldPlugin {
|
||||
(
|
||||
reset_on_world_enter
|
||||
.in_set(crate::ui_script::UiFeed)
|
||||
.after(crate::ui_mail::send_query_next_mail_time_on_enter)
|
||||
.before(feed_battlefield),
|
||||
feed_battlefield
|
||||
.before(crate::ui_battlefield_score::feed_battlefield_score)
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::entities::ItemDisplays;
|
||||
use crate::items::Items;
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{
|
||||
ClientCommand, EnteredWorldMessage, NetCommands, ObjectStore, Objects, SelfPlayer,
|
||||
ClientCommand, NetCommands, ObjectStore, Objects, SelfPlayer, WorldEnterCascadeMessage,
|
||||
};
|
||||
use crate::query_cache::QueryCache;
|
||||
use crate::ui_script::{UiFeed, UiInput};
|
||||
@@ -175,21 +175,22 @@ impl Plugin for UiMailPlugin {
|
||||
close_npc_session_out_of_range::<MailOpen>.before(feed_mail),
|
||||
feed_mail.after(crate::ui_unit::UnitFeed).in_set(UiFeed),
|
||||
drain_mail.after(UiInput),
|
||||
send_query_next_mail_time_on_enter,
|
||||
// The cascade's order: the time query, this, then the battlefield status.
|
||||
send_query_next_mail_time_on_enter.after(crate::net::send_query_time),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `MSG_QUERY_NEXT_MAIL_TIME` at every world enter, from the mail module's init in the world-enter
|
||||
/// cascade (`0x4908c0`). The init stamps "no mail" first, so the icon goes dark across a loading
|
||||
/// screen until the reply.
|
||||
fn send_query_next_mail_time_on_enter(
|
||||
mut entered: MessageReader<EnteredWorldMessage>,
|
||||
/// `MSG_QUERY_NEXT_MAIL_TIME` at each world-enter cascade, from the mail init (`0x4acb10`, called
|
||||
/// at `0x4909f6`), after the time query and before the battlefield status. The init stamps "no
|
||||
/// mail" first (`0x4acb87`).
|
||||
pub(crate) fn send_query_next_mail_time_on_enter(
|
||||
mut cascades: MessageReader<WorldEnterCascadeMessage>,
|
||||
commands: Res<NetCommands>,
|
||||
mut pending: ResMut<MailPending>,
|
||||
) {
|
||||
if entered.read().next().is_some() {
|
||||
if cascades.read().next().is_some() {
|
||||
pending.on_query_sent();
|
||||
let _ = commands.0.send(ClientCommand::QueryNextMailTime);
|
||||
}
|
||||
|
||||
@@ -631,6 +631,17 @@ pub(crate) fn run_pending_reload(world: &mut World) {
|
||||
info!("ui_script: ReloadUI — ending the UI session and building a new one");
|
||||
end_ui_session(world);
|
||||
load_ingame_ui_on_world_entry(world);
|
||||
// `UI_Init` ends by re-entering the world-enter cascade (`0x490168`), its sends included, but
|
||||
// only with the active player set (`0x490166 je`): our own player's entity exists. Not
|
||||
// `SetActiveMover`, which only the player's create runs.
|
||||
let seated = world
|
||||
.query_filtered::<(), With<crate::net::SelfPlayer>>()
|
||||
.iter(world)
|
||||
.next()
|
||||
.is_some();
|
||||
if seated {
|
||||
world.write_message(crate::net::WorldEnterCascadeMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/// `AppExit`, the quit roots, read as a message because a quit from in-world never leaves
|
||||
@@ -842,6 +853,31 @@ mod tests {
|
||||
assert_eq!(*world.resource::<VPlateMode>(), on, "a reload keeps them");
|
||||
}
|
||||
|
||||
/// A `ReloadUI()` re-enters the world-enter cascade (`0x490168`), whose readers re-send the
|
||||
/// time, mail and battlefield queries, only with the active player set (`0x490166 je`).
|
||||
#[test]
|
||||
fn a_reload_reenters_the_world_enter_cascade_once_the_player_exists() {
|
||||
use crate::net::WorldEnterCascadeMessage;
|
||||
let reload = |seated: bool| {
|
||||
let mut world = World::new();
|
||||
world.insert_resource(State::new(crate::char_select::ClientState::InWorld));
|
||||
world.insert_resource(crate::run_mode::CaptureMode);
|
||||
world.init_resource::<Messages<WorldEnterCascadeMessage>>();
|
||||
if seated {
|
||||
world.spawn(crate::net::SelfPlayer);
|
||||
}
|
||||
world.insert_resource(ReloadUiPending(true));
|
||||
run_pending_reload(&mut world);
|
||||
assert!(!world.resource::<ReloadUiPending>().0, "the reload ran");
|
||||
world
|
||||
.resource_mut::<Messages<WorldEnterCascadeMessage>>()
|
||||
.drain()
|
||||
.count()
|
||||
};
|
||||
assert_eq!(reload(true), 1, "in the world");
|
||||
assert_eq!(reload(false), 0, "before our own player's create");
|
||||
}
|
||||
|
||||
/// The reference re-makes its Lua state inside `UI_Init` (`0x48fe97`).
|
||||
#[test]
|
||||
fn the_world_entry_builds_its_own_vm_rather_than_adopting_the_glue_phases() {
|
||||
|
||||
@@ -284,7 +284,8 @@ impl WorldSession {
|
||||
self.send(opcode::CMSG_PLAYER_LOGIN, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
/// Declare the unit we move, as the 1.12 client does at login; vmangos drops moves until then.
|
||||
/// Declare the unit we move, as the 1.12 client does at its own create; vmangos drops moves
|
||||
/// until then.
|
||||
pub fn set_active_mover(&mut self, guid: u64) -> Result<()> {
|
||||
self.send(opcode::CMSG_SET_ACTIVE_MOVER, &messages::full_guid(guid))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user