From 9bf64e1a6be4e0731bdfed71c6466ab51482d801 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 4 Sep 2026 20:59:53 +0700 Subject: [PATCH] the minimap and world map go stock, and the chat loses its white plate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the minimap cluster and the battleground list are the reference's own `Minimap.xml` and `BattlefieldFrame.xml`, ours deleted, and the sixteen engine names the static probe found unbound are built, not stubbed: the battleground list and queue family (the as-group join refused client-side with no packet when the map's `MaxPlayers` is below the group), the bodyless `CMSG_BATTLEFIELD_STATUS` on world enter — benilla never sent it, so a queue joined before a reconnect was invisible — the 5.5556 yd `BATTLEFIELDS_CLOSED` leash, and the meeting-stone pair over the `0x295` five-way table with both asymmetries; the ping's lifetime is the stock Lua's now (held 5 s, faded 0.5 s), `GetPingPosition` answers two numbers always — our nil was a Lua error on every frame of the stock `Minimap_OnUpdate` — and the map-change drop the reference never had is gone (1974) - the world map is the reference's own `WorldMapFrame.xml`, our transcription deleted; built for it: the arrow-frame family (an anonymous `Model` child at its 33.6 px footprint, drawn app-side as the minimap's own arrow sprite through one shared constant), the battleground position family over `MSG_BATTLEGROUND_PLAYER_POSITIONS` both ways (the reference's 5000 ms request throttle), and `GetPlayerMapPosition` resolving `raid1..raid40` where it knew only the party (B320); `SetupFullscreenScale` — the engine binding `0x48c270` the map's and the cinematic frame's OnShow call, which our map had stood in for with Lua — is bound to the carved law `min(0.75 · aspect, 1)` off the configured aspect, written through the frame's own `SetScale` (1.0 at 4:3 and wider, 0.9375 at 5:4); a hovered POI writes the map's own area label, not the tooltip; named gap: `WorldMapPing` draws nothing yet (1980, 1981) - the tutorial frame is the reference's own `TutorialFrame.xml`, over a tutorial system benilla never had: two bit banks filled from `SMSG_TUTORIAL_FLAGS` — the fire-once bank `TriggerTutorial` reads and sets (never a send) and the acknowledged bank `FlagTutorial` writes and sends; there is no tutorial CVar in the binary (210 `CVar::Register` sites censused), so "disable tutorials" is every bit acknowledged; the sites this app can produce are wired (the world-enter Welcome cascade, the level-up arm, item pushes, the loot sparkle, the meeting-stone and battleground arms), the rest named; the loader learned `` — the stock frame's drag handle, a region that emits no quad; and the `TabardModel` widget kind arrives ahead of its window, five ints bounded by the `.rdata` table `{170, 17, 6, 17, 51}` and the constant 100 000 tabard cost (1976, 1977) - the chat window drew as an opaque white plate: the chat-cache loader, gated on `InWorld` alone, restored into the boot VM 130 ms before the deferred entry load filled it, and its per-VM memo then blocked the restore that would have reached the real frames — the plate the stock Lua tints black stayed the XML's white; between the entry edge and the load there is now no VM in the world (the edge parks the boot VM, the load takes it back), so no in-world feed pushes into a VM with no interface; the perf pill reads `59 fps 7.0 ms 17.3 cpu` — the MAIN thread's milliseconds in full text and the process-wide sum dim, because the sum of every thread's CPU time had been read as a frame time (a raid read 17 on it at a solid 60 with the main thread at 7); and `WOW_PROBE_UI_ONE_TEX=1` is a pricing lever, never a look, for the draw-count ceiling a UI atlas would reach before one is built (1978, 1954) Co-Authored-By: Claude Fable 5.1 --- .../benilla-app/assets/ui/MinimapCluster.xml | 645 ------------ crates/benilla-app/assets/ui/UiPanels.xml | 2 + .../benilla-app/assets/ui/WorldMapFrame.xml | 953 ------------------ .../assets/ui/WorldMapFrameTemplates.xml | 72 -- crates/benilla-app/assets/ui/benilla.toc | 47 +- crates/benilla-app/src/char_select/mod.rs | 2 + crates/benilla-app/src/creature_anim.rs | 2 +- .../src/creature_anim/spell_visual.rs | 26 + crates/benilla-app/src/lib.rs | 9 + crates/benilla-app/src/minimap/blips/mod.rs | 8 +- crates/benilla-app/src/minimap/mod.rs | 15 +- crates/benilla-app/src/minimap/ping.rs | 213 ++-- crates/benilla-app/src/net.rs | 37 + crates/benilla-app/src/net/apply.rs | 22 +- crates/benilla-app/src/net/apply/loot.rs | 16 +- crates/benilla-app/src/net/apply/session.rs | 3 + crates/benilla-app/src/net/io.rs | 22 + crates/benilla-app/src/perf/hud.rs | 16 +- crates/benilla-app/src/perf/stats.rs | 23 +- crates/benilla-app/src/player/controller.rs | 10 + crates/benilla-app/src/sound/message.rs | 35 +- crates/benilla-app/src/tutorial.rs | 705 +++++++++++++ crates/benilla-app/src/ui_action/errors.rs | 17 + crates/benilla-app/src/ui_action/mod.rs | 6 +- crates/benilla-app/src/ui_battlefield.rs | 501 +++++++++ .../src/ui_battlefield_positions.rs | 262 +++++ .../benilla-app/src/ui_battlefield_score.rs | 2 +- crates/benilla-app/src/ui_chat/input/mod.rs | 22 +- crates/benilla-app/src/ui_chat/settings.rs | 12 +- crates/benilla-app/src/ui_dialog_verbs.rs | 508 +++++++++- crates/benilla-app/src/ui_mirror.rs | 21 + crates/benilla-app/src/ui_party/feed.rs | 11 + crates/benilla-app/src/ui_pass.rs | 11 +- crates/benilla-app/src/ui_quest.rs | 7 + .../src/ui_script/battlefield_tests.rs | 258 +++++ .../src/ui_script/bottom_hud_tests.rs | 2 +- .../benilla-app/src/ui_script/death_tests.rs | 2 +- .../src/ui_script/durability_tests.rs | 8 +- .../benilla-app/src/ui_script/extract/mod.rs | 45 +- .../src/ui_script/frame_flag_gate.rs | 18 +- .../src/ui_script/game_menu_tests.rs | 24 +- crates/benilla-app/src/ui_script/lifecycle.rs | 64 +- .../src/ui_script/minimap_tests.rs | 166 ++- crates/benilla-app/src/ui_script/mod.rs | 6 + .../src/ui_script/pet_paperdoll_tests.rs | 2 +- .../benilla-app/src/ui_script/reference_ui.rs | 78 +- .../src/ui_script/shipped_xml_tests.rs | 20 +- .../src/ui_script/tutorial_tests.rs | 135 +++ .../src/ui_script/world_map_tests.rs | 201 +++- crates/benilla-app/src/ui_social/feed.rs | 7 + crates/benilla-app/src/ui_world_map.rs | 113 ++- crates/benilla-formats/src/lib.rs | 2 +- crates/benilla-formats/src/maps.rs | 158 ++- crates/benilla-protocol/src/events.rs | 21 + crates/benilla-protocol/src/events/decode.rs | 10 + .../src/messages/battlefield.rs | 217 +++- .../src/messages/meeting_stone.rs | 26 + crates/benilla-protocol/src/messages/mod.rs | 8 +- .../benilla-protocol/src/messages/opcode.rs | 60 +- .../benilla-protocol/src/messages/packet.rs | 39 + crates/benilla-protocol/src/messages/parse.rs | 34 +- .../benilla-protocol/src/messages/tutorial.rs | 44 + crates/benilla-protocol/src/world/session.rs | 17 +- .../src/world/writer/battlefield.rs | 50 + .../src/world/writer/meeting_stone.rs | 6 + .../benilla-protocol/src/world/writer/mod.rs | 1 + .../src/world/writer/tutorial.rs | 25 + crates/benilla-ui/src/loader/geometry.rs | 29 +- crates/benilla-ui/src/loader/mod.rs | 2 + crates/benilla-ui/src/loader/tests.rs | 70 ++ .../src/script/battlefield_positions.rs | 254 +++++ .../src/script/battlefield_queue.rs | 697 +++++++++++++ crates/benilla-ui/src/script/dialog_verbs.rs | 70 ++ crates/benilla-ui/src/script/extract.rs | 5 +- crates/benilla-ui/src/script/minimap.rs | 21 +- crates/benilla-ui/src/script/mod.rs | 30 +- crates/benilla-ui/src/script/model.rs | 64 +- crates/benilla-ui/src/script/modelframe.rs | 17 +- crates/benilla-ui/src/script/object.rs | 103 +- .../src/script/object/frame_state.rs | 3 +- crates/benilla-ui/src/script/tabard.rs | 56 + crates/benilla-ui/src/script/tests/minimap.rs | 24 + .../benilla-ui/src/script/tests/worldmap.rs | 3 +- crates/benilla-ui/src/script/tutorial.rs | 160 +++ crates/benilla-ui/src/script/types.rs | 9 + crates/benilla-ui/src/script/worldmap.rs | 30 +- .../benilla-ui/src/script/worldmap_arrow.rs | 481 +++++++++ crates/benilla-ui/src/widget/kinds/mod.rs | 6 + crates/benilla-ui/src/widget/mod.rs | 7 +- 89 files changed, 6164 insertions(+), 2107 deletions(-) delete mode 100644 crates/benilla-app/assets/ui/MinimapCluster.xml delete mode 100644 crates/benilla-app/assets/ui/WorldMapFrame.xml delete mode 100644 crates/benilla-app/assets/ui/WorldMapFrameTemplates.xml create mode 100644 crates/benilla-app/src/tutorial.rs create mode 100644 crates/benilla-app/src/ui_battlefield.rs create mode 100644 crates/benilla-app/src/ui_battlefield_positions.rs create mode 100644 crates/benilla-app/src/ui_script/battlefield_tests.rs create mode 100644 crates/benilla-app/src/ui_script/tutorial_tests.rs create mode 100644 crates/benilla-protocol/src/messages/tutorial.rs create mode 100644 crates/benilla-protocol/src/world/writer/tutorial.rs create mode 100644 crates/benilla-ui/src/script/battlefield_positions.rs create mode 100644 crates/benilla-ui/src/script/battlefield_queue.rs create mode 100644 crates/benilla-ui/src/script/tabard.rs create mode 100644 crates/benilla-ui/src/script/tutorial.rs create mode 100644 crates/benilla-ui/src/script/worldmap_arrow.rs diff --git a/crates/benilla-app/assets/ui/MinimapCluster.xml b/crates/benilla-app/assets/ui/MinimapCluster.xml deleted file mode 100644 index 96ddb77c..00000000 --- a/crates/benilla-app/assets/ui/MinimapCluster.xml +++ /dev/null @@ -1,645 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - this:RegisterEvent("PLAYER_AURAS_CHANGED"); - - - MiniMapTrackingFrame_OnEvent(); - - - if ( arg1 == "RightButton" ) then - CancelTrackingBuff(); - end - - - MiniMapTrackingFrame_OnEnter(); - - - GameTooltip:Hide(); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - this:RegisterEvent("UPDATE_PENDING_MAIL"); - this:SetFrameLevel(this:GetFrameLevel()+1) - - - MiniMapMailFrame_OnEvent(); - - - MiniMapMailFrame_OnEnter(); - - - GameTooltip:Hide(); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Minimap_OnClick(); - - - - - - - Minimap_Update(); - -- Seed the +/- state from the live zoom (default index 3 ⇒ both enabled) rather than - -- hardcoding a zoom-0 "can't zoom out" — the client's default is 3, not the widest. - Minimap_UpdateZoom(); - this:RegisterEvent("MINIMAP_ZONE_CHANGED"); - this:RegisterEvent("MINIMAP_UPDATE_ZOOM"); - - - if ( event == "MINIMAP_UPDATE_ZOOM" ) then - Minimap_UpdateZoom(); - else - Minimap_Update(); - end - - - - diff --git a/crates/benilla-app/assets/ui/UiPanels.xml b/crates/benilla-app/assets/ui/UiPanels.xml index b1a242b6..a8b69f06 100644 --- a/crates/benilla-app/assets/ui/UiPanels.xml +++ b/crates/benilla-app/assets/ui/UiPanels.xml @@ -70,6 +70,8 @@ -- arrived on both clients, yet no window). pushable=1 lets a pushable=0 panel (a merchant) -- shove it to center, the reference's browse-while-trading arbitration. UIPanelWindows["TradeFrame"] = { area = "left", pushable = 1 } + -- UIParent.lua l.38, verbatim — the battlemaster's instance list (decision 1974). + UIPanelWindows["BattlefieldFrame"] = { area = "left", pushable = 0 } -- The trainer window (decision 0237 phase 3, TrainerFrame.xml): an NPC session exactly like -- Gossip/Merchant above (area "left", pushable 0) — competes for/replaces them at the slot. UIPanelWindows["BenillaTrainerFrame"] = { area = "left", pushable = 0 } diff --git a/crates/benilla-app/assets/ui/WorldMapFrame.xml b/crates/benilla-app/assets/ui/WorldMapFrame.xml deleted file mode 100644 index 20655b36..00000000 --- a/crates/benilla-app/assets/ui/WorldMapFrame.xml +++ /dev/null @@ -1,953 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - WorldMapContinentsDropDown_OnLoad(); - - - - - - - - - - - - - - - - - - - - - WorldMapZoneDropDown_OnLoad(); - this:RegisterEvent("WORLD_MAP_UPDATE"); - - - if ( this:IsVisible() ) then - WorldMapContinentsDropDown_OnLoad(); - WorldMap_UpdateContinentDropDownText(); - WorldMapZoneDropDown_OnLoad(); - WorldMap_UpdateZoneDropDownText(); - end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - this:RegisterEvent("WORLD_MAP_UPDATE"); - -- The hover area label inherits WorldMapTextFont (nominal 102). 33 was the - -- director's taste call (0235) — which the one-to-one raster cap (the fold-back - -- record; wow-re fontstring-overflow.md) now clamps to the byte law's 32, one - -- unit under that call and closer to it than the 36 the atlas snap used to - -- deliver. The real client draws this SetFont(33) at 32 too. Still a taste dial: - -- any value ≤ 32 is honored exactly. - WorldMapFrameAreaLabel:SetFont("Fonts\\FRIZQT__.TTF", 33); - WorldMapFrame_Update(); - - - - BlackoutWorld:Show(); - SetupFullscreenScale(); - SetMapToCurrentZone(); - WorldMapFrame_Update(); - if UpdateMicroButtons then UpdateMicroButtons() end; - PlaySound("igQuestLogOpen"); - CloseDropDownMenus(); - - - BlackoutWorld:Hide(); - if UpdateMicroButtons then UpdateMicroButtons() end; - PlaySound("igQuestLogClose"); - - - if ( WorldMapFrame:IsVisible() ) then - WorldMapFrame_Update(); - end - - - - - - diff --git a/crates/benilla-app/assets/ui/WorldMapFrameTemplates.xml b/crates/benilla-app/assets/ui/WorldMapFrameTemplates.xml deleted file mode 100644 index 63ac7ca1..00000000 --- a/crates/benilla-app/assets/ui/WorldMapFrameTemplates.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - this:SetFrameLevel(this:GetFrameLevel() + 1); - this.unit = "party" .. this:GetID(); - getglobal(this:GetName() .. "Icon"):SetTexture("Interface\\WorldMap\\WorldMapPartyIcon"); - - WorldMapUnit_OnEnter() - WorldMapTooltip:Hide() - - - - - - - - this:SetFrameLevel(this:GetFrameLevel() + 1); - getglobal(this:GetName() .. "Icon"):SetTexture("Interface\\WorldMap\\WorldMapPartyIcon"); - - - - diff --git a/crates/benilla-app/assets/ui/benilla.toc b/crates/benilla-app/assets/ui/benilla.toc index c03712b2..1cba6c08 100644 --- a/crates/benilla-app/assets/ui/benilla.toc +++ b/crates/benilla-app/assets/ui/benilla.toc @@ -441,10 +441,23 @@ Interface\FrameXML\BonusActionBarFrame.xml # their OnShow/OnHide calls the `UpdateMicroButtons` this file defines (the panel frames # themselves are read back through getglobal, so the reverse dependency is call-time). MicroMenu.xml -# The HUD minimap cluster (decision 0203 phase 1): the chrome around the engine-rendered -# widget hole (`crate::minimap` fills it). After GameTooltip (its zone-text -# hover uses the shared tooltip). -MinimapCluster.xml +# ══ THE BATTLEGROUND LIST + THE MINIMAP CLUSTER — the reference's own, off the player's chain +# (1751; decision 1974) ══ +# BattlefieldFrame.xml + BattlefieldFrame.lua: the battlemaster's instance list (the "First +# Available" row, the join and join-as-group buttons) over `crate::ui_battlefield`'s feed and +# the engine's queue family (GetBattlefieldStatus & co.), and — defined here, consumed by the +# minimap file below at LOAD time — the minimap queue icon's status pass and its right-click +# dropdown. The reference's toc seats it right before Minimap.xml (l.29-30) for that reason. +# After UIDropDownMenu.xml (the dropdown template) and UIPanelTemplates (the buttons, the faux +# scroll frame). +Interface\FrameXML\BattlefieldFrame.xml +# Minimap.xml + Minimap.lua: the cluster around the engine-rendered widget hole +# (`crate::minimap` fills it), the zone text, the zoom buttons, the tracking / mail / meeting +# stone / battlefield icons, and `MiniMapPing` — a this engine draws no pixels for; the +# app's ping sprite (`minimap/ping.rs`) follows that frame's own show/hide and alpha, so the +# 5 s lifetime is this file's, as the reference's is. After GameTooltip (the zone-text hover). +# 0203's transcription is gone. +Interface\FrameXML\Minimap.xml # ══ THE TIME-OF-DAY INDICATOR — the reference's own, off the player's chain (1751, window 5) ══ # `GameTimeFrame`, `GameTimeTexture`, and `GameTime_GetTime` / `GameTimeFrame_Update` / # `_UpdateTooltip` / `GAMETIME_DAWN`/`_DUSK` out of `GameTime.lua`. Ours was a re-implementation of @@ -528,13 +541,16 @@ OptionsFrameTemplates.xml # entry and the ESC rung that cancel-clicks it. UIDropDownMenu.xml sits above too, but that # direction is call-time only, so the two could sit either way round. Interface\FrameXML\ColorPickerFrame.xml -# The fullscreen world map (decision 0203 phase 2), over the worldmap host API -# (`crate::ui_world_map` feeds it); the 'M' binding (ui_script/input.rs) calls its -# ToggleWorldMap(). -# Its per-unit blip templates load FIRST: WorldMapFrame.xml instantiates WorldMapParty1..4 and -# WorldMapRaid1..40 with inherits=, so the virtual Frames have to be registered before it. -WorldMapFrameTemplates.xml -WorldMapFrame.xml +# ══ THE WORLD MAP — the reference's own, off the player's chain (1751, migrated 1980) ══ +# The fullscreen map over the worldmap host API (`crate::ui_world_map` feeds the player, corpse, +# party and landmark positions; `crate::ui_battlefield_positions` the battleground teammates and +# the flag carrier); the 'M' binding (ui_script/input.rs) calls its ToggleWorldMap(). The file +# s its own blip templates (WorldMapFrameTemplates.xml) before the frames that inherit +# them. Its player arrow is the Create/Position/Show/UpdateWorldMapArrowFrames family +# (`script/worldmap_arrow.rs`), an anonymous Model pane the app draws as the minimap's arrow +# sprite; its WorldMapPing is a MinimapPing.mdx Model pane this engine holds and draws nothing +# into (named gap, 1980). +Interface\FrameXML\WorldMapFrame.xml # ══ THE CAST BAR — the reference's own, off the player's chain (1751) ══ # `CastingBarFrame` + its Border/Flash/Spark/Text children, driven by the SPELLCAST_* events # `crate::ui_cast` fires (decision 0137 phase 1). Our `CastingBar.xml` was a transcription of this @@ -1091,6 +1107,15 @@ Interface\FrameXML\HelpFrame.xml # nothing else, and nothing needs it — the engine reaches it purely by firing CINEMATIC_START. # Seated late because it is an outermost, screen-owning layer, like the two windows below it. Interface\FrameXML\CinematicFrame.xml +# ══ THE TUTORIAL FRAME — the reference's own, off the player's chain (1751; decision 1976) ══ +# TutorialFrame.xml + TutorialFrame.lua: the alert buttons `TUTORIAL_TRIGGER` queues, the window +# a click opens over `TUTORIAL_TITLE`/`TUTORIAL`, the "Display Tips" checkbox whose unticked +# hide calls ClearTutorials, and the CINEMATIC_STOP arm that clicks the Welcome alert when the +# intro ends. Right after CinematicFrame.xml as the reference's toc has it (l.76-80); its +# engine — the two flag banks, the fire-once trigger, the four verbs — is `crate::tutorial`. +# Needs UIPanelTemplates (UICheckButtonTemplate), BasicControls (the dialog button textures), +# GameTooltip, and UiPanels' SetButtonPulse, all far above. +Interface\FrameXML\TutorialFrame.xml # The game menu (decision 0674): the frame ESC opens. LAST on purpose — it is the outermost # layer of the shell, it depends on everything it touches being loaded (UiPanels' popup engine # + panel manager, MicroMenu's UpdateMicroButtons, BagFrame's Disable_BagButtons), and nothing diff --git a/crates/benilla-app/src/char_select/mod.rs b/crates/benilla-app/src/char_select/mod.rs index e2dd91e4..b00080bc 100644 --- a/crates/benilla-app/src/char_select/mod.rs +++ b/crates/benilla-app/src/char_select/mod.rs @@ -1068,6 +1068,7 @@ mod tests { // One frame carrying both halves of the race, in the order the drain produces them. app.world_mut().write_message(EnteredWorldMessage { billing_time_rested: 0, + tutorial_flags: None, }); app.world_mut() .write_message(crate::net::DisconnectedMessage { @@ -1109,6 +1110,7 @@ mod tests { app.world_mut().write_message(EnteredWorldMessage { billing_time_rested: 0, + tutorial_flags: None, }); app.world_mut() .write_message(crate::net::DisconnectedMessage { diff --git a/crates/benilla-app/src/creature_anim.rs b/crates/benilla-app/src/creature_anim.rs index e23d422e..48558784 100644 --- a/crates/benilla-app/src/creature_anim.rs +++ b/crates/benilla-app/src/creature_anim.rs @@ -803,7 +803,7 @@ pub(crate) use impact::{DefenseAnim, PendingImpacts, SwingFlush, SwingImpact, Sw /// one-shots ([`EmoteAnim`]) and kit sounds ([`spell_visual::SpellKitSound`]). mod blood; mod env_damage; -mod spell_visual; +pub(crate) mod spell_visual; use blood::{blood_spurts, load_blood_tables}; use env_damage::{hard_landing_dust, load_env_damage_table}; pub(crate) use env_damage::{EnvDamageTable, HardLanding, HARD_LANDING_DESCENT}; diff --git a/crates/benilla-app/src/creature_anim/spell_visual.rs b/crates/benilla-app/src/creature_anim/spell_visual.rs index c4691bf7..9f4739eb 100644 --- a/crates/benilla-app/src/creature_anim/spell_visual.rs +++ b/crates/benilla-app/src/creature_anim/spell_visual.rs @@ -55,6 +55,25 @@ const LEVEL_UP_EFFECT: &str = "HARDCODED Unit Level Up"; /// 1185 → `Spells\DruidMorph_Impact_Base.mdx`, i.e. literally the druid-morph puff (wow-re /// `mount-composition.md` Q4b, decision 0927). const MOUNT_POOF_EFFECT: &str = "HARDCODED Mount Poof"; +/// The meeting-stone join — hardcoded index **0xc** (`0x861808`), spawned on the local player by +/// the `SMSG 0x295` status-1 arm with the same `Effect_C` shape as the mount poof (wow-re +/// `meeting-stone-status.md` §8; decision 1974). +const MEETING_STONE_JOIN_EFFECT: &str = "HARDCODED Meeting Stone Join"; + +/// The meeting-stone join visual for `entity`, or `None` without client data or the row — +/// `0x61fae0(kind = 0xc, &guid, completion = 0x5fbf50)`: one-shot, non-persistent, the +/// end-of-clip terminator, attached at `0x13` like every `HARDCODED` effect. +pub(crate) fn meeting_stone_join_fx(visuals: &SpellVisuals, entity: Entity) -> Option { + let path = visuals.0.hardcoded_effect(MEETING_STONE_JOIN_EFFECT)?; + Some(SpellKitFx::Begin { + entity, + spell_id: 0, + persistent: false, + class: FxClass::Hold, + stage: FxStage::OneShot, + effects: vec![(HARDCODED_FX_ATTACH, path.to_string())], + }) +} /// `SpellVisual.dbc` × `SpellVisualKit.dbc` — the stage-kit chain + each kit's anim/sound /// (`crate::creature_anim::spell_visual` module docs). Optional like every other DBC-backed @@ -1296,6 +1315,7 @@ pub(super) fn arm_loot_fx( visuals: Option>, mut fx: MessageWriter, mut armed: Local, + mut tutorials: Option>, ) { let full_sweep = visuals.as_ref().is_some_and(|v| v.is_changed()); let Some(path) = visuals.as_ref().and_then(|v| v.0.loot_art_path()) else { @@ -1317,6 +1337,12 @@ pub(super) fn arm_loot_fx( store.0.unit_is_dead() && store.0.unit_lootable() }; if lootable && armed.insert(entity) { + // `OnDynamicFlagsChanged`'s rise (`0x60049b`): the Looting tutorial (1976). + if let Some(t) = tutorials.as_mut() { + t.write(crate::tutorial::TutorialEvent::trigger( + crate::tutorial::id::LOOTING, + )); + } fx.write(SpellKitFx::Begin { entity, spell_id: LOOT_FX_KEY, diff --git a/crates/benilla-app/src/lib.rs b/crates/benilla-app/src/lib.rs index c7e056f3..005aefb9 100644 --- a/crates/benilla-app/src/lib.rs +++ b/crates/benilla-app/src/lib.rs @@ -116,10 +116,13 @@ mod sound; mod target; mod textinput; mod transport; +mod tutorial; mod ui_action; mod ui_auction; mod ui_aura; mod ui_bank; +mod ui_battlefield; +mod ui_battlefield_positions; mod ui_battlefield_score; mod ui_bind_confirm; mod ui_binder; @@ -206,10 +209,13 @@ use sound::SoundPlugin; use target::TargetPlugin; use textinput::TextInputPlugin; use transport::TransportPlugin; +use tutorial::TutorialPlugin; use ui_action::UiActionPlugin; use ui_auction::UiAuctionPlugin; use ui_aura::UiAuraPlugin; use ui_bank::UiBankPlugin; +use ui_battlefield::BattlefieldPlugin; +use ui_battlefield_positions::BattlefieldPositionsPlugin; use ui_battlefield_score::BattlefieldScorePlugin; use ui_binder::UiBinderPlugin; use ui_cast::UiCastPlugin; @@ -644,6 +650,9 @@ pub fn run(build: BuildId) -> AppExit { // boot clock, the area spirit healer, the battleground queue, the meeting stone. .add_plugins(UiDialogVerbsPlugin) .add_plugins(BattlefieldScorePlugin) + .add_plugins(BattlefieldPlugin) + .add_plugins(BattlefieldPositionsPlugin) + .add_plugins(TutorialPlugin) // Being summoned (decision 1747): SMSG_SUMMON_REQUEST's latch, the CONFIRM_SUMMON dialog it // raises, and the CMSG_SUMMON_RESPONSE its Accept sends. The binder's twin one line up — a // server-asked question whose only wire answer is yes — and here for that reason. diff --git a/crates/benilla-app/src/minimap/blips/mod.rs b/crates/benilla-app/src/minimap/blips/mod.rs index 2dad0ead..377adf11 100644 --- a/crates/benilla-app/src/minimap/blips/mod.rs +++ b/crates/benilla-app/src/minimap/blips/mod.rs @@ -116,8 +116,12 @@ pub(super) const BLIP_BASIS_PX: f32 = 140.8; /// quad rendered at 1280 px per model unit — 33.6 px at stock. Its authored centre sits /// (+0.0004, +0.00135) model units off the frame origin: ≈(+0.5 right, 1.7 up) screen px, /// rotating with the facing (model +y = screen-up at zero rotation). -pub(super) const PLAYER_ARROW_QUAD_PX: f32 = 33.6; -pub(super) const PLAYER_ARROW_OFFSET_PX: bevy::math::Vec2 = bevy::math::Vec2::new(0.51, -1.73); +/// The player arrow's art — the minimap ring's and, since decision 1980, the world map's: the stock +/// `WorldMapFrame.lua` `SetModel`s an anonymous pane to the same M2 and the app draws that pane as +/// this sprite (`ui_script::extract`). +pub(crate) const PLAYER_ARROW_TEXTURE: &str = "Interface\\Minimap\\MinimapArrow"; +pub(crate) const PLAYER_ARROW_QUAD_PX: f32 = 33.6; +pub(crate) const PLAYER_ARROW_OFFSET_PX: bevy::math::Vec2 = bevy::math::Vec2::new(0.51, -1.73); /// The rim arrow's on-screen quad: the `Rotating-MinimapArrow.m2` geometry is a stack of six /// full-texture 0.0500-unit quads (measured from the real M2's 24 vertices, z 0/0.0145/0.0291), /// and the frame renders at 768 px per model unit (modelScale 0.6 × 1280 px/unit): diff --git a/crates/benilla-app/src/minimap/mod.rs b/crates/benilla-app/src/minimap/mod.rs index 63879b66..a5c3213a 100644 --- a/crates/benilla-app/src/minimap/mod.rs +++ b/crates/benilla-app/src/minimap/mod.rs @@ -27,7 +27,7 @@ //! stem; [`blips`] — the phase-3 blip layer (AreaPOI landmark arrows, quest-giver dots, the //! hover tooltip). -mod blips; +pub(crate) mod blips; /// The party blip's position law, shared with the world map (report B320): one function decides /// where a member is, so the two surfaces can never disagree about it. pub(crate) use blips::party_member_pos; @@ -315,7 +315,7 @@ fn setup_minimap( Ok(translate) => { info!("minimap: md5translate.trs — {} tiles", translate.len()); let mask = assets.mask_texture("Textures\\MinimapMask", &mut images); - let arrow = assets.sprite_texture("Interface\\Minimap\\MinimapArrow", &mut images); + let arrow = assets.sprite_texture(blips::PLAYER_ARROW_TEXTURE, &mut images); let poi = assets.sprite_texture("Interface\\Minimap\\POIIcons", &mut images); let mut rim_arrows = blips::RimArrowArt::default(); for kind in blips::RimArrow::ALL { @@ -471,7 +471,14 @@ fn emit_minimap( // always spent in the frame it was made. Held across frames it would seat against geometry // the player never clicked on; and a click made on a frame the map does not draw is simply // not a ping (decision 1596). - let click = script.and_then(|mut s| s.take_minimap_ping_request()); + // The stock `MiniMapPing` frame's alpha is read beside it: the ping's lifetime is that + // frame's (`ping.rs`), so the sprite wants the frame as it stands after this tick. + let (click, ping_shown) = script.map_or((None, None), |mut s| { + ( + s.take_minimap_ping_request(), + s.frame_effective_alpha(ping::PING_FRAME), + ) + }); let (Some(slot), Some(assets), Some(map), Some(catalog)) = (widget.0.as_ref(), assets, map, catalog) else { @@ -939,7 +946,7 @@ fn emit_minimap( // This is also where a `Minimap:PingLocation` click is drained: the geometry it must resolve // against is the geometry standing right here, this frame (decision 1596). if let Some(ctx) = &blip_ctx { - ping::emit_ping(ctx, &mut ping, click, map.0, &assets.ping, &mut quads); + ping::emit_ping(ctx, &mut ping, click, ping_shown, &assets.ping, &mut quads); } } diff --git a/crates/benilla-app/src/minimap/ping.rs b/crates/benilla-app/src/minimap/ping.rs index 67e4f0f2..76e804b9 100644 --- a/crates/benilla-app/src/minimap/ping.rs +++ b/crates/benilla-app/src/minimap/ping.rs @@ -29,14 +29,23 @@ //! offsets the byte-verified relay `0x4ee330` hands Lua (`(−dy·k, dx·k)`, `k = 1/(2·radius)` — //! wow-re `party-group-wire.md` §TU-D). `Minimap:GetPingPosition()` reads the live value back. //! -//! ## Lifetime +//! ## Lifetime — the stock `Minimap.lua`'s, not ours (1974) //! -//! 5 s hold, then the reference's 0.5 s "fade" — see [`PING_ALPHA_IS_A_POP`]. A map change drops -//! it (the point is not here any more). **Nothing else clears it**, and in particular *not* -//! proximity: the first version applied the client's 10-yd `d² < 100` auto-clear to the party -//! ping, and that clear belongs to the **`SMSG_GOSSIP_POI` marker** — a different feature in a -//! different slot (wow-re `party-group-wire.md` §TU-D corrects it explicitly; `MSG_MINIMAP_PING` -//! has no C-side storage at all). Walking to your own ping used to delete it mid-hold. +//! The reference splits the ping in two: the engine stores the world point in a pair of statics +//! **nothing ever clears** (wow-re `minimap-ping-law.md` §3 — six instructions touch those cells +//! and the only zeroing is a CRT initializer), and FrameXML owns everything visible — the +//! `MiniMapPing` `` it shows on `MINIMAP_PING`, re-seats every frame from +//! `GetPingPosition()`, holds 5 s (`MINIMAPPING_TIMER`), "fades" 0.5 s through a `SetAlpha(255·t)` +//! that clamps to full until the last ~2 ms, and hides. Since 1751's swap that file runs here +//! verbatim, and this engine's `` draws no pixels — so the sprite below is **that frame's +//! renderer**: it draws while `MiniMapPing` is effectively visible, at its effective alpha, and +//! keeps no clock of its own. The world point is likewise never cleared: `GetPingPosition()` +//! answers two numbers always, which the stock `Minimap_OnUpdate` multiplies without a nil test. +//! +//! What is *not* a lifetime: proximity. The first version applied the client's 10-yd +//! `d² < 100` auto-clear to the party ping, and that clear belongs to the **`SMSG_GOSSIP_POI` +//! marker** — a different feature in a different slot (wow-re `party-group-wire.md` §TU-D +//! corrects it explicitly). Walking to your own ping used to delete it mid-hold. use bevy::prelude::*; @@ -48,20 +57,12 @@ use crate::net::{ClientCommand, Guid, NetCommands, SelfPlayer}; use crate::player::Player; use crate::ui_pass::{UiQuad, UiQuads}; -/// How long the ping holds at full strength — the reference's `MINIMAPPING_TIMER`. -const PING_HOLD: f32 = 5.0; -/// The tail after the hold — the reference's `MINIMAPPING_FADE_TIMER`. -const PING_FADE: f32 = 0.5; - -/// **The reference's ping does not fade; it pops** — now byte-confirmed, not inferred from the Lua -/// (wow-re `minimap-ping-law.md`). `Minimap_OnUpdate` writes `SetAlpha(255 * (t / 0.5))`, and -/// `Frame:SetAlpha` (`0x774e90`) **clamps to [0,1]** before scaling to a 0–255 byte — so the -/// nominal half-second ramp is at full alpha for 498 of its 500 ms and the whole visible fall is -/// ~12 % of one frame at 60 fps. The real client holds bright for ~5 s and vanishes. -/// -/// Reproduced as the clamp rather than as a constant 1.0, so the mechanism stays visible and the -/// `false` branch is a real alternative (a smooth fade, which wow-re notes will visibly diverge). -const PING_ALPHA_IS_A_POP: bool = true; +/// The stock frame whose pixels this module draws: `Minimap.xml`'s ``. +/// Its show/hide and alpha ARE the ping's lifetime (module doc). The reference's "fade" pops +/// rather than fades — `Minimap_OnUpdate` writes `SetAlpha(255 · t/0.5)` and `Frame:SetAlpha` +/// (`0x774e90`) clamps to [0,1] first, so the frame reads full alpha for 498 of the 500 ms — and +/// that arrives here through the frame's effective alpha, with no constant of ours to keep it. +pub(super) const PING_FRAME: &str = "MiniMapPing"; /// **The marker, byte-measured** (wow-re `system/ui/scratch/minimap-ping-law.md` §10, VERIFIED). /// @@ -165,27 +166,27 @@ pub(super) struct PingArt { pub(super) ping4: Option>, } -/// The live ping. One at a time — the reference keeps no list either. +/// The stored ping — the reference's two statics. One, never cleared: the reference keeps no +/// list, and no map tag either (a worldport mid-ping re-projects the old point against the new +/// map's player position, which the stock Lua's disc test then hides — reproduced as is). struct LivePing { /// **The pin**: the WoW `(x, y)` this ping marks. The only stored position; the screen seat is /// re-derived from it every frame. world: (f32, f32), - /// The map it was placed on. A map change drops the ping rather than re-projecting a point - /// that does not exist here. - map: u32, - /// Seconds since it was seated. - age: f32, /// The pinger's guid, `0` = ourselves — resolved to the `MINIMAP_PING` event's unit token, and /// the test for "this is ours, put it on the wire". sender: u64, } /// The engine-owned ping state (decision 1596). Seated by a click (drained in the renderer, with -/// that frame's geometry) or by a group member's `MSG_MINIMAP_PING`; aged, announced and expired -/// by [`drive_minimap_ping`]. +/// that frame's geometry) or by a group member's `MSG_MINIMAP_PING`; announced by +/// [`drive_minimap_ping`]; shown for exactly as long as the stock `MiniMapPing` frame is. #[derive(Resource, Default)] pub(crate) struct MinimapPing { live: Option, + /// [`PING_FRAME`]'s effective alpha while it is effectively visible, as the renderer last read + /// it — `None` hidden. The model clock below runs only while this is `Some`. + shown: Option, /// The model's own clock, in **milliseconds, accumulated only while a ping is shown** — which /// is the reference's behaviour rather than a simplification of it. `SetSequence(0)` runs once /// in the ref's `OnLoad` and stores an *anchor*; the sampler free-runs from it, and a Model @@ -202,34 +203,10 @@ impl MinimapPing { /// Seat a ping at a world point. Re-pinging replaces: the reference tolerates the same, and a /// group echo of our own click lands on the spot we already drew (`Minimap_SetPing` twice on /// one spot just restarts the timer). - pub(crate) fn seat(&mut self, world: (f32, f32), map: u32, sender: u64) { - self.live = Some(LivePing { - world, - map, - age: 0.0, - sender, - }); + pub(crate) fn seat(&mut self, world: (f32, f32), sender: u64) { + self.live = Some(LivePing { world, sender }); self.fresh = true; } - - /// The ping's alpha at its current age, or `None` once it is over. - fn alpha(&self) -> Option { - let age = self.live.as_ref()?.age; - if age <= PING_HOLD { - return Some(1.0); - } - let tail = (PING_HOLD + PING_FADE - age) / PING_FADE; - if tail <= 0.0 { - None - } else if PING_ALPHA_IS_A_POP { - // The reference's `255 * tail`, clamped by SetAlpha's own 0..1 — full until the last - // ~2 ms. Written as the clamp rather than as a constant 1.0 so the mechanism is - // visible and the `false` branch is a real alternative, not a rewrite. - Some((255.0 * tail).min(1.0)) - } else { - Some(tail) - } - } } /// Convert a `Minimap:PingLocation(x, y)` click into the world point it names, drain-side. @@ -254,8 +231,10 @@ fn click_to_world(ctx: &BlipCtx, ui: (f32, f32), seam: f32) -> Option<(f32, f32) Some((ctx.wx + up_yd, ctx.wy - right_yd)) } -/// Seat this frame's `Minimap:PingLocation` click and draw the live ping — both inside the +/// Seat this frame's `Minimap:PingLocation` click and draw the ping — both inside the /// renderer, against the geometry the player actually clicked on and the map actually drew at. +/// `shown` is the stock [`PING_FRAME`]'s effective alpha this frame (`None` = hidden), read +/// beside the click: the frame is the ping's lifetime (module doc), so nothing draws without it. /// /// The seat happens here rather than in a system of its own precisely so there is no window in /// which a click is held against a *stale* view scale: the first version parked the click for a @@ -266,15 +245,16 @@ pub(super) fn emit_ping( ctx: &BlipCtx, ping: &mut MinimapPing, click: Option<(f32, f32)>, - map: u32, + shown: Option, art: &PingArt, quads: &mut UiQuads, ) { if let Some(world) = click.and_then(|c| click_to_world(ctx, c, ctx.seam)) { - ping.seat(world, map, 0); + ping.seat(world, 0); } - let Some(alpha) = ping.alpha() else { return }; + ping.shown = shown; + let Some(alpha) = shown else { return }; let Some(live) = ping.live.as_ref() else { return; }; @@ -282,7 +262,8 @@ pub(super) fn emit_ping( // The reference's Lua hides the marker outside the disc and keeps the ping alive // (`Minimap_SetPing`'s else-branch is `MiniMapPing:Hide()`, not a clear) — so walking back - // into range brings it back for the rest of its 5 s. + // into range brings it back for the rest of its 5 s. The stock file makes that test itself + // and hides the frame; this one is the same test in yards, for the frame it cannot see. let d = (px - ctx.wx).hypot(py - ctx.wy); if d >= ctx.radius_yd { return; @@ -329,7 +310,7 @@ pub(super) fn emit_ping( ); } -/// Age the ping, announce a fresh one, and expire it — everything that is *not* geometry. +/// Announce a fresh ping and republish the position — everything that is *not* geometry. /// /// Runs before the script tick so the `MINIMAP_PING` event and the position behind /// `Minimap:GetPingPosition()` land in the same tick, and so an addon's handler sees a ping that @@ -340,46 +321,21 @@ pub(super) fn drive_minimap_ping( mut ping: ResMut, time: Res