From 3b6975fdefc0e7462e5337f10a52751b29baa976 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 5 Aug 2026 19:03:43 +0700 Subject: [PATCH] the pet gets its frame, flooded rooms fill, and macros really fire - the pet gets its own unit frame beside yours, and an Attack order no longer latches as the standing command - five rooms that always read bone dry hold their water now, the Deeprun Tram's flooded stretches among them - macros fire through the client's own chat door, and the macro window fits its tabs and scrolls its icon chooser - the options window grows a Nameplates page and its first dropdown, and the name-display switches apply live in the world Co-Authored-By: Claude Fable 5 --- Cargo.lock | 21 +- crates/benilla-app/Cargo.toml | 110 ++ .../assets/shaders/celestial.wgsl | 0 .../assets/shaders/cloud.wgsl | 0 .../assets/shaders/ffx_glow.wgsl | 0 .../assets/shaders/liquid.wgsl | 0 .../assets/shaders/sky.wgsl | 0 .../assets/shaders/star.wgsl | 0 .../assets/shaders/terrain.wgsl | 0 .../assets/shaders/ui_add.wgsl | 0 .../assets/shaders/ui_gamma.wgsl | 0 .../assets/shaders/ui_node_gamma.wgsl | 0 .../assets/shaders/ui_quad.wgsl | 0 .../assets/shaders/ui_slice_gamma.wgsl | 0 .../assets/shaders/wdl.wgsl | 0 .../assets/shaders/wmo_skybox.wgsl | 0 .../assets/shaders/wow_effect.wgsl | 0 .../assets/shaders/wow_model.wgsl | 0 .../assets/ui/ActionBar.xml | 0 .../assets/ui/BagFrame.xml | 0 .../assets/ui/BankFrame.xml | 0 .../assets/ui/BuffFrame.xml | 0 .../assets/ui/CastingBar.xml | 0 .../assets/ui/CharacterFrame.xml | 0 .../assets/ui/ChatFrame.xml | 17 + .../assets/ui/CombatText.xml | 0 .../assets/ui/ComboFrame.xml | 0 .../assets/ui/Cooldown.xml | 0 .../assets/ui/CraftFrame.xml | 0 .../assets/ui/DeathFrame.xml | 0 .../assets/ui/DuelFrame.xml | 0 .../assets/ui/DurabilityFrame.xml | 0 .../assets/ui/EnchantConfirm.xml | 0 .../assets/ui/ErrorsFrame.xml | 0 .../assets/ui/Fonts.xml | 0 .../assets/ui/FriendsFrame.xml | 0 .../assets/ui/GameMenuFrame.xml | 0 .../assets/ui/GameTooltip.xml | 0 .../assets/ui/GossipFrame.xml | 0 .../assets/ui/GroupLootFrame.xml | 0 .../assets/ui/InspectFrame.xml | 0 .../assets/ui/ItemRef.xml | 0 .../assets/ui/ItemTextFrame.xml | 0 .../assets/ui/LootFrame.xml | 0 .../assets/ui/MacroFrame.xml | 39 +- .../assets/ui/MailFrame.xml | 0 .../assets/ui/MerchantFrame.xml | 0 .../assets/ui/MicroMenu.xml | 0 .../assets/ui/MinimapCluster.xml | 0 .../assets/ui/MirrorTimer.xml | 0 .../assets/ui/MultiBars.xml | 0 .../assets/ui/OptionsFrame.xml | 198 +++- .../assets/ui/PartyFrame.xml | 0 .../assets/ui/PetActionBar.xml | 0 .../assets/ui/QuestFrame.xml | 0 .../assets/ui/QuestLogFrame.xml | 0 .../assets/ui/ScrollTemplates.xml | 0 .../assets/ui/SkillFrame.xml | 0 .../assets/ui/SpellBookFrame.xml | 0 .../assets/ui/StackSplit.xml | 0 .../assets/ui/StanceBar.xml | 0 .../assets/ui/TalentFrame.xml | 10 +- .../assets/ui/TaxiFrame.xml | 0 .../assets/ui/TradeFrame.xml | 0 .../assets/ui/TradeSkillFrame.xml | 0 .../assets/ui/TrainerFrame.xml | 0 .../assets/ui/UIDropDownMenu.xml | 20 +- .../assets/ui/UIParent.xml | 0 .../assets/ui/UiPanels.xml | 103 +- .../assets/ui/UnitFrames.xml | 283 ++++++ .../assets/ui/UnitPopup.xml | 0 .../assets/ui/WorldMapFrame.xml | 0 .../assets/ui/ZoneText.xml | 0 crates/{benilla => benilla-app}/src/area.rs | 0 .../src/area_trigger.rs | 0 .../{benilla => benilla-app}/src/art_scope.rs | 0 .../src/asset_churn.rs | 0 .../src/assets/images.rs | 0 .../src/assets/mod.rs | 0 .../src/aura_visual.rs | 0 .../src/aura_visual/tests.rs | 0 crates/{benilla => benilla-app}/src/bgwin.rs | 0 .../{benilla => benilla-app}/src/billboard.rs | 0 .../src/blob_shadow.rs | 0 .../{benilla => benilla-app}/src/bowstring.rs | 0 crates/benilla-app/src/build_id.rs | 60 ++ .../src/capture/depth_probe.rs | 0 .../src/capture/fixtures.rs | 55 + .../src/capture/live_shot.rs | 0 .../src/capture/mod.rs | 16 +- .../src/capture/phase_probe.rs | 0 .../src/capture/pick_probe.rs | 0 .../src/capture/probe_bank.rs | 0 .../src/capture/probe_castcancel.rs | 0 .../src/capture/probe_charcreate.rs | 0 .../src/capture/probe_crossing.rs | 0 .../src/capture/probe_mail.rs | 0 .../src/capture/probe_melee.rs | 0 .../src/capture/probe_partner.rs | 0 .../src/capture/probe_rig.rs | 0 .../src/capture/probe_taxi.rs | 0 .../src/capture/probes.rs | 0 .../src/capture/scenarios.rs | 63 +- .../src/char_create/mod.rs | 0 .../src/char_create/panels.rs | 0 .../src/char_create/parts.rs | 0 .../src/char_create/refresh.rs | 0 .../src/char_create/screen.rs | 0 .../src/char_select/dialog.rs | 0 .../src/char_select/input.rs | 0 .../src/char_select/mod.rs | 0 .../src/char_select/refresh.rs | 0 .../src/char_select/screen.rs | 0 .../src/chat_bubble.rs | 0 .../src/clouds/kernel.rs | 0 .../src/clouds/layer.rs | 0 .../src/clouds/mod.rs | 0 .../src/clouds/tables.rs | 0 .../{benilla => benilla-app}/src/clutter.rs | 17 +- .../{benilla => benilla-app}/src/collision.rs | 0 .../src/collision/one_sided.rs | 0 .../src/combat_text/law.rs | 0 .../src/combat_text/mod.rs | 0 .../{benilla => benilla-app}/src/cooldowns.rs | 0 .../src/creature_anim.rs | 35 +- .../src/creature_anim/blood.rs | 0 .../src/creature_anim/compose.rs | 0 .../src/creature_anim/driver.rs | 61 +- .../src/creature_anim/driver/grip.rs | 0 .../src/creature_anim/driver/mode.rs | 113 +-- .../src/creature_anim/driver/play.rs | 0 .../src/creature_anim/driver/tests.rs | 83 ++ .../src/creature_anim/driver/wound.rs | 0 .../src/creature_anim/emote_anim.rs | 0 .../src/creature_anim/env_damage.rs | 0 .../src/creature_anim/events.rs | 0 .../src/creature_anim/global_seq.rs | 0 .../src/creature_anim/impact.rs | 0 .../src/creature_anim/lod.rs | 2 + .../src/creature_anim/pose.rs | 0 .../src/creature_anim/select.rs | 90 +- .../src/creature_anim/select/tests.rs | 59 +- .../src/creature_anim/sheath.rs | 0 .../src/creature_anim/spell_visual.rs | 0 .../src/creature_anim/spell_visual/tests.rs | 0 .../src/creature_anim/twist.rs | 0 crates/{benilla => benilla-app}/src/cursor.rs | 0 crates/{benilla => benilla-app}/src/cvars.rs | 83 +- .../{benilla => benilla-app}/src/dbg_trace.rs | 0 crates/{benilla => benilla-app}/src/death.rs | 0 .../src/debug_panel/mod.rs | 9 +- .../src/debug_panel/state.rs | 0 .../src/debug_panel/visibility.rs | 0 .../src/debug_panel/world.rs | 0 crates/{benilla => benilla-app}/src/decal.rs | 0 .../src/doodad_anim/lazy.rs | 0 .../src/doodad_anim/mat_anim.rs | 0 .../src/doodad_anim/mod.rs | 0 .../{benilla => benilla-app}/src/entities.rs | 0 .../src/entities/attach/char_skin.rs | 0 .../src/entities/attach/dress.rs | 0 .../src/entities/attach/glue_preview.rs | 0 .../src/entities/attach/mod.rs | 0 .../src/entities/attach/redress.rs | 0 .../src/entities/carried_light.rs | 0 .../src/entities/chain_beam.rs | 0 .../src/entities/collision_height.rs | 0 .../src/entities/conform.rs | 0 .../src/entities/dest_fx.rs | 0 .../src/entities/display.rs | 0 .../src/entities/equipment/mod.rs | 0 .../src/entities/equipment/resolve.rs | 0 .../src/entities/equipment/spawn.rs | 0 .../src/entities/item_glow.rs | 0 .../src/entities/live_display.rs | 0 .../src/entities/missile.rs | 0 .../src/entities/mount.rs | 0 .../src/entities/mount/tests.rs | 0 .../src/entities/spell_fx/lifecycle.rs | 0 .../src/entities/spell_fx/mod.rs | 0 .../src/entities/wmo_props.rs | 0 .../src/entity_shade.rs | 0 .../src/exterior_cull.rs | 0 .../{benilla => benilla-app}/src/ffx_glow.rs | 0 .../src/glue/add_material.rs | 0 .../{benilla => benilla-app}/src/glue/art.rs | 0 .../src/glue/backdrop.rs | 0 .../{benilla => benilla-app}/src/glue/mod.rs | 0 .../src/glue/widgets.rs | 0 .../src/glue_strings.rs | 0 .../{benilla => benilla-app}/src/go_anim.rs | 0 .../src/go_templates.rs | 0 .../{benilla => benilla-app}/src/ground_fx.rs | 0 .../{benilla => benilla-app}/src/hover_log.rs | 0 .../src/instance_tint.rs | 0 .../{benilla => benilla-app}/src/interact.rs | 0 .../src/interact/journal.rs | 0 .../{benilla => benilla-app}/src/interior.rs | 0 crates/{benilla => benilla-app}/src/items.rs | 0 crates/benilla-app/src/lib.rs | 939 ++++++++++++++++++ .../src/lighting/daynight.rs | 0 .../src/lighting/global_light.rs | 0 .../src/lighting/mod.rs | 0 .../src/lighting/prop_probes.rs | 0 .../src/lighting/resolve.rs | 0 .../src/lighting/sh.rs | 0 .../src/liquid/mod.rs | 2 +- .../src/liquid/query.rs | 208 +++- .../src/liquid/real_data.rs | 12 +- .../src/liquid/spatial.rs | 0 .../src/liquid/surface.rs | 0 .../src/loading_screen.rs | 0 .../src/local_state.rs | 0 .../{benilla => benilla-app}/src/login/mod.rs | 0 .../src/login/screen.rs | 0 .../{benilla => benilla-app}/src/map_proj.rs | 0 .../{benilla => benilla-app}/src/mesh_tag.rs | 0 .../src/minimap/blips/dots.rs | 0 .../src/minimap/blips/mod.rs | 0 .../src/minimap/interior.rs | 2 + .../src/minimap/mod.rs | 0 .../src/model_fade.rs | 0 .../src/model_forms.rs | 0 .../src/model_render.rs | 0 .../{benilla => benilla-app}/src/modkeys.rs | 0 .../src/nameplates.rs | 40 +- crates/{benilla => benilla-app}/src/names.rs | 0 crates/{benilla => benilla-app}/src/net.rs | 0 .../{benilla => benilla-app}/src/net/apply.rs | 0 .../src/net/apply/anim.rs | 0 .../src/net/apply/chat.rs | 0 .../src/net/apply/combat.rs | 0 .../src/net/apply/combat_log.rs | 0 .../src/net/apply/death.rs | 0 .../src/net/apply/group.rs | 0 .../src/net/apply/loot.rs | 0 .../src/net/apply/mail.rs | 0 .../src/net/apply/mount.rs | 0 .../src/net/apply/names.rs | 0 .../src/net/apply/npc.rs | 0 .../src/net/apply/objects.rs | 14 +- .../src/net/apply/pet.rs | 0 .../src/net/apply/quests.rs | 0 .../src/net/apply/session.rs | 0 .../src/net/apply/spells.rs | 0 .../src/net/apply/trade.rs | 0 .../src/net/apply/world.rs | 0 crates/{benilla => benilla-app}/src/net/io.rs | 0 .../src/net/motion.rs | 0 .../src/net/motion/facing.rs | 0 .../src/net/motion/relay.rs | 0 .../src/net/motion/remote.rs | 0 .../src/net/motion/spline.rs | 3 +- .../src/net/motion/tests.rs | 0 .../{benilla => benilla-app}/src/npc_text.rs | 0 .../{benilla => benilla-app}/src/particles.rs | 0 .../src/particles/buffer.rs | 0 .../src/particles/census.rs | 0 .../src/particles/depthdump.rs | 0 .../src/particles/dumps.rs | 0 .../src/particles/emit.rs | 0 .../src/particles/emitdump.rs | 0 .../src/particles/model.rs | 0 .../src/particles/quads.rs | 0 .../src/particles/render.rs | 0 .../src/particles/sim.rs | 3 +- .../src/pending_item_ops.rs | 0 crates/{benilla => benilla-app}/src/perf.rs | 0 .../src/pipe_warm/menagerie.rs | 0 .../src/pipe_warm/mod.rs | 0 crates/{benilla => benilla-app}/src/player.rs | 5 +- .../src/player/arc.rs | 0 .../src/player/camera.rs | 0 .../src/player/follow.rs | 0 .../src/player/gait.rs | 0 .../src/player/land.rs | 0 .../src/player/move_trace.rs | 0 .../src/player/movement_net.rs | 0 .../src/player/mover.rs | 0 .../src/player/probe_cam.rs | 0 .../src/player/probe_look.rs | 0 .../src/player/server_ride.rs | 0 .../src/player/setup.rs | 0 .../src/player/state.rs | 0 .../src/player/swim.rs | 0 .../src/player/wire_in.rs | 0 .../src/portrait/booth.rs | 0 .../src/portrait/framing.rs | 0 .../src/portrait/glue_booth.rs | 0 .../src/portrait/light.rs | 0 .../src/portrait/mod.rs | 24 +- .../src/portrait/test_bake.rs | 0 .../{benilla => benilla-app}/src/preflight.rs | 0 .../src/probe_shield.rs | 0 .../src/quest_markers/mod.rs | 0 .../src/quest_markers/query.rs | 0 .../src/raid_marks.rs | 0 .../{benilla => benilla-app}/src/ribbons.rs | 0 .../src/rig_palette.rs | 0 .../{benilla => benilla-app}/src/schedule.rs | 0 crates/{benilla => benilla-app}/src/sky.rs | 0 .../{benilla => benilla-app}/src/sky_order.rs | 0 .../src/smart_rect.rs | 0 .../src/sound/anim_events.rs | 0 .../src/sound/combat.rs | 0 .../src/sound/creature.rs | 0 .../src/sound/emote.rs | 0 .../src/sound/footsteps.rs | 3 +- .../src/sound/gameobject.rs | 0 .../src/sound/glue.rs | 0 .../src/sound/greeting.rs | 0 .../src/sound/interior.rs | 0 .../{benilla => benilla-app}/src/sound/kit.rs | 0 .../src/sound/liquid_loop.rs | 0 .../src/sound/math.rs | 0 .../src/sound/missile.rs | 0 .../src/sound/mixer.rs | 0 .../{benilla => benilla-app}/src/sound/mod.rs | 0 .../src/sound/money.rs | 0 .../src/sound/mount.rs | 0 .../src/sound/reverb.rs | 0 .../src/sound/sheathe.rs | 0 .../src/sound/spell.rs | 0 .../{benilla => benilla-app}/src/sound/ui.rs | 0 .../src/sound/water.rs | 3 +- .../src/sound/weather.rs | 0 .../src/sound/zone.rs | 0 .../src/sun/follow.rs | 0 .../src/sun/materials.rs | 0 .../{benilla => benilla-app}/src/sun/mesh.rs | 0 .../{benilla => benilla-app}/src/sun/mod.rs | 0 .../{benilla => benilla-app}/src/sun/setup.rs | 0 .../src/target/by_name.rs | 0 .../src/target/click.rs | 18 +- .../src/target/cursor_mode.rs | 0 .../src/target/flash.rs | 0 .../src/target/highlight.rs | 0 .../src/target/hover.rs | 0 .../src/target/lock.rs | 0 .../src/target/mod.rs | 0 .../src/target/reticle.rs | 0 .../src/target/ring.rs | 0 .../src/target/scan.rs | 0 .../{benilla => benilla-app}/src/terrain.rs | 0 .../src/terrain_stream.rs | 55 + .../src/terrain_stream/collider.rs | 0 .../src/terrain_stream/furnish.rs | 0 .../src/terrain_stream/queries.rs | 0 .../src/terrain_stream/spawn/assemble.rs | 0 .../src/terrain_stream/spawn/fx.rs | 0 .../src/terrain_stream/spawn/mod.rs | 18 + .../src/terrain_stream/spawn/prop_light.rs | 0 .../src/textinput/clipboard.rs | 0 .../src/textinput/keymap.rs | 0 .../src/textinput/mod.rs | 0 .../src/thread_qos.rs | 0 .../{benilla => benilla-app}/src/transport.rs | 0 .../src/ui_action/cast_fail.rs | 0 .../src/ui_action/cast_send.rs | 0 .../src/ui_action/cast_target.rs | 0 .../src/ui_action/drain.rs | 0 .../src/ui_action/drain_tests.rs | 0 .../src/ui_action/errors.rs | 0 .../src/ui_action/feed.rs | 0 .../src/ui_action/feed_tests.rs | 0 .../src/ui_action/mod.rs | 0 .../src/ui_action/ranks.rs | 0 .../src/ui_action/state.rs | 0 .../src/ui_action/targeting/cursor.rs | 0 .../src/ui_action/targeting/item.rs | 0 .../src/ui_action/targeting/mod.rs | 0 .../src/ui_action/targeting/world.rs | 0 .../src/ui_action/toggle.rs | 0 .../src/ui_action/usable.rs | 0 .../src/ui_action/weapon_icon.rs | 0 .../{benilla => benilla-app}/src/ui_aura.rs | 103 +- .../{benilla => benilla-app}/src/ui_bank.rs | 0 .../{benilla => benilla-app}/src/ui_cast.rs | 0 .../{benilla => benilla-app}/src/ui_char.rs | 0 .../{benilla => benilla-app}/src/ui_chat.rs | 0 .../src/ui_chat/commands.rs | 0 .../src/ui_chat/edit.rs | 0 .../src/ui_chat/event.rs | 0 .../src/ui_chat/feed.rs | 0 .../src/ui_chat/frames.rs | 0 .../src/ui_chat/input/mod.rs | 7 +- .../src/ui_chat/input/parse.rs | 0 .../src/ui_chat/tests.rs | 0 .../{benilla => benilla-app}/src/ui_craft.rs | 0 .../{benilla => benilla-app}/src/ui_duel.rs | 0 .../{benilla => benilla-app}/src/ui_follow.rs | 0 .../{benilla => benilla-app}/src/ui_gamma.rs | 0 .../{benilla => benilla-app}/src/ui_gossip.rs | 0 .../{benilla => benilla-app}/src/ui_hide.rs | 0 .../src/ui_inspect.rs | 0 .../src/ui_item_text.rs | 0 .../src/ui_items/drain.rs | 0 .../src/ui_items/equip_error.rs | 0 .../src/ui_items/feed.rs | 0 .../src/ui_items/mod.rs | 0 .../{benilla => benilla-app}/src/ui_logout.rs | 0 .../{benilla => benilla-app}/src/ui_loot.rs | 0 .../src/ui_loot_roll.rs | 0 .../src/ui_macro/mod.rs | 22 +- .../src/ui_macro/run.rs | 127 ++- .../src/ui_macro/store.rs | 0 .../src/ui_macro/tests.rs | 77 +- .../src/ui_mail/mod.rs | 0 .../src/ui_mail/pending.rs | 0 .../src/ui_merchant.rs | 0 .../{benilla => benilla-app}/src/ui_mirror.rs | 0 crates/{benilla => benilla-app}/src/ui_net.rs | 0 .../src/ui_party/feed.rs | 0 .../src/ui_party/mod.rs | 0 .../{benilla => benilla-app}/src/ui_pass.rs | 0 crates/{benilla => benilla-app}/src/ui_pet.rs | 199 +++- .../{benilla => benilla-app}/src/ui_quest.rs | 0 .../src/ui_quest_log.rs | 0 .../src/ui_script/action_bar_tests.rs | 0 .../src/ui_script/bag_tests.rs | 0 .../src/ui_script/bank_tests.rs | 0 .../src/ui_script/buff_tests.rs | 0 .../src/ui_script/cast_tests.rs | 0 .../src/ui_script/character_tests.rs | 0 .../src/ui_script/chat_tests.rs | 0 .../src/ui_script/combat_text_tests.rs | 0 .../src/ui_script/combo_frame_tests.rs | 0 .../src/ui_script/death_tests.rs | 0 .../src/ui_script/delete_item_tests.rs | 0 .../src/ui_script/duel_tests.rs | 0 .../src/ui_script/durability_tests.rs | 0 .../src/ui_script/enchant_confirm_tests.rs | 0 .../src/ui_script/errors_tests.rs | 0 .../src/ui_script/escape_tests.rs | 2 + .../src/ui_script/extract/cooldown.rs | 0 .../src/ui_script/extract/mod.rs | 46 +- .../src/ui_script/extract/text.rs | 0 .../src/ui_script/friends_tests.rs | 0 .../src/ui_script/game_menu_tests.rs | 2 +- .../src/ui_script/group_loot_tests.rs | 0 .../src/ui_script/input.rs | 0 .../src/ui_script/inspect_tests.rs | 0 .../src/ui_script/loot_tests.rs | 0 .../src/ui_script/macro_tests.rs | 211 ++++ .../src/ui_script/merchant_tests.rs | 2 +- .../src/ui_script/micro_menu_tests.rs | 0 .../src/ui_script/minimap_tests.rs | 0 .../src/ui_script/mirror_timer_tests.rs | 0 .../src/ui_script/mod.rs | 16 +- .../src/ui_script/multibar_stance_tests.rs | 0 .../src/ui_script/options_tests.rs | 144 ++- .../src/ui_script/panel_tests.rs | 4 +- .../src/ui_script/perf_bar_tests.rs | 0 .../src/ui_script/pet_bar_tests.rs | 0 .../src/ui_script/pet_frame_tests.rs | 329 ++++++ .../src/ui_script/quest_tests.rs | 2 +- .../src/ui_script/questlog_tests.rs | 2 +- .../src/ui_script/resolve_bench.rs | 2 +- .../src/ui_script/shipped_xml_tests.rs | 73 ++ .../src/ui_script/spellbook_tests.rs | 0 .../src/ui_script/static_popup_tests.rs | 0 .../src/ui_script/target_aura_tests.rs | 0 .../src/ui_script/taxi_tests.rs | 0 .../src/ui_script/tooltip_anchor_tests.rs | 0 .../src/ui_script/tooltip_compare_tests.rs | 0 .../src/ui_script/trainer_tests.rs | 0 .../src/ui_script/unit_frame_tests.rs | 0 .../src/ui_script/unit_popup_tests.rs | 0 .../src/ui_script/zone_text_tests.rs | 0 .../src/ui_session.rs | 0 .../src/ui_shapeshift.rs | 0 .../src/ui_social/feed.rs | 0 .../src/ui_social/mod.rs | 0 .../src/ui_social/query.rs | 0 .../src/ui_spellbook.rs | 0 .../{benilla => benilla-app}/src/ui_talent.rs | 0 .../src/ui_taxi/mod.rs | 2 +- .../src/ui_taxi/routing.rs | 0 .../src/ui_text/atlas.rs | 0 .../src/ui_text/layout/mod.rs | 0 .../src/ui_text/layout/overflow.rs | 0 .../src/ui_text/layout/wrap.rs | 0 .../src/ui_text/markup.rs | 0 .../src/ui_text/mod.rs | 0 .../src/ui_tooltip.rs | 0 .../{benilla => benilla-app}/src/ui_trade.rs | 0 .../src/ui_tradeskill.rs | 0 .../src/ui_trainer.rs | 0 .../{benilla => benilla-app}/src/ui_unit.rs | 0 .../src/ui_world_map.rs | 0 crates/{benilla => benilla-app}/src/view.rs | 0 .../{benilla => benilla-app}/src/vplates.rs | 0 .../src/vplates/border.rs | 0 .../src/water_fx/mod.rs | 0 .../src/water_fx/params.rs | 0 .../src/water_fx/view.rs | 0 crates/{benilla => benilla-app}/src/wdl.rs | 0 .../src/weather/mod.rs | 0 .../src/weather/precip/mist.rs | 0 .../src/weather/precip/mod.rs | 0 .../src/weather/precip/pool.rs | 0 .../src/weather/precip/render.rs | 0 .../src/weather/precip/wind.rs | 0 .../src/wmo_portal/audit/light_probe.rs | 0 .../src/wmo_portal/audit/mod.rs | 1 + .../src/wmo_portal/audit/pin.rs | 0 .../src/wmo_portal/fog.rs | 0 .../src/wmo_portal/interior.rs | 0 .../src/wmo_portal/mod.rs | 9 + .../src/wmo_portal/probe.rs | 0 .../src/wmo_portal/seed.rs | 1 + .../{benilla => benilla-app}/src/wmo_sky.rs | 0 .../{benilla => benilla-app}/src/world_map.rs | 0 .../src/world_state.rs | 0 crates/{benilla => benilla-app}/src/zfill.rs | 0 .../tests/item_text_frame.rs | 0 .../tests/mail_frame.rs | 0 .../tests/talent_frame.rs | 39 + .../tests/trade_frame.rs | 0 .../tests/tradeskill_frame.rs | 0 crates/benilla-assets/src/wmo.rs | 7 + .../examples/wmo_liquid_scope.rs | 38 + crates/benilla-formats/src/lib.rs | 2 +- .../benilla-formats/src/models/wmo/group.rs | 69 +- crates/benilla-ui/src/loader/mod.rs | 47 + crates/benilla-ui/src/loader/regions.rs | 5 +- crates/benilla-ui/src/loader/tests.rs | 83 +- crates/benilla-ui/src/loader/widgets.rs | 18 +- crates/benilla-ui/src/script/layout.rs | 11 +- crates/benilla-ui/src/script/mod.rs | 33 +- crates/benilla-ui/src/script/region.rs | 27 +- .../src/script/tests/layout_gate.rs | 4 +- crates/benilla-ui/src/script/tests/measure.rs | 82 +- crates/benilla-ui/src/script/tests/regions.rs | 2 +- crates/benilla-ui/src/script/tests/tooltip.rs | 2 +- .../script/tests/tooltip_item/mechanics.rs | 2 +- .../src/script/tests/tooltip_unit.rs | 4 +- crates/benilla-ui/src/script/types.rs | 17 + crates/benilla/Cargo.toml | 111 +-- crates/benilla/build.rs | 20 +- crates/benilla/src/build_id.rs | 49 - crates/benilla/src/main.rs | 924 +---------------- 542 files changed, 4320 insertions(+), 1555 deletions(-) create mode 100644 crates/benilla-app/Cargo.toml rename crates/{benilla => benilla-app}/assets/shaders/celestial.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/cloud.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ffx_glow.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/liquid.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/sky.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/star.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/terrain.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ui_add.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ui_gamma.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ui_node_gamma.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ui_quad.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/ui_slice_gamma.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/wdl.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/wmo_skybox.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/wow_effect.wgsl (100%) rename crates/{benilla => benilla-app}/assets/shaders/wow_model.wgsl (100%) rename crates/{benilla => benilla-app}/assets/ui/ActionBar.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/BagFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/BankFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/BuffFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/CastingBar.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/CharacterFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ChatFrame.xml (97%) rename crates/{benilla => benilla-app}/assets/ui/CombatText.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ComboFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/Cooldown.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/CraftFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/DeathFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/DuelFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/DurabilityFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/EnchantConfirm.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ErrorsFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/Fonts.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/FriendsFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/GameMenuFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/GameTooltip.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/GossipFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/GroupLootFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/InspectFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ItemRef.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ItemTextFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/LootFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MacroFrame.xml (96%) rename crates/{benilla => benilla-app}/assets/ui/MailFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MerchantFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MicroMenu.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MinimapCluster.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MirrorTimer.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/MultiBars.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/OptionsFrame.xml (88%) rename crates/{benilla => benilla-app}/assets/ui/PartyFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/PetActionBar.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/QuestFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/QuestLogFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ScrollTemplates.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/SkillFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/SpellBookFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/StackSplit.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/StanceBar.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/TalentFrame.xml (99%) rename crates/{benilla => benilla-app}/assets/ui/TaxiFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/TradeFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/TradeSkillFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/TrainerFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/UIDropDownMenu.xml (97%) rename crates/{benilla => benilla-app}/assets/ui/UIParent.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/UiPanels.xml (93%) rename crates/{benilla => benilla-app}/assets/ui/UnitFrames.xml (82%) rename crates/{benilla => benilla-app}/assets/ui/UnitPopup.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/WorldMapFrame.xml (100%) rename crates/{benilla => benilla-app}/assets/ui/ZoneText.xml (100%) rename crates/{benilla => benilla-app}/src/area.rs (100%) rename crates/{benilla => benilla-app}/src/area_trigger.rs (100%) rename crates/{benilla => benilla-app}/src/art_scope.rs (100%) rename crates/{benilla => benilla-app}/src/asset_churn.rs (100%) rename crates/{benilla => benilla-app}/src/assets/images.rs (100%) rename crates/{benilla => benilla-app}/src/assets/mod.rs (100%) rename crates/{benilla => benilla-app}/src/aura_visual.rs (100%) rename crates/{benilla => benilla-app}/src/aura_visual/tests.rs (100%) rename crates/{benilla => benilla-app}/src/bgwin.rs (100%) rename crates/{benilla => benilla-app}/src/billboard.rs (100%) rename crates/{benilla => benilla-app}/src/blob_shadow.rs (100%) rename crates/{benilla => benilla-app}/src/bowstring.rs (100%) create mode 100644 crates/benilla-app/src/build_id.rs rename crates/{benilla => benilla-app}/src/capture/depth_probe.rs (100%) rename crates/{benilla => benilla-app}/src/capture/fixtures.rs (95%) rename crates/{benilla => benilla-app}/src/capture/live_shot.rs (100%) rename crates/{benilla => benilla-app}/src/capture/mod.rs (98%) rename crates/{benilla => benilla-app}/src/capture/phase_probe.rs (100%) rename crates/{benilla => benilla-app}/src/capture/pick_probe.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_bank.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_castcancel.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_charcreate.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_crossing.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_mail.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_melee.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_partner.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_rig.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probe_taxi.rs (100%) rename crates/{benilla => benilla-app}/src/capture/probes.rs (100%) rename crates/{benilla => benilla-app}/src/capture/scenarios.rs (93%) rename crates/{benilla => benilla-app}/src/char_create/mod.rs (100%) rename crates/{benilla => benilla-app}/src/char_create/panels.rs (100%) rename crates/{benilla => benilla-app}/src/char_create/parts.rs (100%) rename crates/{benilla => benilla-app}/src/char_create/refresh.rs (100%) rename crates/{benilla => benilla-app}/src/char_create/screen.rs (100%) rename crates/{benilla => benilla-app}/src/char_select/dialog.rs (100%) rename crates/{benilla => benilla-app}/src/char_select/input.rs (100%) rename crates/{benilla => benilla-app}/src/char_select/mod.rs (100%) rename crates/{benilla => benilla-app}/src/char_select/refresh.rs (100%) rename crates/{benilla => benilla-app}/src/char_select/screen.rs (100%) rename crates/{benilla => benilla-app}/src/chat_bubble.rs (100%) rename crates/{benilla => benilla-app}/src/clouds/kernel.rs (100%) rename crates/{benilla => benilla-app}/src/clouds/layer.rs (100%) rename crates/{benilla => benilla-app}/src/clouds/mod.rs (100%) rename crates/{benilla => benilla-app}/src/clouds/tables.rs (100%) rename crates/{benilla => benilla-app}/src/clutter.rs (96%) rename crates/{benilla => benilla-app}/src/collision.rs (100%) rename crates/{benilla => benilla-app}/src/collision/one_sided.rs (100%) rename crates/{benilla => benilla-app}/src/combat_text/law.rs (100%) rename crates/{benilla => benilla-app}/src/combat_text/mod.rs (100%) rename crates/{benilla => benilla-app}/src/cooldowns.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim.rs (97%) rename crates/{benilla => benilla-app}/src/creature_anim/blood.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/compose.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/driver.rs (96%) rename crates/{benilla => benilla-app}/src/creature_anim/driver/grip.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/driver/mode.rs (85%) rename crates/{benilla => benilla-app}/src/creature_anim/driver/play.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/driver/tests.rs (96%) rename crates/{benilla => benilla-app}/src/creature_anim/driver/wound.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/emote_anim.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/env_damage.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/events.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/global_seq.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/impact.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/lod.rs (99%) rename crates/{benilla => benilla-app}/src/creature_anim/pose.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/select.rs (95%) rename crates/{benilla => benilla-app}/src/creature_anim/select/tests.rs (94%) rename crates/{benilla => benilla-app}/src/creature_anim/sheath.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/spell_visual.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/spell_visual/tests.rs (100%) rename crates/{benilla => benilla-app}/src/creature_anim/twist.rs (100%) rename crates/{benilla => benilla-app}/src/cursor.rs (100%) rename crates/{benilla => benilla-app}/src/cvars.rs (83%) rename crates/{benilla => benilla-app}/src/dbg_trace.rs (100%) rename crates/{benilla => benilla-app}/src/death.rs (100%) rename crates/{benilla => benilla-app}/src/debug_panel/mod.rs (99%) rename crates/{benilla => benilla-app}/src/debug_panel/state.rs (100%) rename crates/{benilla => benilla-app}/src/debug_panel/visibility.rs (100%) rename crates/{benilla => benilla-app}/src/debug_panel/world.rs (100%) rename crates/{benilla => benilla-app}/src/decal.rs (100%) rename crates/{benilla => benilla-app}/src/doodad_anim/lazy.rs (100%) rename crates/{benilla => benilla-app}/src/doodad_anim/mat_anim.rs (100%) rename crates/{benilla => benilla-app}/src/doodad_anim/mod.rs (100%) rename crates/{benilla => benilla-app}/src/entities.rs (100%) rename crates/{benilla => benilla-app}/src/entities/attach/char_skin.rs (100%) rename crates/{benilla => benilla-app}/src/entities/attach/dress.rs (100%) rename crates/{benilla => benilla-app}/src/entities/attach/glue_preview.rs (100%) rename crates/{benilla => benilla-app}/src/entities/attach/mod.rs (100%) rename crates/{benilla => benilla-app}/src/entities/attach/redress.rs (100%) rename crates/{benilla => benilla-app}/src/entities/carried_light.rs (100%) rename crates/{benilla => benilla-app}/src/entities/chain_beam.rs (100%) rename crates/{benilla => benilla-app}/src/entities/collision_height.rs (100%) rename crates/{benilla => benilla-app}/src/entities/conform.rs (100%) rename crates/{benilla => benilla-app}/src/entities/dest_fx.rs (100%) rename crates/{benilla => benilla-app}/src/entities/display.rs (100%) rename crates/{benilla => benilla-app}/src/entities/equipment/mod.rs (100%) rename crates/{benilla => benilla-app}/src/entities/equipment/resolve.rs (100%) rename crates/{benilla => benilla-app}/src/entities/equipment/spawn.rs (100%) rename crates/{benilla => benilla-app}/src/entities/item_glow.rs (100%) rename crates/{benilla => benilla-app}/src/entities/live_display.rs (100%) rename crates/{benilla => benilla-app}/src/entities/missile.rs (100%) rename crates/{benilla => benilla-app}/src/entities/mount.rs (100%) rename crates/{benilla => benilla-app}/src/entities/mount/tests.rs (100%) rename crates/{benilla => benilla-app}/src/entities/spell_fx/lifecycle.rs (100%) rename crates/{benilla => benilla-app}/src/entities/spell_fx/mod.rs (100%) rename crates/{benilla => benilla-app}/src/entities/wmo_props.rs (100%) rename crates/{benilla => benilla-app}/src/entity_shade.rs (100%) rename crates/{benilla => benilla-app}/src/exterior_cull.rs (100%) rename crates/{benilla => benilla-app}/src/ffx_glow.rs (100%) rename crates/{benilla => benilla-app}/src/glue/add_material.rs (100%) rename crates/{benilla => benilla-app}/src/glue/art.rs (100%) rename crates/{benilla => benilla-app}/src/glue/backdrop.rs (100%) rename crates/{benilla => benilla-app}/src/glue/mod.rs (100%) rename crates/{benilla => benilla-app}/src/glue/widgets.rs (100%) rename crates/{benilla => benilla-app}/src/glue_strings.rs (100%) rename crates/{benilla => benilla-app}/src/go_anim.rs (100%) rename crates/{benilla => benilla-app}/src/go_templates.rs (100%) rename crates/{benilla => benilla-app}/src/ground_fx.rs (100%) rename crates/{benilla => benilla-app}/src/hover_log.rs (100%) rename crates/{benilla => benilla-app}/src/instance_tint.rs (100%) rename crates/{benilla => benilla-app}/src/interact.rs (100%) rename crates/{benilla => benilla-app}/src/interact/journal.rs (100%) rename crates/{benilla => benilla-app}/src/interior.rs (100%) rename crates/{benilla => benilla-app}/src/items.rs (100%) create mode 100644 crates/benilla-app/src/lib.rs rename crates/{benilla => benilla-app}/src/lighting/daynight.rs (100%) rename crates/{benilla => benilla-app}/src/lighting/global_light.rs (100%) rename crates/{benilla => benilla-app}/src/lighting/mod.rs (100%) rename crates/{benilla => benilla-app}/src/lighting/prop_probes.rs (100%) rename crates/{benilla => benilla-app}/src/lighting/resolve.rs (100%) rename crates/{benilla => benilla-app}/src/lighting/sh.rs (100%) rename crates/{benilla => benilla-app}/src/liquid/mod.rs (98%) rename crates/{benilla => benilla-app}/src/liquid/query.rs (88%) rename crates/{benilla => benilla-app}/src/liquid/real_data.rs (98%) rename crates/{benilla => benilla-app}/src/liquid/spatial.rs (100%) rename crates/{benilla => benilla-app}/src/liquid/surface.rs (100%) rename crates/{benilla => benilla-app}/src/loading_screen.rs (100%) rename crates/{benilla => benilla-app}/src/local_state.rs (100%) rename crates/{benilla => benilla-app}/src/login/mod.rs (100%) rename crates/{benilla => benilla-app}/src/login/screen.rs (100%) rename crates/{benilla => benilla-app}/src/map_proj.rs (100%) rename crates/{benilla => benilla-app}/src/mesh_tag.rs (100%) rename crates/{benilla => benilla-app}/src/minimap/blips/dots.rs (100%) rename crates/{benilla => benilla-app}/src/minimap/blips/mod.rs (100%) rename crates/{benilla => benilla-app}/src/minimap/interior.rs (99%) rename crates/{benilla => benilla-app}/src/minimap/mod.rs (100%) rename crates/{benilla => benilla-app}/src/model_fade.rs (100%) rename crates/{benilla => benilla-app}/src/model_forms.rs (100%) rename crates/{benilla => benilla-app}/src/model_render.rs (100%) rename crates/{benilla => benilla-app}/src/modkeys.rs (100%) rename crates/{benilla => benilla-app}/src/nameplates.rs (97%) rename crates/{benilla => benilla-app}/src/names.rs (100%) rename crates/{benilla => benilla-app}/src/net.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/anim.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/chat.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/combat.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/combat_log.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/death.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/group.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/loot.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/mail.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/mount.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/names.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/npc.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/objects.rs (96%) rename crates/{benilla => benilla-app}/src/net/apply/pet.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/quests.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/session.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/spells.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/trade.rs (100%) rename crates/{benilla => benilla-app}/src/net/apply/world.rs (100%) rename crates/{benilla => benilla-app}/src/net/io.rs (100%) rename crates/{benilla => benilla-app}/src/net/motion.rs (100%) rename crates/{benilla => benilla-app}/src/net/motion/facing.rs (100%) rename crates/{benilla => benilla-app}/src/net/motion/relay.rs (100%) rename crates/{benilla => benilla-app}/src/net/motion/remote.rs (100%) rename crates/{benilla => benilla-app}/src/net/motion/spline.rs (99%) rename crates/{benilla => benilla-app}/src/net/motion/tests.rs (100%) rename crates/{benilla => benilla-app}/src/npc_text.rs (100%) rename crates/{benilla => benilla-app}/src/particles.rs (100%) rename crates/{benilla => benilla-app}/src/particles/buffer.rs (100%) rename crates/{benilla => benilla-app}/src/particles/census.rs (100%) rename crates/{benilla => benilla-app}/src/particles/depthdump.rs (100%) rename crates/{benilla => benilla-app}/src/particles/dumps.rs (100%) rename crates/{benilla => benilla-app}/src/particles/emit.rs (100%) rename crates/{benilla => benilla-app}/src/particles/emitdump.rs (100%) rename crates/{benilla => benilla-app}/src/particles/model.rs (100%) rename crates/{benilla => benilla-app}/src/particles/quads.rs (100%) rename crates/{benilla => benilla-app}/src/particles/render.rs (100%) rename crates/{benilla => benilla-app}/src/particles/sim.rs (99%) rename crates/{benilla => benilla-app}/src/pending_item_ops.rs (100%) rename crates/{benilla => benilla-app}/src/perf.rs (100%) rename crates/{benilla => benilla-app}/src/pipe_warm/menagerie.rs (100%) rename crates/{benilla => benilla-app}/src/pipe_warm/mod.rs (100%) rename crates/{benilla => benilla-app}/src/player.rs (99%) rename crates/{benilla => benilla-app}/src/player/arc.rs (100%) rename crates/{benilla => benilla-app}/src/player/camera.rs (100%) rename crates/{benilla => benilla-app}/src/player/follow.rs (100%) rename crates/{benilla => benilla-app}/src/player/gait.rs (100%) rename crates/{benilla => benilla-app}/src/player/land.rs (100%) rename crates/{benilla => benilla-app}/src/player/move_trace.rs (100%) rename crates/{benilla => benilla-app}/src/player/movement_net.rs (100%) rename crates/{benilla => benilla-app}/src/player/mover.rs (100%) rename crates/{benilla => benilla-app}/src/player/probe_cam.rs (100%) rename crates/{benilla => benilla-app}/src/player/probe_look.rs (100%) rename crates/{benilla => benilla-app}/src/player/server_ride.rs (100%) rename crates/{benilla => benilla-app}/src/player/setup.rs (100%) rename crates/{benilla => benilla-app}/src/player/state.rs (100%) rename crates/{benilla => benilla-app}/src/player/swim.rs (100%) rename crates/{benilla => benilla-app}/src/player/wire_in.rs (100%) rename crates/{benilla => benilla-app}/src/portrait/booth.rs (100%) rename crates/{benilla => benilla-app}/src/portrait/framing.rs (100%) rename crates/{benilla => benilla-app}/src/portrait/glue_booth.rs (100%) rename crates/{benilla => benilla-app}/src/portrait/light.rs (100%) rename crates/{benilla => benilla-app}/src/portrait/mod.rs (98%) rename crates/{benilla => benilla-app}/src/portrait/test_bake.rs (100%) rename crates/{benilla => benilla-app}/src/preflight.rs (100%) rename crates/{benilla => benilla-app}/src/probe_shield.rs (100%) rename crates/{benilla => benilla-app}/src/quest_markers/mod.rs (100%) rename crates/{benilla => benilla-app}/src/quest_markers/query.rs (100%) rename crates/{benilla => benilla-app}/src/raid_marks.rs (100%) rename crates/{benilla => benilla-app}/src/ribbons.rs (100%) rename crates/{benilla => benilla-app}/src/rig_palette.rs (100%) rename crates/{benilla => benilla-app}/src/schedule.rs (100%) rename crates/{benilla => benilla-app}/src/sky.rs (100%) rename crates/{benilla => benilla-app}/src/sky_order.rs (100%) rename crates/{benilla => benilla-app}/src/smart_rect.rs (100%) rename crates/{benilla => benilla-app}/src/sound/anim_events.rs (100%) rename crates/{benilla => benilla-app}/src/sound/combat.rs (100%) rename crates/{benilla => benilla-app}/src/sound/creature.rs (100%) rename crates/{benilla => benilla-app}/src/sound/emote.rs (100%) rename crates/{benilla => benilla-app}/src/sound/footsteps.rs (98%) rename crates/{benilla => benilla-app}/src/sound/gameobject.rs (100%) rename crates/{benilla => benilla-app}/src/sound/glue.rs (100%) rename crates/{benilla => benilla-app}/src/sound/greeting.rs (100%) rename crates/{benilla => benilla-app}/src/sound/interior.rs (100%) rename crates/{benilla => benilla-app}/src/sound/kit.rs (100%) rename crates/{benilla => benilla-app}/src/sound/liquid_loop.rs (100%) rename crates/{benilla => benilla-app}/src/sound/math.rs (100%) rename crates/{benilla => benilla-app}/src/sound/missile.rs (100%) rename crates/{benilla => benilla-app}/src/sound/mixer.rs (100%) rename crates/{benilla => benilla-app}/src/sound/mod.rs (100%) rename crates/{benilla => benilla-app}/src/sound/money.rs (100%) rename crates/{benilla => benilla-app}/src/sound/mount.rs (100%) rename crates/{benilla => benilla-app}/src/sound/reverb.rs (100%) rename crates/{benilla => benilla-app}/src/sound/sheathe.rs (100%) rename crates/{benilla => benilla-app}/src/sound/spell.rs (100%) rename crates/{benilla => benilla-app}/src/sound/ui.rs (100%) rename crates/{benilla => benilla-app}/src/sound/water.rs (98%) rename crates/{benilla => benilla-app}/src/sound/weather.rs (100%) rename crates/{benilla => benilla-app}/src/sound/zone.rs (100%) rename crates/{benilla => benilla-app}/src/sun/follow.rs (100%) rename crates/{benilla => benilla-app}/src/sun/materials.rs (100%) rename crates/{benilla => benilla-app}/src/sun/mesh.rs (100%) rename crates/{benilla => benilla-app}/src/sun/mod.rs (100%) rename crates/{benilla => benilla-app}/src/sun/setup.rs (100%) rename crates/{benilla => benilla-app}/src/target/by_name.rs (100%) rename crates/{benilla => benilla-app}/src/target/click.rs (97%) rename crates/{benilla => benilla-app}/src/target/cursor_mode.rs (100%) rename crates/{benilla => benilla-app}/src/target/flash.rs (100%) rename crates/{benilla => benilla-app}/src/target/highlight.rs (100%) rename crates/{benilla => benilla-app}/src/target/hover.rs (100%) rename crates/{benilla => benilla-app}/src/target/lock.rs (100%) rename crates/{benilla => benilla-app}/src/target/mod.rs (100%) rename crates/{benilla => benilla-app}/src/target/reticle.rs (100%) rename crates/{benilla => benilla-app}/src/target/ring.rs (100%) rename crates/{benilla => benilla-app}/src/target/scan.rs (100%) rename crates/{benilla => benilla-app}/src/terrain.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream.rs (96%) rename crates/{benilla => benilla-app}/src/terrain_stream/collider.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream/furnish.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream/queries.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream/spawn/assemble.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream/spawn/fx.rs (100%) rename crates/{benilla => benilla-app}/src/terrain_stream/spawn/mod.rs (97%) rename crates/{benilla => benilla-app}/src/terrain_stream/spawn/prop_light.rs (100%) rename crates/{benilla => benilla-app}/src/textinput/clipboard.rs (100%) rename crates/{benilla => benilla-app}/src/textinput/keymap.rs (100%) rename crates/{benilla => benilla-app}/src/textinput/mod.rs (100%) rename crates/{benilla => benilla-app}/src/thread_qos.rs (100%) rename crates/{benilla => benilla-app}/src/transport.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/cast_fail.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/cast_send.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/cast_target.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/drain.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/drain_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/errors.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/feed.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/feed_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/ranks.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/state.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/targeting/cursor.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/targeting/item.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/targeting/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/targeting/world.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/toggle.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/usable.rs (100%) rename crates/{benilla => benilla-app}/src/ui_action/weapon_icon.rs (100%) rename crates/{benilla => benilla-app}/src/ui_aura.rs (92%) rename crates/{benilla => benilla-app}/src/ui_bank.rs (100%) rename crates/{benilla => benilla-app}/src/ui_cast.rs (100%) rename crates/{benilla => benilla-app}/src/ui_char.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/commands.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/edit.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/event.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/feed.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/frames.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/input/mod.rs (99%) rename crates/{benilla => benilla-app}/src/ui_chat/input/parse.rs (100%) rename crates/{benilla => benilla-app}/src/ui_chat/tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_craft.rs (100%) rename crates/{benilla => benilla-app}/src/ui_duel.rs (100%) rename crates/{benilla => benilla-app}/src/ui_follow.rs (100%) rename crates/{benilla => benilla-app}/src/ui_gamma.rs (100%) rename crates/{benilla => benilla-app}/src/ui_gossip.rs (100%) rename crates/{benilla => benilla-app}/src/ui_hide.rs (100%) rename crates/{benilla => benilla-app}/src/ui_inspect.rs (100%) rename crates/{benilla => benilla-app}/src/ui_item_text.rs (100%) rename crates/{benilla => benilla-app}/src/ui_items/drain.rs (100%) rename crates/{benilla => benilla-app}/src/ui_items/equip_error.rs (100%) rename crates/{benilla => benilla-app}/src/ui_items/feed.rs (100%) rename crates/{benilla => benilla-app}/src/ui_items/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_logout.rs (100%) rename crates/{benilla => benilla-app}/src/ui_loot.rs (100%) rename crates/{benilla => benilla-app}/src/ui_loot_roll.rs (100%) rename crates/{benilla => benilla-app}/src/ui_macro/mod.rs (89%) rename crates/{benilla => benilla-app}/src/ui_macro/run.rs (50%) rename crates/{benilla => benilla-app}/src/ui_macro/store.rs (100%) rename crates/{benilla => benilla-app}/src/ui_macro/tests.rs (65%) rename crates/{benilla => benilla-app}/src/ui_mail/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_mail/pending.rs (100%) rename crates/{benilla => benilla-app}/src/ui_merchant.rs (100%) rename crates/{benilla => benilla-app}/src/ui_mirror.rs (100%) rename crates/{benilla => benilla-app}/src/ui_net.rs (100%) rename crates/{benilla => benilla-app}/src/ui_party/feed.rs (100%) rename crates/{benilla => benilla-app}/src/ui_party/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_pass.rs (100%) rename crates/{benilla => benilla-app}/src/ui_pet.rs (78%) rename crates/{benilla => benilla-app}/src/ui_quest.rs (100%) rename crates/{benilla => benilla-app}/src/ui_quest_log.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/action_bar_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/bag_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/bank_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/buff_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/cast_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/character_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/chat_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/combat_text_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/combo_frame_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/death_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/delete_item_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/duel_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/durability_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/enchant_confirm_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/errors_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/escape_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/extract/cooldown.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/extract/mod.rs (96%) rename crates/{benilla => benilla-app}/src/ui_script/extract/text.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/friends_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/game_menu_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/group_loot_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/input.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/inspect_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/loot_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/macro_tests.rs (57%) rename crates/{benilla => benilla-app}/src/ui_script/merchant_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/micro_menu_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/minimap_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/mirror_timer_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/mod.rs (98%) rename crates/{benilla => benilla-app}/src/ui_script/multibar_stance_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/options_tests.rs (87%) rename crates/{benilla => benilla-app}/src/ui_script/panel_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/perf_bar_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/pet_bar_tests.rs (100%) create mode 100644 crates/benilla-app/src/ui_script/pet_frame_tests.rs rename crates/{benilla => benilla-app}/src/ui_script/quest_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/questlog_tests.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/resolve_bench.rs (99%) rename crates/{benilla => benilla-app}/src/ui_script/shipped_xml_tests.rs (67%) rename crates/{benilla => benilla-app}/src/ui_script/spellbook_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/static_popup_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/target_aura_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/taxi_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/tooltip_anchor_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/tooltip_compare_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/trainer_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/unit_frame_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/unit_popup_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_script/zone_text_tests.rs (100%) rename crates/{benilla => benilla-app}/src/ui_session.rs (100%) rename crates/{benilla => benilla-app}/src/ui_shapeshift.rs (100%) rename crates/{benilla => benilla-app}/src/ui_social/feed.rs (100%) rename crates/{benilla => benilla-app}/src/ui_social/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_social/query.rs (100%) rename crates/{benilla => benilla-app}/src/ui_spellbook.rs (100%) rename crates/{benilla => benilla-app}/src/ui_talent.rs (100%) rename crates/{benilla => benilla-app}/src/ui_taxi/mod.rs (99%) rename crates/{benilla => benilla-app}/src/ui_taxi/routing.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/atlas.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/layout/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/layout/overflow.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/layout/wrap.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/markup.rs (100%) rename crates/{benilla => benilla-app}/src/ui_text/mod.rs (100%) rename crates/{benilla => benilla-app}/src/ui_tooltip.rs (100%) rename crates/{benilla => benilla-app}/src/ui_trade.rs (100%) rename crates/{benilla => benilla-app}/src/ui_tradeskill.rs (100%) rename crates/{benilla => benilla-app}/src/ui_trainer.rs (100%) rename crates/{benilla => benilla-app}/src/ui_unit.rs (100%) rename crates/{benilla => benilla-app}/src/ui_world_map.rs (100%) rename crates/{benilla => benilla-app}/src/view.rs (100%) rename crates/{benilla => benilla-app}/src/vplates.rs (100%) rename crates/{benilla => benilla-app}/src/vplates/border.rs (100%) rename crates/{benilla => benilla-app}/src/water_fx/mod.rs (100%) rename crates/{benilla => benilla-app}/src/water_fx/params.rs (100%) rename crates/{benilla => benilla-app}/src/water_fx/view.rs (100%) rename crates/{benilla => benilla-app}/src/wdl.rs (100%) rename crates/{benilla => benilla-app}/src/weather/mod.rs (100%) rename crates/{benilla => benilla-app}/src/weather/precip/mist.rs (100%) rename crates/{benilla => benilla-app}/src/weather/precip/mod.rs (100%) rename crates/{benilla => benilla-app}/src/weather/precip/pool.rs (100%) rename crates/{benilla => benilla-app}/src/weather/precip/render.rs (100%) rename crates/{benilla => benilla-app}/src/weather/precip/wind.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/audit/light_probe.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/audit/mod.rs (99%) rename crates/{benilla => benilla-app}/src/wmo_portal/audit/pin.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/fog.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/interior.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/mod.rs (98%) rename crates/{benilla => benilla-app}/src/wmo_portal/probe.rs (100%) rename crates/{benilla => benilla-app}/src/wmo_portal/seed.rs (99%) rename crates/{benilla => benilla-app}/src/wmo_sky.rs (100%) rename crates/{benilla => benilla-app}/src/world_map.rs (100%) rename crates/{benilla => benilla-app}/src/world_state.rs (100%) rename crates/{benilla => benilla-app}/src/zfill.rs (100%) rename crates/{benilla => benilla-app}/tests/item_text_frame.rs (100%) rename crates/{benilla => benilla-app}/tests/mail_frame.rs (100%) rename crates/{benilla => benilla-app}/tests/talent_frame.rs (83%) rename crates/{benilla => benilla-app}/tests/trade_frame.rs (100%) rename crates/{benilla => benilla-app}/tests/tradeskill_frame.rs (100%) delete mode 100644 crates/benilla/src/build_id.rs diff --git a/Cargo.lock b/Cargo.lock index 67ad2012..bb4f2b2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,6 +570,20 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "benilla" version = "0.1.0" +dependencies = [ + "benilla-app", +] + +[[package]] +name = "benilla-adt" +version = "0.1.0" +dependencies = [ + "benilla-bytes", +] + +[[package]] +name = "benilla-app" +version = "0.1.0" dependencies = [ "anyhow", "arboard", @@ -599,13 +613,6 @@ dependencies = [ "toml", ] -[[package]] -name = "benilla-adt" -version = "0.1.0" -dependencies = [ - "benilla-bytes", -] - [[package]] name = "benilla-assets" version = "0.1.0" diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml new file mode 100644 index 00000000..69ffa096 --- /dev/null +++ b/crates/benilla-app/Cargo.toml @@ -0,0 +1,110 @@ +[package] +name = "benilla-app" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "The Bevy app as a library: everything but the build-id stamp, which lives in the `benilla` launcher shim so a commit dirties ~30 lines instead of this crate (decision 0993)" + +[features] +# Opt-in Tracy profiling: `cargo run --features tracy` streams per-system `info_span!` markers (on the +# hot systems) + render-pass CPU timings to a Tracy capture (decision 0026 — the dev/player seam; +# a `docs/howto/running.md` this once cited never landed). +tracy = ["bevy/trace_tracy"] +# Opt-in Chrome-trace profiling: `cargo run --features trace_chrome` writes a `trace-.json` of +# every system's per-run span next to the working dir. Tracy's offline twin — it needs no GUI and no +# live capture, so a per-system cost question is answerable from a terminal (and from a run someone +# else recorded). Aggregate it with `scripts/tracesum.py`. +trace_chrome = ["bevy/trace_chrome"] + +[dependencies] +anyhow.workspace = true +# OS clipboard access for the UI EditBox copy/cut/paste chords (chat input). Already in the tree via +# bevy_egui; pinned to the same 3.x so there's one copy. Read on the main thread (the UI key feed is +# a NonSend system) — required for the macOS NSPasteboard path. Covers macOS, Windows, and Linux/BSD +# under X11; the Wayland backend is `smithay-clipboard` below (decision 0702). +arboard = "3" +benilla-ui = { path = "../benilla-ui" } +bevy.workspace = true +bevy_egui = "0.39" +# Zero-copy POD→bytes for the shared light storage buffer (one per-frame write_buffer). Already in +# the tree transitively; pinned to the same version as the dev-dep below. +bytemuck = { version = "1", features = ["derive"] } +# `f16` decode for the booth-target dump (`WOW_BOOTH_DUMP`): the booth render targets are +# `Rgba16Float` (decision 0804) and the PNG wants display bytes. Already in the tree via +# wgpu/bevy_image; declared so the call site is an honest dependency. +half = "2" +# `getrusage` — whole-process CPU time (all threads), the perf probes' `cpu_ms`/`cpu_pct` metric +# (`perf::process_cpu_secs`). Wall-clock fps is unusable as a regression instrument on a machine +# running parallel sessions' builds; CPU-per-frame is, and it is also the number the Mac report +# is about ("250 % CPU at 59 fps"). Already in the tree transitively; declared so the call site is +# an honest dependency. +libc = "0.2" +# Resample varying-size layer BLPs to a uniform size for the terrain texture array; `png` so we can +# encode the cursor image into data an NSImage can load (macOS hardware cursor). +image = { version = "0.25", default-features = false, features = ["png"] } +# The net↔ECS bridge: a background IO thread streams typed `SessionEvent`s to the ECS over a +# channel, and outbound `ClientCommand`s back. crossbeam's Sender/Receiver are `Send + Sync + Clone` +# (std's mpsc Sender is `!Sync`, so it can't be a Bevy resource without a mutex — the very thing this +# bridge deletes). The canonical Bevy background-thread→channel→system idiom. See decision 0006. +crossbeam-channel = "0.5" +benilla-assets = { path = "../benilla-assets" } +benilla-formats = { path = "../benilla-formats" } +benilla-protocol = { path = "../benilla-protocol" } +# The CVar config diff (`benilla/config.toml`, decision 0954): serde derive on the persisted +# shape, toml as its format. (serde_json left with the era manifest — 0978.) +serde = { version = "1", features = ["derive"] } +toml = "0.8" +# ECS-native physics: collider storage + broadphase BVH + shape-casts, with a thin custom kinematic +# character controller on top (decision 0009). Pinned 0.6: it targets Bevy 0.18 (verified single, +# unified bevy 0.18.1 in the tree); 0.7.0 jumps to Bevy 0.19 and would duplicate bevy. avian wraps +# parry under the hood, so adopting it lets us drop the *direct* parry3d dep once the bespoke +# CollisionWorld is gone. See docs… (decision 0007 superseded by 0009). +avian3d = "0.6" +# The glyph shaper + rasterizer for `FontString` text (decision 0068 §2, probe-verified in +# `probes/text-glyph`). Pinned to 0.16 — the exact version already in the tree transitively +# (`bevy_text`'s own dependency, see `Cargo.lock`) — so this is the *same* `cosmic-text` the decision +# already commits to, not a second copy. Used as a standalone shaper/rasterizer (`FontSystem` + +# `Buffer` + `SwashCache`) in `ui_text.rs`, never `bevy_text`'s `Text2d`/ECS integration (that path +# doesn't feed our custom z-ordered `UiQuad` stream). +cosmic-text = "0.16" +# The font database `cosmic-text` 0.16 uses (its `Source`/`ID`/`FaceInfo` appear in `ui_text.rs`'s +# public-in-crate types). Pinned to the exact version `cosmic-text` 0.16 depends on so there's one +# `fontdb` in the graph. +fontdb = "0.23" +kira = { version = "0.12", default-features = false, features = ["cpal", "mp3", "wav", "pcm", "adpcm"] } +mint = "0.5" +# The gossip greeting is a weighted random DRAW per menu-open, not a fixed pick (wow-re +# `gossip-npctext-law.md` — the reference re-rolls in its gossip handler), so the app needs a uniform +# source. Pinned to 0.8: the version `benilla-srp` already depends on, so this adds no crate. +rand = "0.8" + +# Linux/BSD: the Wayland clipboard (decision 0702). We build with bevy's `wayland` feature, so on a +# Wayland session winit gives us a native Wayland window and `arboard` — X11-only without its +# non-default `wayland-data-control` feature — finds no X11 CLIPBOARD owner and every paste fails. +# smithay-clipboard is the right crate for a windowed app: it drives the *standard* `wl_data_device` +# off our own `wl_display`/seat, which every compositor supports. arboard's own Wayland feature would +# not do: it speaks `wlr-data-control`, a privileged manager protocol GNOME/Mutter deliberately +# doesn't implement. `raw-window-handle` (pinned to the 0.6 already in the tree via bevy_window, so +# this adds no crate) is only needed to read the `wl_display` out of bevy's `RawHandleWrapper`. +[target.'cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))'.dependencies] +raw-window-handle = "0.6" +smithay-clipboard = "0.7" + +# macOS: drive a native NSCursor for a crisp hardware cursor. winit drives the cursor via AppKit +# cursor-rects, which our continuous Metal render loop doesn't honor on mouse-move (custom cursor / +# hide revert to the arrow); we bypass it with NSCursor + NSWindow::disableCursorRects. See +# docs/knowledge/camera-control.md. +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.5" +objc2-app-kit = { version = "0.2", features = [ + "NSApplication", + "NSResponder", + # NSApplicationActivationPolicy (bgwin's Accessory demotion) is declared in this header. + "NSRunningApplication", + "NSWindow", + "NSView", + "NSImage", + "NSCursor", + "NSEvent", +] } +objc2-foundation = { version = "0.2", features = ["NSArray", "NSData", "NSGeometry"] } diff --git a/crates/benilla/assets/shaders/celestial.wgsl b/crates/benilla-app/assets/shaders/celestial.wgsl similarity index 100% rename from crates/benilla/assets/shaders/celestial.wgsl rename to crates/benilla-app/assets/shaders/celestial.wgsl diff --git a/crates/benilla/assets/shaders/cloud.wgsl b/crates/benilla-app/assets/shaders/cloud.wgsl similarity index 100% rename from crates/benilla/assets/shaders/cloud.wgsl rename to crates/benilla-app/assets/shaders/cloud.wgsl diff --git a/crates/benilla/assets/shaders/ffx_glow.wgsl b/crates/benilla-app/assets/shaders/ffx_glow.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ffx_glow.wgsl rename to crates/benilla-app/assets/shaders/ffx_glow.wgsl diff --git a/crates/benilla/assets/shaders/liquid.wgsl b/crates/benilla-app/assets/shaders/liquid.wgsl similarity index 100% rename from crates/benilla/assets/shaders/liquid.wgsl rename to crates/benilla-app/assets/shaders/liquid.wgsl diff --git a/crates/benilla/assets/shaders/sky.wgsl b/crates/benilla-app/assets/shaders/sky.wgsl similarity index 100% rename from crates/benilla/assets/shaders/sky.wgsl rename to crates/benilla-app/assets/shaders/sky.wgsl diff --git a/crates/benilla/assets/shaders/star.wgsl b/crates/benilla-app/assets/shaders/star.wgsl similarity index 100% rename from crates/benilla/assets/shaders/star.wgsl rename to crates/benilla-app/assets/shaders/star.wgsl diff --git a/crates/benilla/assets/shaders/terrain.wgsl b/crates/benilla-app/assets/shaders/terrain.wgsl similarity index 100% rename from crates/benilla/assets/shaders/terrain.wgsl rename to crates/benilla-app/assets/shaders/terrain.wgsl diff --git a/crates/benilla/assets/shaders/ui_add.wgsl b/crates/benilla-app/assets/shaders/ui_add.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ui_add.wgsl rename to crates/benilla-app/assets/shaders/ui_add.wgsl diff --git a/crates/benilla/assets/shaders/ui_gamma.wgsl b/crates/benilla-app/assets/shaders/ui_gamma.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ui_gamma.wgsl rename to crates/benilla-app/assets/shaders/ui_gamma.wgsl diff --git a/crates/benilla/assets/shaders/ui_node_gamma.wgsl b/crates/benilla-app/assets/shaders/ui_node_gamma.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ui_node_gamma.wgsl rename to crates/benilla-app/assets/shaders/ui_node_gamma.wgsl diff --git a/crates/benilla/assets/shaders/ui_quad.wgsl b/crates/benilla-app/assets/shaders/ui_quad.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ui_quad.wgsl rename to crates/benilla-app/assets/shaders/ui_quad.wgsl diff --git a/crates/benilla/assets/shaders/ui_slice_gamma.wgsl b/crates/benilla-app/assets/shaders/ui_slice_gamma.wgsl similarity index 100% rename from crates/benilla/assets/shaders/ui_slice_gamma.wgsl rename to crates/benilla-app/assets/shaders/ui_slice_gamma.wgsl diff --git a/crates/benilla/assets/shaders/wdl.wgsl b/crates/benilla-app/assets/shaders/wdl.wgsl similarity index 100% rename from crates/benilla/assets/shaders/wdl.wgsl rename to crates/benilla-app/assets/shaders/wdl.wgsl diff --git a/crates/benilla/assets/shaders/wmo_skybox.wgsl b/crates/benilla-app/assets/shaders/wmo_skybox.wgsl similarity index 100% rename from crates/benilla/assets/shaders/wmo_skybox.wgsl rename to crates/benilla-app/assets/shaders/wmo_skybox.wgsl diff --git a/crates/benilla/assets/shaders/wow_effect.wgsl b/crates/benilla-app/assets/shaders/wow_effect.wgsl similarity index 100% rename from crates/benilla/assets/shaders/wow_effect.wgsl rename to crates/benilla-app/assets/shaders/wow_effect.wgsl diff --git a/crates/benilla/assets/shaders/wow_model.wgsl b/crates/benilla-app/assets/shaders/wow_model.wgsl similarity index 100% rename from crates/benilla/assets/shaders/wow_model.wgsl rename to crates/benilla-app/assets/shaders/wow_model.wgsl diff --git a/crates/benilla/assets/ui/ActionBar.xml b/crates/benilla-app/assets/ui/ActionBar.xml similarity index 100% rename from crates/benilla/assets/ui/ActionBar.xml rename to crates/benilla-app/assets/ui/ActionBar.xml diff --git a/crates/benilla/assets/ui/BagFrame.xml b/crates/benilla-app/assets/ui/BagFrame.xml similarity index 100% rename from crates/benilla/assets/ui/BagFrame.xml rename to crates/benilla-app/assets/ui/BagFrame.xml diff --git a/crates/benilla/assets/ui/BankFrame.xml b/crates/benilla-app/assets/ui/BankFrame.xml similarity index 100% rename from crates/benilla/assets/ui/BankFrame.xml rename to crates/benilla-app/assets/ui/BankFrame.xml diff --git a/crates/benilla/assets/ui/BuffFrame.xml b/crates/benilla-app/assets/ui/BuffFrame.xml similarity index 100% rename from crates/benilla/assets/ui/BuffFrame.xml rename to crates/benilla-app/assets/ui/BuffFrame.xml diff --git a/crates/benilla/assets/ui/CastingBar.xml b/crates/benilla-app/assets/ui/CastingBar.xml similarity index 100% rename from crates/benilla/assets/ui/CastingBar.xml rename to crates/benilla-app/assets/ui/CastingBar.xml diff --git a/crates/benilla/assets/ui/CharacterFrame.xml b/crates/benilla-app/assets/ui/CharacterFrame.xml similarity index 100% rename from crates/benilla/assets/ui/CharacterFrame.xml rename to crates/benilla-app/assets/ui/CharacterFrame.xml diff --git a/crates/benilla/assets/ui/ChatFrame.xml b/crates/benilla-app/assets/ui/ChatFrame.xml similarity index 97% rename from crates/benilla/assets/ui/ChatFrame.xml rename to crates/benilla-app/assets/ui/ChatFrame.xml index fa11186e..3e517bd2 100644 --- a/crates/benilla/assets/ui/ChatFrame.xml +++ b/crates/benilla-app/assets/ui/ChatFrame.xml @@ -542,6 +542,23 @@ + + + this:RegisterEvent("EXECUTE_CHAT_LINE") + + + if event == "EXECUTE_CHAT_LINE" then SubmitChatInput(arg1) end + BenillaFCF_OnUpdate(arg1) BenillaChatFrame_OnUpdate(this, arg1) diff --git a/crates/benilla/assets/ui/CombatText.xml b/crates/benilla-app/assets/ui/CombatText.xml similarity index 100% rename from crates/benilla/assets/ui/CombatText.xml rename to crates/benilla-app/assets/ui/CombatText.xml diff --git a/crates/benilla/assets/ui/ComboFrame.xml b/crates/benilla-app/assets/ui/ComboFrame.xml similarity index 100% rename from crates/benilla/assets/ui/ComboFrame.xml rename to crates/benilla-app/assets/ui/ComboFrame.xml diff --git a/crates/benilla/assets/ui/Cooldown.xml b/crates/benilla-app/assets/ui/Cooldown.xml similarity index 100% rename from crates/benilla/assets/ui/Cooldown.xml rename to crates/benilla-app/assets/ui/Cooldown.xml diff --git a/crates/benilla/assets/ui/CraftFrame.xml b/crates/benilla-app/assets/ui/CraftFrame.xml similarity index 100% rename from crates/benilla/assets/ui/CraftFrame.xml rename to crates/benilla-app/assets/ui/CraftFrame.xml diff --git a/crates/benilla/assets/ui/DeathFrame.xml b/crates/benilla-app/assets/ui/DeathFrame.xml similarity index 100% rename from crates/benilla/assets/ui/DeathFrame.xml rename to crates/benilla-app/assets/ui/DeathFrame.xml diff --git a/crates/benilla/assets/ui/DuelFrame.xml b/crates/benilla-app/assets/ui/DuelFrame.xml similarity index 100% rename from crates/benilla/assets/ui/DuelFrame.xml rename to crates/benilla-app/assets/ui/DuelFrame.xml diff --git a/crates/benilla/assets/ui/DurabilityFrame.xml b/crates/benilla-app/assets/ui/DurabilityFrame.xml similarity index 100% rename from crates/benilla/assets/ui/DurabilityFrame.xml rename to crates/benilla-app/assets/ui/DurabilityFrame.xml diff --git a/crates/benilla/assets/ui/EnchantConfirm.xml b/crates/benilla-app/assets/ui/EnchantConfirm.xml similarity index 100% rename from crates/benilla/assets/ui/EnchantConfirm.xml rename to crates/benilla-app/assets/ui/EnchantConfirm.xml diff --git a/crates/benilla/assets/ui/ErrorsFrame.xml b/crates/benilla-app/assets/ui/ErrorsFrame.xml similarity index 100% rename from crates/benilla/assets/ui/ErrorsFrame.xml rename to crates/benilla-app/assets/ui/ErrorsFrame.xml diff --git a/crates/benilla/assets/ui/Fonts.xml b/crates/benilla-app/assets/ui/Fonts.xml similarity index 100% rename from crates/benilla/assets/ui/Fonts.xml rename to crates/benilla-app/assets/ui/Fonts.xml diff --git a/crates/benilla/assets/ui/FriendsFrame.xml b/crates/benilla-app/assets/ui/FriendsFrame.xml similarity index 100% rename from crates/benilla/assets/ui/FriendsFrame.xml rename to crates/benilla-app/assets/ui/FriendsFrame.xml diff --git a/crates/benilla/assets/ui/GameMenuFrame.xml b/crates/benilla-app/assets/ui/GameMenuFrame.xml similarity index 100% rename from crates/benilla/assets/ui/GameMenuFrame.xml rename to crates/benilla-app/assets/ui/GameMenuFrame.xml diff --git a/crates/benilla/assets/ui/GameTooltip.xml b/crates/benilla-app/assets/ui/GameTooltip.xml similarity index 100% rename from crates/benilla/assets/ui/GameTooltip.xml rename to crates/benilla-app/assets/ui/GameTooltip.xml diff --git a/crates/benilla/assets/ui/GossipFrame.xml b/crates/benilla-app/assets/ui/GossipFrame.xml similarity index 100% rename from crates/benilla/assets/ui/GossipFrame.xml rename to crates/benilla-app/assets/ui/GossipFrame.xml diff --git a/crates/benilla/assets/ui/GroupLootFrame.xml b/crates/benilla-app/assets/ui/GroupLootFrame.xml similarity index 100% rename from crates/benilla/assets/ui/GroupLootFrame.xml rename to crates/benilla-app/assets/ui/GroupLootFrame.xml diff --git a/crates/benilla/assets/ui/InspectFrame.xml b/crates/benilla-app/assets/ui/InspectFrame.xml similarity index 100% rename from crates/benilla/assets/ui/InspectFrame.xml rename to crates/benilla-app/assets/ui/InspectFrame.xml diff --git a/crates/benilla/assets/ui/ItemRef.xml b/crates/benilla-app/assets/ui/ItemRef.xml similarity index 100% rename from crates/benilla/assets/ui/ItemRef.xml rename to crates/benilla-app/assets/ui/ItemRef.xml diff --git a/crates/benilla/assets/ui/ItemTextFrame.xml b/crates/benilla-app/assets/ui/ItemTextFrame.xml similarity index 100% rename from crates/benilla/assets/ui/ItemTextFrame.xml rename to crates/benilla-app/assets/ui/ItemTextFrame.xml diff --git a/crates/benilla/assets/ui/LootFrame.xml b/crates/benilla-app/assets/ui/LootFrame.xml similarity index 100% rename from crates/benilla/assets/ui/LootFrame.xml rename to crates/benilla-app/assets/ui/LootFrame.xml diff --git a/crates/benilla/assets/ui/MacroFrame.xml b/crates/benilla-app/assets/ui/MacroFrame.xml similarity index 96% rename from crates/benilla/assets/ui/MacroFrame.xml rename to crates/benilla-app/assets/ui/MacroFrame.xml index c9b21f37..4942ff5c 100644 --- a/crates/benilla/assets/ui/MacroFrame.xml +++ b/crates/benilla-app/assets/ui/MacroFrame.xml @@ -35,10 +35,13 @@ (`BenillaMacroFrameText_Resize`), over-estimating wrapped lines rather than clipping. - `MacroFrameTextButton` (the ref's invisible click-to-focus plate over the body) is kept — this engine's EditBox does not self-focus on a click through a ScrollFrame. - - The ref's `PanelTemplates_TabResize(-15, ...)` width tuning and its manual - `HighlightTexture:SetWidth(GetTextWidth() + 31)` are dropped: `TabButtonTemplate` - (UiPanels.xml) already does its own text-fit on load, and re-doing it here would fight - it (the same call the trainer/talent tabs already omit). + - The ref's `PanelTemplates_TabResize(-15, ...)` width tuning is KEPT, but expressed as + two fields on the tab (`benillaTabPadding`/`benillaTabMaxWidth`) rather than as a call: + `TabButtonTemplate` (UiPanels.xml) owns the one text-fit call site, and an instance + calling TabResize itself would just be overwritten on the next settle. Dropping the + tuning (0983's original call) is what let the character tab run off the window's right + edge — 384 units does not hold two +0-fit tabs. The ref's manual + `HighlightTexture:SetWidth(GetTextWidth() + 31)` stays folded into that same settle. - `PlaySound("igCharacterInfoOpen"/"Close"/"gsTitleOptionOK")` are kept — this engine has PlaySound and those kit names resolve. --> @@ -704,6 +707,10 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BenillaPetFrame_OnLoad(this) + BenillaPetFrame_OnEvent(this) + BenillaPetFrame_OnUpdate(this, arg1) + BenillaPetFrame_OnEnter(this) + GameTooltip:FadeOut() + BenillaPetFrame_OnClick(arg1) + + diff --git a/crates/benilla/assets/ui/UnitPopup.xml b/crates/benilla-app/assets/ui/UnitPopup.xml similarity index 100% rename from crates/benilla/assets/ui/UnitPopup.xml rename to crates/benilla-app/assets/ui/UnitPopup.xml diff --git a/crates/benilla/assets/ui/WorldMapFrame.xml b/crates/benilla-app/assets/ui/WorldMapFrame.xml similarity index 100% rename from crates/benilla/assets/ui/WorldMapFrame.xml rename to crates/benilla-app/assets/ui/WorldMapFrame.xml diff --git a/crates/benilla/assets/ui/ZoneText.xml b/crates/benilla-app/assets/ui/ZoneText.xml similarity index 100% rename from crates/benilla/assets/ui/ZoneText.xml rename to crates/benilla-app/assets/ui/ZoneText.xml diff --git a/crates/benilla/src/area.rs b/crates/benilla-app/src/area.rs similarity index 100% rename from crates/benilla/src/area.rs rename to crates/benilla-app/src/area.rs diff --git a/crates/benilla/src/area_trigger.rs b/crates/benilla-app/src/area_trigger.rs similarity index 100% rename from crates/benilla/src/area_trigger.rs rename to crates/benilla-app/src/area_trigger.rs diff --git a/crates/benilla/src/art_scope.rs b/crates/benilla-app/src/art_scope.rs similarity index 100% rename from crates/benilla/src/art_scope.rs rename to crates/benilla-app/src/art_scope.rs diff --git a/crates/benilla/src/asset_churn.rs b/crates/benilla-app/src/asset_churn.rs similarity index 100% rename from crates/benilla/src/asset_churn.rs rename to crates/benilla-app/src/asset_churn.rs diff --git a/crates/benilla/src/assets/images.rs b/crates/benilla-app/src/assets/images.rs similarity index 100% rename from crates/benilla/src/assets/images.rs rename to crates/benilla-app/src/assets/images.rs diff --git a/crates/benilla/src/assets/mod.rs b/crates/benilla-app/src/assets/mod.rs similarity index 100% rename from crates/benilla/src/assets/mod.rs rename to crates/benilla-app/src/assets/mod.rs diff --git a/crates/benilla/src/aura_visual.rs b/crates/benilla-app/src/aura_visual.rs similarity index 100% rename from crates/benilla/src/aura_visual.rs rename to crates/benilla-app/src/aura_visual.rs diff --git a/crates/benilla/src/aura_visual/tests.rs b/crates/benilla-app/src/aura_visual/tests.rs similarity index 100% rename from crates/benilla/src/aura_visual/tests.rs rename to crates/benilla-app/src/aura_visual/tests.rs diff --git a/crates/benilla/src/bgwin.rs b/crates/benilla-app/src/bgwin.rs similarity index 100% rename from crates/benilla/src/bgwin.rs rename to crates/benilla-app/src/bgwin.rs diff --git a/crates/benilla/src/billboard.rs b/crates/benilla-app/src/billboard.rs similarity index 100% rename from crates/benilla/src/billboard.rs rename to crates/benilla-app/src/billboard.rs diff --git a/crates/benilla/src/blob_shadow.rs b/crates/benilla-app/src/blob_shadow.rs similarity index 100% rename from crates/benilla/src/blob_shadow.rs rename to crates/benilla-app/src/blob_shadow.rs diff --git a/crates/benilla/src/bowstring.rs b/crates/benilla-app/src/bowstring.rs similarity index 100% rename from crates/benilla/src/bowstring.rs rename to crates/benilla-app/src/bowstring.rs diff --git a/crates/benilla-app/src/build_id.rs b/crates/benilla-app/src/build_id.rs new file mode 100644 index 00000000..90928ca9 --- /dev/null +++ b/crates/benilla-app/src/build_id.rs @@ -0,0 +1,60 @@ +//! **Which build is this?** — the commit the running binary was built from, stamped in at +//! compile time by the `benilla` launcher shim's `build.rs` (which owns the *how*, including why +//! the stamp can't go stale — and why it is stamped into the ~30-line shim rather than this +//! crate: cargo re-dirties whatever package carries the stamp on every commit, decision 0993) +//! and handed to [`crate::run`] as the [`BuildId`] resource. +//! +//! A report from someone else's machine — "the water reads wrong here", "it panicked on login" — +//! is only actionable against known code, and nothing else in the binary identifies it: the crate +//! version is a permanent `0.1.0`, and people build from a clone of the public snapshot repo whose +//! HEAD moves with every sync. So the sha is the version. +//! +//! Two surfaces, deliberately: +//! +//! - **The startup log line** ([`banner`], run from [`crate::preflight`]'s banner family) — this is +//! the one that matters for someone else's run. They launch from a terminal, so the id is already +//! in the output they paste; nobody has to know a hotkey to answer "what are you on?". +//! - **The debug panel's footer** (`` ` ``) — the same line where the reader already is when they +//! are looking at a readout, click-to-copy for the full sha. +//! +//! Mapping a reported sha back: a **public** snapshot sha is tagged `pub/` on the private +//! commit it shipped, so `git show pub/` (or `git rev-parse pub/^{commit}`) names the code +//! exactly; a **private** sha is just a commit here. + +use bevy::prelude::*; + +/// The launcher shim's compile-time git stamp, inserted as a resource by [`crate::run`]. The git +/// fields are empty when the build had no checkout to read (a downloaded source zip, or no `git` +/// on `PATH`), which [`BuildId::summary`] reports as an unknown build. +#[derive(Resource, Clone, Copy)] +pub struct BuildId { + /// The full 40-char sha of the commit this binary was built from (what the panel copies). + pub sha: &'static str, + /// Git's own abbreviation of [`sha`](Self::sha), from the same repo `pubsync` abbreviates in — + /// so a public build's string is literally the suffix of its `pub/` tag. + pub short: &'static str, + /// The commit date of [`sha`](Self::sha) (`YYYY-MM-DD`) — a property of the sha, so it can + /// never disagree with it. + pub date: &'static str, + /// The cargo profile directory this was built in: `debug`, `release`, or `ship` (0736). Half + /// of every "it runs badly" report is a debug build. + pub profile: &'static str, +} + +impl BuildId { + /// The one-line build id: `f5fd009 · 2026-07-30 · release`. + pub(crate) fn summary(&self) -> String { + if self.short.is_empty() { + format!("unknown (built without a git checkout) · {}", self.profile) + } else { + format!("{} · {} · {}", self.short, self.date, self.profile) + } + } +} + +/// Log the build id once at startup — the line that answers "what version are you on?" from a +/// pasted terminal log. Not env-gated, for [`crate::preflight`]'s reason: an id nobody knows to +/// switch on is not an id. +pub(crate) fn banner(build: Res) { + info!("benilla build {}", build.summary()); +} diff --git a/crates/benilla/src/capture/depth_probe.rs b/crates/benilla-app/src/capture/depth_probe.rs similarity index 100% rename from crates/benilla/src/capture/depth_probe.rs rename to crates/benilla-app/src/capture/depth_probe.rs diff --git a/crates/benilla/src/capture/fixtures.rs b/crates/benilla-app/src/capture/fixtures.rs similarity index 95% rename from crates/benilla/src/capture/fixtures.rs rename to crates/benilla-app/src/capture/fixtures.rs index 1ca4cbb3..f9d8c08f 100644 --- a/crates/benilla/src/capture/fixtures.rs +++ b/crates/benilla-app/src/capture/fixtures.rs @@ -958,6 +958,21 @@ pub(super) fn seed_ui_fixture( warn!("capture: ui-options-graphics seed failed: {e}"); } } + UiFixture::OptionsWorldDetail => { + let Some(mut script) = script else { + return; + }; + // The dropdown list open (0992), same posture as the page fixtures: real CVar set, + // the live open-select-toggle path. The list's width settles from its OnUpdate a + // frame later (the kit's WIDTH SETTLE law) — inside the capture's settle frames. + script.register_cvars(crate::cvars::REGISTERED.iter().copied()); + if let Err(e) = script.run( + "ShowUIPanel(OptionsFrame); OptionsFrameCategoryListRowGraphics:Click(); \ + OptionsFrameContainerBodyGraphicsRowWorldDetailDropdownButton:Click()", + ) { + warn!("capture: ui-options-worlddetail seed failed: {e}"); + } + } UiFixture::OptionsSearch => { let Some(mut script) = script else { return; @@ -1032,6 +1047,46 @@ pub(super) fn seed_ui_fixture( warn!("capture: ui-spellbook seed failed to open the book: {e}"); } } + UiFixture::Macro | UiFixture::MacroPopup => { + let Some(script) = script.as_mut() else { + return; + }; + // Made through the LIVE `CreateMacro` path, not by pushing a table in — so the capture + // exercises engine table → `UPDATE_MACROS` → window exactly as a player's own edit + // does. Icon indices are into the real `SpellIcon.dbc` catalog `ui_macro` loads at + // PostStartup, so they resolve to real art. The bodies are the shapes that matter: a + // plain `/cast`, a MULTI-LINE macro (what the body box has to lay out), and a + // `/script` line (the non-`/cast` case the bar's bound-spell resolve must decline). + const SEED: [(&str, u32, &str); 4] = [ + ("Charge", 1, "/cast Charge"), + ( + "Pull", + 9, + "/cast Charge\n/say Incoming!\n/script CastSpellByName(\"Battle Shout\")", + ), + ("Shout", 17, "/cast Battle Shout"), + ("Sit", 25, "/sit"), + ]; + let mut seed = String::new(); + for (name, icon, body) in SEED { + seed.push_str(&format!( + "CreateMacro(\"{name}\", {icon}, \"{}\", 1, nil)\n", + // Lua-escape the body: a `/script` line carries its own quotes, and a macro + // body's newlines have to survive as newlines. + body.replace('"', "\\\"").replace('\n', "\\n") + )); + } + // Slot 2 selected: the multi-line body, so the detail pane and the body box both have + // something in them. The popup fixture then opens the chooser over that selection via + // the ref's `MacroEditButton` path — an EDIT, so the name box arrives pre-filled. + seed.push_str("ShowMacroFrame()\nBenillaMacroButton2:Click()\n"); + if fixture == UiFixture::MacroPopup { + seed.push_str("BenillaMacroEditButton:Click()\n"); + } + if let Err(e) = script.run(&seed) { + warn!("capture: ui-macro seed failed: {e}"); + } + } UiFixture::ChatEdit => { let Some(script) = script.as_mut() else { return; diff --git a/crates/benilla/src/capture/live_shot.rs b/crates/benilla-app/src/capture/live_shot.rs similarity index 100% rename from crates/benilla/src/capture/live_shot.rs rename to crates/benilla-app/src/capture/live_shot.rs diff --git a/crates/benilla/src/capture/mod.rs b/crates/benilla-app/src/capture/mod.rs similarity index 98% rename from crates/benilla/src/capture/mod.rs rename to crates/benilla-app/src/capture/mod.rs index 206e998a..0ca2f2a9 100644 --- a/crates/benilla/src/capture/mod.rs +++ b/crates/benilla-app/src/capture/mod.rs @@ -42,14 +42,14 @@ //! WOW_DATA= WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-unitframes \ //! WOW_CAPTURE_OUT=/tmp/shot.png cargo run -q -p benilla //! ``` -//! `cargo run` exports `CARGO_MANIFEST_DIR`, which is how Bevy's `AssetServer` finds -//! `crates/benilla/assets/` — in particular the WGSL shaders (`ui_quad.wgsl`, `sky.wgsl`, …). -//! Launching `target/debug/benilla` directly has no manifest dir, so it resolves `assets/` next -//! to the executable (`target/debug/assets/`, which doesn't exist), silently loads **no** shaders, -//! and every custom-shader layer — the *entire* player UI, sky, liquid, models — renders blank -//! while only the built-in-pipeline terrain still draws. A UI capture that comes back as bare sky -//! is this mistake, not a UI bug. `WOW_CAPTURE_UI=1` opts the player UI into the shot (off by -//! default so world baselines stay UI-free; omit it for world-only scenes). `WOW_CAPTURE=list` +//! `cargo run` rebuilds first; a bare `target/debug/benilla` can silently be **stale code** — the +//! classic way a capture "disproves" a fix that was never in the binary. (Before 0993 it was +//! worse: `assets/` resolved through Cargo's runtime `CARGO_MANIFEST_DIR`, so a bare run silently +//! loaded **no** WGSL shaders and every custom-shader layer — the *entire* player UI, sky, liquid, +//! models — rendered blank, read as a UI bug for hours once. The app now bakes the absolute +//! `crates/benilla-app/assets/` path in at compile time, so a bare run finds its shaders; +//! staleness is the trap that remains.) `WOW_CAPTURE_UI=1` opts the player UI into the shot (off +//! by default so world baselines stay UI-free; omit it for world-only scenes). `WOW_CAPTURE=list` //! prints the scenario names. `scripts/visual.sh` wraps all of this. use std::path::Path; diff --git a/crates/benilla/src/capture/phase_probe.rs b/crates/benilla-app/src/capture/phase_probe.rs similarity index 100% rename from crates/benilla/src/capture/phase_probe.rs rename to crates/benilla-app/src/capture/phase_probe.rs diff --git a/crates/benilla/src/capture/pick_probe.rs b/crates/benilla-app/src/capture/pick_probe.rs similarity index 100% rename from crates/benilla/src/capture/pick_probe.rs rename to crates/benilla-app/src/capture/pick_probe.rs diff --git a/crates/benilla/src/capture/probe_bank.rs b/crates/benilla-app/src/capture/probe_bank.rs similarity index 100% rename from crates/benilla/src/capture/probe_bank.rs rename to crates/benilla-app/src/capture/probe_bank.rs diff --git a/crates/benilla/src/capture/probe_castcancel.rs b/crates/benilla-app/src/capture/probe_castcancel.rs similarity index 100% rename from crates/benilla/src/capture/probe_castcancel.rs rename to crates/benilla-app/src/capture/probe_castcancel.rs diff --git a/crates/benilla/src/capture/probe_charcreate.rs b/crates/benilla-app/src/capture/probe_charcreate.rs similarity index 100% rename from crates/benilla/src/capture/probe_charcreate.rs rename to crates/benilla-app/src/capture/probe_charcreate.rs diff --git a/crates/benilla/src/capture/probe_crossing.rs b/crates/benilla-app/src/capture/probe_crossing.rs similarity index 100% rename from crates/benilla/src/capture/probe_crossing.rs rename to crates/benilla-app/src/capture/probe_crossing.rs diff --git a/crates/benilla/src/capture/probe_mail.rs b/crates/benilla-app/src/capture/probe_mail.rs similarity index 100% rename from crates/benilla/src/capture/probe_mail.rs rename to crates/benilla-app/src/capture/probe_mail.rs diff --git a/crates/benilla/src/capture/probe_melee.rs b/crates/benilla-app/src/capture/probe_melee.rs similarity index 100% rename from crates/benilla/src/capture/probe_melee.rs rename to crates/benilla-app/src/capture/probe_melee.rs diff --git a/crates/benilla/src/capture/probe_partner.rs b/crates/benilla-app/src/capture/probe_partner.rs similarity index 100% rename from crates/benilla/src/capture/probe_partner.rs rename to crates/benilla-app/src/capture/probe_partner.rs diff --git a/crates/benilla/src/capture/probe_rig.rs b/crates/benilla-app/src/capture/probe_rig.rs similarity index 100% rename from crates/benilla/src/capture/probe_rig.rs rename to crates/benilla-app/src/capture/probe_rig.rs diff --git a/crates/benilla/src/capture/probe_taxi.rs b/crates/benilla-app/src/capture/probe_taxi.rs similarity index 100% rename from crates/benilla/src/capture/probe_taxi.rs rename to crates/benilla-app/src/capture/probe_taxi.rs diff --git a/crates/benilla/src/capture/probes.rs b/crates/benilla-app/src/capture/probes.rs similarity index 100% rename from crates/benilla/src/capture/probes.rs rename to crates/benilla-app/src/capture/probes.rs diff --git a/crates/benilla/src/capture/scenarios.rs b/crates/benilla-app/src/capture/scenarios.rs similarity index 93% rename from crates/benilla/src/capture/scenarios.rs rename to crates/benilla-app/src/capture/scenarios.rs index 1d40ff81..79f0c581 100644 --- a/crates/benilla/src/capture/scenarios.rs +++ b/crates/benilla-app/src/capture/scenarios.rs @@ -82,6 +82,17 @@ pub(super) enum UiFixture { /// the book feed → Lua → render): the panel plates, the 12-slot page with name/rank text, /// passive graying, the skill-line tab strip, and the page footer. SpellBook, + /// The macro window (decision 0983) over a seeded macro set, second slot selected — the look + /// instrument the window shipped WITHOUT, which is why "CREATE_MACROS" across its title bar + /// reached the director's screen instead of a capture (0991). Pins the two-tab row inside the + /// 384-wide plate, the 18-slot grid, the selected-macro detail pane, the body box, and the + /// bottom button row. Macros are made through the live `CreateMacro` path, so the fixture + /// exercises the real engine table → `UPDATE_MACROS` → window chain. + Macro, + /// The macro window's NAME/ICON POPUP open over the same set (0991) — the other half of the + /// window, and the denser one: the 5×4 icon grid off the real `SpellIcon.dbc` catalog, its + /// faux scroll bar, the name box, and the Okay/Cancel row. + MacroPopup, /// The chat window with the edit box OPEN over a say/yell line mix (decision 0288 P5's look /// instrument, added for the centered-"Say:"/invisible-typing regression): the box focused /// with a typed draft through the live open path (`focus_editbox` + `chat_edit_live`), so the @@ -94,17 +105,21 @@ pub(super) enum UiFixture { /// so its pixels move only when the window or the atlas seam does. Options, /// The Options window ON THE AUDIO PAGE (decision 0957) — the setting-row look instrument: - /// checkbox art at its era seats, the minimal_sliderbar three-slice + thumb + steppers, - /// child-row indent/small-font, the percent readouts, Defaults armed. Rows read the CVar - /// registration defaults (hermetic capture = no config file), so the pixels move only with - /// the window, the atlas seam, or a registered default. + /// checkbox art at its era seats, the 1.12 slider groove + thumb on the bare full-width bar + /// (steppers cut 0989), child-row indent/small-font, the percent readouts, Defaults armed. + /// Rows read the CVar registration defaults (hermetic capture = no config file), so the + /// pixels move only with the window, the atlas seam, or a registered default. OptionsAudio, - /// The Options window ON THE GRAPHICS PAGE (decision 0959) — the two 1.12 Video Options - /// sliders on the era row seats: uiScale at its 0.64..1.0 panel range with the percent - /// readout, farclip at 177..777 with the raw-yards readout. Rows read the CVar registration - /// defaults (hermetic capture = no config file), so the pixels move only with the window, - /// the atlas seam, or a registered default. + /// The Options window ON THE GRAPHICS PAGE (decision 0959; farclip row retired 0961; + /// Environment Detail joined 0992) — uiScale at its 0.64..1.0 panel range with the percent + /// readout, and the dropdown row's closed capsule (the 1.12 kit art) reading "High". Rows + /// read the CVar registration defaults (hermetic capture = no config file), so the pixels + /// move only with the window, the atlas seam, or a registered default. OptionsGraphics, + /// The Graphics page with the Environment Detail MENU OPEN (0992) — the dropdown-list look + /// instrument: the shared DropDownList1 at the window's effective scale (the kit's uiScale + /// correction), three entries with High checked, the kit's dialog backdrop. + OptionsWorldDetail, /// The Options window MID-SEARCH (decision 0984) — the results-view look instrument: the /// "volume" query reflows the four live volume sliders under the clickable Audio head /// (GameFontNormalLarge), title "Search Results", Defaults hidden, the clear-X shown in @@ -847,6 +862,26 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ minute: 720, ui: Some(UiFixture::SpellBook), }, + // The macro window over a seeded macro set. Run with + // `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-macro`. + Scenario { + name: "ui-macro", + map: MAP_AZEROTH, + eye: GROUND_EYE, + look: GROUND_LOOK, + minute: 720, + ui: Some(UiFixture::Macro), + }, + // The same window with the name/icon popup open. Run with + // `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-macro-popup`. + Scenario { + name: "ui-macro-popup", + map: MAP_AZEROTH, + eye: GROUND_EYE, + look: GROUND_LOOK, + minute: 720, + ui: Some(UiFixture::MacroPopup), + }, // The chat edit box open with a typed draft over seeded lines. Run with // `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-chatedit`. Scenario { @@ -887,6 +922,16 @@ pub(super) const ON_DEMAND: &[Scenario] = &[ minute: 720, ui: Some(UiFixture::OptionsGraphics), }, + // The Graphics page with the Environment Detail dropdown OPEN (0992). Run with + // `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-worlddetail`. + Scenario { + name: "ui-options-worlddetail", + map: MAP_AZEROTH, + eye: GROUND_EYE, + look: GROUND_LOOK, + minute: 720, + ui: Some(UiFixture::OptionsWorldDetail), + }, // The same window MID-SEARCH (0984): the "volume" results view. Run with // `WOW_CAPTURE_UI=1 WOW_CAPTURE=ui-options-search`. Scenario { diff --git a/crates/benilla/src/char_create/mod.rs b/crates/benilla-app/src/char_create/mod.rs similarity index 100% rename from crates/benilla/src/char_create/mod.rs rename to crates/benilla-app/src/char_create/mod.rs diff --git a/crates/benilla/src/char_create/panels.rs b/crates/benilla-app/src/char_create/panels.rs similarity index 100% rename from crates/benilla/src/char_create/panels.rs rename to crates/benilla-app/src/char_create/panels.rs diff --git a/crates/benilla/src/char_create/parts.rs b/crates/benilla-app/src/char_create/parts.rs similarity index 100% rename from crates/benilla/src/char_create/parts.rs rename to crates/benilla-app/src/char_create/parts.rs diff --git a/crates/benilla/src/char_create/refresh.rs b/crates/benilla-app/src/char_create/refresh.rs similarity index 100% rename from crates/benilla/src/char_create/refresh.rs rename to crates/benilla-app/src/char_create/refresh.rs diff --git a/crates/benilla/src/char_create/screen.rs b/crates/benilla-app/src/char_create/screen.rs similarity index 100% rename from crates/benilla/src/char_create/screen.rs rename to crates/benilla-app/src/char_create/screen.rs diff --git a/crates/benilla/src/char_select/dialog.rs b/crates/benilla-app/src/char_select/dialog.rs similarity index 100% rename from crates/benilla/src/char_select/dialog.rs rename to crates/benilla-app/src/char_select/dialog.rs diff --git a/crates/benilla/src/char_select/input.rs b/crates/benilla-app/src/char_select/input.rs similarity index 100% rename from crates/benilla/src/char_select/input.rs rename to crates/benilla-app/src/char_select/input.rs diff --git a/crates/benilla/src/char_select/mod.rs b/crates/benilla-app/src/char_select/mod.rs similarity index 100% rename from crates/benilla/src/char_select/mod.rs rename to crates/benilla-app/src/char_select/mod.rs diff --git a/crates/benilla/src/char_select/refresh.rs b/crates/benilla-app/src/char_select/refresh.rs similarity index 100% rename from crates/benilla/src/char_select/refresh.rs rename to crates/benilla-app/src/char_select/refresh.rs diff --git a/crates/benilla/src/char_select/screen.rs b/crates/benilla-app/src/char_select/screen.rs similarity index 100% rename from crates/benilla/src/char_select/screen.rs rename to crates/benilla-app/src/char_select/screen.rs diff --git a/crates/benilla/src/chat_bubble.rs b/crates/benilla-app/src/chat_bubble.rs similarity index 100% rename from crates/benilla/src/chat_bubble.rs rename to crates/benilla-app/src/chat_bubble.rs diff --git a/crates/benilla/src/clouds/kernel.rs b/crates/benilla-app/src/clouds/kernel.rs similarity index 100% rename from crates/benilla/src/clouds/kernel.rs rename to crates/benilla-app/src/clouds/kernel.rs diff --git a/crates/benilla/src/clouds/layer.rs b/crates/benilla-app/src/clouds/layer.rs similarity index 100% rename from crates/benilla/src/clouds/layer.rs rename to crates/benilla-app/src/clouds/layer.rs diff --git a/crates/benilla/src/clouds/mod.rs b/crates/benilla-app/src/clouds/mod.rs similarity index 100% rename from crates/benilla/src/clouds/mod.rs rename to crates/benilla-app/src/clouds/mod.rs diff --git a/crates/benilla/src/clouds/tables.rs b/crates/benilla-app/src/clouds/tables.rs similarity index 100% rename from crates/benilla/src/clouds/tables.rs rename to crates/benilla-app/src/clouds/tables.rs diff --git a/crates/benilla/src/clutter.rs b/crates/benilla-app/src/clutter.rs similarity index 96% rename from crates/benilla/src/clutter.rs rename to crates/benilla-app/src/clutter.rs index df67e508..1e967fbc 100644 --- a/crates/benilla/src/clutter.rs +++ b/crates/benilla-app/src/clutter.rs @@ -32,7 +32,11 @@ pub(crate) struct ClutterPlugin; impl Plugin for ClutterPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, setup_clutter.after(AssetSet::Open)) + // The config exists from plugin build, not `setup_clutter`: the CVar loader + // (`crate::cvars::load_config`, also Startup) applies a saved `WorldDetail` onto it, and + // an ordering flip must not silently drop the file's value (0992). + app.init_resource::() + .add_systems(Startup, setup_clutter.after(AssetSet::Open)) .add_systems( Update, ( @@ -74,15 +78,14 @@ fn scope_clutter_geometry( } } -/// Startup: load the ground-effect (clutter) catalog off the shared chain + insert the live config and -/// the per-chunk geometry cache. (Moved out of the terrain streamer's `setup_terrain`.) +/// Startup: load the ground-effect (clutter) catalog off the shared chain + insert the per-chunk +/// geometry cache. (Moved out of the terrain streamer's `setup_terrain`.) fn setup_clutter(mut commands: Commands, world_assets: Option>) { let Some(world_assets) = world_assets else { return; }; // Per-chunk clutter geometry cache, read by the lazy `stream_chunk_clutter` build. commands.insert_resource(ClutterGeometry::default()); - commands.insert_resource(ClutterConfig::default()); let mut chain = world_assets.chain.lock_recover(); // key_by_internal_id = true: the faithful GroundEffectDoodad keying (see load_ground_effect_catalog). match load_ground_effect_catalog(&mut chain, true) { @@ -116,8 +119,10 @@ pub(crate) const DETAIL_DOODAD_ALPHA_REF: f32 = 128.0 / 255.0; /// client's clutter "appears as you walk into it" rather than being drawn to the horizon. pub(crate) const DETAIL_DOODAD_FADE_FAR: f32 = 70.0; -/// Ground-clutter tunables (live-editable in the debug panel → applied by `apply_clutter`): -/// `density` multiplies the per-chunk cell-visit count (the client's `frillDensity`, faithful=16 at ×1); +/// Ground-clutter tunables (read at tile scatter; a `density` change re-scatters LOADED tiles too — +/// `terrain_stream::rescatter_clutter`, the 1.12 setter's own chunk-rebuild law, 0992): +/// `density` multiplies the per-chunk cell-visit count (the client's `frillDensity`, faithful=16 at ×1) +/// — player-settable as the `WorldDetail` CVar (panel 0/1/2 → ×1/×2/×3; the arm in [`crate::cvars`]); /// `scale` resizes each doodad model; `alpha_ref` is the alpha-test cutout threshold /// ([`DETAIL_DOODAD_ALPHA_REF`]); `fade_far` is the clutter draw-distance horizon (yd, /// [`DETAIL_DOODAD_FADE_FAR`]; fade starts at 0.75×). Initial values from `$WOW_CLUTTER_DENSITY` / diff --git a/crates/benilla/src/collision.rs b/crates/benilla-app/src/collision.rs similarity index 100% rename from crates/benilla/src/collision.rs rename to crates/benilla-app/src/collision.rs diff --git a/crates/benilla/src/collision/one_sided.rs b/crates/benilla-app/src/collision/one_sided.rs similarity index 100% rename from crates/benilla/src/collision/one_sided.rs rename to crates/benilla-app/src/collision/one_sided.rs diff --git a/crates/benilla/src/combat_text/law.rs b/crates/benilla-app/src/combat_text/law.rs similarity index 100% rename from crates/benilla/src/combat_text/law.rs rename to crates/benilla-app/src/combat_text/law.rs diff --git a/crates/benilla/src/combat_text/mod.rs b/crates/benilla-app/src/combat_text/mod.rs similarity index 100% rename from crates/benilla/src/combat_text/mod.rs rename to crates/benilla-app/src/combat_text/mod.rs diff --git a/crates/benilla/src/cooldowns.rs b/crates/benilla-app/src/cooldowns.rs similarity index 100% rename from crates/benilla/src/cooldowns.rs rename to crates/benilla-app/src/cooldowns.rs diff --git a/crates/benilla/src/creature_anim.rs b/crates/benilla-app/src/creature_anim.rs similarity index 97% rename from crates/benilla/src/creature_anim.rs rename to crates/benilla-app/src/creature_anim.rs index 7b5d60fa..19fbcc3a 100644 --- a/crates/benilla/src/creature_anim.rs +++ b/crates/benilla-app/src/creature_anim.rs @@ -120,9 +120,11 @@ pub(crate) struct Engaged; /// `[+0xd58] & 0x200`, whose **only writer binary-wide** is the local cast-send tail (`0x6e593b`, /// gated `AttributesEx2 & 0x20`; byte-verified, wow-re `ranged-shot-anim.md`). While it and the /// ranged **sheath** gate (`CUR == 2`) both hold, a standing unit's base idle is the ranged -/// weapon's Load/Hold clip ([`select::ranged_load_anim`]) — the client's resolver `0x5fd460`, +/// weapon's Load clip ([`select::ranged_load_anim`]) — the client's resolver `0x5fd460`, /// which gates on exactly this bit; local-player-only by construction (remote units never run -/// the local cast-send). A REMOTE shooter's drawn idle rides [`RangedHold`] instead. Cleared +/// the local cast-send). A REMOTE shooter has no drawn idle at all: it plays LoadBow **once** at +/// the volley's single `SMSG_SPELL_START` through the PrecastKit ([`CastHold`]), then the fire +/// clip per GO over its ordinary idle (wow-re `shooter-stop-law.md` §J6 claim 2). Cleared /// only by [`cancel_auto_repeat_local`]'s callers — the client's cancel `0x6ea080` (its `0x200` /// clear `0x6ea113`), reached from the cast-result fail of the cached spell, the button /// re-press toggle, melee attack-start, target death, the wand-only new-cast handoff, and @@ -136,15 +138,20 @@ pub(crate) struct AutoRepeatArmed; /// The weapon-visual hold — the client's `[+0xd58] & 0x400`, set for **ANY caster** (self, /// remote players, NPCs) whose **ranged** spell visual plays (`0x60d020`, sole caller `0x6ec2cf` /// inside `PlaySpellVisual`; byte-verified, wow-re `ranged-sheath-exempt-autorepeat.md` §Q4). -/// It is what sustains the drawn Load→Hold idle between a remote shooter's shots — HoldBow -/// self-loops on `0x200 | 0x400`, HoldRifle/HoldThrown on `0x400` alone (`0x5fc3f0`'s -/// continuation tables; ours folds both bits into one gait gate, the asymmetry unmodeled — a -/// bow's first-GO lands well inside the Load pass either way). Cleared like the client's -/// id-matched `0x60d040` + its unconditional sibling `0x60fc50`: a **non-ranged** visual play by -/// the same caster (the stale-visual cleanup `0x6ec39e`), the local cancel (`0x6ea12b`), and -/// melee attack-start. Deliberately NOT cleared on sheath change or volley end — the client's -/// bit persists latent (a stopped remote shooter keeps holding until its next action; the gait -/// gate needs `CUR == 2` anyway). +/// Cleared like the client's id-matched `0x60d040` + its unconditional sibling `0x60fc50`: a +/// **non-ranged** visual play by the same caster (the stale-visual cleanup `0x6ec39e`), the +/// local cancel (`0x6ea12b`), melee attack-start, and the spline-move apply (`0x6020e8`, wow-re +/// `shooter-stop-law.md` §J1.3). Deliberately NOT cleared on sheath change or volley end — the +/// client's bit persists latent. +/// +/// **It gates nothing here, and that is the reference's own shape.** `0x400` is tested in +/// exactly one place image-wide — `0x5fc3f0`'s Hold self-loop gates (`test ah,0x6` for HoldBow +/// 109, `test ah,0x4` for 110/111/112) — and that dispatcher is never reached for a bow id +/// (`shooter-stop-law.md` §J4.1), so no `0x400` test ever executes for a shooter. Reading it as +/// a second *entry* into the drawn idle is what left a shooter aiming forever after one Serpent +/// Sting (decision 0991) and what kept a fabricated Hold twin alive (0994). It is kept modelled, +/// not deleted: §J4.1 is a proof-by-absence carrying an open `LIVE-CAPTURE` item, and this bit is +/// what a correction would need. Nothing may gate on it until that capture lands. #[derive(Component)] pub(crate) struct RangedHold; @@ -589,11 +596,6 @@ pub(crate) struct AnimDriver { /// base-recompute read). Any normal arm clears it (the client's `0x5fe48e` writes −1 on /// every non-fast-path PlayAnimation; the driver clears at its play sites). deferred: Option, - /// The ranged standing idle has completed one pass of its **Load** clip — the idle now - /// plays the drawn HOLD twin ([`select::ranged_hold_anim`]) until a one-shot (the next - /// shot's release) resets the nock. INTERIM (decision 0400 §2's dispatch, Q4 — the - /// `[+0xd58] |= 0x400` hold layer's stand-in). - ranged_held: bool, /// The armed looping arm's **replay window** (decision 0516 — wow-re `loop-replay-fidget` /// §7/§7d): the variation node the loop armed + its rolled budget `R` (the client's /// `block+0xbc`; the window is `R` clip-lengths wide, op4 `0x7126d8` — live for LOOPS too, @@ -721,7 +723,6 @@ impl Default for AnimDriver { jump_arc: false, was_falling: false, deferred: None, - ranged_held: false, loop_window: None, gait_rate: 1.0, mount_display: 0, diff --git a/crates/benilla/src/creature_anim/blood.rs b/crates/benilla-app/src/creature_anim/blood.rs similarity index 100% rename from crates/benilla/src/creature_anim/blood.rs rename to crates/benilla-app/src/creature_anim/blood.rs diff --git a/crates/benilla/src/creature_anim/compose.rs b/crates/benilla-app/src/creature_anim/compose.rs similarity index 100% rename from crates/benilla/src/creature_anim/compose.rs rename to crates/benilla-app/src/creature_anim/compose.rs diff --git a/crates/benilla/src/creature_anim/driver.rs b/crates/benilla-app/src/creature_anim/driver.rs similarity index 96% rename from crates/benilla/src/creature_anim/driver.rs rename to crates/benilla-app/src/creature_anim/driver.rs index 8991130f..2497cd0f 100644 --- a/crates/benilla/src/creature_anim/driver.rs +++ b/crates/benilla-app/src/creature_anim/driver.rs @@ -35,7 +35,7 @@ use super::select::{ use super::sheath::{advance_sheath_ceremony, start_sheath_ceremony}; use super::{ find_resolved, move_flags, AnimData, AnimDriver, AutoRepeatArmed, CastHold, DefenseAnim, - EmoteAnim, Engaged, MovementState, NockLatch, Overlay, OverlayFade, RangedHold, SheathRequest, + EmoteAnim, Engaged, MovementState, NockLatch, Overlay, OverlayFade, SheathRequest, SheathSwapMessage, SwingImpact, SwingMessage, SwingSlowdown, Wielded, WoundAnim, }; @@ -282,7 +282,6 @@ pub(super) fn drive_animations( ( Has, Has, - Has, Has, Option<&crate::entities::mount::MountBody>, Has, @@ -435,16 +434,7 @@ pub(super) fn drive_animations( wielded, cast_hold, facing_step, - ( - engaged, - auto_repeat, - ranged_hold, - is_self, - mount_body, - creature_swimming, - nock_latched, - transform, - ), + (engaged, auto_repeat, is_self, mount_body, creature_swimming, nock_latched, transform), ) in &mut units { // A mount child drives from its HOST's movement view (decision 0441) — same inputs the @@ -1082,7 +1072,6 @@ pub(super) fn drive_animations( engaged, airborne_frozen, auto_repeat, - ranged_hold, cast_hold, wielded, store, @@ -1096,7 +1085,6 @@ pub(super) fn drive_animations( &mut tr, &mut player, &mut rng, - &mut commands, ); // ── The base slot's **playback rate**, written once per frame over whatever the mode @@ -1269,19 +1257,6 @@ pub(super) fn drive_animations( ); } - // A one-shot during the armed ranged idle latches the hold — the byte law (wow-re - // Q-I, decision 0412): mid-volley the client plays the FULL fire clip (whose tail - // is the visible quick re-nock) and returns to the hold; it NEVER replays the Load - // and NEVER rate-scales a bow clip (the `0x5fee80` scaler gate excludes every bow - // id). 0411's RENOCK_RATE replay is refuted and removed. Same gate as the idle's - // own ([`select::ranged_idle_gate`]) so the two cannot drift: a unit that could - // not have entered the family has no hold to return to. - if played_oneshot.is_some() - && select::ranged_idle_gate(auto_repeat, ranged_hold, drv.ranged_held, drv.sheath_cur) - { - drv.ranged_held = true; - } - // ── The per-animation sheath reconcile (decision 0080 structure 3 — the client's // `0x5fdf80`, run after every animation pick). The playing clip's `AnimationData.dbc` // WeaponFlags + engagement force the state, data-driven — swimming/looting/casting stow, @@ -1373,24 +1348,22 @@ pub(super) fn drive_animations( .entity(entity) .remove::<(super::NockedAmmo, super::NockLatch)>(); } - // …and MOVING un-nocks too (the arrow leaves the hand, the string relaxes; the cached - // ammo display survives for the next pull). The nock latch has exactly two authored - // writers — `$BWP` sets, `$BWR` clears — and both live in clips only a STANDING unit - // plays: the Load pull and the fire clip. Nothing in a run cycle can ever clear it, so a - // latch carried into locomotion is permanent: the director's report and screenshot - // (2026-08-05) are a warrior sprinting with the arrow still nocked and the bowstring - // still drawn. The hold-pick re-latch (decision 0409's INTERIM, still in the gait arm) - // guarantees the leak on every volley — it re-latches each time the drawn hold is picked, - // with no writer of its own to undo it. + // …and **TRANSLATING** un-nocks too — the arrow leaves the hand and the string relaxes, + // while the cached ammo display survives for the next pull and the weapon stays DRAWN. + // Byte-verified (wow-re `shooter-stop-law.md` §J1.1, which refuted our recorded "nothing + // in the client un-nocks on movement"): `0x60e480` is a movement-OPCODE dispatcher whose + // jump-table arm 0 runs `0x60e4ac call 0x60f530` (un-nock) + `RecomputeBaseAnim(-1)`, and + // arm 0 is reached by exactly MSG_MOVE_START_{FORWARD, BACKWARD, STRAFE_LEFT, + // STRAFE_RIGHT, SWIM} (181/182/184/185/202). **Turning and stopping do NOT un-nock** — + // they take arm 1, which recomputes only. So the mask is the four translation bits plus + // SWIMMING, never the turn bits: [`move_flags::ANY_MOVE`] is exactly the former, and this + // is the one place that needs the swim bit with it. // - // **Named approximation, director-grounded.** The reference's un-nock `0x60f530` has six - // callers; three (`0x6020ef`, `0x60c951`, `0x60e4ac`) are still uncensused (dispatched to - // wow-re 2026-08-05, alongside the asset finding that AttackBow(46) authors NO `$BWP` — - // only LoadBow/LoadRifle do — which refutes the Q-I §I2 re-attach inference our INTERIM - // stands in for). One of the three may well be exactly this edge. Until that lands, the - // rule is what the screen requires and what closes our state machine: every latch we can - // set has an owner that tears it down. - if moving && nock_latched { + // The reference's trigger is the START opcode (an edge); a level test is equivalent here + // because the only thing that can re-latch is `$BWP`, authored solely in the Load clips — + // which a translating unit never plays. Sheath is untouched (§J1's claim-4 split: REFUTED + // for un-nock, CONFIRMED for stow — `[+0xd40]` has no movement-path writer). + if mv.flags & (move_flags::ANY_MOVE | move_flags::SWIMMING) != 0 && nock_latched { commands.entity(entity).remove::(); } } diff --git a/crates/benilla/src/creature_anim/driver/grip.rs b/crates/benilla-app/src/creature_anim/driver/grip.rs similarity index 100% rename from crates/benilla/src/creature_anim/driver/grip.rs rename to crates/benilla-app/src/creature_anim/driver/grip.rs diff --git a/crates/benilla/src/creature_anim/driver/mode.rs b/crates/benilla-app/src/creature_anim/driver/mode.rs similarity index 85% rename from crates/benilla/src/creature_anim/driver/mode.rs rename to crates/benilla-app/src/creature_anim/driver/mode.rs index 32c4e1dd..6ce745dc 100644 --- a/crates/benilla/src/creature_anim/driver/mode.rs +++ b/crates/benilla-app/src/creature_anim/driver/mode.rs @@ -45,7 +45,6 @@ pub(super) struct Frame<'a> { pub(super) engaged: bool, pub(super) airborne_frozen: bool, pub(super) auto_repeat: bool, - pub(super) ranged_hold: bool, pub(super) cast_hold: Option<&'a CastHold>, pub(super) wielded: Option<&'a Wielded>, pub(super) store: Option<&'a ObjectStore>, @@ -65,7 +64,6 @@ pub(super) fn run( tr: &mut AnimationTransitions, player: &mut AnimationPlayer, rng: &mut u32, - commands: &mut Commands, ) { let Frame { entity, @@ -80,7 +78,6 @@ pub(super) fn run( engaged, airborne_frozen, auto_repeat, - ranged_hold, cast_hold, wielded, store, @@ -286,7 +283,21 @@ pub(super) fn run( ); drv.mode = Mode::Looping(sp); } - } else if oneshot_finished(player, anims, id, catalog) || mv.flags != drv.gait_flags { + } else if (oneshot_finished(player, anims, id, catalog) + && !(select::is_ranged_fire(id) && auto_repeat)) + || mv.flags != drv.gait_flags + { + // The ranged fire clip is the ONE one-shot whose completion does not recompute + // the base (wow-re `shooter-stop-law.md` §J4, four converging lines): the + // completion dispatcher `0x5fc3f0` is never reached for a bow id, so AttackBow + // clamps and holds its authored tail — the quick re-draw — until a real + // recompute. Recomputing here instead re-picked the gait, which for an armed + // shooter is the Load clip, so **every shot replayed the full pull**. The + // hold-out is scoped to `auto_repeat` because that is the state the reference + // recomputes out of: the cancel `0x6ea080` ends with `RecomputeBaseAnim(-1)`, + // and dropping the arm re-opens this branch on the very next frame — the same + // observable, with no extra latch to own. A movement-flag change still + // recomputes through the second leg (opcode arms 0 and 1 both recompute). // Finished — or a movement-flag change: the client's base re-arm lands on the // change and blindly overwrites bone 0, one-shot or not (the same re-arm // decision 0280 named for the un-finishable looping kit clip, and @@ -365,39 +376,19 @@ pub(super) fn run( // Normal gait: select, cross-fade on change, keep the rate synced each frame. // The engaged standing idle: the weapon-class Ready pick (decision 0073). let ready = (engaged && !moving).then(|| ready_anim(wielded.and_then(|w| w.main))); - // The ranged standing idle (0099 phase 5): the byte-verified entry/sustain - // gate ([`select::ranged_idle_gate`]) → the ranged weapon's Load/Hold clip. - // ENTRY is the local `0x200` ([`auto_repeat`]) alone — the resolver - // `0x5fd460`'s own and only claim test; the any-caster `0x400` - // ([`ranged_hold`]) merely SUSTAINS a hold already on the base track - // (`0x5fc3f0`'s self-loop). Folding the two into one entry gate is what - // left a shooter permanently aiming after any single ranged spell and a - // remote archer permanently drawn (director-reported 2026-08-05). - // After one full pass of the Load clip the idle swaps to the drawn HOLD - // twin — nock once, then HOLD through every shot: a mid-volley fire clip - // returns straight to the hold, never a full re-pull (director-refuted; - // decision 0409). The pull replays only when the idle is LEFT and - // re-entered (volley start, movement, sheath change — the reset at this - // arm's tail). Named deviation: a remote's post-movement re-entry replays - // the pull like the local player's, where the byte corollary (the - // `0x200`-gated resolver) would leave a remote in plain Stand — unobserved - // on the ref, and the derivation rests on the LOAD-ANIM-RECOMPUTE-MODE - // open item; revisit if the ref's remote hunters visibly differ. + // The ranged standing idle (0099 phase 5): the byte-verified entry gate + // ([`select::ranged_idle_gate`]) → the ranged weapon's Load clip, played + // ONCE and frozen at full draw. ENTRY is the local `0x200` ([`auto_repeat`]) + // alone — `0x5fd460`'s own and only claim test besides the sheath. There is + // no HOLD twin and no promotion: the completion dispatcher that would play + // 109 is never reached for a bow id (wow-re `shooter-stop-law.md` §J4.1), + // and mid-volley the base is not re-picked at all — the fire clip clamps on + // its own authored tail (the `is_ranged_fire` hold-out in `Mode::Swing`). + // The pull replays only on a real recompute: movement start/stop, a sheath + // change, the auto-repeat cancel. let ranged_load = (!moving - && select::ranged_idle_gate( - auto_repeat, - ranged_hold, - drv.ranged_held, - drv.sheath_cur, - )) - .then(|| select::ranged_load_anim(wielded.and_then(|w| w.ranged))) - .map(|id| { - if drv.ranged_held { - select::ranged_hold_anim(id) - } else { - id - } - }); + && select::ranged_idle_gate(auto_repeat, drv.sheath_cur)) + .then(|| select::ranged_load_anim(wielded.and_then(|w| w.ranged))); let cands = gait_candidates(&mv, walk, ready, ranged_load); // The stationary cast/channel hold pins its pose **full-body in the gait slot** // (decision 0107 — the client's `[CGUnit+0xb4]` stationary-cast gate), @@ -479,29 +470,11 @@ pub(super) fn run( .iter() .find_map(|&id| find_resolved(anims, id, catalog)); if drv.gait == Some(target) { - if let Some(c) = clip { - // The gait is already armed and stays armed — its rate is - // [`play::sync_base_rate`]'s per-frame write below, which finds - // whichever *rolled variation* (decision 0123) is actually the live - // one rather than sweeping every node of the id. - // - // One completed pass of the ranged Load clip arms the HOLD twin - // (the `ranged_load` map above) — next frame the idle cross-fades - // to the drawn hold instead of replaying the nock. - if select::is_ranged_load(target) - && anims - .clips - .iter() - .filter(|v| v.anim_id == c.anim_id) - .any(|v| { - player - .animation(v.node) - .is_some_and(|a| a.completions() >= 1) - }) - { - drv.ranged_held = true; - } - } + // The gait is already armed and stays armed — its rate is + // [`play::sync_base_rate`]'s per-frame write below, which finds whichever + // *rolled variation* (decision 0123) is actually the live one rather than + // sweeping every node of the id. A completed ranged Load simply clamps at + // full draw and stays there; nothing promotes it (0994). } else if let Some(c) = clip { // A looping base arm rolls its variation when relaxed (decision 0123 — // the client's base-arm `variationIdx = −1`; a combat/cast arm keeps the @@ -527,14 +500,14 @@ pub(super) fn run( // gait it WRAPPED to its head at completion — the frames + cross-fade // against the restarted reach-to-quiver were the director's "jumps // back to the start the moment it gets fully pulled", in every build - // that replayed a pull (trace-caught, decision 0412). The promotion - // then cross-fades the hold from the matching drawn pose. + // that replayed a pull (trace-caught, decision 0412). The clamp IS the + // drawn pose; nothing follows it (0994). // Loot 50 is likewise authored clamp — one 0.5 s kneel-down that must // FREEZE in the rummage pose; as Forever it would wrap back to standing // and re-kneel every half second. let repeat = if select::is_ranged_load(target) || target == select::LOOT { - // A deliberate freeze — no window either: the client's ranged - // hand-off is the HOLD promotion, not a watchdog re-pull. + // A deliberate freeze — no window either: a watchdog re-pull is + // exactly what the reference's missing completion dispatch rules out. drv.loop_window = None; bevy::animation::RepeatAnimation::Never } else { @@ -544,15 +517,6 @@ pub(super) fn run( drv.gait_rate = playback_rate(c, mv.speed, model_scale); play_clip(tr, player, c, repeat, drv.gait_rate); drv.gait = Some(target); - // A fresh pick of the drawn HOLD re-nocks (INTERIM, decision 0409): the - // hold resuming after a fire clip (or after the Load promotion) shows - // the arrow + drawn string again without a full re-pull — the `$BWP` - // re-latch only lives in the Load clip our mid-volley cycle no longer - // replays. The ref's exact mid-volley re-attach source is the Q-I - // dispatch; the arrow itself still gates on a cached NockedAmmo. - if drv.ranged_held && ranged_load == Some(target) { - commands.entity(entity).insert(super::super::NockLatch); - } } else { drv.gait = Some(target); // no clip (bind pose) — record the target so we don't churn } @@ -561,11 +525,6 @@ pub(super) fn run( // this, not its own arm-time flags, to know whether the movement state has // moved on since (decision 0894). drv.gait_flags = mv.flags; - // Leaving the ranged idle (moving, sheath change, cancel) drops the hold — - // the next shooting session opens with a fresh nock. - if ranged_load.is_none() { - drv.ranged_held = false; - } } } } diff --git a/crates/benilla/src/creature_anim/driver/play.rs b/crates/benilla-app/src/creature_anim/driver/play.rs similarity index 100% rename from crates/benilla/src/creature_anim/driver/play.rs rename to crates/benilla-app/src/creature_anim/driver/play.rs diff --git a/crates/benilla/src/creature_anim/driver/tests.rs b/crates/benilla-app/src/creature_anim/driver/tests.rs similarity index 96% rename from crates/benilla/src/creature_anim/driver/tests.rs rename to crates/benilla-app/src/creature_anim/driver/tests.rs index 8ba87a99..80944767 100644 --- a/crates/benilla/src/creature_anim/driver/tests.rs +++ b/crates/benilla-app/src/creature_anim/driver/tests.rs @@ -2762,3 +2762,86 @@ fn the_weapon_visual_hold_alone_never_puts_a_shooter_in_the_drawn_idle() { "…while an actively auto-repeating shooter pulls the bow: the `0x200` entry" ); } + +/// The mid-volley half of the same report ("when it's on and I'm running it keeps repeating the +/// aim animation weirdly") — and the deeper cause of the aiming look the screenshot caught. +/// +/// wow-re `shooter-stop-law.md` §J4: the anim-completion dispatcher `0x5fc3f0` is **never reached +/// for a bow id** (its sole fire site pushes a literal completion-mode `1`), so a finished +/// AttackBow triggers no `RecomputeBaseAnim(-1)`. It clamps on its own authored tail — the quick +/// re-draw — and stays there. Our machine instead recomputed on every one-shot completion, and +/// for an armed shooter the recompute re-picks the Load clip: **a full re-pull on every shot**, +/// with the arrow re-attached each time by 0409's re-latch. Four independent lines settle it +/// (the byte mechanism, vmangos sending `SMSG_SPELL_START` once per volley so shots 2..N carry no +/// PrecastKit, the asset census showing `$BWP` only in the Load clips, and the director's screen). +/// +/// The cancel is what recomputes out of it (`0x6ea080` ends with `RecomputeBaseAnim(-1)`), which +/// here is the arm simply dropping. +#[test] +fn a_mid_volley_fire_clip_does_not_replay_the_pull_and_the_cancel_recomputes() { + let mut app = app(); + let unit = app + .world_mut() + .spawn(( + archer_model(), + AnimationPlayer::default(), + AnimationTransitions::new(), + AnimDriver::default(), + crate::net::SelfPlayer, + Wielded { + ranged: Some((2, 0x2)), // bow + ..Default::default() + }, + crate::creature_anim::AutoRepeatArmed, + )) + .id(); + app.world_mut().write_message(SheathRequest { + entity: unit, + state: 2, + ceremony: false, + }); + app.update(); + app.update(); + let gait = |app: &App| app.world().entity(unit).get::().unwrap().gait; + let mode = |app: &App| app.world().entity(unit).get::().unwrap().mode; + assert_eq!(gait(&app), Some(105), "the volley opens with the pull"); + + // Shot 1 fires. AttackBow(46) takes the body as a one-shot; this model authors no 46, so it + // reads as finished the moment the machine looks at it — the state right after any fire clip. + { + let mut e = app.world_mut().entity_mut(unit); + let mut d = e.get_mut::().unwrap(); + d.mode = super::super::select::Mode::Swing { + id: 46, + under: None, + }; + } + app.update(); + assert_eq!( + mode(&app), + super::super::select::Mode::Swing { + id: 46, + under: None, + }, + "the fire clip's completion must NOT recompute the base — the reference has no completion \ + dispatch for a bow id, so it clamps on its tail" + ); + assert_eq!( + gait(&app), + Some(105), + "…and nothing re-armed the gait: no second pull, and never the HoldBow twin (109), which \ + the reference never plays at all" + ); + + // The volley ends — the cancel's `RecomputeBaseAnim(-1)`. + app.world_mut() + .entity_mut(unit) + .remove::(); + app.update(); + app.update(); + assert_eq!( + gait(&app), + Some(0), + "dropping the arm recomputes out of the fire clip and the shooter stands up" + ); +} diff --git a/crates/benilla/src/creature_anim/driver/wound.rs b/crates/benilla-app/src/creature_anim/driver/wound.rs similarity index 100% rename from crates/benilla/src/creature_anim/driver/wound.rs rename to crates/benilla-app/src/creature_anim/driver/wound.rs diff --git a/crates/benilla/src/creature_anim/emote_anim.rs b/crates/benilla-app/src/creature_anim/emote_anim.rs similarity index 100% rename from crates/benilla/src/creature_anim/emote_anim.rs rename to crates/benilla-app/src/creature_anim/emote_anim.rs diff --git a/crates/benilla/src/creature_anim/env_damage.rs b/crates/benilla-app/src/creature_anim/env_damage.rs similarity index 100% rename from crates/benilla/src/creature_anim/env_damage.rs rename to crates/benilla-app/src/creature_anim/env_damage.rs diff --git a/crates/benilla/src/creature_anim/events.rs b/crates/benilla-app/src/creature_anim/events.rs similarity index 100% rename from crates/benilla/src/creature_anim/events.rs rename to crates/benilla-app/src/creature_anim/events.rs diff --git a/crates/benilla/src/creature_anim/global_seq.rs b/crates/benilla-app/src/creature_anim/global_seq.rs similarity index 100% rename from crates/benilla/src/creature_anim/global_seq.rs rename to crates/benilla-app/src/creature_anim/global_seq.rs diff --git a/crates/benilla/src/creature_anim/impact.rs b/crates/benilla-app/src/creature_anim/impact.rs similarity index 100% rename from crates/benilla/src/creature_anim/impact.rs rename to crates/benilla-app/src/creature_anim/impact.rs diff --git a/crates/benilla/src/creature_anim/lod.rs b/crates/benilla-app/src/creature_anim/lod.rs similarity index 99% rename from crates/benilla/src/creature_anim/lod.rs rename to crates/benilla-app/src/creature_anim/lod.rs index bce30051..4a5497e4 100644 --- a/crates/benilla/src/creature_anim/lod.rs +++ b/crates/benilla-app/src/creature_anim/lod.rs @@ -490,6 +490,7 @@ mod tests { ref_count, area_table_id: 0, fog_indices: [0; 4], + group_liquid: benilla_formats::NO_GROUP_LIQUID, } } @@ -561,6 +562,7 @@ mod tests { world_from_local: bevy::math::Affine3A::IDENTITY, name_set: 0, visible: vec![false, false], + flooded: vec![None, None], }) .id(); app.world_mut() diff --git a/crates/benilla/src/creature_anim/pose.rs b/crates/benilla-app/src/creature_anim/pose.rs similarity index 100% rename from crates/benilla/src/creature_anim/pose.rs rename to crates/benilla-app/src/creature_anim/pose.rs diff --git a/crates/benilla/src/creature_anim/select.rs b/crates/benilla-app/src/creature_anim/select.rs similarity index 95% rename from crates/benilla/src/creature_anim/select.rs rename to crates/benilla-app/src/creature_anim/select.rs index e38b27a6..8d57b5de 100644 --- a/crates/benilla/src/creature_anim/select.rs +++ b/crates/benilla-app/src/creature_anim/select.rs @@ -510,11 +510,13 @@ pub(super) fn gait_candidates( // (they can't co-occur — auto-shot never sets the engaged GUID) and before the chair loops // (OUR ordering; the client's call-site order vs the chair resolver isn't pinned). if let Some(l) = ranged_load { + // 109/110 (the Hold twins) are deliberately absent: `0x5fd460`'s jump table writes only + // 105/106/111/112, and the completion dispatcher that would promote a Load to its Hold is + // never reached for a bow id (wow-re `shooter-stop-law.md` §J4.1) — the reference plays + // no HoldBow, ever. return match l { 105 => &[105, 25, 0], 106 => &[106, 25, 0], - 109 => &[109, 25, 0], - 110 => &[110, 25, 0], 111 => &[111, 25, 0], 112 => &[112, 25, 0], _ => &[25, 0], @@ -553,60 +555,54 @@ pub(super) fn ranged_load_anim(ranged: Option<(u8, u8)>) -> u16 { } } -/// The Load clip's post-nock **HOLD twin** — the drawn pose held between shots (LoadBow 105 → -/// HoldBow 109 · LoadRifle 106 → HoldRifle 110 · LoadThrown 112 → HoldThrown 111; the wand's -/// idle is already the hold). **INTERIM** (decision 0400 §2's dispatch, Q4): the director's -/// reference observable — the character nocks once, then "holds the arrow much longer in the -/// bow before each shot" — plus the recorded-but-underived `[+0xd58] |= 0x400` post-shot -/// hold-pose layer (`0x5fc3f0`, flagged open in wow-re and 0370's deferred list). The idle -/// plays the Load clip ONCE, then the driver swaps to this twin until the next shot resets it; -/// the dispatched verdict pins the real mechanism. -pub(super) fn ranged_hold_anim(load: u16) -> u16 { - match load { - 105 => 109, - 106 => 110, - 112 => 111, - other => other, - } -} - -/// Whether `id` is one of the ranged Load clips whose single pass arms the hold twin -/// ([`ranged_hold_anim`]). The wand's HoldThrown 111 is already a hold — not a Load. +/// Whether `id` is one of the ranged Load clips the drawn idle arms — the clips that must play +/// **once and freeze at full draw** rather than looping ([`ranged_load_anim`]'s outputs, minus +/// the wand's HoldThrown 111, which is already a hold pose). pub(super) fn is_ranged_load(id: u16) -> bool { matches!(id, 105 | 106 | 112) } -/// Does the drawn ranged Load/Hold family own this unit's standing idle? The **entry/sustain -/// asymmetry**, byte-verified (wow-re `ranged-shot-anim.md` §b + `nocked-ammo-cancel.md` §Q-I-I0): +/// Whether `id` is a ranged **fire** clip — AttackBow(46) · AttackRifle(49) · AttackThrown(107). /// -/// - **ENTRY is `[+0xd58] & 0x200` alone** — the resolver `0x5fd460` (tier 9 of `ComputeAnimation -/// 0x5fd8b0`) claims on `cmp [+0xd40],2` (sheath ranged) `&& test ah,0x2` and nothing else. That -/// bit's ONLY writer image-wide is the LOCAL cast-send `0x6e593b`, gated `AttributesEx2 & 0x20` -/// — so entry means *this client's player is actively auto-repeating*. Nothing else can start -/// the family: 109/110/111 are reachable only through their Load clip's completion -/// (`0x5fc3f0` slot 11 → `PlayAnimation(109)`), and the Load comes only from `0x5fd460`. -/// - **`0x400` ([`super::RangedHold`], the any-caster weapon-visual hold) only SUSTAINS** — it -/// appears in exactly one gait gate, HoldBow's self-loop at `0x5fc3f0` slot 13 -/// (`[+0xd24] != 0 && (channel || [+0xd58] & 0x600)`), which re-plays a hold **already on the -/// base track**. It can never admit the family. +/// These are the one-shots whose completion must **not** recompute the base (wow-re +/// `shooter-stop-law.md` §J4): the anim-completion dispatcher `0x5fc3f0` is never reached for a +/// bow id — its sole fire site `0x7122c1` pushes a literal completion-mode `1`, routing every bow +/// id to a bare epilogue — so `AttackBow`'s finish triggers no `RecomputeBaseAnim(-1)` and the +/// clip clamps on its last frame. Between shots the reference therefore stands in the fire clip's +/// **authored tail** (the quick re-draw), not in a re-pulled Load and not in a hold twin. Letting +/// the finish recompute is what replayed the full pull on every shot. +pub(super) fn is_ranged_fire(id: u16) -> bool { + matches!(id, 46 | 49 | 107) +} + +/// Does the drawn ranged Load idle own this unit's standing pose? Byte-verified, wow-re +/// `shooter-stop-law.md` §J6 claim 1 (§5, four independent pairs + byte arbitration): /// -/// Folding `0x400` into the *entry* gate (which this codebase did, with the asymmetry knowingly -/// "unmodeled") is what made a single Multi-Shot / Arcane Shot — any ranged-slot spell whose -/// visual sets the hold bit — leave the shooter aiming a drawn bow forever, and left a REMOTE -/// shooter permanently drawn where the reference stands it up (§c's verified negative: a remote -/// unit never runs the local cast-send, so its standing pose is Stand). `held` is our -/// "the Load pass finished, the hold is what's playing" latch — the base-track state the -/// self-loop gate reads. +/// `0x5fd460` — tier 9 of `ComputeAnimation 0x5fd8b0`, and the **only** writer of 105/106/111/112 +/// image-wide — claims on exactly two tests and nothing else: +/// +/// ```text +/// 5fd463: cmp dword [ecx+0xd40], 2 ; sheath CUR == RANGED +/// 5fd476: test ah, 0x2 ; [+0xd58] & 0x200 = auto-repeat active +/// ``` +/// +/// `0x200`'s only writer image-wide is the LOCAL cast-send `0x6e593b`, gated `AttributesEx2 & +/// 0x20` — so entry means *this client's player is actively auto-repeating*. **`0x400` +/// ([`super::RangedHold`]) is never tested in this function.** It appears only in `0x5fc3f0`'s +/// sustain gates (`test ah,0x6` for 109, `test ah,0x4` for 110/111/112), which §J4 shows never +/// run. Folding it into this gate is what let one Serpent Sting / Multi-Shot — any ranged-slot +/// spell whose visual sets the hold bit — leave the shooter aiming a drawn bow with nothing able +/// to clear it (director-reported 2026-08-05). +/// +/// A REMOTE shooter never runs the local cast-send, so it never enters here. It is not simply +/// Stand, though (§J6 claim 2, CORRECTED): it plays **LoadBow(105) once** at the volley's single +/// `SMSG_SPELL_START`, through the PrecastKit — our [`super::CastHold`] path — then AttackBow per +/// GO over its ordinary idle. Never a sustained aim pose. /// /// The caller applies the tier order: locomotion (tier 4) outranks tier 9, so a moving unit is /// never in the idle whatever these bits say. -pub(super) fn ranged_idle_gate( - auto_repeat: bool, - ranged_hold: bool, - held: bool, - sheath_cur: Option, -) -> bool { - sheath_cur == Some(2) && (auto_repeat || (held && ranged_hold)) +pub(super) fn ranged_idle_gate(auto_repeat: bool, sheath_cur: Option) -> bool { + sheath_cur == Some(2) && auto_repeat } /// The victim **defense-reaction** id (decision 0279 — the `$CPP` dispatch, byte-verified diff --git a/crates/benilla/src/creature_anim/select/tests.rs b/crates/benilla-app/src/creature_anim/select/tests.rs similarity index 94% rename from crates/benilla/src/creature_anim/select/tests.rs rename to crates/benilla-app/src/creature_anim/select/tests.rs index e6506188..a4d71a12 100644 --- a/crates/benilla/src/creature_anim/select/tests.rs +++ b/crates/benilla-app/src/creature_anim/select/tests.rs @@ -464,34 +464,43 @@ fn ranged_load_idle_selects_by_weapon_and_ranks_below_ready() { ))); } -/// The drawn ranged idle's **entry/sustain asymmetry** ([`ranged_idle_gate`], byte-verified): -/// `0x5fd460` claims on the local auto-repeat bit `0x200` ALONE, while the any-caster -/// weapon-visual hold `0x400` appears only in `0x5fc3f0`'s HoldBow self-loop — it sustains a hold -/// already playing and can never start one. Folding the two into one entry gate is the defect the -/// director reported on 2026-08-05 ("they keep aiming like they are going to shoot at something"): -/// a single Multi-Shot sets `0x400`, and nothing in the client ever clears it on volley end. +/// The drawn ranged idle is claimed by **two tests and no others** ([`ranged_idle_gate`], +/// byte-verified — wow-re `shooter-stop-law.md` §J6 claim 1): `0x5fd460` reads the ranged sheath +/// (`cmp [+0xd40],2`) and the local auto-repeat bit (`test ah,0x2` = `0x200`). The any-caster +/// weapon-visual hold `0x400` is **never tested in that function**; it appears only in +/// `0x5fc3f0`'s Hold self-loop gates, which are never reached for a bow id. Admitting it here is +/// the defect the director reported on 2026-08-05 ("they keep aiming like they are going to shoot +/// at something"): one Serpent Sting sets `0x400`, and no volley end ever clears it. #[test] -fn the_ranged_idle_is_entered_by_auto_repeat_alone_and_only_sustained_by_the_hold_bit() { - // ENTRY: the local `0x200`, with the ranged sheath. This is the whole claim of `0x5fd460`. - assert!(ranged_idle_gate(true, false, false, Some(2))); +fn the_ranged_idle_is_entered_by_the_auto_repeat_bit_and_the_ranged_sheath_alone() { + // The whole claim of `0x5fd460`: the local `0x200`, with the ranged sheath. + assert!(ranged_idle_gate(true, Some(2))); // …and it needs CUR == 2: a melee-drawn or stowed unit is never in the family. - assert!(!ranged_idle_gate(true, false, false, Some(1))); - assert!(!ranged_idle_gate(true, false, false, Some(0))); - assert!(!ranged_idle_gate(true, false, false, None)); + assert!(!ranged_idle_gate(true, Some(1))); + assert!(!ranged_idle_gate(true, Some(0))); + assert!(!ranged_idle_gate(true, None)); + // Drawn ranged but not auto-repeating — a hunter who fired one Serpent Sting and stopped, + // and every REMOTE shooter (which never runs the local cast-send, so `0x200` cannot exist + // on it). No aim pose, whatever its weapon-visual hold says. + assert!(!ranged_idle_gate(false, Some(2))); +} - // `0x400` ALONE never enters — the regression under the report. This is the state of a - // hunter who fired one Multi-Shot and stopped, and of every REMOTE shooter (which never - // runs the local cast-send, so `0x200` cannot exist on it — §c's verified negative). - assert!(!ranged_idle_gate(false, true, false, Some(2))); - // Nor does "no bits at all" with the stance still drawn. - assert!(!ranged_idle_gate(false, false, false, Some(2))); - - // SUSTAIN: mid-volley the local shooter has both bits; the hold survives a frame where the - // auto-repeat bit is the only one to have dropped, exactly like 109's `& 0x600` self-loop. - assert!(ranged_idle_gate(true, true, true, Some(2))); - assert!(ranged_idle_gate(false, true, true, Some(2))); - // …and once BOTH bits are gone (the cancel `0x6ea080` clears them together), the hold ends. - assert!(!ranged_idle_gate(false, false, true, Some(2))); +/// The ranged **fire** clips ([`is_ranged_fire`]) — the one-shots whose completion must not +/// recompute the base (wow-re `shooter-stop-law.md` §J4: `0x5fc3f0` is never reached for a bow +/// id, so AttackBow clamps on its authored tail). Recomputing re-picks the gait, which for an +/// armed shooter is the Load clip — that is a full re-pull on every single shot. +#[test] +fn the_ranged_fire_clips_are_the_three_attack_ids_and_not_the_loads() { + assert!(is_ranged_fire(46)); // AttackBow + assert!(is_ranged_fire(49)); // AttackRifle + assert!(is_ranged_fire(107)); // AttackThrown + // The Load clips are the base idle, not one-shots — they must never take the hold-out. + assert!(!is_ranged_fire(105)); + assert!(!is_ranged_fire(106)); + assert!(!is_ranged_fire(112)); + // Nor any melee swing: those return to the gait the moment they finish, as they always have. + assert!(!is_ranged_fire(0)); + assert!(!is_ranged_fire(15)); // AttackUnarmed } #[test] diff --git a/crates/benilla/src/creature_anim/sheath.rs b/crates/benilla-app/src/creature_anim/sheath.rs similarity index 100% rename from crates/benilla/src/creature_anim/sheath.rs rename to crates/benilla-app/src/creature_anim/sheath.rs diff --git a/crates/benilla/src/creature_anim/spell_visual.rs b/crates/benilla-app/src/creature_anim/spell_visual.rs similarity index 100% rename from crates/benilla/src/creature_anim/spell_visual.rs rename to crates/benilla-app/src/creature_anim/spell_visual.rs diff --git a/crates/benilla/src/creature_anim/spell_visual/tests.rs b/crates/benilla-app/src/creature_anim/spell_visual/tests.rs similarity index 100% rename from crates/benilla/src/creature_anim/spell_visual/tests.rs rename to crates/benilla-app/src/creature_anim/spell_visual/tests.rs diff --git a/crates/benilla/src/creature_anim/twist.rs b/crates/benilla-app/src/creature_anim/twist.rs similarity index 100% rename from crates/benilla/src/creature_anim/twist.rs rename to crates/benilla-app/src/creature_anim/twist.rs diff --git a/crates/benilla/src/cursor.rs b/crates/benilla-app/src/cursor.rs similarity index 100% rename from crates/benilla/src/cursor.rs rename to crates/benilla-app/src/cursor.rs diff --git a/crates/benilla/src/cvars.rs b/crates/benilla-app/src/cvars.rs similarity index 83% rename from crates/benilla/src/cvars.rs rename to crates/benilla-app/src/cvars.rs index 69715e9b..3acce0ba 100644 --- a/crates/benilla/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -27,6 +27,8 @@ use std::time::Instant; use bevy::prelude::*; +use crate::clutter::ClutterConfig; +use crate::nameplates::NameConfig; use crate::player::camera::LookConfig; use crate::sound::SoundConfig; use crate::target::ClickConfig; @@ -59,6 +61,17 @@ pub(crate) const REGISTERED: &[(&str, &str)] = &[ ("deselectOnClick", "1"), ("mouseInvertPitch", "0"), ("autoLootDefault", "0"), + // The overhead-name trio (0992): 1.12's own UnitName* CVars (UIOptionsFrame.lua indices + // 21/30/67) over the nameplates module's gates. Defaults mirror NameConfig::default() — + // npc/own ON are director directives, diverging from the binary's "0" defaults on purpose + // (the divergence and its dates live on NameConfig's doc). + ("UnitNamePlayer", "1"), + ("UnitNameNPC", "1"), + ("UnitNameOwn", "1"), + // World detail (0992): 1.12's video-panel var (the ENVIRONMENT_DETAIL slider, 0..2) over + // the clutter-density knob — 0 is the client's bare frillDensity baseline (×1 = 16 visits), + // each step +1×; the "2" default IS ClutterConfig's shipped ×3 (the reference's High). + ("WorldDetail", "2"), ]; /// `config.toml`'s shape: a `[cvars]` table of `Name = "value"` strings (CVars are strings in @@ -109,6 +122,8 @@ struct Knobs<'a> { look: &'a mut LookConfig, click: &'a mut ClickConfig, loot: &'a mut LootConfig, + names: &'a mut NameConfig, + clutter: &'a mut ClutterConfig, } /// Apply one CVar to its knob resource (parse + the knob's own clamp). `false` = not a knob this @@ -132,6 +147,12 @@ fn apply_to_knobs(name: &str, value: &str, knobs: &mut Knobs) -> bool { "deselectonclick" => knobs.click.deselect_on_click = v != 0.0, "mouseinvertpitch" => knobs.look.invert_pitch = v != 0.0, "autolootdefault" => knobs.loot.auto_loot = v != 0.0, + "unitnameplayer" => knobs.names.player = v != 0.0, + "unitnamenpc" => knobs.names.npc = v != 0.0, + "unitnameown" => knobs.names.own = v != 0.0, + // The panel's 0/1/2 lands as the density multiplier ×1/×2/×3; the clamp is the 1.12 + // slider's own range (an off-grid hand-edit rides between stops, like every slider). + "worlddetail" => knobs.clutter.density = v.clamp(0.0, 2.0) + 1.0, _ => return false, } true @@ -141,6 +162,7 @@ fn apply_to_knobs(name: &str, value: &str, knobs: &mut Knobs) -> bool { /// to the knob resources — except keys the environment overrides this session (their resources /// already read the env var in their `Default`s). The VM does not exist yet; [`sync_cvars`] /// seeds the table when it does. +#[allow(clippy::too_many_arguments)] // one knob resource per registered CVar family fn load_config( mut persist: ResMut, mut sound: ResMut, @@ -149,6 +171,8 @@ fn load_config( mut look: ResMut, mut click: ResMut, mut loot: ResMut, + mut names: ResMut, + mut clutter: ResMut, ) { let mut knobs = Knobs { sound: &mut sound, @@ -157,6 +181,8 @@ fn load_config( look: &mut look, click: &mut click, loot: &mut loot, + names: &mut names, + clutter: &mut clutter, }; if std::env::var_os("WOW_UI_SCALE").is_some() { persist.env_overridden.insert("uiscale".into()); @@ -164,6 +190,10 @@ fn load_config( if std::env::var_os("WOW_FARCLIP").is_some() { persist.env_overridden.insert("farclip".into()); } + // The clutter A/B env drives the same knob WorldDetail lands on — same session-only law. + if std::env::var_os("WOW_CLUTTER_DENSITY").is_some() { + persist.env_overridden.insert("worlddetail".into()); + } let Some(path) = crate::local_state::config_path() else { return; // hermetic capture, or no install — session-only state }; @@ -219,6 +249,8 @@ fn sync_cvars( mut look: ResMut, mut click: ResMut, mut loot: ResMut, + mut names: ResMut, + mut clutter: ResMut, ) { let Some(mut script) = script else { return; @@ -226,7 +258,7 @@ fn sync_cvars( if !persist.registered { script.register_cvars(REGISTERED.iter().copied()); let flag = |b: bool| if b { "1" } else { "0" }.to_string(); - let session: [(&str, String); 12] = [ + let session: [(&str, String); 16] = [ ("MasterVolume", sound.master.to_string()), ("SoundVolume", sound.sfx.to_string()), ("MusicVolume", sound.music.to_string()), @@ -239,12 +271,26 @@ fn sync_cvars( ("deselectOnClick", flag(click.deselect_on_click)), ("mouseInvertPitch", flag(look.invert_pitch)), ("autoLootDefault", flag(loot.auto_loot)), + ("UnitNamePlayer", flag(names.player)), + ("UnitNameNPC", flag(names.npc)), + ("UnitNameOwn", flag(names.own)), + // The session density on the panel scale (×1..×3 → 0..2). An env-driven off-grid + // multiplier seeds off-grid honestly — the dropdown shows the raw number, checks + // nothing (the 0959 out-of-range posture, dropdown-flavored). + ("WorldDetail", (clutter.density - 1.0).to_string()), ]; for (name, value) in session { script.set_cvar_host(name, &value); } persist.registered = true; } + // Take the changes BEFORE touching the knobs: constructing `Knobs` deref-muts every knob + // resource, which trips Bevy change detection even when nothing is written — and the + // clutter re-scatter is downstream of exactly that signal staying honest (0992). + let changes = script.take_cvar_changes(); + if changes.is_empty() { + return; + } let mut knobs = Knobs { sound: &mut sound, scale: &mut scale, @@ -252,8 +298,10 @@ fn sync_cvars( look: &mut look, click: &mut click, loot: &mut loot, + names: &mut names, + clutter: &mut clutter, }; - for (name, value) in script.take_cvar_changes() { + for (name, value) in changes { if apply_to_knobs(&name, &value, &mut knobs) { persist.dirty = true; persist.last_change = Some(Instant::now()); @@ -368,6 +416,14 @@ mod tests { LookConfig::default().invert_pitch ); assert_eq!(d["autoLootDefault"] != 0.0, LootConfig::default().auto_loot); + // The name trio (0992) welds to NameConfig's defaults the same way. + let names = NameConfig::default(); + assert_eq!(d["UnitNamePlayer"] != 0.0, names.player); + assert_eq!(d["UnitNameNPC"] != 0.0, names.npc); + assert_eq!(d["UnitNameOwn"] != 0.0, names.own); + // ClutterConfig::default() reads $WOW_CLUTTER_DENSITY; the registered default mirrors + // the env-less ×3 literal (clutter.rs: "Default ×3 = High") on the panel's 0..2 scale. + assert_eq!(d["WorldDetail"], 2.0); } #[test] @@ -378,6 +434,14 @@ mod tests { let mut look = LookConfig::default(); let mut click = ClickConfig::default(); let mut loot = LootConfig::default(); + let mut names = NameConfig::default(); + // Literal fields, not Default: ClutterConfig::default() reads the env A/B vars. + let mut clutter = ClutterConfig { + density: 3.0, + scale: 1.0, + alpha_ref: 0.5, + fade_far: 70.0, + }; let mut knobs = Knobs { sound: &mut sound, scale: &mut scale, @@ -385,6 +449,8 @@ mod tests { look: &mut look, click: &mut click, loot: &mut loot, + names: &mut names, + clutter: &mut clutter, }; assert!(apply_to_knobs("MusicVolume", "0.7", &mut knobs)); assert_eq!(knobs.sound.music, 0.7); @@ -405,6 +471,16 @@ mod tests { assert!(knobs.look.invert_pitch); assert!(apply_to_knobs("autoLootDefault", "1", &mut knobs)); assert!(knobs.loot.auto_loot); + // The name trio lands on its gates (0992). + assert!(apply_to_knobs("UnitNameNPC", "0", &mut knobs)); + assert!(!knobs.names.npc); + assert!(apply_to_knobs("unitnameown", "1", &mut knobs)); + assert!(knobs.names.own); + // WorldDetail: panel 0/1/2 → density ×1/×2/×3, clamped to the 1.12 slider's range. + assert!(apply_to_knobs("WorldDetail", "0", &mut knobs)); + assert_eq!(knobs.clutter.density, 1.0); + assert!(apply_to_knobs("worlddetail", "7", &mut knobs)); + assert_eq!(knobs.clutter.density, 3.0); // A bad value is consumed (known key) and the resource keeps its truth. assert!(apply_to_knobs("uiScale", "banana", &mut knobs)); assert_eq!(knobs.scale.0, 0.9); @@ -448,6 +524,7 @@ mod tests { let _c = EnvGuard::unset("WOW_CAPTURE"); let _u = EnvGuard::unset("WOW_UI_SCALE"); let _f = EnvGuard::unset("WOW_FARCLIP"); + let _d = EnvGuard::unset("WOW_CLUTTER_DENSITY"); let _h = EnvGuard::set("BENILLA_HOME", tmp.to_str().unwrap()); crate::local_state::write_atomic( &tmp.join("config.toml"), @@ -463,6 +540,8 @@ mod tests { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() .add_plugins(CvarPlugin); app.insert_non_send_resource(UiScript::new().unwrap()); diff --git a/crates/benilla/src/dbg_trace.rs b/crates/benilla-app/src/dbg_trace.rs similarity index 100% rename from crates/benilla/src/dbg_trace.rs rename to crates/benilla-app/src/dbg_trace.rs diff --git a/crates/benilla/src/death.rs b/crates/benilla-app/src/death.rs similarity index 100% rename from crates/benilla/src/death.rs rename to crates/benilla-app/src/death.rs diff --git a/crates/benilla/src/debug_panel/mod.rs b/crates/benilla-app/src/debug_panel/mod.rs similarity index 99% rename from crates/benilla/src/debug_panel/mod.rs rename to crates/benilla-app/src/debug_panel/mod.rs index 95e1eadf..7bb939c7 100644 --- a/crates/benilla/src/debug_panel/mod.rs +++ b/crates/benilla-app/src/debug_panel/mod.rs @@ -338,6 +338,7 @@ fn dev_plane(keys: &ButtonInput) -> bool { #[allow(clippy::too_many_arguments)] fn debug_panel_ui( mut contexts: EguiContexts, + stamp: Res, mut debug: ResMut, mut view: ResMut, clock: Res, @@ -683,18 +684,18 @@ fn debug_panel_ui( // this here" can name the code it happened on. let build = ui.add( egui::Label::new( - egui::RichText::new(format!("build {}", crate::build_id::summary())) + egui::RichText::new(format!("build {}", stamp.summary())) .small() .color(OVERLAY_TEXT_DIM), ) .sense(egui::Sense::click()), ); - if !crate::build_id::SHA.is_empty() + if !stamp.sha.is_empty() && build - .on_hover_text(format!("{}\n(click to copy)", crate::build_id::SHA)) + .on_hover_text(format!("{}\n(click to copy)", stamp.sha)) .clicked() { - ui.ctx().copy_text(crate::build_id::SHA.to_string()); + ui.ctx().copy_text(stamp.sha.to_string()); } }); Ok(()) diff --git a/crates/benilla/src/debug_panel/state.rs b/crates/benilla-app/src/debug_panel/state.rs similarity index 100% rename from crates/benilla/src/debug_panel/state.rs rename to crates/benilla-app/src/debug_panel/state.rs diff --git a/crates/benilla/src/debug_panel/visibility.rs b/crates/benilla-app/src/debug_panel/visibility.rs similarity index 100% rename from crates/benilla/src/debug_panel/visibility.rs rename to crates/benilla-app/src/debug_panel/visibility.rs diff --git a/crates/benilla/src/debug_panel/world.rs b/crates/benilla-app/src/debug_panel/world.rs similarity index 100% rename from crates/benilla/src/debug_panel/world.rs rename to crates/benilla-app/src/debug_panel/world.rs diff --git a/crates/benilla/src/decal.rs b/crates/benilla-app/src/decal.rs similarity index 100% rename from crates/benilla/src/decal.rs rename to crates/benilla-app/src/decal.rs diff --git a/crates/benilla/src/doodad_anim/lazy.rs b/crates/benilla-app/src/doodad_anim/lazy.rs similarity index 100% rename from crates/benilla/src/doodad_anim/lazy.rs rename to crates/benilla-app/src/doodad_anim/lazy.rs diff --git a/crates/benilla/src/doodad_anim/mat_anim.rs b/crates/benilla-app/src/doodad_anim/mat_anim.rs similarity index 100% rename from crates/benilla/src/doodad_anim/mat_anim.rs rename to crates/benilla-app/src/doodad_anim/mat_anim.rs diff --git a/crates/benilla/src/doodad_anim/mod.rs b/crates/benilla-app/src/doodad_anim/mod.rs similarity index 100% rename from crates/benilla/src/doodad_anim/mod.rs rename to crates/benilla-app/src/doodad_anim/mod.rs diff --git a/crates/benilla/src/entities.rs b/crates/benilla-app/src/entities.rs similarity index 100% rename from crates/benilla/src/entities.rs rename to crates/benilla-app/src/entities.rs diff --git a/crates/benilla/src/entities/attach/char_skin.rs b/crates/benilla-app/src/entities/attach/char_skin.rs similarity index 100% rename from crates/benilla/src/entities/attach/char_skin.rs rename to crates/benilla-app/src/entities/attach/char_skin.rs diff --git a/crates/benilla/src/entities/attach/dress.rs b/crates/benilla-app/src/entities/attach/dress.rs similarity index 100% rename from crates/benilla/src/entities/attach/dress.rs rename to crates/benilla-app/src/entities/attach/dress.rs diff --git a/crates/benilla/src/entities/attach/glue_preview.rs b/crates/benilla-app/src/entities/attach/glue_preview.rs similarity index 100% rename from crates/benilla/src/entities/attach/glue_preview.rs rename to crates/benilla-app/src/entities/attach/glue_preview.rs diff --git a/crates/benilla/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs similarity index 100% rename from crates/benilla/src/entities/attach/mod.rs rename to crates/benilla-app/src/entities/attach/mod.rs diff --git a/crates/benilla/src/entities/attach/redress.rs b/crates/benilla-app/src/entities/attach/redress.rs similarity index 100% rename from crates/benilla/src/entities/attach/redress.rs rename to crates/benilla-app/src/entities/attach/redress.rs diff --git a/crates/benilla/src/entities/carried_light.rs b/crates/benilla-app/src/entities/carried_light.rs similarity index 100% rename from crates/benilla/src/entities/carried_light.rs rename to crates/benilla-app/src/entities/carried_light.rs diff --git a/crates/benilla/src/entities/chain_beam.rs b/crates/benilla-app/src/entities/chain_beam.rs similarity index 100% rename from crates/benilla/src/entities/chain_beam.rs rename to crates/benilla-app/src/entities/chain_beam.rs diff --git a/crates/benilla/src/entities/collision_height.rs b/crates/benilla-app/src/entities/collision_height.rs similarity index 100% rename from crates/benilla/src/entities/collision_height.rs rename to crates/benilla-app/src/entities/collision_height.rs diff --git a/crates/benilla/src/entities/conform.rs b/crates/benilla-app/src/entities/conform.rs similarity index 100% rename from crates/benilla/src/entities/conform.rs rename to crates/benilla-app/src/entities/conform.rs diff --git a/crates/benilla/src/entities/dest_fx.rs b/crates/benilla-app/src/entities/dest_fx.rs similarity index 100% rename from crates/benilla/src/entities/dest_fx.rs rename to crates/benilla-app/src/entities/dest_fx.rs diff --git a/crates/benilla/src/entities/display.rs b/crates/benilla-app/src/entities/display.rs similarity index 100% rename from crates/benilla/src/entities/display.rs rename to crates/benilla-app/src/entities/display.rs diff --git a/crates/benilla/src/entities/equipment/mod.rs b/crates/benilla-app/src/entities/equipment/mod.rs similarity index 100% rename from crates/benilla/src/entities/equipment/mod.rs rename to crates/benilla-app/src/entities/equipment/mod.rs diff --git a/crates/benilla/src/entities/equipment/resolve.rs b/crates/benilla-app/src/entities/equipment/resolve.rs similarity index 100% rename from crates/benilla/src/entities/equipment/resolve.rs rename to crates/benilla-app/src/entities/equipment/resolve.rs diff --git a/crates/benilla/src/entities/equipment/spawn.rs b/crates/benilla-app/src/entities/equipment/spawn.rs similarity index 100% rename from crates/benilla/src/entities/equipment/spawn.rs rename to crates/benilla-app/src/entities/equipment/spawn.rs diff --git a/crates/benilla/src/entities/item_glow.rs b/crates/benilla-app/src/entities/item_glow.rs similarity index 100% rename from crates/benilla/src/entities/item_glow.rs rename to crates/benilla-app/src/entities/item_glow.rs diff --git a/crates/benilla/src/entities/live_display.rs b/crates/benilla-app/src/entities/live_display.rs similarity index 100% rename from crates/benilla/src/entities/live_display.rs rename to crates/benilla-app/src/entities/live_display.rs diff --git a/crates/benilla/src/entities/missile.rs b/crates/benilla-app/src/entities/missile.rs similarity index 100% rename from crates/benilla/src/entities/missile.rs rename to crates/benilla-app/src/entities/missile.rs diff --git a/crates/benilla/src/entities/mount.rs b/crates/benilla-app/src/entities/mount.rs similarity index 100% rename from crates/benilla/src/entities/mount.rs rename to crates/benilla-app/src/entities/mount.rs diff --git a/crates/benilla/src/entities/mount/tests.rs b/crates/benilla-app/src/entities/mount/tests.rs similarity index 100% rename from crates/benilla/src/entities/mount/tests.rs rename to crates/benilla-app/src/entities/mount/tests.rs diff --git a/crates/benilla/src/entities/spell_fx/lifecycle.rs b/crates/benilla-app/src/entities/spell_fx/lifecycle.rs similarity index 100% rename from crates/benilla/src/entities/spell_fx/lifecycle.rs rename to crates/benilla-app/src/entities/spell_fx/lifecycle.rs diff --git a/crates/benilla/src/entities/spell_fx/mod.rs b/crates/benilla-app/src/entities/spell_fx/mod.rs similarity index 100% rename from crates/benilla/src/entities/spell_fx/mod.rs rename to crates/benilla-app/src/entities/spell_fx/mod.rs diff --git a/crates/benilla/src/entities/wmo_props.rs b/crates/benilla-app/src/entities/wmo_props.rs similarity index 100% rename from crates/benilla/src/entities/wmo_props.rs rename to crates/benilla-app/src/entities/wmo_props.rs diff --git a/crates/benilla/src/entity_shade.rs b/crates/benilla-app/src/entity_shade.rs similarity index 100% rename from crates/benilla/src/entity_shade.rs rename to crates/benilla-app/src/entity_shade.rs diff --git a/crates/benilla/src/exterior_cull.rs b/crates/benilla-app/src/exterior_cull.rs similarity index 100% rename from crates/benilla/src/exterior_cull.rs rename to crates/benilla-app/src/exterior_cull.rs diff --git a/crates/benilla/src/ffx_glow.rs b/crates/benilla-app/src/ffx_glow.rs similarity index 100% rename from crates/benilla/src/ffx_glow.rs rename to crates/benilla-app/src/ffx_glow.rs diff --git a/crates/benilla/src/glue/add_material.rs b/crates/benilla-app/src/glue/add_material.rs similarity index 100% rename from crates/benilla/src/glue/add_material.rs rename to crates/benilla-app/src/glue/add_material.rs diff --git a/crates/benilla/src/glue/art.rs b/crates/benilla-app/src/glue/art.rs similarity index 100% rename from crates/benilla/src/glue/art.rs rename to crates/benilla-app/src/glue/art.rs diff --git a/crates/benilla/src/glue/backdrop.rs b/crates/benilla-app/src/glue/backdrop.rs similarity index 100% rename from crates/benilla/src/glue/backdrop.rs rename to crates/benilla-app/src/glue/backdrop.rs diff --git a/crates/benilla/src/glue/mod.rs b/crates/benilla-app/src/glue/mod.rs similarity index 100% rename from crates/benilla/src/glue/mod.rs rename to crates/benilla-app/src/glue/mod.rs diff --git a/crates/benilla/src/glue/widgets.rs b/crates/benilla-app/src/glue/widgets.rs similarity index 100% rename from crates/benilla/src/glue/widgets.rs rename to crates/benilla-app/src/glue/widgets.rs diff --git a/crates/benilla/src/glue_strings.rs b/crates/benilla-app/src/glue_strings.rs similarity index 100% rename from crates/benilla/src/glue_strings.rs rename to crates/benilla-app/src/glue_strings.rs diff --git a/crates/benilla/src/go_anim.rs b/crates/benilla-app/src/go_anim.rs similarity index 100% rename from crates/benilla/src/go_anim.rs rename to crates/benilla-app/src/go_anim.rs diff --git a/crates/benilla/src/go_templates.rs b/crates/benilla-app/src/go_templates.rs similarity index 100% rename from crates/benilla/src/go_templates.rs rename to crates/benilla-app/src/go_templates.rs diff --git a/crates/benilla/src/ground_fx.rs b/crates/benilla-app/src/ground_fx.rs similarity index 100% rename from crates/benilla/src/ground_fx.rs rename to crates/benilla-app/src/ground_fx.rs diff --git a/crates/benilla/src/hover_log.rs b/crates/benilla-app/src/hover_log.rs similarity index 100% rename from crates/benilla/src/hover_log.rs rename to crates/benilla-app/src/hover_log.rs diff --git a/crates/benilla/src/instance_tint.rs b/crates/benilla-app/src/instance_tint.rs similarity index 100% rename from crates/benilla/src/instance_tint.rs rename to crates/benilla-app/src/instance_tint.rs diff --git a/crates/benilla/src/interact.rs b/crates/benilla-app/src/interact.rs similarity index 100% rename from crates/benilla/src/interact.rs rename to crates/benilla-app/src/interact.rs diff --git a/crates/benilla/src/interact/journal.rs b/crates/benilla-app/src/interact/journal.rs similarity index 100% rename from crates/benilla/src/interact/journal.rs rename to crates/benilla-app/src/interact/journal.rs diff --git a/crates/benilla/src/interior.rs b/crates/benilla-app/src/interior.rs similarity index 100% rename from crates/benilla/src/interior.rs rename to crates/benilla-app/src/interior.rs diff --git a/crates/benilla/src/items.rs b/crates/benilla-app/src/items.rs similarity index 100% rename from crates/benilla/src/items.rs rename to crates/benilla-app/src/items.rs diff --git a/crates/benilla-app/src/lib.rs b/crates/benilla-app/src/lib.rs new file mode 100644 index 00000000..2eac4d2d --- /dev/null +++ b/crates/benilla-app/src/lib.rs @@ -0,0 +1,939 @@ +//! `benilla` — a from-scratch World of Warcraft 1.12.1 client on Bevy, talking to a local vmangos server. +//! +//! Opens the vanilla patch chain from `$WOW_DATA` (default `WoW/Data`) and streams the world around the +//! player through Bevy's `AssetServer` (the `benilla-assets` `mpq://` pipeline): ADT terrain tiles within +//! `$WOW_TILE_RADIUS` — their splat-blended ground (tiling `$WOW_TEX_TILES`), doodads/WMOs, water, and +//! ground clutter — plus the avian colliders the character controller walks on. Lit by a time-of-day WoW +//! lighting model (`Light.dbc` sampled against the server clock) with a sky dome, sun/moon discs, and +//! distance fog; a faithful `EffectGlow` bloom on top. +//! +//! In parallel a background thread ([`net`]) logs in (`$WOW_USER`/`$WOW_PASS`/`$WOW_HOST`, default +//! `one`/`pone`/`localhost`), enters the world, and streams object updates. NPCs and GameObjects +//! render as their real models (resolved from the display id via the creature/GameObject catalogs); other +//! players stay cyan cubes, and our own avatar is blue until we take third-person control of it. +//! **The world is loaded when a character enters it, and released when they leave** — the glue +//! screens have no world behind them (decision 0777). With no server there is no world: the client +//! sits at the login screen, which is what the real one does. The scene harness (`$WOW_CAPTURE`) +//! boots straight in-world and is unaffected. +//! +//! Controls: WASD walks the avatar (Ctrl sprints); right-drag turns it, left-drag orbits the camera +//! (both hide/freeze the cursor while held), scroll wheel zooms; `F` toggles free-fly (then WASD flies +//! the camera with Space up / C down, Ctrl boost). + +mod area; +mod area_trigger; +mod art_scope; +mod asset_churn; +mod assets; +mod aura_visual; +mod bgwin; +mod billboard; +mod blob_shadow; +mod bowstring; +mod build_id; +mod capture; +mod char_create; +mod char_select; +mod chat_bubble; +mod clouds; +mod clutter; +mod collision; +mod combat_text; +mod cooldowns; +mod creature_anim; +mod cursor; +mod cvars; +mod dbg_trace; +mod death; +mod debug_panel; +mod decal; +mod doodad_anim; +mod entities; +mod entity_shade; +mod exterior_cull; +mod ffx_glow; +mod glue; +mod glue_strings; +mod go_anim; +mod go_templates; +mod ground_fx; +mod hover_log; +mod instance_tint; +mod interact; +mod interior; +mod items; +mod lighting; +mod liquid; +mod loading_screen; +mod local_state; +mod login; +mod map_proj; +mod mesh_tag; +mod minimap; +mod model_fade; +mod model_forms; +mod model_render; +mod modkeys; +mod nameplates; +mod names; +mod net; +mod npc_text; +mod particles; +mod pending_item_ops; +mod perf; +mod pipe_warm; +mod player; +mod portrait; +mod preflight; +mod probe_shield; +mod quest_markers; +mod raid_marks; +mod ribbons; +mod rig_palette; +mod schedule; +mod sky; +mod sky_order; +mod smart_rect; +mod sound; +mod sun; +mod target; +mod terrain; +mod terrain_stream; +mod textinput; +mod thread_qos; +mod transport; +mod ui_action; +mod ui_aura; +mod ui_bank; +mod ui_cast; +mod ui_char; +mod ui_chat; +mod ui_craft; +mod ui_duel; +mod ui_follow; +mod ui_gamma; +mod ui_gossip; +mod ui_hide; +mod ui_inspect; +mod ui_item_text; +mod ui_items; +mod ui_logout; +mod ui_loot; +mod ui_loot_roll; +mod ui_macro; +mod ui_mail; +mod ui_merchant; +mod ui_mirror; +mod ui_net; +mod ui_party; +mod ui_pass; +mod ui_pet; +mod ui_quest; +mod ui_quest_log; +mod ui_script; +mod ui_session; +mod ui_shapeshift; +mod ui_social; +mod ui_spellbook; +mod ui_talent; +mod ui_taxi; +mod ui_text; +mod ui_tooltip; +mod ui_trade; +mod ui_tradeskill; +mod ui_trainer; +mod ui_unit; +mod ui_world_map; +mod view; +mod vplates; +mod water_fx; +mod wdl; +mod weather; +mod wmo_portal; +mod wmo_sky; +mod world_map; +mod world_state; +mod zfill; + +use assets::AssetPlugin; +use avian3d::prelude::*; +use bevy::prelude::*; +use billboard::BillboardPlugin; +use blob_shadow::BlobShadowPlugin; +use bowstring::BowstringPlugin; +use clouds::CloudsPlugin; +use clutter::ClutterPlugin; +use creature_anim::CreatureAnimPlugin; +use cursor::CursorPlugin; +use debug_panel::DebugPanelPlugin; +use doodad_anim::DoodadAnimPlugin; +use entities::EntitiesPlugin; +use entity_shade::EntityShadePlugin; +use interact::InteractPlugin; +use interior::InteriorPlugin; +use lighting::LightingPlugin; +use liquid::LiquidPlugin; +use loading_screen::LoadingScreenPlugin; +use net::NetPlugin; +use particles::ParticlePlugin; +use perf::PerfPlugin; +use player::PlayerPlugin; +use portrait::PortraitPlugin; +use quest_markers::QuestMarkersPlugin; +use ribbons::RibbonPlugin; +use schedule::SchedulePlugin; +use sky::SkyPlugin; +use sound::SoundPlugin; +use sun::SunPlugin; +use target::TargetPlugin; +use terrain::{TerrainMaterial, WowModelMaterial}; +use terrain_stream::TerrainPlugin; +use textinput::TextInputPlugin; +use transport::TransportPlugin; +use ui_action::UiActionPlugin; +use ui_aura::UiAuraPlugin; +use ui_bank::UiBankPlugin; +use ui_cast::UiCastPlugin; +use ui_char::UiCharPlugin; +use ui_chat::UiChatPlugin; +use ui_craft::UiCraftPlugin; +use ui_duel::UiDuelPlugin; +use ui_follow::UiFollowPlugin; +use ui_gossip::UiGossipPlugin; +use ui_item_text::UiItemTextPlugin; +use ui_items::UiItemsPlugin; +use ui_logout::UiLogoutPlugin; +use ui_loot::UiLootPlugin; +use ui_loot_roll::UiLootRollPlugin; +use ui_mail::UiMailPlugin; +use ui_merchant::UiMerchantPlugin; +use ui_mirror::UiMirrorPlugin; +use ui_net::UiNetPlugin; +use ui_party::UiPartyPlugin; +use ui_pass::PlayerUiPlugin; +use ui_pet::UiPetPlugin; +use ui_quest::UiQuestPlugin; +use ui_quest_log::UiQuestLogPlugin; +use ui_script::UiScriptPlugin; +use ui_shapeshift::UiShapeshiftPlugin; +use ui_social::UiSocialPlugin; +use ui_spellbook::UiSpellbookPlugin; +use ui_talent::UiTalentPlugin; +use ui_taxi::UiTaxiPlugin; +use ui_text::UiTextPlugin; +use ui_tooltip::UiTooltipPlugin; +use ui_trade::UiTradePlugin; +use ui_tradeskill::UiTradeSkillPlugin; +use ui_trainer::UiTrainerPlugin; +use ui_unit::UiUnitPlugin; +use wdl::WdlPlugin; +use weather::WeatherPlugin; +use wmo_portal::WmoPortalPlugin; +use world_map::WorldMapPlugin; + +// The `benilla` launcher shim (the bin package) is this library's only caller: it stamps the +// build id at compile time and hands it into [`run`]. Re-exported so the shim needs no bevy +// dep of its own. +pub use bevy::app::AppExit; +pub use build_id::BuildId; + +/// Anchor the loaded terrain block on the Human start (Northshire), where `one`/`One` +/// logs in — so the player sits in the middle of the block instead of Stormwind's edge. +const SPAWN_XY: (f32, f32) = (-8949.95, -132.49); + +/// Build and run the client app. `build` is the launcher shim's compile-time git stamp +/// ([`build_id`]) — passed in as plain data so the sha lives in the shim's fingerprint, not +/// this crate's, and a commit stops recompiling the app (decision 0993). +pub fn run(build: BuildId) -> AppExit { + // `WOW_CAPTURE=list` just prints the harness scenario names (the source of truth `scripts/visual.sh` + // reads) and exits before any window/asset setup. + // `WOW_HOVER_LOG_REPORT=` re-reads a recorded run and prints its report, then exits — + // no window, no game. New analysis lands on runs already captured (see `hover_log`). + if let Ok(path) = std::env::var("WOW_HOVER_LOG_REPORT") { + hover_log::report_recorded_file(&path); + return AppExit::Success; + } + if std::env::var("WOW_CAPTURE").as_deref() == Ok("list") { + capture::print_scenario_names(); + return AppExit::Success; + } + + let mut app = App::new(); + // The stamp is plain data from here on — the panel footer and preflight banner read it back. + app.insert_resource(build); + + // Visual A/B harness (decision 0008): with `$WOW_CAPTURE` set, the app runs a deterministic, + // server-less capture (net off so no NPCs stream in nondeterministically) and exits. See `capture`. + let capturing = capture::scenario_active(); + // Any instrumented run — captures AND the live-probe fleet — opens in the background so it + // never fights the director's screen (decision 0703; `WOW_BG` overrides). See `bgwin`. + let background = bgwin::background_run(); + if capturing { + // Ground clutter scatters with per-run randomness, so disable it for byte-stable baselines + // — clutter isn't what the lighting rework validates, and the regression diff must not be + // masked by grass wobble. Set before plugins build so `ClutterConfig::from_env` reads it. + // It is not the only source of per-run drift, though it was long documented as such: the + // other is the frame clock itself, frozen in `capture` (decision 0723). + std::env::set_var("WOW_CLUTTER_DENSITY", "0"); + // Third source of per-run drift, and the one the lighting matrix (decision 0746) hit: the + // anim-LOD park/wake gate (`creature_anim::lod::gate_rig_animation`). Whether a rig is + // parked, and which pose it wakes into, depends on when its model finished loading relative + // to the frustum/room evaluation — asset-load timing, which the frozen clock does not + // control. MEASURED: the seeded wolf lands in one of exactly two poses, the pair always + // MAE 4.123 apart, and three runs with the gate off are bit-identical. + // + // Off for captures, unless explicitly overridden. A still frame is meant to contain only + // rigs that are IN view, and a rig in view is one the gate should never park — so what a + // capture loses is the gate's own correctness, not the shot's subject. That loss is named, + // not free: a rig wrongly parked in frame stays invisible to the sweep, and the deeper + // question the measurement raises — why a woken rig does not converge on the absolute-clock + // pose the gate promises — is open in 0746 and worth its own hunt. + if std::env::var("WOW_NO_ANIM_LOD").is_err() { + std::env::set_var("WOW_NO_ANIM_LOD", "1"); + } + } + + // The `mpq://` asset source must be registered BEFORE `AssetPlugin` (inside `DefaultPlugins`) + // builds. Reads the same `$WOW_DATA` dir as the `WorldAssets` foundation; on failure the source is + // simply absent and the AdtTile-pipeline loads fail gracefully (the terrain just doesn't appear). + let data_dir = + std::path::PathBuf::from(std::env::var("WOW_DATA").unwrap_or_else(|_| "WoW/Data".into())); + if let Err(e) = benilla_assets::register_mpq_source(&mut app, &data_dir) { + eprintln!("benilla-assets: mpq:// source unavailable ({e:#})"); + } + + app.add_plugins( + DefaultPlugins + .set(WindowPlugin { + primary_window: Some(Window { + title: "benilla".into(), + // UI-fixture captures shrink the window so the docked panel fills the frame — + // the capture is the look-pass instrument and the window is its subject. The + // action bar is the exception: it spans 1024px + 128px end caps along the screen + // bottom, so it gets a wide, short window instead of the tall default (else the + // caps crop). The vplates scenario pins the 1:1 gx window: at 1024×768 one gx + // unit = 1280 px, so the plate must land at the border texture's native + // 128×32 — directly diffable against the decoded BLP. Sized per-capture off + // WOW_CAPTURE. + resolution: if capturing + && std::env::var("WOW_CAPTURE_UI").as_deref() == Ok("1") + { + // `$WOW_WIN` overrides here too — the resolution-A/B instrument for UI + // scenarios (a scale-dependent text bug looks fine at the scenario's + // default size and truncates at fullscreen heights). + if let Some(win) = std::env::var("WOW_WIN").ok().and_then(|v| { + let (w, h) = v.split_once('x')?; + Some(UVec2::new(w.parse().ok()?, h.parse().ok()?)) + }) { + win + } else { + match std::env::var("WOW_CAPTURE").as_deref() { + Ok("ui-actionbar") => UVec2::new(1300, 260), + Ok("vplates") => UVec2::new(1024, 768), + // The director's small-window shape: short enough that the action bar + // strip overlaps the chat edit box rows — the overlap is the subject. + Ok("ui-chatedit") => UVec2::new(566, 377), + // The fullscreen map's chrome is a centered 1024×768 block; a hair of + // margin shows the blackout doing its job. + Ok("ui-worldmap") => UVec2::new(1100, 800), + // The 920×724 era options window wants air on every side so the + // straddling right-edge tile and the hung close X stay in frame. + Ok("ui-options") + | Ok("ui-options-audio") + | Ok("ui-options-graphics") => UVec2::new(1200, 900), + _ => UVec2::new(640, 700), + } + } + .into() + } else { + // `$WOW_WIN=WxH` (logical px): override the world capture/window size — + // the resolution-A/B instrument. The FFXGlow blur geometry is byte-pinned + // in TEXELS, so its angular footprint shrinks as resolution grows and + // thin bright features (fence rails) self-amplify at 4K where the + // 1024-era reference diluted them; matching the era's pixel density + // (e.g. `WOW_WIN=512x288` on a 2× display → 1024×576 physical) isolates + // that term. Also the knob for any future era-resolution comparison. + std::env::var("WOW_WIN") + .ok() + .and_then(|v| { + let (w, h) = v.split_once('x')?; + Some(UVec2::new(w.parse().ok()?, h.parse().ok()?)) + }) + .unwrap_or(UVec2::new(1600, 900)) + .into() + }, + // `$WOW_NOVSYNC=1`: uncap presentation so a headless FPS-journal run measures + // true frame cost, not the vsync ceiling — the same uncap the capture probe + // flips mid-run, available from boot for non-capture probes (perf triage at + // the glue screens, where no capture scenario runs). + present_mode: if std::env::var("WOW_NOVSYNC").as_deref() == Ok("1") { + bevy::window::PresentMode::AutoNoVsync + } else { + bevy::window::PresentMode::default() + }, + // An instrumented run must never fight the director's screen (decision 0703). + // Focused, it steals the keyboard — on 2026-07-19 a login-shot run swallowed + // their keystrokes out of another app and typed them into the account box, + // which is also how that capture lost the bare caret it was taken to measure. + // So every probe/capture/regression run (`bgwin`) opens unfocused; an ordinary + // `cargo run` is unaffected and focuses normally. + // + // A background run is BORN at `AlwaysOnBottom` (`kCGNormalWindowLevel - 1`) + // so it can never flash over their work on the way up — winit raises a new + // window twice before our first frame runs, and at the normal level that + // showed as ~half a second of probe window on top (measured). But it does not + // STAY there: that level is a cage, and 0703 leaving it on for the whole run + // is why an instrumented window could never be raised again however hard you + // clicked it. `BgWinPlugin` promotes it back to Normal the moment the launch + // settles (decision 0709), and owns the app-level half — winit's forced macOS + // app activation — as well. + focused: !background, + window_level: if background { + bevy::window::WindowLevel::AlwaysOnBottom + } else { + bevy::window::WindowLevel::Normal + }, + ..default() + }), + ..default() + }) + // Our file assets (the WGSL shaders, the default UI XML) live beside THIS crate — + // but the binary is built by the `benilla` shim package, so Bevy's runtime + // `CARGO_MANIFEST_DIR` fallback would resolve `assets/` in the shim's dir (and a + // bare-binary run has no manifest dir at all — the silently-no-shaders trap in + // `capture/mod.rs`'s header). Bake this crate's absolute path at compile time: + // right under `cargo run` from any package, and a bare `target/debug/benilla` + // now finds its shaders too. Machine-local, like any dev build (decision 0993). + .set(bevy::asset::AssetPlugin { + file_path: concat!(env!("CARGO_MANIFEST_DIR"), "/assets").into(), + ..default() + }) + // Quiet wgpu/naga; our own crates stay at info. (This filter once also quieted the + // warcraft-rs parsers `wow_m2`/`wow_blp` — retired in-repo by decision 0021; the + // in-repo parsers don't log, so those entries were dead and are gone.) + .set(bevy::log::LogPlugin { + filter: "wgpu=error,naga=warn".into(), + ..default() + }) + // Asset streaming is this client's load bottleneck: every M2/WMO/BLP read decompresses + // from MPQ and parses **synchronously** on Bevy's IO task pool, and the AssetServer runs + // *all* loads there. Bevy's default caps that pool at 4 threads, so a teleport into a + // dense area — terrain + WMOs + their doodad props, all bursting at once — saturates it + // and the net-driven NPC/GameObject models queue behind the flood. Give IO more of the + // box (it sits idle when not streaming); the world-render path is GPU/IO-bound, not + // compute-bound, so trading some compute threads for streaming throughput is the right call. + // Thread QoS (decision 0609): Bevy's workers spawn at default QoS — the same Darwin + // scheduling class as rustc or an OBS encoder — so under a background build the + // frame's own threads queue behind the compiler for P-core time. Promote them at + // spawn: compute runs this frame's systems (user-interactive); IO/async-compute feed + // upcoming frames (user-initiated — above default, below the frame itself). The + // render thread has no spawn hook; `ThreadQosPlugin` promotes it from inside. + .set(TaskPoolPlugin { + task_pool_options: TaskPoolOptions { + io: bevy::app::TaskPoolThreadAssignmentPolicy { + min_threads: 2, + max_threads: 8, + percent: 0.5, + on_thread_spawn: Some(std::sync::Arc::new(|| { + thread_qos::promote_current_thread(thread_qos::QosClass::UserInitiated) + })), + on_thread_destroy: None, + }, + async_compute: bevy::app::TaskPoolThreadAssignmentPolicy { + on_thread_spawn: Some(std::sync::Arc::new(|| { + thread_qos::promote_current_thread(thread_qos::QosClass::UserInitiated) + })), + ..TaskPoolOptions::default().async_compute + }, + compute: bevy::app::TaskPoolThreadAssignmentPolicy { + on_thread_spawn: Some(std::sync::Arc::new(|| { + thread_qos::promote_current_thread( + thread_qos::QosClass::UserInteractive, + ) + })), + // `WOW_THREADS=1` serialises the systems that run this frame. Not a + // performance dial — a **diagnostic**: a defect that alternates frame to + // frame with no camera, geometry or draw-order change behind it is what an + // unordered write between two systems looks like, and that is separable from + // every other cause only by taking the concurrency away. Anything that + // survives `WOW_THREADS=1` is not a race. + max_threads: match std::env::var("WOW_THREADS").ok().as_deref() { + Some("1") => 1, + _ => TaskPoolOptions::default().compute.max_threads, + }, + ..TaskPoolOptions::default().compute + }, + ..default() + }, + }) + // Sound is kira behind our own mixer seam (decision 0070); Bevy's AudioPlugin would + // only open a second, never-used OS output stream at startup. Off (0530). Its + // rodio/cpal stack still compiles in via bevy's default feature — trimming the + // feature set is a separate, wider call. + .disable::(), + ) + .add_plugins(thread_qos::ThreadQosPlugin) + // The app-side half of background instrumented runs: undo winit's forced macOS app + // activation so a probe/capture launch never yanks focus off the director's screen. The + // window-side half (unfocused + always-on-bottom) is in the `Window` above. Decision 0703. + .add_plugins(bgwin::BgWinPlugin) + .add_plugins(MaterialPlugin::::default()) + .add_plugins(MaterialPlugin::::default()) + // Physics (avian3d): collider storage + broadphase BVH + shape-casts for the character + // controller (decision 0009). The streamed terrain/placement entities carry `Collider`s; the + // player drives `MoveAndSlide` against them. WoW gravity (19.29 yd/s², binary-derived — now a + // feel knob, not a fidelity target) replaces avian's 9.81 default. + .add_plugins(PhysicsPlugins::default()) + // One solver substep, not avian's 6: the world has NO dynamic bodies (static terrain, + // kinematic transports/attachments; the player is a shape-cast controller), so the substep + // loop's contact/joint solving iterates over nothing — and kinematic motion integrates + // exactly (constant velocity, no forces) at any substep count. Six substeps were pure + // fixed-tick schedule overhead (~10 substep-schedule runs per frame on the idle-floor + // ledger). Revisit if a dynamic body ever enters the world. + .insert_resource(avian3d::prelude::SubstepCount(1)) + .insert_resource(Gravity(Vec3::NEG_Y * 19.291_105)) + // The per-frame world-transition ordering (Input → Stream → Present) the loading screen relies + // on to cover a teleport the same frame it happens. See `schedule.rs`. + .add_plugins(SchedulePlugin) + // The faithful view distance (`farclip`) — one source of truth for the wall + the per-object + // cull (and, post-split, the stream radius). See `view.rs`. + .init_resource::() + .add_plugins(DebugPanelPlugin) + // World-interaction foundation: mouseover picking + object identity (the debug inspector reads it + // now; hover tooltips / contextual cursor / targeting will later). + .add_plugins(InteractPlugin) + // M2 billboard cards (glow halos, chains) — faced to the camera each frame. + .add_plugins(BillboardPlugin) + // The owned skin palette (decision 0720): every skinned rig's joint matrices, computed by + // us and skinned in wow_model.wgsl — Bevy's SkinnedMesh lane is fully replaced. + .add_plugins(rig_palette::plugin) + // The per-instance body tint (decision 0812), on the same slot index as that palette: the aura + // state kit's CharProc-1 colour, uploaded to its own region of the shared light buffer. + .add_plugins(instance_tint::plugin) + .add_plugins(BowstringPlugin) + .add_plugins(QuestMarkersPlugin) + // Frame-time HUD + diagnostics — the performance standard. + // After DebugPanelPlugin so the egui plugin/context it sets up already exists. Toggle: P. + .add_plugins(PerfPlugin) + // Pipeline-compile counters + the live-compile tripwire (decision 0837: macOS builds every + // pipeline synchronously on the render thread, so a live compile is a felt stall). + .add_plugins(pipe_warm::plugin) + .add_plugins(hover_log::HoverLogPlugin) + .add_plugins(asset_churn::AssetChurnPlugin) + // Within-map art residency (decision 0793): the dedup caches expire by DISTANCE, so a + // long flight inside one map stops ratcheting. Registered before AssetPlugin only so the + // census resource exists for anything that reads it at startup; it needs no ordering. + .add_plugins(art_scope::ArtScopePlugin) + .add_plugins(particles::census::plugin) + // Foundation: opens the patch chain + inserts WorldAssets/RenderConfig (AssetSet::Open), which + // every other subsystem's startup runs after. + .add_plugins(AssetPlugin) + // World-map state (Map.dbc catalog + CurrentMap), loaded right after the chain opens — the + // terrain/WDL streamers, loading screen, and lighting all key off it. + .add_plugins(WorldMapPlugin) + // Time-of-day lighting: sun + WoW shader colors, sky background, per-frame update→apply. + .add_plugins(LightingPlugin) + // Sky dome: the Light.dbc gradient backdrop (camera-centred), driven by the same lighting. + .add_plugins(SkyPlugin) + // WMO skybox: the authored sky a building's `0x40000` group swaps in for that gradient + // (Stratholme's burning city) — registered after SkyPlugin, whose dome it stands down. + .add_plugins(wmo_sky::WmoSkyPlugin) + // Cloud coverage: the reference's procedural field — glare occlusion (occ1) + the visible layer. + .add_plugins(CloudsPlugin) + // Weather: the SMSG_WEATHER state machine driving the storm light-blend + precipitation + // (decision 0310). Lighting reads its densities `.after(WeatherTick)`. + .add_plugins(WeatherPlugin) + // Sun disc + glow halo: the celestial sprites WoW draws at the sun (RE'd from CSky::Render). + .add_plugins(SunPlugin) + // Interior lighting classifier: lights M2 entities (GameObjects/NPCs/other players) standing inside a + // WMO room off the baked floor colour, day/night-independent (the streamer fills its volume registry). + .add_plugins(InteriorPlugin) + .add_plugins(EntityShadePlugin) + // WMO portal visibility: per-frame, decides which of a building's groups are reachable through + // portals from the camera's group, so the Stormwind cathedral culls from the Trade District. Only + // computes the PVS; the Visibility authority (DebugPanelPlugin) applies it (decisions 0025/0031). + .add_plugins(WmoPortalPlugin) + // The exterior scene draws only through portal windows the flood left behind (decision 0774): + // from inside a building, terrain and ADT doodads are gated on the deferred window worklist. + .add_plugins(exterior_cull::ExteriorCullPlugin) + // Streamed world entities: cube assets + display catalogs at startup, sync each frame. + .add_plugins(EntitiesPlugin) + // Creature animation: pick Stand/Walk/Run from each creature's movement state each frame (Milestone C). + .add_plugins(CreatureAnimPlugin) + // The unit blob shadow: the dark ground oval under every unit, sized from the playing + // animation's box (the byte-verified law — wow-re unit-blob-shadow RE), on the same + // surface-decal projector as the selection ring. + .add_plugins(BlobShadowPlugin) + // Doodad animation (decision 0130): placed M2s loop their first sequence + global sequences, + // gated to drawn instances. + .add_plugins(DoodadAnimPlugin) + // GameObject animation (decision 0242): net-streamed GObjects (doors/chests) play an M2 sequence + // on GAMEOBJECT_STATE change — the state-machine sibling of the doodad idle loop above. + .add_plugins(go_anim::plugin) + // Ground clutter: the GroundEffect catalog + the lazy per-chunk build lifecycle, owned + // independently of the terrain streamer (so the streamer can be swapped). Whichever streamer is + // active scatters into the ClutterChunks this builds. + .add_plugins(ClutterPlugin) + // Terrain streaming (the AdtTile pipeline) is added after this chain — see below. + // Distant low-detail terrain (WDL): the fogged horizon hills beyond the streamed tiles. + .add_plugins(WdlPlugin) + // Liquid: animated lake/river/ocean water surfaces (MCLQ), spawned with their terrain tile. + .add_plugins(LiquidPlugin) + // Particle emitters: the additive flames/glows of campfires, torches, braziers (decision 0014). + .add_plugins(ParticlePlugin) + // Water foam decals (CWater0Ripple wake/ring/step-in splash) — the record model, rebuilt from + // the byte RE + two reference-trace reconstructions (decision 0264). + .add_plugins(water_fx::WaterFxPlugin) + .add_plugins(ffx_glow::FfxGlowPlugin) + .add_plugins(RibbonPlugin) + // Avatar + camera + input. + .add_plugins(PlayerPlugin) + // The real client's hardware mouse cursor (native NSCursor on macOS). + .add_plugins(CursorPlugin) + // Stuck-modifier reconciliation: macOS system shortcuts (⇧⌘5) swallow modifier releases + // without a focus loss, wedging every bare-key binding (decision 0606). + .add_plugins(modkeys::ModKeysPlugin) + // Net↔ECS bridge: spawns the world thread, exposes the snapshot + writer resources. In capture + // mode the IO thread is skipped (`connect: false`) so the scene is deterministic. + .add_plugins(NetPlugin { + connect: !capturing, + }) + // The death arc (decision 0308): the wire-fed death stores + the root/water-walk ack messages. + .add_plugins(death::DeathPlugin) + // The shared glue vocabulary both pre-world screens stand on (decision 0465): the ADD-mode UI + // material, the client-data art set, the GlueStrings table. + .add_plugins(glue::GluePlugin) + // The glue layer (decision 0193): the ClientState machine + the character-select screen + // that answers the parked IO thread's pick. Captures boot straight InWorld (no net, no picker). + .add_plugins(char_select::CharSelectPlugin { + start_in_world: capturing, + }) + // The login screen (decision 0539): the faithful AccountLogin glue + the credential policy + // that answers the IO thread's pre-logon park. + .add_plugins(login::LoginPlugin) + // The character-creation screen + its live preview booth (decision 0423). + .add_plugins(char_create::CharCreatePlugin) + // The session preflight (decision 0649): one banner per world entry naming the body we logged + // into, and loud warnings for the states — dead/ghost, GM mode, server-blocked movement — that + // silently invalidate a session's readings. Never env-gated; a warning nobody switches on isn't one. + .add_plugins(preflight::PreflightPlugin) + // The probe shield (decision 0677): a body on a probe account is put into vmangos's `.cheat + // god` on every world entry — damage clamps at 1 hp instead of killing — and GM mode is turned + // OFF, because the shield replaces the only reason it was ever on. Inert on any other account. + .add_plugins(probe_shield::ProbeShieldPlugin) + // Audio: the delegated mixer + WoW's owned selection layer (decision 0070). + .add_plugins(SoundPlugin) + // Targeting: left-click a unit to select it (→ CMSG_SET_SELECTION) + draw its ground ring. + .add_plugins(TargetPlugin) + .add_plugins(TransportPlugin) + // Faithful world-load splash + progress bar on startup + cross-map teleport (the load latency + // streaming can't hide); per-map art via the Map.dbc→LoadingScreens.dbc→BLP chain. + .add_plugins(LoadingScreenPlugin) + // The player-UI quad pass (decision 0068 §2): its own composited-above-the-world, + // below-the-egui-dev-overlays camera + sorted-quad renderer. `$WOW_UI_DEMO=1` seeds a proof scene. + .add_plugins(PlayerUiPlugin) + // The HUD minimap (decision 0203 phase 1): fills the `` widget's extracted hole with + // the streamed tile window + mask + player arrow, and feeds the zone text. + .add_plugins(minimap::MinimapPlugin) + // The shared AreaTable catalog + the ZONE_CHANGED event family / zone-text host globals + // behind GetZoneText & co. (the zone-entry splash arc, decision 0287). + .add_plugins(area::AreaPlugin) + // The `AreaTrigger.dbc` volumes + the per-frame containment check that reports walking into + // one (`CMSG_AREATRIGGER`) — the client's whole part in portals, instance entrances and + // explore objectives; the server owns what each trigger means. + .add_plugins(area_trigger::AreaTriggerPlugin) + .add_plugins(ui_world_map::WorldMapUiPlugin) + // The glyph atlas (client TTFs -> baked bitmap) `ui_script`'s extraction draws `FontString` + // regions through. Loads at Startup, after the asset chain opens (decision 0068 §2). + .add_plugins(UiTextPlugin) + // Unit-frame portraits: the token -> off-screen-baked-face bridge the UI extract samples for a + // `SetPortraitTexture`-bound region (the modern high-res 2D model bake). + .add_plugins(PortraitPlugin) + .add_plugins((TextInputPlugin, UiScriptPlugin)) + // The CVar host (decision 0954): registration, knob sync, config.toml persistence. After + // UiScriptPlugin only for reading order — its systems gate on the VM existing anyway. + .add_plugins(cvars::CvarPlugin) + // The unit snapshot + event feed (decision 0068 §3): pushes ECS game state into the VM as the + // plain data the `Unit*` bindings read, and fires the matching WoW events. + .add_plugins(UiUnitPlugin) + .add_plugins(UiPartyPlugin) + // Duels (decision 0633): the wire session, the client-side countdown tick, the four Era + // events, and the accept/cancel/challenge intents. + .add_plugins(UiDuelPlugin) + // Auto-follow's UI seam: the popup's Follow row + `FollowUnit`/`FollowByName` inbound, and + // the AUTOFOLLOW_BEGIN/END pair that drives the centre-screen status line outbound. + .add_plugins(UiFollowPlugin) + // Leaving (decision 0674): the game menu's Logout/Exit Game — the request, the server's + // 20-second answer narrated as the CAMP/QUIT countdown, and the process exit. + .add_plugins(UiLogoutPlugin) + .add_plugins(UiSocialPlugin) + .add_plugins(UiTooltipPlugin) + // The character-window feed (decision 0208): the combat-stats/inventory snapshots + events + // the paper doll reads, and the paper-doll booth's yaw mirror. + .add_plugins(UiCharPlugin) + // The inspect feed (decision 0631): another player's equipment off their PUBLIC visible-item + // entries, plus the "inspect" booth's unit + yaw. Right after the character feed it mirrors. + .add_plugins(ui_inspect::InspectUiPlugin) + .add_plugins(UiActionPlugin) + // The aura feed (decisions 0255/0257): the player's insertion-ordered buff/debuff cache + the + // self-only durations, pushed as the data the `UnitAura` bindings read; fires UNIT_AURA and + // drains the right-click cancels. After UiActionPlugin (shares its `Spells` catalog). + .add_plugins(UiAuraPlugin) + // The spellbook window feed (decision 0216 §8, slice 5): builds the book from + // PlayerActions.spells through the Spell.dbc/SkillLine.dbc join and drives + // SpellBookFrame.xml's snapshot + cast-drain seam — the spell SOURCE for the cursor payload + // arc (bags/doll/bars/book). After UiActionPlugin (shares its `Spells` resource + the cast + // tail `send_spell_cast`). + .add_plugins(UiSpellbookPlugin) + // The macro system (decision 0983): the icon chooser's catalog, the `benilla/macros/` + // files, `UPDATE_MACROS`, and the macro→bound-spell table the action bar's MACRO slots + // resolve their cooldown/usability through. After UiSpellbookPlugin — the bound spell is + // resolved against the book that feed pushes, by the same law `CastSpellByName` uses. + .add_plugins(ui_macro::UiMacroPlugin) + // The talent window feed (decision 0304): builds the class pages from Talent.dbc × the + // known-spell set + PLAYER_CHARACTER_POINTS, drives TalentFrame.xml through the engine's + // talent seam, and drains learn clicks into CMSG_LEARN_TALENT. After UiActionPlugin + // (shares its `Spells` catalog), beside the spellbook it mirrors. + .add_plugins(UiTalentPlugin) + // The stance/shapeshift bar feed (wow-re shapeshift-bar-api.md): builds the form list from + // PlayerActions.spells per the byte-verified admission/order, drives StanceBar.xml through + // the engine's shapeshift seam, and drains its clicks (cancel-if-active else cast). After + // UiActionPlugin (shares `Spells`, the `usable` walk, and the cast tail). + .add_plugins(UiShapeshiftPlugin) + // The pet action bar (decision 0982) — the stance bar's mirror image: server-authoritative, + // so this renders the ten packed words the last `SMSG_PET_SPELLS` delivered and sends + // intents back. After UiActionPlugin (shares `Spells` and the cooldown triple's clock). + .add_plugins(UiPetPlugin) + // The connection-telemetry feed: the averaged ping RTT behind `GetNetStats()`, which the main + // bar's performance meter polls (decision 0658). + .add_plugins(UiNetPlugin) + .add_plugins(UiCastPlugin) + // The breath / fatigue bars (decision 0874): server-authoritative mirror timers off the + // wire into the transcribed MirrorTimer1/2/3 frames. Beside the cast bar it shares its + // feed→drain shape (and its art: the same UI-CastingBar-Border chrome). + .add_plugins(UiMirrorPlugin) + // Floating combat text (decision 0137 phase 2): the WORLDTEXTSTRING law — world-anchored + // damage numbers/outcome words projected into the UI quad pass each frame. + .add_plugins(combat_text::CombatTextPlugin) + // Overhead unit names (nameplates): world-billboard name text over players + NPCs. + .add_plugins(nameplates::NameplatesPlugin) + // Raid-target marker billboards (0434 §6): the mark icon over marked units, one line-pitch + // above the overhead name; plated units show the plate's raid child instead. + .add_plugins(raid_marks::RaidMarksPlugin) + // V-key nameplates (0167): the toggled health-bar plates, a 2-D overlay replacing the + // overhead name on plated units. + .add_plugins(vplates::VPlatesPlugin) + // Chat speech bubbles (0598): the over-the-head bubble a say/yell/party line spawns, the + // plates' 2-D overlay sibling — mutually exclusive with both the plate and the name. + .add_plugins(chat_bubble::ChatBubblePlugin) + // TOGGLEUI (`CTRL-Z`/`Cmd-Z`): the whole quad layer goes dark — frames, minimap, plates, + // bubbles, combat text — leaving the world and the cursor. + .add_plugins(ui_hide::UiHidePlugin) + .add_plugins(UiItemsPlugin) + // The gossip window (decision 0081): fills from the net drain's GossipState and drives + // GossipFrame.xml over the Era gossip API. + .add_plugins(UiGossipPlugin) + // The merchant window (decision 0081 phase 4): fills from the net drain's MerchantOpen and + // drives MerchantFrame.xml over the Era vendor API + the money display. + .add_plugins(UiMerchantPlugin) + // The bank window (decision 0604): the SHOW_BANK session (BankOpen) + the purchase row; + // the vault's slots ride the container feed as bags −1/5..=10. + .add_plugins(UiBankPlugin) + // The mail window (decision 0544 P1/P2): the client-side mailbox session (MailOpen), the + // NPC-session range guard, and MailFrame.xml over the Era mail API (inbox, open-letter, + // send tab). + .add_plugins(UiMailPlugin) + // Player-to-player trade (TradeFrame.xml): the two-sided trade window, driven server-side over + // the P0 wire; the partner's portrait rides the shared "npc" booth (decision 0592 P1). + .add_plugins(UiTradePlugin) + // The item-text reader (ItemTextFrame.xml): right-clicked bag letters (mail-made permanent + // copies) read in the reference reader window over the shared ask-once item-text cache. + .add_plugins(UiItemTextPlugin) + .add_plugins(UiTrainerPlugin) + // The taxi map (decision 0484 phases 1-2): the SMSG_SHOWTAXINODES-fed TaxiState resource, the + // NPC-session range guard, and the TaxiFrame.xml window feed/drain (catalogs, node + // projection/route computation, the activate send, the UnitOnTaxi ride flag). + .add_plugins(UiTaxiPlugin) + .add_plugins(UiTradeSkillPlugin) + .add_plugins(UiCraftPlugin) + // The loot window (decision 0084): fills from the net drain's LootState and drives + // LootFrame.xml over the Era loot API (coin + rows, paging). + .add_plugins(UiLootPlugin) + .add_plugins(UiLootRollPlugin) + // The questgiver window (decision 0088): fills from the net drain's QuestGiver and drives + // QuestFrame.xml's four sub-panels over the Era quest API. + .add_plugins(UiQuestPlugin) + // The quest-log window (decision 0088's deferred second slice): fills from the self player's + // PLAYER_QUEST_LOG descriptor slots + the SMSG_QUEST_QUERY_RESPONSE template cache, and drives + // QuestLogFrame.xml over the Era quest-log API. + .add_plugins(UiQuestLogPlugin) + .add_plugins(UiChatPlugin); + + // Terrain streaming: the benilla-assets `AdtTile` pipeline — streams tiles around the player through + // the `AssetServer`, owning the terrain mesh/material/collision, doodads/WMOs, liquid, clutter, and + // loading-screen residency. + app.add_plugins(TerrainPlugin); + + // Register benilla-assets' loaders AFTER `AssetPlugin` (they go into the live `AssetServer`). + benilla_assets::register_asset_loaders(&mut app); + + // The capture harness drives one deterministic screenshot then exits — added last so it observes + // the fully-built app. Inert unless `$WOW_CAPTURE` is set. + if capturing { + app.add_plugins(capture::CapturePlugin); + } + // The LIVE probe shot (orthogonal to the harness): `WOW_LIVE_SHOT=` on a NORMAL connected + // run writes one screenshot `WOW_LIVE_SHOT_AT` seconds (default 12) after startup and keeps + // running — the agent-side instrument for seeing a live server scene (NPCs, GameObjects, event + // spawns) without a scenario. Pair with `WOW_USER`/`WOW_CHAR` + an outer `timeout`. + if std::env::var("WOW_LIVE_SHOT").is_ok() { + app.add_plugins(capture::LiveShotPlugin); + } + // The probe RIG (decision 0651): `WOW_RIG="tauren druid 60 gear:heal-preraid-bis"` finds-or- + // creates that body on this slot's probe account, logs in as it, and applies level/spells/gear/ + // spec/place — the one verb that replaces the hand-assembled GM recipe every session used to + // re-derive (see `capture::ProbeRigPlugin`). + if std::env::var("WOW_RIG").is_ok() { + app.add_plugins(capture::ProbeRigPlugin); + } + // Any scripted probe keeps its window un-occludable: a fully covered macOS window drops to + // ~1 fps drawables, and every probe schedule is wall-clock — a throttled run doesn't measure + // slowly, it runs the wrong script (see `capture::ProbeFocusPlugin`, decision 0906). + // (`WOW_LIVE_FPS` is in the list because an occluded SETTLE phase streams the world at ~1 fps + // and under-warms the scene before sampling even starts — the assertion has to be live from + // the first tick, not at the uncap.) + if [ + "WOW_PROBE", + "WOW_PROBE_CHAT", + "WOW_PROBE_KEY", + "WOW_PROBE_LUA", + "WOW_RIG", + "WOW_LIVE_FPS", + ] + .iter() + .any(|k| std::env::var(k).is_ok()) + { + app.add_plugins(capture::ProbeFocusPlugin); + } + // The probe-chat one-shot: `WOW_PROBE_CHAT=".go xyz …"` sends GM/chat lines once in-world — + // the "park the probe character anywhere" instrument (see `capture::ProbeChatPlugin`). + if std::env::var("WOW_PROBE_CHAT").is_ok() { + app.add_plugins(capture::ProbeChatPlugin); + } + // The probe-lua one-shot: `WOW_PROBE_LUA="CastSpell(…)"` runs a chunk in the live UI VM once + // in-world — the "press the button headlessly" instrument (see `capture::ProbeLuaPlugin`). + if std::env::var("WOW_PROBE_LUA").is_ok() { + app.add_plugins(capture::ProbeLuaPlugin); + } + // The probe-key taps: `WOW_PROBE_KEY="Space@14"` presses keys once in-world — the "press + // space headlessly" instrument for input-gated behavior (see `capture::ProbeKeyPlugin`). + if std::env::var("WOW_PROBE_KEY").is_ok() { + app.add_plugins(capture::ProbeKeyPlugin); + } + // The probe self-termination: `WOW_PROBE_EXIT_AT=` bounds any scripted live probe's + // lifetime — its own knob, not a rider on the Lua probe (see `capture::ProbeExitPlugin`). + if std::env::var("WOW_PROBE_EXIT_AT").is_ok() { + app.add_plugins(capture::ProbeExitPlugin); + } + // The ray pick: `WOW_PICK=","` names every surface along the ray through a screenshot + // pixel, nearest first — "what is at the spot `benilla-visual hotspot` flagged, and what is + // right behind it" (see `capture::PickProbePlugin`). + if std::env::var("WOW_PICK").is_ok() { + app.add_plugins(capture::PickProbePlugin); + } + // The render-phase census: `WOW_PHASE=` reports, per frame, which phase each of one + // placement's batches landed in and where in the draw order — the one thing every scene-side + // instrument is blind to, namely whether a surface was submitted at all (see + // `capture::PhaseProbePlugin`). + if std::env::var("WOW_PHASE").is_ok() { + app.add_plugins(capture::PhaseProbePlugin); + } + // The depth readback: `WOW_DEPTH=","` reports what depth actually won each named pixel, + // per frame, as a distance in yards — the link past submission that decides the pixel. Pair it + // with `WOW_PICK` at the same pixels to turn "what won" into "whose it was" (see + // `capture::DepthProbePlugin`). + // `WOW_DEPTH_QUADS=…` is the same readback taken at a particle quad's OWN pixels — the + // moving-subject form, which no hand-written pixel list can hold (see `capture::depth_probe`). + if std::env::var("WOW_DEPTH").is_ok() || std::env::var("WOW_DEPTH_QUADS").is_ok() { + app.add_plugins(capture::DepthProbePlugin); + } + // The bevy_ui node census — "who owns this rectangle" for UI outside the FrameXML quad pass + // (see `capture::NodeProbePlugin`). + if std::env::var("WOW_NODE_PROBE").is_ok() { + app.add_plugins(capture::NodeProbePlugin); + } + // The mid-run window resize: `WOW_PROBE_RESIZE=":x"` — the headless fullscreen- + // toggle stand-in for resize-reactive layout (see `capture::ProbeResizePlugin`). + if std::env::var("WOW_PROBE_RESIZE").is_ok() { + app.add_plugins(capture::ProbeResizePlugin); + } + // The particle census: `WOW_PARTICLE_CENSUS=` prints per-emitter live counts once — + // the trace-comparable coverage number (see `capture::ParticleCensusPlugin`). + if std::env::var("WOW_PARTICLE_CENSUS").is_ok() { + app.add_plugins(capture::ParticleCensusPlugin); + } + // The entity census: `WOW_ENTITY_CENSUS=` prints per-archetype entity counts once — + // what the resident entity count is made of (see `capture::EntityCensusPlugin`). + if std::env::var("WOW_ENTITY_CENSUS").is_ok() { + app.add_plugins(capture::EntityCensusPlugin); + } + // The melee live probe: `WOW_PROBE=melee` auto-fights the nearest enemy so the dbg-trace + // sink can record the combat-text timeline (see `capture::ProbeMeleePlugin`). + if std::env::var("WOW_PROBE").as_deref() == Ok("melee") { + app.add_plugins(capture::ProbeMeleePlugin); + } + // The partner live probe: `WOW_PROBE=partner` auto-accepts group invites — the party arc's + // second-client instrument (decision 0434; see `capture::ProbePartnerPlugin`). + if std::env::var("WOW_PROBE").as_deref() == Ok("partner") { + app.add_plugins(capture::ProbePartnerPlugin); + } + // The sea-crossing live probe: `WOW_PROBE=crossing` boards a cross-continent boat and reports + // the map seam surviving — decision 0455's instrument (see `capture::ProbeCrossingPlugin`). + if std::env::var("WOW_PROBE").as_deref() == Ok("crossing") { + app.add_plugins(capture::ProbeCrossingPlugin); + } + // The taxi-flight live probe: `WOW_PROBE=taxi` opens the flight-master menu on the real wire + // and rides Stormwind → Sentinel Hill to a measured verdict — decision 0484's end-to-end + // instrument (see `capture::ProbeTaxiPlugin`). + if std::env::var("WOW_PROBE").as_deref() == Ok("taxi") { + app.add_plugins(capture::ProbeTaxiPlugin); + } + // The mail-arc live probe: `WOW_PROBE_MAIL=1` GM-mails the probe's own character, opens the + // Goldshire mailbox on the real wire, and drives the inbox/take/send/delete surface through + // the live Lua VM — decisions 0544/0548's end-to-end instrument (see `capture::ProbeMailPlugin`). + if std::env::var("WOW_PROBE_MAIL").is_ok() { + app.add_plugins(capture::ProbeMailPlugin); + } + // The bank-arc live probe: `WOW_PROBE_BANK=1` GM-hops to a pure banker, drives the whole + // six-opcode bank wire (activate/deposit/withdraw/buy-slot/refusal) — decision 0604's + // end-to-end instrument (see `capture::ProbeBankPlugin`). + if std::env::var("WOW_PROBE_BANK").is_ok() { + app.add_plugins(capture::ProbeBankPlugin); + } + // The cast-cancel live probe: `WOW_PROBE=castcancel` hearths and presses W mid-cast — the + // local self-cancel's end-to-end timing instrument (see `capture::ProbeCastCancelPlugin`). + if std::env::var("WOW_PROBE").as_deref() == Ok("castcancel") { + app.add_plugins(capture::ProbeCastCancelPlugin); + } + // The char-create live probe: `WOW_PROBE_CHARCREATE="[,race,class,gender,…]"` creates (and + // cleans up) a character at select to verify the char-create/delete wire (decision 0423 phase 1; + // see `capture::ProbeCharCreatePlugin`). + if std::env::var("WOW_PROBE_CHARCREATE").is_ok() { + app.add_plugins(capture::ProbeCharCreatePlugin); + } + // The live FPS probe: `WOW_LIVE_FPS=` samples frame times on a NORMAL connected run + // and exits — the harness probe's numbers with the live world in (see `capture::LiveFpsPlugin`). + if std::env::var("WOW_LIVE_FPS").is_ok() { + app.add_plugins(capture::LiveFpsPlugin); + } + // The FPS journal: `WOW_FPS_JOURNAL=` appends per-second position + frame-time rows on a + // director-driven run — "where does it dip" as coordinates (see `perf::FpsJournalPlugin`). + if std::env::var("WOW_FPS_JOURNAL").is_ok() { + app.add_plugins(perf::FpsJournalPlugin); + } + + // Return the app's own exit status instead of dropping it: a failed capture writes + // `AppExit::error()` (see `capture::drive_capture`), and discarding it made the process exit 0 + // with no PNG on disk — which is how a sweep carried on around a missing shot (decision 0743). + app.run() +} diff --git a/crates/benilla/src/lighting/daynight.rs b/crates/benilla-app/src/lighting/daynight.rs similarity index 100% rename from crates/benilla/src/lighting/daynight.rs rename to crates/benilla-app/src/lighting/daynight.rs diff --git a/crates/benilla/src/lighting/global_light.rs b/crates/benilla-app/src/lighting/global_light.rs similarity index 100% rename from crates/benilla/src/lighting/global_light.rs rename to crates/benilla-app/src/lighting/global_light.rs diff --git a/crates/benilla/src/lighting/mod.rs b/crates/benilla-app/src/lighting/mod.rs similarity index 100% rename from crates/benilla/src/lighting/mod.rs rename to crates/benilla-app/src/lighting/mod.rs diff --git a/crates/benilla/src/lighting/prop_probes.rs b/crates/benilla-app/src/lighting/prop_probes.rs similarity index 100% rename from crates/benilla/src/lighting/prop_probes.rs rename to crates/benilla-app/src/lighting/prop_probes.rs diff --git a/crates/benilla/src/lighting/resolve.rs b/crates/benilla-app/src/lighting/resolve.rs similarity index 100% rename from crates/benilla/src/lighting/resolve.rs rename to crates/benilla-app/src/lighting/resolve.rs diff --git a/crates/benilla/src/lighting/sh.rs b/crates/benilla-app/src/lighting/sh.rs similarity index 100% rename from crates/benilla/src/lighting/sh.rs rename to crates/benilla-app/src/lighting/sh.rs diff --git a/crates/benilla/src/liquid/mod.rs b/crates/benilla-app/src/liquid/mod.rs similarity index 98% rename from crates/benilla/src/liquid/mod.rs rename to crates/benilla-app/src/liquid/mod.rs index 6ec26c87..aa1beeb9 100644 --- a/crates/benilla/src/liquid/mod.rs +++ b/crates/benilla-app/src/liquid/mod.rs @@ -70,7 +70,7 @@ mod surface; // the against-real-client-files tests — they span both halves // spawned, never named from outside; add them here the day something needs to. pub(crate) use query::{ camera_claim, describe_at, liquid_at, player_claim, surfaces_at, unit_claim, water_surface_at, - FoamPatch, LiquidClaim, LiquidSource, Underwater, WaterChunkInfo, WmoPool, + FoamPatch, LiquidClaim, LiquidSource, RoomPlacements, Underwater, WaterChunkInfo, WmoPool, }; pub(crate) use spatial::WaterIndex; pub(crate) use surface::{spawn_liquids, spawn_wmo_liquids, LiquidAssets, LiquidSoundSource}; diff --git a/crates/benilla/src/liquid/query.rs b/crates/benilla-app/src/liquid/query.rs similarity index 88% rename from crates/benilla/src/liquid/query.rs rename to crates/benilla-app/src/liquid/query.rs index 72a0cc54..e20c46ee 100644 --- a/crates/benilla/src/liquid/query.rs +++ b/crates/benilla-app/src/liquid/query.rs @@ -12,23 +12,51 @@ use bevy::prelude::*; use crate::player::WorldCamera; -use crate::wmo_portal::{CameraInteriorClaim, PlayerWmoRoom, UnitWmoRoom, WmoRoom}; +use crate::wmo_portal::{ + CameraInteriorClaim, PlayerWmoRoom, UnitWmoRoom, WmoPortalInstance, WmoRoom, +}; use benilla_assets::coords::{bevy_to_wow, wow_to_bevy}; use benilla_formats::{LiquidKind, LiquidMesh}; +/// The placements a claim resolves its room's [`flood`](LiquidClaim::Inside) against — the +/// per-group override baked onto [`WmoPortalInstance`] at spawn. +/// +/// Threaded to every claim builder rather than read once into a resource so the answer cannot +/// outlive the placement: a building streams out and its rooms leave with it, which a keyed side +/// table would have to be told about. +pub(crate) type RoomPlacements<'w, 's> = Query<'w, 's, &'static WmoPortalInstance>; + +/// One room's whole-group submersion override, or `None` when the group carries none (5219 of the +/// archive's 5220 groups' worth of the time) or its placement has already streamed out. +fn flood_of(room: WmoRoom, placements: &RoomPlacements) -> Option { + placements + .get(room.instance) + .ok() + .and_then(|i| i.flooded.get(room.group as usize).copied().flatten()) +} + /// The **camera eye's** liquid claim — the reference's `[0xc7b748]` branch in the environment probe /// `0x6809c0`: a containing map-object selects its MLIQ, otherwise the ADT liquid answers. -pub(crate) fn camera_claim(claim: &CameraInteriorClaim) -> LiquidClaim { +pub(crate) fn camera_claim( + claim: &CameraInteriorClaim, + placements: &RoomPlacements, +) -> LiquidClaim { match claim.0 { - Some(c) => LiquidClaim::Inside(c.room), + Some(c) => LiquidClaim::Inside { + room: c.room, + flooded: flood_of(c.room, placements), + }, None => LiquidClaim::Outdoors, } } /// The **player's** liquid claim, from the interior down-ray `wmo_portal` already runs each frame. -pub(crate) fn player_claim(room: &PlayerWmoRoom) -> LiquidClaim { +pub(crate) fn player_claim(room: &PlayerWmoRoom, placements: &RoomPlacements) -> LiquidClaim { match room.0 { - Some(room) => LiquidClaim::Inside(room), + Some(room) => LiquidClaim::Inside { + room, + flooded: flood_of(room, placements), + }, None => LiquidClaim::Outdoors, } } @@ -36,9 +64,12 @@ pub(crate) fn player_claim(room: &PlayerWmoRoom) -> LiquidClaim { /// A **remote unit's** liquid claim, from its own per-unit room. The component is absent only on a /// unit's first frame, before `wmo_portal::track_unit_interiors` has reached it — that, and only /// that, is [`LiquidClaim::Unknown`]. -pub(crate) fn unit_claim(room: Option<&UnitWmoRoom>) -> LiquidClaim { +pub(crate) fn unit_claim(room: Option<&UnitWmoRoom>, placements: &RoomPlacements) -> LiquidClaim { match room.map(UnitWmoRoom::room) { - Some(Some(room)) => LiquidClaim::Inside(room), + Some(Some(room)) => LiquidClaim::Inside { + room, + flooded: flood_of(room, placements), + }, Some(None) => LiquidClaim::Outdoors, None => LiquidClaim::Unknown, } @@ -163,13 +194,45 @@ pub(crate) enum LiquidClaim { /// The subject stands in the open world: the ADT's MCLQ answers, nothing else. Outdoors, /// The subject stands in this placed building: **that placement's** MLIQ answers, nothing else. - Inside(WmoRoom), + Inside { + room: WmoRoom, + /// The room's **whole-group submersion override** — its MOGP `groupLiquid`, when that word + /// is not the `0xf` no-liquid sentinel. + /// + /// Not a surface and not a depth. The client's group probe `0x6b9f10` reads this word + /// *first* and, when it is set, returns a hit unconditionally: the raw value as the kind, + /// `FLT_MAX` as the height, **no Z compare and no MLIQ test at all**. So it rides on the + /// claim rather than on a [`WaterChunkInfo`] — because there is no chunk. All 13 groups in + /// the archive that set it carry no `MLIQ` whatsoever, which is the entire point of the + /// mechanism (wow-re `models/scratch/wmo-liquid-scoping.md` §5; census reproduced with our + /// own reader). + /// + /// This is what makes an underwater cave or a flooded tunnel wet. Five are placed and can + /// be stood in: the Deeprun Tram's two submerged sections, the Prison Oubliette, the MD + /// crypt and the MD mountain cave — all of which read bone dry before decision 1000, + /// because the ADT leg is suppressed indoors and the room offered nothing in its place. + flooded: Option, + }, /// No interior claim has been computed for this subject yet — its first frame, before the /// tracker has run. Both sources answer (the pre-0634 behaviour), which is the wrong answer for /// exactly one frame rather than a silent guess at the right one. Unknown, } +impl LiquidClaim { + /// A claim on a room that carries **no** whole-group override — the shape 5207 of the archive's + /// 5220 groups have, and what every offline scene and test fixture builds. The live claim + /// builders resolve the flood from the placement instead; this is for the callers that have a + /// room and no world to look it up in. + #[cfg(test)] + pub(crate) fn inside(room: WmoRoom) -> Self { + Self::Inside { + room, + flooded: None, + } + } +} + /// One liquid surface as the submersion/swim/foam/sound queries see it: its **grid**, in world WoW /// space with the placement transform already baked in, plus the XY bounds, which file it came /// from, and which liquid it is. Attached to each [`LiquidSurface`]; despawns with its tile, so no @@ -451,8 +514,8 @@ impl WaterChunkInfo { (LiquidClaim::Unknown, _) => true, (LiquidClaim::Outdoors, LiquidSource::AdtChunk) => true, (LiquidClaim::Outdoors, LiquidSource::WmoGroup(_)) => false, - (LiquidClaim::Inside(_), LiquidSource::AdtChunk) => false, - (LiquidClaim::Inside(room), LiquidSource::WmoGroup(pool)) => { + (LiquidClaim::Inside { .. }, LiquidSource::AdtChunk) => false, + (LiquidClaim::Inside { room, .. }, LiquidSource::WmoGroup(pool)) => { pool.owner.is_some_and(|o| o.instance == room.instance) } } @@ -585,6 +648,20 @@ pub(crate) fn liquid_at<'a>( wow: [f32; 3], claim: LiquidClaim, ) -> Option { + // The whole-group override answers FIRST and alone, exactly where `0x6b9f10` reads it: before + // any grid, with no Z compare, at `FLT_MAX`. Not folded in as one more candidate below, because + // `min_by` would then hand the room to any sibling pool that happens to sit lower — and the + // reference never gets that far. A flooded room's answer is the room. + if let LiquidClaim::Inside { + flooded: Some(kind), + .. + } = claim + { + return Some(LiquidHit { + surface_z: f32::MAX, + kind, + }); + } liquids .filter(|w| w.answers(claim, wow[2])) .filter_map(|w| { @@ -620,6 +697,21 @@ pub(crate) fn describe_at<'a>( wow: [f32; 3], claim: LiquidClaim, ) -> Vec { + // A flooded room has NOTHING in the surface list — that is the mechanism, not a failure — so an + // instrument that only walked footprints would print "no liquid here" beside a verdict that + // says submerged. Naming the override is the whole difference between the two readings. + let mut lines = Vec::new(); + if let LiquidClaim::Inside { + room, + flooded: Some(kind), + } = claim + { + lines.push(format!( + "WHOLE-GROUP OVERRIDE g{} ◀ YOURS {kind:?} — the ROOM is submerged (MOGP groupLiquid, \ + no MLIQ grid): surface FLT_MAX, no Z bound, nothing below to sample", + room.group, + )); + } let mut out: Vec<(f32, String)> = liquids .filter(|w| w.contains(wow[0], wow[1])) .map(|w| { @@ -667,7 +759,8 @@ pub(crate) fn describe_at<'a>( }) .collect(); out.sort_by(|a, b| a.0.total_cmp(&b.0)); - out.into_iter().map(|(_, line)| line).collect() + lines.extend(out.into_iter().map(|(_, line)| line)); + lines } /// Every **admitted** liquid surface height over `wow`'s XY under the given claim — the same @@ -811,13 +904,14 @@ pub(super) fn detect_submersion( camera: Query<(&Transform, &Projection), With>, water: Query<&WaterChunkInfo>, eye_claim: Res, + placements: RoomPlacements, time: Res