The meeting-stone query runs at every world entry, as 1.12.1 sends it (#810)
The meeting-stone status query (`CMSG 0x296`) and its text reset ran once per UI VM, and the text drop only on leaving the world. So after a cross-map worldport benilla kept the old text and never asked again. In 1.12.1 the world-enter cascade's meeting-stone init (`0x490a14` → `0x4c9f40`) resets the text to `UNKNOWN` and sends `0x296`, right after the battlefield init, on every pass: login, every cross-map worldport and every `/reload`. The leave-world sweep `0x490a80` drops the text (`0x490b2f` → `0x4c9f80`) on a worldport, a `/reload` and leaving the world. The enter leg now runs on the world-enter cascade after the battlefield status. The sweep is one edge, written whenever its latch is spent, and the drop reads it. Tests cover the cascade, a worldport, a reload and the schedule order; a live trace shows `0x296` after `0x2D3` and its reply at login, on `ReloadUI()` and after `.tele`. Fixes #99 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
85980939f5
commit
2e82d34c1b
@@ -486,14 +486,53 @@ pub(crate) fn seat_from_roster(
|
||||
pub(crate) struct AddOnIdentity(pub(crate) Option<(String, String)>);
|
||||
|
||||
/// The world latch, the reference's `[0xb4b424]`: set at `0x4908ce` in the world-enter cascade
|
||||
/// `0x4908c0` and cleared at `0x490a8d` as `PLAYER_LEAVING_WORLD` fires (`0x490b4d`, its one
|
||||
/// site), so the first departure in a world fires and the rest are no-ops until the next entry.
|
||||
/// Ours has two producers, [`shutdown_ui_state`]'s tail and `ui_unit`'s worldport fire. A
|
||||
/// `0x4908c0` and cleared at `0x490a8d` by the leave-world sweep `0x490a80`, which then fires
|
||||
/// `PLAYER_LEAVING_WORLD` (`0x490b4d`, its one site), so the first departure in a world sweeps
|
||||
/// and the rest are no-ops until the next entry. Ours is spent through [`LeaveWorldSweep`] or
|
||||
/// [`run_leave_world_sweep`], by [`shutdown_ui_state`]'s roots and `ui_unit`'s worldport fire. A
|
||||
/// cross-map worldport spends it without leaving `InWorld`, so a quit on its loading screen
|
||||
/// fires nothing; a `/reload` fires, and its rebuild re-arms.
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct LeavingWorldArmed(bool);
|
||||
|
||||
/// The leave-world sweep `0x490a80` ran past its latch: a cross-map worldport (the local
|
||||
/// player's destructor, `0x5dd543`), a `/reload` (`0x490c20`) or leaving the world. Its legs
|
||||
/// past the clear read this, as the meeting stone's text drop (`0x490b2f` → `0x4c9f80`) does; a
|
||||
/// same-map teleport destroys nothing and never sweeps.
|
||||
#[derive(Message)]
|
||||
pub(crate) struct WorldLeaveSweepMessage;
|
||||
|
||||
/// The latch and the sweep it gates, for a system: spending one writes the other.
|
||||
#[derive(bevy::ecs::system::SystemParam)]
|
||||
pub(crate) struct LeaveWorldSweep<'w> {
|
||||
armed: ResMut<'w, LeavingWorldArmed>,
|
||||
sweeps: MessageWriter<'w, WorldLeaveSweepMessage>,
|
||||
}
|
||||
|
||||
impl LeaveWorldSweep<'_> {
|
||||
/// Run the sweep: `true`, and [`WorldLeaveSweepMessage`] written, at most once per world.
|
||||
pub(crate) fn run(&mut self) -> bool {
|
||||
let swept = self.armed.spend();
|
||||
if swept {
|
||||
self.sweeps.write(WorldLeaveSweepMessage);
|
||||
}
|
||||
swept
|
||||
}
|
||||
}
|
||||
|
||||
/// [`LeaveWorldSweep::run`] for an exclusive edge; no latch is no world to leave.
|
||||
pub(crate) fn run_leave_world_sweep(world: &mut World) -> bool {
|
||||
let swept = world
|
||||
.get_resource_mut::<LeavingWorldArmed>()
|
||||
.is_some_and(|mut l| l.spend());
|
||||
if swept {
|
||||
if let Some(mut sweeps) = world.get_resource_mut::<Messages<WorldLeaveSweepMessage>>() {
|
||||
sweeps.write(WorldLeaveSweepMessage);
|
||||
}
|
||||
}
|
||||
swept
|
||||
}
|
||||
|
||||
impl LeavingWorldArmed {
|
||||
/// A world began. Idempotent, like the reference's `mov byte [0xb4b424],1`.
|
||||
pub(crate) fn arm(&mut self) {
|
||||
@@ -501,7 +540,7 @@ impl LeavingWorldArmed {
|
||||
}
|
||||
|
||||
/// Take the one-shot: `true` at most once per world, `0x490a8d`'s clear folded in.
|
||||
pub(crate) fn spend(&mut self) -> bool {
|
||||
fn spend(&mut self) -> bool {
|
||||
std::mem::take(&mut self.0)
|
||||
}
|
||||
|
||||
@@ -566,9 +605,7 @@ pub(crate) fn end_ui_session(world: &mut World) {
|
||||
.get_resource::<AddOnIdentity>()
|
||||
.and_then(|id| id.0.clone());
|
||||
// Spent at the root: of this edge and the worldport's, the first to a departure owns it.
|
||||
let leaving_world = world
|
||||
.get_resource_mut::<LeavingWorldArmed>()
|
||||
.is_some_and(|mut l| l.spend());
|
||||
let leaving_world = run_leave_world_sweep(world);
|
||||
if !ui_never_loaded {
|
||||
if let Some(mut script) = world.get_non_send_resource_mut::<UiScript>() {
|
||||
shutdown_ui_state(&mut script, identity.as_ref(), leaving_world);
|
||||
@@ -651,7 +688,7 @@ pub(crate) fn shutdown_on_exit(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
id: Res<AddOnIdentity>,
|
||||
pending_entry: Option<Res<PendingEntryUiLoad>>,
|
||||
mut armed: ResMut<LeavingWorldArmed>,
|
||||
mut sweep: LeaveWorldSweep,
|
||||
mut exits: MessageReader<AppExit>,
|
||||
) {
|
||||
if exits.read().next().is_none() {
|
||||
@@ -661,7 +698,7 @@ pub(crate) fn shutdown_on_exit(
|
||||
if pending_entry.is_some() {
|
||||
return;
|
||||
}
|
||||
let leaving_world = armed.spend();
|
||||
let leaving_world = sweep.run();
|
||||
if let Some(mut script) = script {
|
||||
shutdown_ui_state(&mut script, id.0.as_ref(), leaving_world);
|
||||
}
|
||||
@@ -853,29 +890,42 @@ 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`).
|
||||
/// A `ReloadUI()` runs the leave-world sweep (`0x490c20`, past the armed latch) and then
|
||||
/// re-enters the world-enter cascade (`0x490168`), whose readers re-send the time, mail,
|
||||
/// battlefield and meeting-stone queries, only with the active player set (`0x490166 je`).
|
||||
#[test]
|
||||
fn a_reload_reenters_the_world_enter_cascade_once_the_player_exists() {
|
||||
fn a_reload_sweeps_and_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>>();
|
||||
world.init_resource::<Messages<WorldLeaveSweepMessage>>();
|
||||
world.init_resource::<LeavingWorldArmed>();
|
||||
world.resource_mut::<LeavingWorldArmed>().arm();
|
||||
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
|
||||
let sweeps = world
|
||||
.resource_mut::<Messages<WorldLeaveSweepMessage>>()
|
||||
.drain()
|
||||
.count();
|
||||
let cascades = world
|
||||
.resource_mut::<Messages<WorldEnterCascadeMessage>>()
|
||||
.drain()
|
||||
.count()
|
||||
.count();
|
||||
(sweeps, cascades)
|
||||
};
|
||||
assert_eq!(reload(true), 1, "in the world");
|
||||
assert_eq!(reload(false), 0, "before our own player's create");
|
||||
assert_eq!(reload(true), (1, 1), "in the world");
|
||||
assert_eq!(
|
||||
reload(false).1,
|
||||
0,
|
||||
"no cascade before our own player's create"
|
||||
);
|
||||
}
|
||||
|
||||
/// The reference re-makes its Lua state inside `UI_Init` (`0x48fe97`).
|
||||
|
||||
@@ -295,6 +295,7 @@ impl Plugin for UiScriptPlugin {
|
||||
)
|
||||
// `init_` here and in `UiUnitPlugin`: either plugin may be built alone in a test.
|
||||
.init_resource::<LeavingWorldArmed>()
|
||||
.add_message::<WorldLeaveSweepMessage>()
|
||||
.add_systems(Update, lifecycle::arm_leaving_world_on_self_create)
|
||||
// A queued `ReloadUI()` runs in `PreUpdate`, a frame after its drain (the reference's
|
||||
// deferral, `0x495590`) and before every `Update` seed or feed.
|
||||
@@ -381,8 +382,8 @@ fn arbitrate_pointer_over_ui(
|
||||
/// The session lifecycle: the VM's birth, identity, death and reload.
|
||||
mod lifecycle;
|
||||
pub(crate) use lifecycle::{
|
||||
end_ui_session, ingame_ui_up, run_pending_reload, setup_script, AddOnIdentity,
|
||||
LeavingWorldArmed, PendingEntryUiLoad, ReloadUiPending,
|
||||
end_ui_session, ingame_ui_up, run_pending_reload, setup_script, AddOnIdentity, LeaveWorldSweep,
|
||||
LeavingWorldArmed, PendingEntryUiLoad, ReloadUiPending, WorldLeaveSweepMessage,
|
||||
};
|
||||
// Test-only: other modules' tests consume these, and a plain re-export would warn unused.
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user