fix: a same-map teleport shows no loading screen, as 1.12.1 does (#1018)
1.12.1's MSG_MOVE_TELEPORT_ACK handler (0x603f90) reaches none of the
loading screen's raises: 0x406800 is entered only from SMSG_TRANSFER_PENDING
(0x401900), the glue EnterWorld (0x46b6f1) and 0x407de0. benilla raised the
cover on a same-map snap twice over: the "focus not resident" backstop on the
snap frame, and a >=100 yd relocation raise. The relocation raise goes, and
the backstop stays off while a snap's settle hold is up; the body is held over
the destination as before.
Verified live against the local realm (map 0, Gmagent): .tele ironforge from
Northshire (~3960 yd) and back covered 50 and 93 frames (0-916 ms, 0-1680 ms)
before, 0 and 0 after; cross-map .tele valley and back still raise the cover
("worldport"). New tests in loading_screen.rs fail on the old code at the snap
frame. benilla-app and benilla-world lib tests green, install-reading tests
green with BENILLA_REQUIRE_DATA=1 (the addon corpus is absent on this
machine). Built on macOS (arm64).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d759008172
commit
7547081a58
@@ -52,10 +52,6 @@ const CLEAR_AFTER_READY_FRAMES: u32 = 3;
|
||||
/// [`WAIT_LOG_EVERY`] seconds.
|
||||
const WAIT_LOG_AFTER: f32 = 3.0;
|
||||
const WAIT_LOG_EVERY: f32 = 2.0;
|
||||
/// How far (yd) a same-map snap must move the body to count as a load. It clears every combat
|
||||
/// relocation (charge and intercept 25 yd, blink 20, knockbacks under 30), which also ends in a
|
||||
/// server teleport and must never black out the screen.
|
||||
const SNAP_LOAD_MIN_YD: f32 = 100.0;
|
||||
|
||||
/// Consecutive covered, in-world frames before the cover is on the glass: renders are serial, so
|
||||
/// at 3 the two frames before have presented.
|
||||
@@ -162,8 +158,6 @@ pub(crate) struct LoadingScreen {
|
||||
/// `Time::elapsed_secs` at the last raise and at the last wait log line.
|
||||
active_since: f32,
|
||||
last_wait_log: f32,
|
||||
/// The avatar's position last frame, to measure a snap (applied in `Input`, a stage earlier).
|
||||
last_pos: Option<Vec3>,
|
||||
}
|
||||
|
||||
impl LoadingScreen {
|
||||
@@ -270,7 +264,7 @@ impl Plugin for LoadingScreenPlugin {
|
||||
// In `First`, ahead of every consumer in `PreUpdate` and `Update`.
|
||||
.add_systems(First, count_entry_cover)
|
||||
.add_systems(Startup, setup_loading_screen.after(AssetSet::Open))
|
||||
// In `WorldStage::Present`, after Input and Stream: a teleport snaps in Input and the
|
||||
// In `WorldStage::Present`, after Input and Stream: a worldport snaps in Input and the
|
||||
// streamer refocuses in Stream, so the cover lands on the same frame, never a flash.
|
||||
.add_systems(Update, drive_loading_screen.in_set(WorldStage::Present));
|
||||
// The cover's input rule, registered here so the two cannot be registered apart.
|
||||
@@ -485,9 +479,9 @@ fn drive_loading_screen(
|
||||
screen.raise("worldport", false, Some(w.map_id), now);
|
||||
}
|
||||
}
|
||||
// A same-map teleport ends any awaited snap; whether it needs a screen is decided below.
|
||||
let teleported = teleports.read().next().is_some();
|
||||
if teleported {
|
||||
// A same-map teleport ends any awaited snap and raises nothing: the reference's
|
||||
// `MSG_MOVE_TELEPORT_ACK` handler (`0x603f90`) reaches none of the screen's raises.
|
||||
if teleports.read().next().is_some() {
|
||||
screen.snap_landed(None);
|
||||
}
|
||||
// Logout, a lost session or a refused login: `CharSelect` applies next frame, so cover the torn
|
||||
@@ -516,35 +510,19 @@ fn drive_loading_screen(
|
||||
}
|
||||
|
||||
// --- Backstop: the ground under the focus is not resident and nothing raised us. In world
|
||||
// only, since behind the glue nothing loads; and only while the focus is the body, since in a
|
||||
// cinematic or free-fly residency describes the camera's tile. ---
|
||||
// only, since behind the glue nothing loads; only while the focus is the body, since in a
|
||||
// cinematic or free-fly residency describes the camera's tile; and never while a same-map
|
||||
// teleport's settle hold is up. ---
|
||||
if !screen.active
|
||||
&& !progress.focus_resident
|
||||
&& focus.follows_body()
|
||||
&& !player_settling
|
||||
&& *state.get() == crate::char_select::ClientState::InWorld
|
||||
{
|
||||
// Same map by construction: the ground under the body.
|
||||
screen.raise("focus not resident", false, map_id, now);
|
||||
}
|
||||
|
||||
// --- At a snap, the same test against the clear's own predicate: a teleport inside the keep
|
||||
// band lands on resident terrain while the destination's buildings may still be arriving. ---
|
||||
let body = player.as_ref().map(|p| p.pos);
|
||||
let relocated = match (teleported, screen.last_pos, body) {
|
||||
(true, Some(was), Some(now_pos)) => was.distance(now_pos) >= SNAP_LOAD_MIN_YD,
|
||||
// No previous position (the entry frame): treat it as a relocation.
|
||||
(true, _, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
screen.last_pos = body;
|
||||
if relocated
|
||||
&& !screen.active
|
||||
&& *state.get() == crate::char_select::ClientState::InWorld
|
||||
&& !(progress.is_ready() && progress.presentable())
|
||||
{
|
||||
screen.raise("teleport, destination not presentable", false, map_id, now);
|
||||
}
|
||||
|
||||
// --- Clear, sustained a few frames. ---
|
||||
if screen.active && !screen.held {
|
||||
if progress.is_ready()
|
||||
@@ -788,4 +766,109 @@ mod tests {
|
||||
screen.dismiss();
|
||||
assert!(!screen.art_resolved);
|
||||
}
|
||||
|
||||
/// `drive_loading_screen` alone, in world, standing on resident ground at `at` (wow coords).
|
||||
fn in_world(at: [f32; 3]) -> App {
|
||||
let mut player = crate::player::Player::default();
|
||||
player.active = true;
|
||||
player.pos = benilla_assets::coords::wow_to_bevy(at);
|
||||
let mut app = App::new();
|
||||
app.add_plugins((MinimalPlugins, bevy::state::app::StatesPlugin))
|
||||
.insert_state(crate::char_select::ClientState::InWorld)
|
||||
.add_message::<crate::net::EnteredWorldMessage>()
|
||||
.add_message::<crate::net::WorldportMessage>()
|
||||
.add_message::<crate::net::TeleportMessage>()
|
||||
.add_message::<crate::net::LoggedOutMessage>()
|
||||
.add_message::<crate::net::DisconnectedMessage>()
|
||||
.add_message::<crate::net::CharacterLoginFailedMessage>()
|
||||
.init_resource::<LoadingScreen>()
|
||||
.init_resource::<crate::pipe_warm::WarmPass>()
|
||||
.init_resource::<Assets<Image>>()
|
||||
.insert_resource(CurrentMap(1))
|
||||
.insert_resource(benilla_world::terrain_stream::ViewFocus::body(at, true))
|
||||
.insert_resource(WorldLoadProgress {
|
||||
ready: 9,
|
||||
total: 9,
|
||||
focus_resident: true,
|
||||
scene_ready: true,
|
||||
..Default::default()
|
||||
})
|
||||
.insert_resource(player)
|
||||
.add_systems(Update, drive_loading_screen);
|
||||
app.update();
|
||||
app
|
||||
}
|
||||
|
||||
/// The snap's frame as the stream sees it: the body held over ground not yet loaded.
|
||||
fn land_unloaded(app: &mut App, at: [f32; 3]) {
|
||||
let pos = benilla_assets::coords::wow_to_bevy(at);
|
||||
let mut player = app.world_mut().resource_mut::<crate::player::Player>();
|
||||
player.pos = pos;
|
||||
player.settling = true;
|
||||
app.insert_resource(benilla_world::terrain_stream::ViewFocus::body(at, false));
|
||||
app.insert_resource(WorldLoadProgress {
|
||||
ready: 0,
|
||||
total: 9,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn covering(app: &App) -> bool {
|
||||
app.world().resource::<LoadingScreen>().covering()
|
||||
}
|
||||
|
||||
const GM_ISLAND: [f32; 3] = [16222.0, 16252.0, 12.5];
|
||||
const SOUTH_SEAS: [f32; 3] = [-11880.0, -4700.0, 2.0];
|
||||
const NORTHSHIRE: [f32; 3] = [-8949.95, -132.493, 83.5312];
|
||||
|
||||
/// GM Island to the South Seas, both on Kalimdor.
|
||||
#[test]
|
||||
fn a_same_map_teleport_across_the_continent_shows_no_loading_screen() {
|
||||
let mut app = in_world(GM_ISLAND);
|
||||
assert!(!covering(&app), "standing on resident ground");
|
||||
|
||||
land_unloaded(&mut app, SOUTH_SEAS);
|
||||
app.world_mut().write_message(crate::net::TeleportMessage {
|
||||
guid: 1,
|
||||
counter: 0,
|
||||
position: SOUTH_SEAS,
|
||||
orientation: 0.0,
|
||||
});
|
||||
app.update();
|
||||
assert!(!covering(&app), "the snap frame raises no screen");
|
||||
|
||||
for _ in 0..30 {
|
||||
app.update();
|
||||
assert!(
|
||||
!covering(&app),
|
||||
"nor does the destination streaming in under the held body"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unloaded_ground_under_a_body_no_snap_holds_is_covered() {
|
||||
let mut app = in_world(GM_ISLAND);
|
||||
land_unloaded(&mut app, SOUTH_SEAS);
|
||||
app.world_mut()
|
||||
.resource_mut::<crate::player::Player>()
|
||||
.settling = false;
|
||||
app.update();
|
||||
assert!(covering(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_worldport_covers_its_load() {
|
||||
let mut app = in_world(GM_ISLAND);
|
||||
land_unloaded(&mut app, NORTHSHIRE);
|
||||
app.world_mut().write_message(crate::net::WorldportMessage {
|
||||
map_id: 0,
|
||||
position: NORTHSHIRE,
|
||||
orientation: 0.0,
|
||||
needs_ack: true,
|
||||
transport_entry: None,
|
||||
});
|
||||
app.update();
|
||||
assert!(covering(&app));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@ pub(super) fn publish_view_focus(
|
||||
}
|
||||
}
|
||||
*was_flying = flying;
|
||||
// Spawn caps pace only a live avatar in a settled world; under the loading cover (entry, a
|
||||
// teleport, a world swap) a cap would only lengthen the reveal.
|
||||
// Spawn caps pace only a live avatar in a settled world; through entry, a teleport or a world
|
||||
// swap a cap would only lengthen the load.
|
||||
*focus = match player.as_deref() {
|
||||
Some(p) if p.active => {
|
||||
let wow = benilla_assets::coords::bevy_to_wow(p.pos);
|
||||
|
||||
@@ -174,7 +174,7 @@ pub struct WorldLoadProgress {
|
||||
pub ready: usize,
|
||||
pub total: usize,
|
||||
/// The tile under the view focus is furnished; false at startup and whenever a teleport lands
|
||||
/// on unloaded ground, same map or not, which is the loading screen's backstop trigger.
|
||||
/// on unloaded ground, same map or not. The loading screen's backstop trigger.
|
||||
pub focus_resident: bool,
|
||||
/// The focus tile and its 8 neighbours are up with every placement they reference; consumers
|
||||
/// read [`Self::presentable`], which adds the backlogs.
|
||||
@@ -233,7 +233,7 @@ pub struct ViewFocus {
|
||||
/// The picked character's map and position, before an avatar exists.
|
||||
entry: Option<(u32, [f32; 3])>,
|
||||
/// The focus is settled, so spawning is paced; false through entry, a teleport and a world
|
||||
/// swap, while the loading cover absorbs the burst.
|
||||
/// swap, while the loading cover or the settle hold absorbs the burst.
|
||||
pub(crate) paced: bool,
|
||||
}
|
||||
|
||||
@@ -657,7 +657,7 @@ fn stream_terrain(
|
||||
}
|
||||
|
||||
// Request new tiles once the WDT has answered. While paced, one per frame and nearest first,
|
||||
// so a fresh row's landings stagger; entry and teleports request all at once under the cover.
|
||||
// so a fresh row's landings stagger; entry and teleports request all at once.
|
||||
let mut fresh_empty = true;
|
||||
if wdt_index.is_some() || state.wdt_ungated {
|
||||
let live = focus.paced;
|
||||
|
||||
Reference in New Issue
Block a user