Files
moonwell-client-core/crates/benilla-ui/src/script/session.rs
T
Sam Sung 05e7cf4b93 A level-60 character shows the max-level bar, not the XP bar (#1976)
A level-60 character entered the world with the XP bar shown instead of the max-level bar.

The stock watch bar sets the XP bar from `UnitLevel("player")` on `UPDATE_FACTION` (`ReputationFrame.lua:161-244`). The server sends the faction list before the player's create, and benilla's only login signals came from that first push, before `"player"` was seated, so the bar read level 0 and stayed shown.

1.12.1 rebuilds the list in the world-enter cascade (`0x4909d3` calls `0x4d5430`), after `PLAYER_ENTERING_WORLD`, and that rebuild's sort signals `UPDATE_FACTION` (`0x4d55de` → `0x4d5db0`); the rebuild does nothing without a player (`0x4d5453`). benilla now runs that rebuild in the cascade, and the first push into a VM is silent. A test drives the server's order: level 60 hides the XP bar and shows the max-level art, level 59 the reverse.

Fixes #1973

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:09:38 +07:00

126 lines
5.7 KiB
Rust

//! The session verbs: the game menu's Logout and Exit Game, their dialogs' answers, `ReloadUI`
//! and the cinematic pair. Each queues a [`SessionRequest`] the app turns into a packet or an
//! exit; the server decides whether a logout is instant or a 20-second countdown. Beside them,
//! the world-enter cascade's two events and the `PLAYER_LOGIN` arm a UI load sets.
use mlua::Lua;
use super::Model;
/// A session intent queued by a Lua call, drained by the app.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SessionRequest {
/// `Logout()`: `CMSG_LOGOUT_REQUEST` (`0x5ab000`), back to character select.
Logout,
/// `Quit()`: the same request, then the process ends once the logout completes.
Quit,
/// `CancelLogout()`: `CMSG_LOGOUT_CANCEL`; the server's ack fires `LOGOUT_CANCEL`.
CancelLogout,
/// `ForceQuit()`: end the process now, with no server round trip.
ForceQuit,
/// `ForceLogout()` (`0x48ab50`): an empty `CMSG_PLAYER_LOGOUT` (`0x4A`), bypassing the
/// pending-logout latch, and nothing without a live in-world session (`0x5ab020`).
ForceLogout,
/// `ReloadUI()` (`0x4884d0`) only sets `0xb4b3f4`; the next frame's `0x495590` tears the VM
/// down and rebuilds it, so it is never destroyed mid-call. The app's drain, outside any call,
/// is the same guard.
ReloadUi,
/// `StopCinematic()` (`0x48b970`), the only skip: no native caller and no binding, so ESC
/// reaches it only through `CinematicFrame`'s `OnKeyDown`; playback therefore waits for the
/// in-game UI.
StopCinematic,
}
impl super::UiScript {
/// Drain the session intents queued since the last call.
pub fn take_session_requests(&mut self) -> Vec<SessionRequest> {
std::mem::take(&mut self.model_mut().session_requests)
}
/// Queue an intent from the app: `/logout`, `/camp`, `/quit` and `/reload`, which benilla
/// parses in Rust rather than through `SlashCmdList`, take their Lua verbs' route this way.
pub fn queue_session_request(&mut self, request: SessionRequest) {
self.model_mut().session_requests.push(request);
}
/// Arm `PLAYER_LOGIN`, the UI load's last step: `UI_Init` sets the one-shot `[0xb4e260]` at
/// `0x49011d`, unconditionally, and [`Self::fire_world_enter`] spends it. The flag lives in
/// this VM, so each load arms its own and a worldport, which loads nothing, re-arms nothing.
pub fn arm_player_login(&mut self) {
self.model_mut().player_login_armed = true;
}
/// The world-enter cascade's events (`0x4908c0`): a full collection
/// (`FrameScriptCollectGarbage(1)`, `0x490946`), then `PLAYER_LOGIN` (270, `0x490959`) only if
/// a load armed it (read at `0x49094b`, cleared at `0x49095e`), then `PLAYER_ENTERING_WORLD`
/// (272, `0x49096a`) every time, then the minimap's zoom reseed
/// ([`Self::minimap_enter_world`]), the quest log's rebuild and its `QUEST_LOG_UPDATE`
/// ([`Self::quest_log_enter_world`]) and the reputation list's rebuild and its `UPDATE_FACTION`
/// ([`Self::reputation_enter_world`]). The cascade runs with the player object in the world, so its
/// caller seats `"player"` first, and inside its own sound-suppression bracket (`0x4908d5` to
/// `0x490a56`), so a handler's `PlaySound` is dropped.
pub fn fire_world_enter(&mut self) {
let armed = std::mem::take(&mut self.model_mut().player_login_armed);
self.push_sound_suppression();
// `0x703bc0` with 1: the threshold set to 0 (`0x703bd6`), so the collection runs now.
self.lua.set_gc_threshold_kb(0);
if armed {
self.fire_event("PLAYER_LOGIN", Vec::new());
}
self.fire_event("PLAYER_ENTERING_WORLD", Vec::new());
// The calls after the signal, inside the bracket: the minimap initializer first
// (`0x49096f`), then among the rest the quest log's set-up (`0x4909a1`) and the
// reputation rebuild (`0x4909d3`).
self.minimap_enter_world();
self.quest_log_enter_world();
self.reputation_enter_world();
self.pop_sound_suppression();
}
}
pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
let g = lua.globals();
for (name, request) in [
("Logout", SessionRequest::Logout),
("Quit", SessionRequest::Quit),
("CancelLogout", SessionRequest::CancelLogout),
("ForceQuit", SessionRequest::ForceQuit),
("ForceLogout", SessionRequest::ForceLogout),
("ReloadUI", SessionRequest::ReloadUi),
("StopCinematic", SessionRequest::StopCinematic),
] {
g.set(
name,
lua.create_function(move |lua, ()| {
// `ReloadUI` (`0x4884d0`) sets the reload byte only if `CGGameUI::Reload`'s gate
// (`0x491380`, tag 10) allows: a script's own call needs an input event, and is
// otherwise ignored with no message.
if request == SessionRequest::ReloadUi
&& !super::protected::gate(lua, super::protected::Tag::RELOAD)
{
return Ok(());
}
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.session_requests.push(request);
Ok(())
})?,
)?;
}
// `0x48c930`: the number 1 while a cinematic plays, else nil, one result either way.
g.set(
"InCinematic",
lua.create_function(|lua, ()| {
let model = lua.app_data_ref::<Model>().expect("model app_data");
Ok(if model.in_cinematic {
mlua::Value::Integer(1)
} else {
mlua::Value::Nil
})
})?,
)?;
Ok(())
}