From bdef9cd7909210957cca82f641fe0cee70371ae9 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 11 Aug 2026 18:13:44 +0700 Subject: [PATCH] benilla-world: the engine becomes its own crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the world engine splits from the game into a benilla-world crate — 112 files, and the wall is ratcheted to zero names of the game inside the engine - the engine's public doorway is counted and gated at 32 items, and the render materials, shaders and asset cache move down to benilla-assets - the game's files leave the engine's asset root — game:// is its own space — and a spike binary boots the engine with no game attached - weather rides along: snowfall emission is denominated in the reference's frame cap, and a long teleport retires the stranded precipitation field instead of leaving it behind Co-Authored-By: Claude Fable 5 --- Cargo.lock | 31 + crates/benilla-app/Cargo.toml | 1 + crates/benilla-app/src/area.rs | 28 +- crates/benilla-app/src/area_trigger.rs | 6 +- crates/benilla-app/src/art_scope.rs | 589 --------------- crates/benilla-app/src/aura_visual.rs | 104 ++- crates/benilla-app/src/aura_visual/tests.rs | 81 ++- crates/benilla-app/src/bindings.rs | 4 +- crates/benilla-app/src/blob_shadow.rs | 76 +- crates/benilla-app/src/bowstring.rs | 2 +- crates/benilla-app/src/capture/depth_probe.rs | 10 +- crates/benilla-app/src/capture/live_shot.rs | 2 +- crates/benilla-app/src/capture/mod.rs | 51 +- crates/benilla-app/src/capture/phase_probe.rs | 22 +- crates/benilla-app/src/capture/pick_probe.rs | 22 +- .../benilla-app/src/capture/probe_crossing.rs | 2 +- crates/benilla-app/src/capture/probe_melee.rs | 2 +- crates/benilla-app/src/capture/probe_taxi.rs | 6 +- .../probes/fx_draw_census.rs} | 4 +- .../src/capture/probes/live_fps.rs | 384 +++------- crates/benilla-app/src/capture/probes/mod.rs | 25 +- .../src/capture/probes/particle_census.rs | 8 +- crates/benilla-app/src/capture/probes/run.rs | 6 +- crates/benilla-app/src/capture/scenarios.rs | 2 +- .../{water_fx/view.rs => capture/waterfx.rs} | 19 +- crates/benilla-app/src/char_create/mod.rs | 2 +- crates/benilla-app/src/char_create/screen.rs | 2 +- crates/benilla-app/src/char_select/mod.rs | 26 +- crates/benilla-app/src/char_select/screen.rs | 2 +- crates/benilla-app/src/chat_bubble.rs | 4 +- crates/benilla-app/src/collision.rs | 86 --- crates/benilla-app/src/combat_text/mod.rs | 6 +- crates/benilla-app/src/cooldowns.rs | 11 +- crates/benilla-app/src/creature_anim.rs | 22 +- crates/benilla-app/src/creature_anim/blood.rs | 2 +- .../benilla-app/src/creature_anim/breath.rs | 11 +- .../benilla-app/src/creature_anim/driver.rs | 22 +- .../src/creature_anim/driver/mode.rs | 4 +- .../src/creature_anim/env_damage.rs | 2 +- .../benilla-app/src/creature_anim/impact.rs | 16 +- crates/benilla-app/src/creature_anim/lod.rs | 28 +- .../benilla-app/src/creature_anim/sheath.rs | 2 +- .../src/creature_anim/spell_visual.rs | 2 +- crates/benilla-app/src/creature_anim/twist.rs | 15 +- crates/benilla-app/src/cursor.rs | 12 +- crates/benilla-app/src/cvars.rs | 4 +- crates/benilla-app/src/death.rs | 4 +- .../src/{interact => debug_panel}/inspect.rs | 77 +- .../src/{interact => debug_panel}/journal.rs | 0 crates/benilla-app/src/debug_panel/mod.rs | 312 +++----- crates/benilla-app/src/debug_panel/world.rs | 26 +- crates/benilla-app/src/entities.rs | 648 +++++++++-------- .../src/entities/attach/char_skin.rs | 150 +--- .../benilla-app/src/entities/attach/dress.rs | 51 +- crates/benilla-app/src/entities/attach/mod.rs | 162 ++--- .../src/entities/attach/preview.rs | 36 +- .../src/entities/attach/redress.rs | 54 +- .../benilla-app/src/entities/carried_light.rs | 6 +- crates/benilla-app/src/entities/chain_beam.rs | 41 +- crates/benilla-app/src/entities/conform.rs | 10 +- crates/benilla-app/src/entities/dest_fx.rs | 6 +- crates/benilla-app/src/entities/display.rs | 278 +------- .../benilla-app/src/entities/equipment/mod.rs | 2 +- .../src/entities/equipment/spawn.rs | 134 ++-- crates/benilla-app/src/entities/item_glow.rs | 6 +- .../benilla-app/src/entities/live_display.rs | 46 +- crates/benilla-app/src/entities/missile.rs | 6 +- crates/benilla-app/src/entities/mount.rs | 6 +- .../src/entities/spell_fx/lifecycle.rs | 6 +- .../benilla-app/src/entities/spell_fx/mod.rs | 118 +-- crates/benilla-app/src/entities/wmo_props.rs | 61 +- crates/benilla-app/src/fishing_line.rs | 6 +- crates/benilla-app/src/footprints.rs | 71 +- crates/benilla-app/src/glue/add_material.rs | 2 +- crates/benilla-app/src/glue/art.rs | 2 +- crates/benilla-app/src/glue/backdrop.rs | 2 +- crates/benilla-app/src/glue/mod.rs | 2 +- crates/benilla-app/src/glue_strings.rs | 2 +- crates/benilla-app/src/go_anim.rs | 2 +- crates/benilla-app/src/interact/mod.rs | 183 ----- crates/benilla-app/src/lib.rs | 490 ++++--------- crates/benilla-app/src/loading_screen.rs | 66 +- crates/benilla-app/src/login/mod.rs | 2 +- crates/benilla-app/src/login/screen.rs | 2 +- crates/benilla-app/src/minimap/mod.rs | 38 +- crates/benilla-app/src/modkeys.rs | 92 --- crates/benilla-app/src/nameplates.rs | 15 +- crates/benilla-app/src/net.rs | 40 +- crates/benilla-app/src/net/apply.rs | 3 +- crates/benilla-app/src/net/apply/chat.rs | 11 +- crates/benilla-app/src/net/apply/combat.rs | 4 +- .../benilla-app/src/net/apply/combat_log.rs | 8 +- crates/benilla-app/src/net/apply/objects.rs | 2 +- crates/benilla-app/src/net/apply/spells.rs | 8 +- crates/benilla-app/src/net/apply/world.rs | 3 +- crates/benilla-app/src/net/io.rs | 24 +- crates/benilla-app/src/net/motion/remote.rs | 28 +- crates/benilla-app/src/net/motion/spline.rs | 54 +- crates/benilla-app/src/perf.rs | 70 +- crates/benilla-app/src/pipe_warm/menagerie.rs | 20 +- crates/benilla-app/src/pipe_warm/mod.rs | 24 +- crates/benilla-app/src/player.rs | 81 +-- crates/benilla-app/src/player/camera.rs | 124 ++-- crates/benilla-app/src/player/camera_saved.rs | 5 +- crates/benilla-app/src/player/land.rs | 8 +- crates/benilla-app/src/player/move_trace.rs | 20 +- crates/benilla-app/src/player/mover.rs | 141 ++-- crates/benilla-app/src/player/probe_cam.rs | 2 +- crates/benilla-app/src/player/server_ride.rs | 2 +- crates/benilla-app/src/player/setup.rs | 21 +- crates/benilla-app/src/player/step_probe.rs | 30 +- crates/benilla-app/src/player/swim.rs | 56 +- crates/benilla-app/src/player/wire_in.rs | 4 +- crates/benilla-app/src/player/world_focus.rs | 129 ++++ crates/benilla-app/src/portrait/booth.rs | 106 +-- crates/benilla-app/src/portrait/dressup.rs | 8 +- crates/benilla-app/src/portrait/glue_booth.rs | 204 ++---- crates/benilla-app/src/portrait/light.rs | 73 +- crates/benilla-app/src/portrait/mod.rs | 58 +- crates/benilla-app/src/portrait/test_bake.rs | 93 +-- crates/benilla-app/src/preflight.rs | 15 +- crates/benilla-app/src/probe_shield.rs | 6 +- crates/benilla-app/src/quest_markers/mod.rs | 78 +- crates/benilla-app/src/raid_marks.rs | 2 +- crates/benilla-app/src/sound/anim_events.rs | 4 +- crates/benilla-app/src/sound/combat.rs | 4 +- crates/benilla-app/src/sound/creature.rs | 4 +- crates/benilla-app/src/sound/emote.rs | 4 +- crates/benilla-app/src/sound/footsteps.rs | 39 +- crates/benilla-app/src/sound/gameobject.rs | 4 +- crates/benilla-app/src/sound/glue.rs | 2 +- crates/benilla-app/src/sound/greeting.rs | 6 +- crates/benilla-app/src/sound/interior.rs | 36 +- crates/benilla-app/src/sound/kit.rs | 8 +- crates/benilla-app/src/sound/liquid_loop.rs | 35 +- crates/benilla-app/src/sound/missile.rs | 4 +- crates/benilla-app/src/sound/mixer.rs | 4 +- crates/benilla-app/src/sound/mod.rs | 11 +- crates/benilla-app/src/sound/money.rs | 2 +- crates/benilla-app/src/sound/mount.rs | 4 +- crates/benilla-app/src/sound/reverb.rs | 16 +- crates/benilla-app/src/sound/sheathe.rs | 4 +- crates/benilla-app/src/sound/spell.rs | 4 +- crates/benilla-app/src/sound/ui.rs | 2 +- crates/benilla-app/src/sound/water.rs | 30 +- crates/benilla-app/src/sound/weather.rs | 4 +- crates/benilla-app/src/sound/zone.rs | 26 +- crates/benilla-app/src/target/highlight.rs | 4 +- crates/benilla-app/src/target/hover.rs | 173 +---- crates/benilla-app/src/target/mod.rs | 13 +- crates/benilla-app/src/target/reticle.rs | 47 +- crates/benilla-app/src/target/ring.rs | 62 +- crates/benilla-app/src/target/scan.rs | 4 +- crates/benilla-app/src/transport.rs | 10 +- crates/benilla-app/src/ui_action/drop_item.rs | 2 +- crates/benilla-app/src/ui_action/mod.rs | 2 +- crates/benilla-app/src/ui_action/state.rs | 10 +- .../src/ui_action/targeting/mod.rs | 2 +- .../src/ui_action/targeting/world.rs | 2 +- crates/benilla-app/src/ui_chat/input/mod.rs | 35 +- crates/benilla-app/src/ui_gamma.rs | 10 +- crates/benilla-app/src/ui_items/mod.rs | 9 +- crates/benilla-app/src/ui_macro/mod.rs | 2 +- crates/benilla-app/src/ui_macro/tests.rs | 4 +- crates/benilla-app/src/ui_pass.rs | 8 +- crates/benilla-app/src/ui_pet_book.rs | 2 +- crates/benilla-app/src/ui_quest_log.rs | 6 +- .../src/ui_script/extract/cooldown.rs | 2 +- .../benilla-app/src/ui_script/extract/mod.rs | 2 +- crates/benilla-app/src/ui_script/mod.rs | 12 +- .../src/ui_script/shipped_xml_tests.rs | 2 +- crates/benilla-app/src/ui_spellbook.rs | 2 +- crates/benilla-app/src/ui_talent.rs | 2 +- crates/benilla-app/src/ui_taxi/routing.rs | 2 +- crates/benilla-app/src/ui_text/atlas.rs | 2 +- crates/benilla-app/src/ui_tradeskill.rs | 2 +- crates/benilla-app/src/ui_unit.rs | 8 +- crates/benilla-app/src/ui_world_map.rs | 16 +- crates/benilla-app/src/view.rs | 122 ---- crates/benilla-app/src/vplates.rs | 6 +- crates/benilla-app/tests/world_api_wall.rs | 675 ++++++++++++++++++ crates/benilla-assets/src/lib.rs | 57 ++ .../src/materials.rs} | 69 +- .../src}/shaders/liquid.wgsl | 0 .../src}/shaders/terrain.wgsl | 0 .../src}/shaders/wdl.wgsl | 0 .../src}/shaders/wow_model.wgsl | 0 crates/benilla-assets/src/spatial_cache.rs | 299 ++++++++ .../src/trace.rs} | 18 +- .../src/world_assets}/images.rs | 20 +- .../src/world_assets}/mod.rs | 204 ++---- crates/benilla-buildstamp/Cargo.toml | 11 + crates/benilla-buildstamp/src/lib.rs | 117 +++ crates/benilla-visual/src/lib.rs | 43 +- crates/benilla-visual/src/main.rs | 14 +- crates/benilla-world/Cargo.toml | 32 + .../assets/shaders/celestial.wgsl | 0 .../assets/shaders/cloud.wgsl | 0 .../assets/shaders/ffx_glow.wgsl | 0 .../assets/shaders/sky.wgsl | 0 .../assets/shaders/star.wgsl | 0 .../assets/shaders/wmo_skybox.wgsl | 0 .../assets/shaders/wow_effect.wgsl | 0 crates/benilla-world/src/art_scope.rs | 318 +++++++++ crates/benilla-world/src/assets/mod.rs | 100 +++ .../src/bgwin.rs | 5 + .../src/billboard.rs | 44 +- crates/benilla-world/src/boot.rs | 102 +++ .../src/build_id.rs | 4 +- .../src/clouds/kernel.rs | 12 +- .../src/clouds/layer.rs | 14 +- .../src/clouds/mod.rs | 4 +- .../src/clouds/tables.rs | 0 .../src/clutter.rs | 20 +- crates/benilla-world/src/collision.rs | 193 +++++ .../src/collision/one_sided.rs | 10 +- .../src/decal.rs | 48 +- .../src/dev_state.rs} | 58 +- .../src/doodad_anim/lazy.rs | 0 .../src/doodad_anim/mat_anim.rs | 38 +- .../src/doodad_anim/mod.rs | 50 +- .../src/entity_shade.rs | 4 +- .../src/exterior_cull.rs | 16 +- .../src/ffx_glow.rs | 21 +- .../src/ground_fx.rs | 42 +- .../src/instance_tint.rs | 20 +- crates/benilla-world/src/interact/mod.rs | 98 +++ .../src/interact/pick.rs | 146 ++++ .../src/interior.rs | 28 +- crates/benilla-world/src/lib.rs | 153 ++++ crates/benilla-world/src/lighting/blob.rs | 235 ++++++ .../src/lighting/daynight.rs | 0 .../src/lighting/global_light.rs | 22 +- .../src/lighting/mod.rs | 83 ++- .../src/lighting/prop_probes.rs | 12 +- .../src/lighting/resolve.rs | 41 +- .../src/lighting/sh.rs | 2 +- .../src/liquid/mod.rs | 14 +- .../src/liquid/query.rs | 41 +- .../src/liquid/real_data.rs | 0 .../src/liquid/spatial.rs | 0 .../src/liquid/surface.rs | 10 +- .../src/map_proj.rs | 29 +- .../src/mesh_tag.rs | 31 +- .../src/model_fade.rs | 204 ++++-- .../src/model_forms.rs | 66 +- .../src/model_render.rs | 178 +++-- .../benilla-world/src/model_render/batch.rs | 374 ++++++++++ .../src/model_render}/visibility.rs | 13 +- crates/benilla-world/src/modkeys.rs | 217 ++++++ .../src/particles.rs | 25 +- .../src/particles/buffer.rs | 142 +++- .../src/particles/depthdump.rs | 0 .../src/particles/dumps.rs | 0 .../src/particles/emit.rs | 54 +- .../src/particles/emitdump.rs | 0 .../src/particles/model.rs | 12 +- .../src/particles/quads.rs | 12 +- .../src/particles/render.rs | 12 +- .../src/particles/sim.rs | 12 +- .../src/ribbons.rs | 8 +- crates/benilla-world/src/rig_anim.rs | 44 ++ .../src/rig_anim}/compose.rs | 8 +- .../src/rig_anim}/global_seq.rs | 8 +- .../src/rig_anim}/pose.rs | 26 +- .../src/rig_palette.rs | 85 ++- .../src/schedule.rs | 55 +- .../{benilla-app => benilla-world}/src/sky.rs | 30 +- .../src/sky_order.rs | 59 +- .../src/sun/follow.rs | 48 +- .../src/sun/materials.rs | 8 +- .../src/sun/mesh.rs | 0 .../src/sun/mod.rs | 4 +- .../src/sun/setup.rs | 4 +- .../src/surface.rs | 32 +- .../src/terrain_stream.rs | 393 ++++++---- .../src/terrain_stream/collider.rs | 20 +- .../src/terrain_stream/furnish.rs | 10 +- .../src/terrain_stream/queries.rs | 24 +- .../src/terrain_stream/spawn/assemble.rs | 12 +- .../src/terrain_stream/spawn/fx.rs | 2 +- .../src/terrain_stream/spawn/mod.rs | 26 +- .../src/terrain_stream/spawn/prop_light.rs | 12 +- .../src/thread_qos.rs | 0 crates/benilla-world/src/view.rs | 231 ++++++ .../src/water_fx/mod.rs | 56 +- .../src/water_fx/params.rs | 10 +- .../{benilla-app => benilla-world}/src/wdl.rs | 36 +- .../src/weather/mod.rs | 37 +- .../src/weather/precip/census.rs | 235 ++++++ .../src/weather/precip/mist.rs | 73 +- .../src/weather/precip/mod.rs | 119 ++- .../src/weather/precip/pool.rs | 233 ++++-- .../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 | 0 .../src/wmo_portal/audit/pin.rs | 2 +- .../src/wmo_portal/fog.rs | 6 +- .../src/wmo_portal/interior.rs | 67 +- .../src/wmo_portal/mod.rs | 56 +- .../src/wmo_portal/probe.rs | 2 +- .../src/wmo_portal/seed.rs | 18 +- .../src/wmo_sky.rs | 6 +- crates/benilla-world/src/world_census.rs | 342 +++++++++ .../src/world_map.rs | 16 +- crates/benilla-world/src/world_plugins.rs | 186 +++++ crates/benilla-world/src/world_point.rs | 326 +++++++++ crates/benilla-world/src/world_unit.rs | 59 ++ crates/benilla-world/src/worldview.rs | 398 +++++++++++ .../src/zfill.rs | 12 +- crates/benilla-worldview/Cargo.toml | 21 + crates/benilla-worldview/build.rs | 5 + crates/benilla-worldview/src/main.rs | 26 + crates/benilla/Cargo.toml | 3 + crates/benilla/build.rs | 114 +-- 316 files changed, 9840 insertions(+), 6121 deletions(-) delete mode 100644 crates/benilla-app/src/art_scope.rs rename crates/benilla-app/src/{particles/census.rs => capture/probes/fx_draw_census.rs} (95%) rename crates/benilla-app/src/{water_fx/view.rs => capture/waterfx.rs} (89%) delete mode 100644 crates/benilla-app/src/collision.rs rename crates/benilla-app/src/{interact => debug_panel}/inspect.rs (89%) rename crates/benilla-app/src/{interact => debug_panel}/journal.rs (100%) delete mode 100644 crates/benilla-app/src/interact/mod.rs delete mode 100644 crates/benilla-app/src/modkeys.rs create mode 100644 crates/benilla-app/src/player/world_focus.rs delete mode 100644 crates/benilla-app/src/view.rs create mode 100644 crates/benilla-app/tests/world_api_wall.rs rename crates/{benilla-app/src/terrain.rs => benilla-assets/src/materials.rs} (88%) rename crates/{benilla-app/assets => benilla-assets/src}/shaders/liquid.wgsl (100%) rename crates/{benilla-app/assets => benilla-assets/src}/shaders/terrain.wgsl (100%) rename crates/{benilla-app/assets => benilla-assets/src}/shaders/wdl.wgsl (100%) rename crates/{benilla-app/assets => benilla-assets/src}/shaders/wow_model.wgsl (100%) create mode 100644 crates/benilla-assets/src/spatial_cache.rs rename crates/{benilla-app/src/dbg_trace.rs => benilla-assets/src/trace.rs} (82%) rename crates/{benilla-app/src/assets => benilla-assets/src/world_assets}/images.rs (94%) rename crates/{benilla-app/src/assets => benilla-assets/src/world_assets}/mod.rs (78%) create mode 100644 crates/benilla-buildstamp/Cargo.toml create mode 100644 crates/benilla-buildstamp/src/lib.rs create mode 100644 crates/benilla-world/Cargo.toml rename crates/{benilla-app => benilla-world}/assets/shaders/celestial.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/cloud.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/ffx_glow.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/sky.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/star.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/wmo_skybox.wgsl (100%) rename crates/{benilla-app => benilla-world}/assets/shaders/wow_effect.wgsl (100%) create mode 100644 crates/benilla-world/src/art_scope.rs create mode 100644 crates/benilla-world/src/assets/mod.rs rename crates/{benilla-app => benilla-world}/src/bgwin.rs (98%) rename crates/{benilla-app => benilla-world}/src/billboard.rs (98%) create mode 100644 crates/benilla-world/src/boot.rs rename crates/{benilla-app => benilla-world}/src/build_id.rs (97%) rename crates/{benilla-app => benilla-world}/src/clouds/kernel.rs (99%) rename crates/{benilla-app => benilla-world}/src/clouds/layer.rs (97%) rename crates/{benilla-app => benilla-world}/src/clouds/mod.rs (99%) rename crates/{benilla-app => benilla-world}/src/clouds/tables.rs (100%) rename crates/{benilla-app => benilla-world}/src/clutter.rs (98%) create mode 100644 crates/benilla-world/src/collision.rs rename crates/{benilla-app => benilla-world}/src/collision/one_sided.rs (99%) rename crates/{benilla-app => benilla-world}/src/decal.rs (79%) rename crates/{benilla-app/src/debug_panel/state.rs => benilla-world/src/dev_state.rs} (64%) rename crates/{benilla-app => benilla-world}/src/doodad_anim/lazy.rs (100%) rename crates/{benilla-app => benilla-world}/src/doodad_anim/mat_anim.rs (95%) rename crates/{benilla-app => benilla-world}/src/doodad_anim/mod.rs (97%) rename crates/{benilla-app => benilla-world}/src/entity_shade.rs (99%) rename crates/{benilla-app => benilla-world}/src/exterior_cull.rs (98%) rename crates/{benilla-app => benilla-world}/src/ffx_glow.rs (97%) rename crates/{benilla-app => benilla-world}/src/ground_fx.rs (91%) rename crates/{benilla-app => benilla-world}/src/instance_tint.rs (95%) create mode 100644 crates/benilla-world/src/interact/mod.rs rename crates/{benilla-app => benilla-world}/src/interact/pick.rs (80%) rename crates/{benilla-app => benilla-world}/src/interior.rs (99%) create mode 100644 crates/benilla-world/src/lib.rs create mode 100644 crates/benilla-world/src/lighting/blob.rs rename crates/{benilla-app => benilla-world}/src/lighting/daynight.rs (100%) rename crates/{benilla-app => benilla-world}/src/lighting/global_light.rs (98%) rename crates/{benilla-app => benilla-world}/src/lighting/mod.rs (85%) rename crates/{benilla-app => benilla-world}/src/lighting/prop_probes.rs (97%) rename crates/{benilla-app => benilla-world}/src/lighting/resolve.rs (96%) rename crates/{benilla-app => benilla-world}/src/lighting/sh.rs (98%) rename crates/{benilla-app => benilla-world}/src/liquid/mod.rs (94%) rename crates/{benilla-app => benilla-world}/src/liquid/query.rs (98%) rename crates/{benilla-app => benilla-world}/src/liquid/real_data.rs (100%) rename crates/{benilla-app => benilla-world}/src/liquid/spatial.rs (100%) rename crates/{benilla-app => benilla-world}/src/liquid/surface.rs (99%) rename crates/{benilla-app => benilla-world}/src/map_proj.rs (95%) rename crates/{benilla-app => benilla-world}/src/mesh_tag.rs (95%) rename crates/{benilla-app => benilla-world}/src/model_fade.rs (89%) rename crates/{benilla-app => benilla-world}/src/model_forms.rs (83%) rename crates/{benilla-app => benilla-world}/src/model_render.rs (89%) create mode 100644 crates/benilla-world/src/model_render/batch.rs rename crates/{benilla-app/src/debug_panel => benilla-world/src/model_render}/visibility.rs (97%) create mode 100644 crates/benilla-world/src/modkeys.rs rename crates/{benilla-app => benilla-world}/src/particles.rs (99%) rename crates/{benilla-app => benilla-world}/src/particles/buffer.rs (83%) rename crates/{benilla-app => benilla-world}/src/particles/depthdump.rs (100%) rename crates/{benilla-app => benilla-world}/src/particles/dumps.rs (100%) rename crates/{benilla-app => benilla-world}/src/particles/emit.rs (90%) rename crates/{benilla-app => benilla-world}/src/particles/emitdump.rs (100%) rename crates/{benilla-app => benilla-world}/src/particles/model.rs (97%) rename crates/{benilla-app => benilla-world}/src/particles/quads.rs (99%) rename crates/{benilla-app => benilla-world}/src/particles/render.rs (98%) rename crates/{benilla-app => benilla-world}/src/particles/sim.rs (99%) rename crates/{benilla-app => benilla-world}/src/ribbons.rs (99%) create mode 100644 crates/benilla-world/src/rig_anim.rs rename crates/{benilla-app/src/creature_anim => benilla-world/src/rig_anim}/compose.rs (99%) rename crates/{benilla-app/src/creature_anim => benilla-world/src/rig_anim}/global_seq.rs (97%) rename crates/{benilla-app/src/creature_anim => benilla-world/src/rig_anim}/pose.rs (98%) rename crates/{benilla-app => benilla-world}/src/rig_palette.rs (94%) rename crates/{benilla-app => benilla-world}/src/schedule.rs (76%) rename crates/{benilla-app => benilla-world}/src/sky.rs (96%) rename crates/{benilla-app => benilla-world}/src/sky_order.rs (73%) rename crates/{benilla-app => benilla-world}/src/sun/follow.rs (96%) rename crates/{benilla-app => benilla-world}/src/sun/materials.rs (95%) rename crates/{benilla-app => benilla-world}/src/sun/mesh.rs (100%) rename crates/{benilla-app => benilla-world}/src/sun/mod.rs (99%) rename crates/{benilla-app => benilla-world}/src/sun/setup.rs (99%) rename crates/{benilla-app => benilla-world}/src/surface.rs (75%) rename crates/{benilla-app => benilla-world}/src/terrain_stream.rs (77%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/collider.rs (96%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/furnish.rs (96%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/queries.rs (94%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/spawn/assemble.rs (99%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/spawn/fx.rs (99%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/spawn/mod.rs (98%) rename crates/{benilla-app => benilla-world}/src/terrain_stream/spawn/prop_light.rs (96%) rename crates/{benilla-app => benilla-world}/src/thread_qos.rs (100%) create mode 100644 crates/benilla-world/src/view.rs rename crates/{benilla-app => benilla-world}/src/water_fx/mod.rs (93%) rename crates/{benilla-app => benilla-world}/src/water_fx/params.rs (98%) rename crates/{benilla-app => benilla-world}/src/wdl.rs (93%) rename crates/{benilla-app => benilla-world}/src/weather/mod.rs (93%) create mode 100644 crates/benilla-world/src/weather/precip/census.rs rename crates/{benilla-app => benilla-world}/src/weather/precip/mist.rs (85%) rename crates/{benilla-app => benilla-world}/src/weather/precip/mod.rs (81%) rename crates/{benilla-app => benilla-world}/src/weather/precip/pool.rs (78%) rename crates/{benilla-app => benilla-world}/src/weather/precip/render.rs (100%) rename crates/{benilla-app => benilla-world}/src/weather/precip/wind.rs (100%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/audit/light_probe.rs (100%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/audit/mod.rs (100%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/audit/pin.rs (99%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/fog.rs (98%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/interior.rs (95%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/mod.rs (97%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/probe.rs (99%) rename crates/{benilla-app => benilla-world}/src/wmo_portal/seed.rs (99%) rename crates/{benilla-app => benilla-world}/src/wmo_sky.rs (99%) create mode 100644 crates/benilla-world/src/world_census.rs rename crates/{benilla-app => benilla-world}/src/world_map.rs (91%) create mode 100644 crates/benilla-world/src/world_plugins.rs create mode 100644 crates/benilla-world/src/world_point.rs create mode 100644 crates/benilla-world/src/world_unit.rs create mode 100644 crates/benilla-world/src/worldview.rs rename crates/{benilla-app => benilla-world}/src/zfill.rs (93%) create mode 100644 crates/benilla-worldview/Cargo.toml create mode 100644 crates/benilla-worldview/build.rs create mode 100644 crates/benilla-worldview/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d0d3a75b..42e6de4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,6 +572,7 @@ name = "benilla" version = "0.1.0" dependencies = [ "benilla-app", + "benilla-buildstamp", ] [[package]] @@ -593,6 +594,7 @@ dependencies = [ "benilla-protocol", "benilla-ui", "benilla-wmo", + "benilla-world", "bevy", "bevy_egui", "bytemuck", @@ -636,6 +638,10 @@ dependencies = [ "texpresso", ] +[[package]] +name = "benilla-buildstamp" +version = "0.1.0" + [[package]] name = "benilla-bytes" version = "0.1.0" @@ -729,6 +735,31 @@ dependencies = [ "benilla-bytes", ] +[[package]] +name = "benilla-world" +version = "0.1.0" +dependencies = [ + "avian3d", + "benilla-assets", + "benilla-formats", + "benilla-protocol", + "benilla-wmo", + "bevy", + "bytemuck", + "image", + "libc", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "benilla-worldview" +version = "0.1.0" +dependencies = [ + "benilla-buildstamp", + "benilla-world", +] + [[package]] name = "bevy" version = "0.18.1" diff --git a/crates/benilla-app/Cargo.toml b/crates/benilla-app/Cargo.toml index a7e5b4fa..bedb17f8 100644 --- a/crates/benilla-app/Cargo.toml +++ b/crates/benilla-app/Cargo.toml @@ -50,6 +50,7 @@ crossbeam-channel = "0.5" benilla-assets = { path = "../benilla-assets" } benilla-formats = { path = "../benilla-formats" } benilla-protocol = { path = "../benilla-protocol" } +benilla-world = { path = "../benilla-world" } # 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"] } diff --git a/crates/benilla-app/src/area.rs b/crates/benilla-app/src/area.rs index 8bd523cd..cae90146 100644 --- a/crates/benilla-app/src/area.rs +++ b/crates/benilla-app/src/area.rs @@ -23,7 +23,7 @@ //! ownerless zone** (nil is structural failure only); `factionName` = FactionGroup.dbc's //! localized Name for the zone's mask bit; pvpType is never "arena"; realm type never enters. //! - **The indoor bit** is the player's faces-only down-ray verdict -//! ([`crate::wmo_portal::CurrentAreaInterior`] — wow-re `zonetext-indoor-bit.md`, the CGLight +//! ([`benilla_world::wmo_portal::CurrentAreaInterior`] — wow-re `zonetext-indoor-bit.md`, the CGLight //! node's `+0x90` bit 0): indoors ⇔ the nearest surface below is a WMO face whose group lacks //! MOGP `0x8` EXTERIOR. The abbey yard is terrain-below ⇒ outdoors; the flip is the doorway. //! - **The indoor naming** (`0x67e670` (d), byte-pinned by (d-ii)): while indoors and the hit @@ -55,11 +55,9 @@ use bevy::prelude::*; use benilla_formats::AreaTableCatalog; use benilla_ui::script::UiScript; -use crate::assets::{AssetSet, LockRecover, WorldAssets}; use crate::net::{ObjectStore, SelfPlayer}; use crate::target::Factions; -use crate::terrain_stream::CurrentArea; -use crate::wmo_portal::CurrentAreaInterior; +use benilla_assets::{AssetSet, LockRecover, WorldAssets}; /// The shared `AreaTable.dbc` catalog: id → (name, parent zone, flags, faction mask). Loaded once /// at Startup; absent if the DBC failed to read (consumers take `Option` and go quiet). @@ -132,9 +130,7 @@ fn elect_event(cache: &ZoneCache, next: &ZoneSignal) -> Option<&'static str> { #[allow(clippy::too_many_arguments)] fn feed_zone_events( script: Option>, - area: Res, - interior: Res, - wmo_areas: Option>, + world: benilla_world::world_point::WorldPoint, areas: Option>, factions: Option>, self_store: Query<&ObjectStore, With>, @@ -143,7 +139,7 @@ fn feed_zone_events( let (Some(mut script), Some(areas)) = (script, areas) else { return; }; - let Some(leaf) = area.0 else { return }; + let Some(leaf) = world.area() else { return }; let Some(leaf_row) = areas.0.get(leaf) else { // An id the catalog doesn't know stays unpublished (keep the last real state, like the // resolver itself does for areaId 0). @@ -157,7 +153,7 @@ fn feed_zone_events( leaf_row.zone_id }; let real_zone_text = areas.0.name(zone).unwrap_or_default().to_string(); - let indoor = interior.0.is_some(); + let indoor = world.area_interior().is_some(); // The outdoor texts — the locals `0x67e670` starts from. let mut zone_text = real_zone_text.clone(); @@ -171,7 +167,7 @@ fn feed_zone_events( // holds; else the default-row name (query A) may override the zone slot and the group-row // name (query B) re-populates the subzone — both EXACT-key lookups (no name-set retry). let mut wmo_group = 0u32; - if let Some((k, cat)) = interior.0.zip(wmo_areas.as_ref()) { + if let Some((k, group, default)) = world.area_interior_rows() { // The dedup key: the hit GROUP's identity (the client's `[groupRec+0x7c]` group index, // nonzero-gated — (d-ii)). We key the MOGP uniqueID scoped by WMO id: it changes exactly // when the hit group does, and — a named divergence — never aliases across two adjacent @@ -184,8 +180,6 @@ fn feed_zone_events( return; // the dedup: same WMO group ⇒ the whole updater is skipped } cache.wmo_id = k.wmo_id; - let group = cat.0.group_row(k.wmo_id, k.name_set, k.group_area_id); - let default = cat.0.default_row(k.wmo_id, k.name_set); // Query A (the whole-WMO −1 row): override the zone name + null the subzone — only when // the resolved name is non-empty and differs from the current subzone (the abbey skip). // An EXISTING-but-unnamed row falls back to an AreaTable name — the row's own @@ -193,7 +187,7 @@ fn feed_zone_events( // "Ironforge"; the client's area-0 arm reads the raw terrain areaId at the node — the // current leaf equals it in that arm, since an area-0 row also never overrode the leaf). // A MISSING row never overrides (the client's `""` sentinel bypass). - let a_name = default.map(|d| { + let a_name = default.as_ref().map(|d| { if !d.name.is_empty() { d.name.clone() } else if d.area_table_id != 0 { @@ -213,7 +207,11 @@ fn feed_zone_events( } } // Query B (the hit group's own row): re-populate the subzone (empty/missing = leave it). - if let Some(b) = group.map(|r| r.name.as_str()).filter(|n| !n.is_empty()) { + if let Some(b) = group + .as_ref() + .map(|r| r.name.as_str()) + .filter(|n| !n.is_empty()) + { subzone_text = b.to_string(); } } @@ -308,7 +306,7 @@ impl Plugin for AreaPlugin { // spurious big splash. feed_zone_events .after(crate::ui_script::UiInput) - .after(crate::terrain_stream::AreaAuthoritySet), + .after(benilla_world::terrain_stream::AreaAuthoritySet), ); } } diff --git a/crates/benilla-app/src/area_trigger.rs b/crates/benilla-app/src/area_trigger.rs index d903e61b..1a3e9645 100644 --- a/crates/benilla-app/src/area_trigger.rs +++ b/crates/benilla-app/src/area_trigger.rs @@ -50,11 +50,11 @@ use benilla_assets::coords::bevy_to_wow; use benilla_formats::AreaTriggerCatalog; use bevy::prelude::*; -use crate::assets::{AssetSet, LockRecover, WorldAssets}; use crate::net::{ClientCommand, NetCommands}; use crate::player::Player; -use crate::schedule::WorldStage; -use crate::world_map::CurrentMap; +use benilla_assets::{AssetSet, LockRecover, WorldAssets}; +use benilla_world::schedule::WorldStage; +use benilla_world::world_map::CurrentMap; /// The `AreaTrigger.dbc` catalog, bucketed by map. Absent when the client data didn't load — the /// check then does nothing, like every other data-driven system here. diff --git a/crates/benilla-app/src/art_scope.rs b/crates/benilla-app/src/art_scope.rs deleted file mode 100644 index 61c7b8ac..00000000 --- a/crates/benilla-app/src/art_scope.rs +++ /dev/null @@ -1,589 +0,0 @@ -//! **Within-map art residency** — the distance half of decision 0729's map-scope teardown. -//! -//! 0729 gave the world-art dedup caches a *reset point*: a cross-map transition clears them, and the -//! loading screen pays for the rebuild. B131 is the case that argument does not reach — a long flight -//! **inside** one map never fires that transition, so nothing ever evicts and `mats`/`images` ratchet -//! for as long as you stay on a continent (decision 0785's open half: 26.8 k materials / 2.8 k images -//! after ten minutes here; 3.2 → 4.9 GiB of VRAM on the reporter's tour). -//! -//! This module turns clear-on-*transition* into clear-on-*distance*: every cache entry remembers the -//! view focus it was last used from, and a sweep drops what was last wanted more than -//! [`DEFAULT_RADIUS_YD`] away. So residency is bounded by *where you are*, not by how long the process -//! has run, and the map-change clear stays exactly as it was — the hard reset on top of the soft one. -//! -//! **Distance decides what expires; a dwell floor decides how soon.** [`MIN_DWELL_SECS`] exists -//! because the radius alone is sized for gameplay speed — the detached free-fly camera crosses it in -//! five seconds and would lose a city's dedup only to rebuild it on the way back (found on the -//! director's first real run; see the constant). -//! -//! **Distance, not a TTL**, and the difference is safety rather than taste: a time-expired entry can -//! be one the streamer is about to ask for again from ten yards away, and dropping it costs a -//! duplicate; a distance-expired entry is one whose tiles unloaded long ago. The radius is therefore -//! floored at the streamer's own reach ([`radius_floor`]) so eviction can never outrun the thing that -//! placed the art. -//! -//! **Two properties worth knowing before reading the code**, both of which buy the design its -//! smallness: -//! -//! 1. **Stamping needs no plumbing.** A *use* (a [`SpatialCache::fetch`] hit or an insert) clears the -//! stamp to `None` — "used since the last sweep" — and the *sweep* is what writes today's focus -//! onto every `None`. So no call site has to know where the camera is, and a cache nothing sweeps -//! (the `Local`s in the glue booth and the portrait bake) simply never expires. -//! 2. **Nothing needs ordering.** The stamp is at worst one sweep interval stale (≈25 yd at flight -//! speed, against a 2.6 km radius) and `due` being read a frame before it is set only moves the -//! sweep one frame. There are no `.before`/`.after` constraints anywhere in this module. -//! -//! The failure mode if the radius were ever too small: evicting the dedup for art that is still -//! drawn. That is not a visual bug and not a leak — the drawn entity holds its own handles, and the -//! next spawn of that art re-creates one duplicate material which becomes the new cache entry. It -//! costs a lost batch, and it self-heals. That asymmetry (too-eager = mild and transient, too-lazy = -//! the bug we are fixing) is why the floor is a clamp and not a warning. - -use std::collections::HashMap; -use std::hash::Hash; - -use bevy::ecs::system::SystemParam; -use bevy::prelude::*; -use bevy::time::Real; - -use benilla_formats::TILE_SIZE; - -/// Wall seconds between sweeps ([`Real`], not the virtual clock — this is a housekeeping cadence, -/// and decision 0789's reading applies: "what time is it" is not "how much world time to advance"). -/// One `retain` over ~30 k entries a second is not measurable; the interval exists so it is not run -/// per frame, and it doubles as the stamp's resolution. -const SWEEP_SECS: f32 = 1.0; - -/// Default eviction radius in yards — five ADT tiles (2667 yd). Chosen as the smallest round -/// multiple of the tile grid comfortably clear of [`radius_floor`] at the default -/// `$WOW_TILE_RADIUS` (2 ⇒ 2419 yd): far enough that art still on screen is never evicted, close -/// enough that leaving one city for another recovers it (Stormwind → Booty Bay is ~5500 yd, -/// Ironforge → Stormwind ~4250). -const DEFAULT_RADIUS_YD: f32 = 5.0 * TILE_SIZE; - -/// The **dwell floor**: nothing is dropped within this many wall seconds of its last use, however far -/// away you have gone. Distance decides *what* expires; this decides how soon it is allowed to. -/// -/// Found by the director's first real run, and it is the case the distance rule alone gets wrong. The -/// detached free-fly camera (`F`, `FlyCam::speed` 40–100 yd/s, ×5 on Ctrl) travels at up to 500 yd/s — -/// so it crosses the 2667 yd radius in **five seconds**, and a sweep then drops a whole city's dedup -/// that the same camera re-creates on the way back. Their 48-second leg evicted 25 570 entries and -/// rebuilt `pmat` 3086 → 15354 doing exactly that: the eviction was *correct* by the radius and -/// useless in effect, because nothing had finished being wanted. -/// -/// 30 s covers a 15 000 yd round trip at boosted fly speed. Under gameplay speed it never binds at -/// all (a gryphon needs ~107 s to cover the radius), so B131's own case is unchanged — this only -/// stops the thrash at speeds the radius was never sized for. Residency stays bounded either way: -/// what you hold is at most the art of the last 30 seconds of travel, which does not grow with how -/// long the process has run. -const MIN_DWELL_SECS: f32 = 30.0; - -/// The smallest radius that cannot evict art the streamer is still holding: the far **corner** of -/// the `(2r+1)²` tile block it keeps resident, plus a tile of margin. -/// -/// A stamp records where *the viewer* was when the art was last wanted, not where the art is — so an -/// entry can be up to a streaming radius "behind" the art it belongs to. Flooring the radius at the -/// block's circumscribed radius means you have to leave the art's neighbourhood entirely before its -/// dedup expires. -fn radius_floor(tile_radius: u32) -> f32 { - (tile_radius as f32 + 0.5) * TILE_SIZE * std::f32::consts::SQRT_2 + TILE_SIZE -} - -/// Resolve `$WOW_ART_RADIUS` (yards; `0` or negative ⇒ eviction off, the A/B leg that reproduces the -/// unbounded behaviour on a fixed build) against the streamer's reach. An explicitly-requested radius -/// below [`radius_floor`] is raised **and** warned about, rather than silently obeyed: a knob that -/// quietly means something else than it says is how decision 0789 happened. -fn radius_from_env(tile_radius: u32) -> f32 { - let floor = radius_floor(tile_radius); - match std::env::var("WOW_ART_RADIUS") - .ok() - .and_then(|s| s.parse::().ok()) - { - Some(r) if r <= 0.0 => 0.0, - Some(r) if r < floor => { - warn!( - "art-scope: WOW_ART_RADIUS={r} is inside the streamer's own reach ({floor:.0} yd \ - at tile radius {tile_radius}) — using {floor:.0}" - ); - floor - } - Some(r) => r, - None => DEFAULT_RADIUS_YD.max(floor), - } -} - -/// Horizontal (tile-plane) distance between two wow-space points. Height is deliberately ignored: -/// residency is a 2D question — the streamer's window is a tile square, and art 200 yd below a -/// gryphon is in front of you, not away from you. -fn plan_dist(a: [f32; 3], b: [f32; 3]) -> f32 { - let (dx, dy) = (a[0] - b[0], a[1] - b[1]); - (dx * dx + dy * dy).sqrt() -} - -/// A dedup cache whose entries remember **where they were last used from**, so a sweep can drop the -/// art of a place you have left. A drop-in for the `HashMap>` these caches used to be: -/// the only difference at a call site is that a lookup is [`Self::fetch`] (it counts as a use). -/// -/// `None` in the stamp means "used since the last sweep" — see the module docs for why that inverted -/// spelling is what removes all the plumbing. -pub(crate) struct SpatialCache { - map: HashMap)>, -} - -/// Where and when an entry was last wanted: the view focus at the first sweep after its last use. -/// Both halves are load-bearing — `at` decides whether it is far enough to expire, `t` whether it has -/// been idle long enough to be allowed to ([`MIN_DWELL_SECS`]). -#[derive(Clone, Copy)] -struct Stamp { - at: [f32; 3], - t: f32, -} - -impl Default for SpatialCache { - fn default() -> Self { - Self { - map: HashMap::new(), - } - } -} - -impl SpatialCache { - /// Live entry count — what the census reports and the map-change evictors log. - pub(crate) fn len(&self) -> usize { - self.map.len() - } - - /// Drop everything (the map-scope teardown, decision 0729 — still the hard reset). - pub(crate) fn clear(&mut self) { - self.map.clear(); - } - - /// Fetch a deduped value, **counting the hit as a use** so it restarts its distance grace. - /// Clones out rather than lending a reference: every caller wants an owned `Handle` anyway, and - /// ending the borrow at the call is what lets the miss path insert in the same expression. - pub(crate) fn fetch(&mut self, key: &K) -> Option - where - V: Clone, - { - let slot = self.map.get_mut(key)?; - slot.1 = None; - Some(slot.0.clone()) - } - - /// Install a freshly-built value, counted as a use. - pub(crate) fn insert(&mut self, key: K, value: V) { - self.map.insert(key, (value, None)); - } - - /// Get-or-build in place, counted as a use — for the caches whose value is too big to clone out - /// ([`crate::clutter::ClutterGeometry`]'s decoded submeshes). - pub(crate) fn or_insert_with(&mut self, key: K, build: impl FnOnce() -> V) -> &mut V { - let slot = self.map.entry(key).or_insert_with(|| (build(), None)); - slot.1 = None; - &mut slot.0 - } - - /// Stamp everything used since the last sweep at `focus`/`now`, then drop every entry that is - /// **both** more than `radius` yards away **and** idle for at least [`MIN_DWELL_SECS`]. Returns - /// how many were dropped. - /// - /// The two conditions are not redundant. Distance alone expires art the moment a fast camera - /// pulls away from it, which is how the director's free-fly leg lost a city's dedup and rebuilt - /// it seconds later; the dwell floor is what makes "not immediate" true at any speed, not just at - /// gameplay speed. - pub(crate) fn scope(&mut self, focus: [f32; 3], radius: f32, now: f32) -> usize { - let before = self.map.len(); - self.map.retain(|_, (_, stamp)| match *stamp { - None => { - *stamp = Some(Stamp { at: focus, t: now }); - true - } - Some(s) => plan_dist(s.at, focus) <= radius || now - s.t < MIN_DWELL_SECS, - }); - before - self.map.len() - } -} - -/// Which cache a census row is about. Fixed set, in journal-column order — the CSV's columns only -/// ever grow at the end, so this enum's order is part of the file format. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) enum ArtSlot { - /// `Placements::materials` — streamed doodad/WMO submesh materials. - PlaceMats, - /// `EntityMaterials` — creature/player/GameObject model materials. - EntityMats, - /// `SkinComposites` — composited character body atlases (decision 0044). - Skins, - /// `WorldAssets::model_materials` — the ground-clutter material dedup. - ClutterMats, - /// `WorldAssets::textures` — decoded world BLPs by (path, wrap). - Textures, - /// `ClutterGeometry` — CPU submesh copies of every detail M2 decoded. - ClutterGeo, -} - -impl ArtSlot { - /// Every slot, in journal-column order. - pub(crate) const ALL: [ArtSlot; 6] = [ - ArtSlot::PlaceMats, - ArtSlot::EntityMats, - ArtSlot::Skins, - ArtSlot::ClutterMats, - ArtSlot::Textures, - ArtSlot::ClutterGeo, - ]; - - /// The journal's column name for this slot. - pub(crate) fn column(self) -> &'static str { - match self { - ArtSlot::PlaceMats => "pmat", - ArtSlot::EntityMats => "emat", - ArtSlot::Skins => "skin", - ArtSlot::ClutterMats => "cmat", - ArtSlot::Textures => "tex", - ArtSlot::ClutterGeo => "cgeo", - } - } - - fn idx(self) -> usize { - match self { - ArtSlot::PlaceMats => 0, - ArtSlot::EntityMats => 1, - ArtSlot::Skins => 2, - ArtSlot::ClutterMats => 3, - ArtSlot::Textures => 4, - ArtSlot::ClutterGeo => 5, - } - } -} - -/// Per-cache residency, refreshed by every [`ArtScope::apply`] call — the instrument that localizes -/// a residency ratchet to **one cache** in a single journal row, instead of the run-length probe that -/// found B131. `Assets::len()` says "materials are growing"; this says which map is holding them. -#[derive(Resource, Default)] -pub(crate) struct ArtCensus { - live: [usize; ArtSlot::ALL.len()], - dropped: [usize; ArtSlot::ALL.len()], -} - -impl ArtCensus { - /// Live entries in one cache as of its last sweep. - pub(crate) fn live(&self, slot: ArtSlot) -> usize { - self.live[slot.idx()] - } - - /// Entries dropped by distance across every cache since the run began. - pub(crate) fn dropped_total(&self) -> usize { - self.dropped.iter().sum() - } -} - -/// The sweep's shared state: the radius, this tick's focus, and whether this frame is a sweep frame. -#[derive(Resource, Default)] -pub(crate) struct ArtScopeState { - /// Eviction radius in yards; `0` ⇒ never evict. Zero until [`configure_art_scope`] runs, so - /// nothing can be dropped before the streamer's reach is known. - radius: f32, - /// The view focus, wow coords ([`crate::terrain_stream::view_focus`] — the same ladder the - /// streamer and the WDL ring use). `None` when there is no focus at all (no avatar, no camera), - /// which suspends both stamping and sweeping. - focus: Option<[f32; 3]>, - /// Set on the one frame per [`SWEEP_SECS`] that sweeps. - due: bool, - last_sweep: f32, - /// Wall seconds since app start, this frame — the clock the dwell floor is measured on. - now: f32, -} - -impl ArtScopeState { - /// The view focus this frame, wow coords — what the sweep measures from. Exposed for the FPS - /// journal: its `x,y,z` are the *avatar*, which stands still through a whole detached free-fly, so - /// on that leg the position columns say nothing about where the art was being asked for. This is - /// the column that does. - pub(crate) fn focus(&self) -> Option<[f32; 3]> { - self.focus - } -} - -/// What an owning module needs to scope its caches: the policy, plus the census to report into. -#[derive(SystemParam)] -pub(crate) struct ArtScope<'w> { - state: Res<'w, ArtScopeState>, - census: ResMut<'w, ArtCensus>, -} - -impl ArtScope<'_> { - /// Sweep one cache (on sweep frames) and record its residency. Safe to call for a cache that is - /// empty, unconfigured, or in a run with eviction switched off — it degrades to a census read. - pub(crate) fn apply(&mut self, cache: &mut SpatialCache, slot: ArtSlot) { - let dropped = match self.state.focus { - Some(focus) if self.state.due && self.state.radius > 0.0 => { - cache.scope(focus, self.state.radius, self.state.now) - } - _ => 0, - }; - let i = slot.idx(); - self.census.live[i] = cache.len(); - self.census.dropped[i] += dropped; - if dropped > 0 { - debug!( - "art-scope: dropped {dropped} {} entries beyond {:.0} yd ({} live)", - slot.column(), - self.state.radius, - cache.len() - ); - } - } -} - -/// Resolve the radius once the streamer's configuration exists (it decides the floor). -fn configure_art_scope( - mut state: ResMut, - cfg: Option>, -) { - let tile_radius = cfg.map(|c| c.tile_radius).unwrap_or(2); - state.radius = radius_from_env(tile_radius); - if state.radius > 0.0 { - info!( - "art-scope: within-map art evicts beyond {:.0} yd of the view focus", - state.radius - ); - } else { - warn!("art-scope: eviction OFF ($WOW_ART_RADIUS=0) — within-map residency is unbounded"); - } -} - -/// Publish this frame's focus and decide whether it is a sweep frame. Deliberately unordered against -/// every consumer — see the module docs. -fn track_art_scope( - mut state: ResMut, - time: Res>, - player: Res, - camera: Query<&Transform, With>, - roster: Option>, -) { - let cam = camera.single().ok().map(|c| c.translation); - state.focus = (player.active || cam.is_some()).then(|| { - crate::terrain_stream::view_focus( - &player, - cam, - roster - .as_deref() - .and_then(crate::char_select::Roster::pending_entry), - ) - }); - let now = time.elapsed_secs(); - state.now = now; - state.due = now - state.last_sweep >= SWEEP_SECS; - if state.due { - state.last_sweep = now; - } -} - -/// Owns the sweep policy + the census. The caches themselves stay private to their modules; each -/// registers its own three-line system that hands its caches to [`ArtScope::apply`]. -pub(crate) struct ArtScopePlugin; - -impl Plugin for ArtScopePlugin { - fn build(&self, app: &mut App) { - app.init_resource::() - .init_resource::() - .add_systems( - Startup, - configure_art_scope.after(crate::assets::AssetSet::Open), - ) - .add_systems(Update, track_art_scope); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// When the art is used and stamped. - const NOW: f32 = 1000.0; - /// A sweep past the dwell floor — the earliest moment distance is *allowed* to expire anything. - /// Every "…is dropped" assertion sweeps here, which is itself the dwell floor's regression test: - /// swap this back to `NOW` and three of them fail (verified). - const LATER: f32 = NOW + MIN_DWELL_SECS + 1.0; - const HOME: [f32; 3] = [0.0, 0.0, 0.0]; - const FAR: [f32; 3] = [4000.0, 0.0, 0.0]; - const R: f32 = DEFAULT_RADIUS_YD; - - /// A fresh entry survives its first sweep — that is the sweep that *stamps* it. Without this the - /// grace period would depend on where in the second the art happened to load. - #[test] - fn the_first_sweep_stamps_rather_than_judges() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - // Swept from the far side immediately: the entry has no stamp yet, so it cannot be too far. - assert_eq!(c.scope(FAR, R, NOW), 0); - assert_eq!(c.len(), 1); - // Now it is stamped at FAR, so a later sweep from HOME drops it. - assert_eq!(c.scope(HOME, R, LATER), 1); - assert!(c.fetch(&1).is_none()); - } - - #[test] - fn art_you_walked_away_from_is_dropped() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - c.scope(HOME, R, NOW); // stamp at home - assert_eq!( - c.scope([R * 0.9, 0.0, 0.0], R, LATER), - 0, - "inside the radius" - ); - assert_eq!(c.scope(FAR, R, LATER), 1, "beyond it"); - } - - /// A hit restarts the grace, which is what keeps art in the direction of travel resident: the - /// same tree material is re-fetched by every new placement that streams in ahead of you. - #[test] - fn a_use_restarts_the_grace() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - c.scope(HOME, R, NOW); - for step in 1..20 { - let at = [step as f32 * R * 0.5, 0.0, 0.0]; - assert_eq!(c.fetch(&1), Some(10)); - // Swept well past the dwell floor every step, so what keeps it alive is the use. - assert_eq!( - c.scope(at, R, NOW + step as f32 * (MIN_DWELL_SECS + 1.0)), - 0, - "re-used at step {step}" - ); - } - } - - /// `or_insert_with` builds once and counts as a use, like `fetch`. - #[test] - fn or_insert_with_builds_once_and_counts_as_a_use() { - let mut c: SpatialCache> = SpatialCache::default(); - let mut builds = 0; - for _ in 0..3 { - let v = c.or_insert_with(7, || { - builds += 1; - vec![1, 2, 3] - }); - assert_eq!(v.len(), 3); - } - assert_eq!(builds, 1); - assert_eq!( - c.scope(FAR, R, LATER), - 0, - "the last use restarted the grace" - ); - } - - /// **The director's free-fly case.** A camera at boosted fly speed crosses the radius in ~5 s; if - /// distance alone decided, the art it just streamed would be dropped and then rebuilt as the same - /// camera came back. Nothing expires inside the dwell floor however far away the focus goes. - #[test] - fn a_fast_camera_cannot_outrun_the_dwell_floor() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - c.scope(HOME, R, NOW); - // 500 yd/s (FlyCam 100 × the Ctrl boost 5) for five seconds: far past the radius already. - for s in 1..=5 { - let at = [500.0 * s as f32, 0.0, 0.0]; - assert_eq!( - c.scope(at, R, NOW + s as f32), - 0, - "dropped {s} s out — the free-fly thrash" - ); - } - // Fly all the way back inside the radius and the entry is still the same one: no rebuild. - assert_eq!(c.scope(HOME, R, NOW + 6.0), 0); - assert_eq!(c.fetch(&1), Some(10)); - // Stay away past the floor and it does expire — the bound is delayed, not removed. - c.scope(HOME, R, NOW + 7.0); - assert_eq!(c.scope(FAR, R, NOW + 7.0 + MIN_DWELL_SECS), 1); - } - - /// Height is not distance: flying over a city must not evict it. - #[test] - fn altitude_is_not_distance() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - c.scope(HOME, R, NOW); - assert_eq!(c.scope([0.0, 0.0, 10_000.0], R, LATER), 0); - } - - /// Eviction must never outrun the streamer: the default radius clears the far corner of the - /// resident tile block at the default `$WOW_TILE_RADIUS`, and the floor rises with it. - #[test] - fn the_radius_clears_the_streamers_own_reach() { - assert!( - DEFAULT_RADIUS_YD > radius_floor(2), - "default {DEFAULT_RADIUS_YD} must clear the floor {}", - radius_floor(2) - ); - // The corner of the 5×5 block at tile radius 2 is ~1886 yd; the floor adds a tile. - assert!( - (radius_floor(2) - 2418.7).abs() < 1.0, - "{}", - radius_floor(2) - ); - assert!( - radius_floor(4) > DEFAULT_RADIUS_YD, - "a wider window raises it" - ); - } - - /// With `$WOW_ART_RADIUS` unset (every ordinary run, and the test binary) the radius is the - /// default, floored — never zero, so a shipped build always bounds itself. - #[test] - fn an_unconfigured_run_still_bounds_itself() { - assert_eq!(radius_from_env(2), DEFAULT_RADIUS_YD.max(radius_floor(2))); - assert!(radius_from_env(8) >= radius_floor(8)); - } - - /// **The mechanism the whole design rests on**: dropping the cache's handle really does free the - /// asset. If `Assets` kept the value alive past its last strong handle, eviction would cost us - /// the dedup and recover no memory — a failure indistinguishable, from the outside, from "the fix - /// did nothing". Checked here rather than assumed, because the *reason* B131's `images` column - /// never fell is precisely that these caches held the last handle. - #[test] - fn dropping_the_last_handle_frees_the_asset() { - let mut app = App::new(); - app.add_plugins(( - bevy::app::TaskPoolPlugin::default(), - bevy::asset::AssetPlugin::default(), - )) - .init_asset::(); - let mut cache: SpatialCache> = SpatialCache::default(); - let id = { - let mut images = app.world_mut().resource_mut::>(); - let handle = images.add(Image::default()); - let id = handle.id(); - cache.insert(1, handle); - id - }; - app.update(); - assert!( - app.world().resource::>().contains(id), - "held by the cache" - ); - cache.scope(HOME, R, NOW); // stamp it here… - assert_eq!(cache.scope(FAR, R, LATER), 1, "…then walk away"); - // The drop is processed by `Assets::track_assets` on a later frame, not at the drop itself. - for _ in 0..3 { - app.update(); - } - assert!( - !app.world().resource::>().contains(id), - "the image outlived its last handle — eviction would free nothing" - ); - } - - /// A cache nobody sweeps (the `Local`s in the glue booth / portrait bake) expires - /// nothing: without a `scope` call the stamps stay `None` forever. - #[test] - fn an_unswept_cache_never_expires() { - let mut c: SpatialCache = SpatialCache::default(); - c.insert(1, 10); - assert_eq!(c.fetch(&1), Some(10)); - assert_eq!(c.len(), 1); - } -} diff --git a/crates/benilla-app/src/aura_visual.rs b/crates/benilla-app/src/aura_visual.rs index f224f23f..f7e402c1 100644 --- a/crates/benilla-app/src/aura_visual.rs +++ b/crates/benilla-app/src/aura_visual.rs @@ -27,7 +27,7 @@ //! and player vtables: "players 1.0" was the data talking, not a type fork — §6) — and handed //! to **`0x614f80` StartAlphaFade(target, 1000 ms)**, the same ramp block (`+0xec..0x100`) and //! the same `clamp01(t)³` ease (`0x614a90`) our appear-fade already rides -//! ([`crate::model_fade::fade_alpha`]). Per frame `model+0x180 = master × fade` +//! ([`benilla_world::model_fade::fade_alpha`]). Per frame `model+0x180 = master × fade` //! (`0x614baa → 0x710cb0`), which multiplies into the per-batch alpha (`0x707680`). //! The recompute has a second, aura-free caller: the DISPLAYID watcher's refresh (`0x60abe0 → //! 0x60ad9f`), which is how a display whose row says `CreatureModelAlpha < 255` — Ghost Wolf's @@ -63,7 +63,7 @@ //! //! **Proc 1 ships too** (decision 0812): its nodes live here ([`AuraNodes::tint`], head-node-wins as //! the reference's `unit+0xce0` list is) and [`apply_aura_tint`] publishes the head to -//! [`crate::instance_tint`], the per-instance modulate channel keyed on the part's rig slot. Written +//! [`benilla_world::instance_tint`], the per-instance modulate channel keyed on the part's rig slot. Written //! on change, never eased — the reference's own asymmetry (`unit+0xd04` change-detection straight to //! `0x710cf0`, with none of the alpha's 1000 ms ramp). //! @@ -82,7 +82,7 @@ //! - a **rigged** one carries its OWN slot, because the vertex stage indexes the skin palette with //! that same field and cannot borrow it (a spell-effect instance; since 0841, the seven shoulder //! models that weld geometry to a billboard bone). [`chained_tint`] walks such an instance's -//! [`crate::model_fade::ParentModel`] link up to the unit — the recursion itself, and the same walk +//! [`benilla_world::model_fade::ParentModel`] link up to the unit — the recursion itself, and the same walk //! `ModelAlphas` already does for alpha. //! //! **Proc 11 ships too** (decisions 0889 + 0891): the **freeze**. Its param is a playback *rate* @@ -102,9 +102,9 @@ use benilla_protocol::EntityKind; use bevy::mesh::MeshTag; use bevy::prelude::*; -use crate::interior::InteriorLit; -use crate::model_fade::{fade_alpha, FadeMaterials, PendingAppearFade, RenderFade}; -use crate::terrain::WowModelMaterial; +use benilla_assets::materials::WowModelMaterial; +use benilla_world::interior::InteriorLit; +use benilla_world::model_fade::{fade_alpha, FadeMaterials, PendingAppearFade, RenderFade}; /// The reference's aura-alpha ramp: `StartAlphaFade(target, **1000 ms**)` (`0x614f80` from /// `0x60d180`'s recompute), eased `clamp01(t)³` by `0x614a90` — [`fade_alpha`] is that curve. @@ -137,7 +137,7 @@ pub(crate) enum AuraNode { /// The CharProc node lists of one unit — the ECS twin of the reference's per-unit `unit+0xb50` /// (alpha) and `unit+0xce0` (tint) lists, plus the ramp `0x614f80` drives from their heads. /// -/// Lives on the **net entity root**, like [`crate::entity_shade::GroundShade`]: the reference has one +/// Lives on the **net entity root**, like [`benilla_world::entity_shade::GroundShade`]: the reference has one /// of these per CGUnit and every attached model (held item, helm, shoulder) renders off the owner's, /// which is also why [`apply_aura_alpha`] walks the root's whole descendant tree rather than its /// direct children. @@ -235,7 +235,7 @@ impl AuraNodes { } } -/// Publish every rig's body tint to the per-instance channel ([`crate::instance_tint`], decision +/// Publish every rig's body tint to the per-instance channel ([`benilla_world::instance_tint`], decision /// 0812): the head node's RGB packed the reference's way, keyed on the unit's rig slot — the slot /// each of its skinned parts already carries in its `MeshTag`, and which the fragment stage reads to /// index the table. @@ -250,17 +250,21 @@ impl AuraNodes { /// went away, not a unit whose last tint aura dropped. (The despawn/rebuild edge is covered /// structurally too, in `RigSkin`'s free hook.) pub(crate) fn apply_aura_tint( - rigs: Query<(Entity, &crate::rig_palette::RigSkin)>, - chain: Query<(Option<&AuraNodes>, Option<&crate::model_fade::ParentModel>)>, - mut tints: ResMut, + rigs: Query<(Entity, &benilla_world::rig_palette::RigSkin)>, + chain: Query<( + Option<&AuraNodes>, + Option<&benilla_world::model_fade::ParentModel>, + )>, + mut tints: ResMut, mut probe_logged: Local, ) { let probe = tint_probe(); let mut painted = 0usize; for (root, rig) in &rigs { - let word = probe - .or_else(|| chained_tint(&chain, root)) - .map_or(crate::instance_tint::IDENTITY, crate::instance_tint::pack); + let word = probe.or_else(|| chained_tint(&chain, root)).map_or( + benilla_world::instance_tint::IDENTITY, + benilla_world::instance_tint::pack, + ); tints.set(rig.slot, word); painted += 1; } @@ -271,7 +275,7 @@ pub(crate) fn apply_aura_tint( } /// The tint a rig instance renders with: its own head node, else the nearest one **up its -/// [`crate::model_fade::ParentModel`] chain** — the reference's `0x714000` recursion, which composes +/// [`benilla_world::model_fade::ParentModel`] chain** — the reference's `0x714000` recursion, which composes /// an attached model's colours onto its parent's computed ones (the same walk [`ModelAlphas`] does /// for alpha, and the same law `mesh_tag` cites for why an item's parts carry their wearer's slot). /// @@ -280,11 +284,14 @@ pub(crate) fn apply_aura_tint( /// without the walk, a dwarf's Stoneform tint would stop at exactly the pauldrons the rig was added /// for. A despawned link ends the walk. fn chained_tint( - chain: &Query<(Option<&AuraNodes>, Option<&crate::model_fade::ParentModel>)>, + chain: &Query<( + Option<&AuraNodes>, + Option<&benilla_world::model_fade::ParentModel>, + )>, instance: Entity, ) -> Option<[u8; 3]> { let mut at = instance; - for _ in 0..crate::model_fade::MAX_MODEL_CHAIN { + for _ in 0..benilla_world::model_fade::MAX_MODEL_CHAIN { let Ok((nodes, parent)) = chain.get(at) else { return None; }; @@ -417,14 +424,14 @@ pub(crate) fn apply_aura_anim_rate( /// It prints the speeds precisely because the first version's bug was invisible without them: a `@0` /// on the thaw line is a clip that will never advance again. fn trace_clock_edge(what: &str, rig: Entity, player: &AnimationPlayer) { - if !crate::dbg_trace::enabled_for("aur") { + if !benilla_assets::trace::enabled_for("aur") { return; } let clips: Vec = player .playing_animations() .map(|(n, a)| format!("{}@{}", n.index(), a.speed())) .collect(); - crate::dbg_trace::line( + benilla_assets::trace::line( "aur", &format!("{what} rig={rig} clips=[{}]", clips.join(" ")), ); @@ -576,7 +583,7 @@ fn install(n: &mut AuraNodes, spell_id: u32, procs: &[AuraNode], now: f32) { /// one timeline with the movement/anim lines — a "why did it not fade when I moved" question needs /// exactly that. fn trace_edge(what: &str, entity: Entity, spell_id: u32, n: &AuraNodes) { - if !crate::dbg_trace::enabled() { + if !benilla_assets::trace::enabled() { return; } let alphas: Vec = n.alpha.iter().map(|(_, f)| *f).collect(); @@ -586,7 +593,7 @@ fn trace_edge(what: &str, entity: Entity, spell_id: u32, n: &AuraNodes) { let rate = n .head_anim_rate() .map_or_else(String::new, |r| format!(" animrate={r}")); - crate::dbg_trace::line( + benilla_assets::trace::line( "aur", &format!( "{what} e={entity} spell={spell_id} alpha={alphas:?}{tint}{rate} base {:.2} cur {:.2} target {:.2}", @@ -691,16 +698,16 @@ type AuraParts<'w, 's> = Query< &'static FadeMaterials, &'static mut MeshTag, &'static mut MeshMaterial3d, - Option<&'static crate::doodad_anim::MatAnim>, + Option<&'static benilla_world::doodad_anim::MatAnim>, Option<&'static InteriorLit>, Option<&'static RenderFade>, Has, - Has, + Has, ), // Disjointness for the card pass (both want `&mut MeshTag`); a card never carries // `FadeMaterials`, so this filter excludes nothing that would otherwise match — the same // trick `apply_self_model_fade` uses for the same pair of queries. - Without, + Without, >; /// Drive every unit's aura-alpha ramp and author its parts' render alpha + material. @@ -720,27 +727,46 @@ type AuraParts<'w, 's> = Query< /// **The material swap is load-bearing, not cosmetic.** A cutout/opaque batch ignores instance /// alpha, so writing `0.3` into the tag without moving the part onto its **blend twin** renders no /// translucency at all — the same "blend pass while `0 < fade < 1`" the reference runs for its -/// doodad fade ([`crate::model_fade`]). It goes through [`FadeMaterials::material_for`], the one +/// doodad fade ([`benilla_world::model_fade`]). It goes through [`FadeMaterials::material_for`], the one /// place the blend-vs-law axes compose, so this writer and the classifier can never disagree. /// Note the swap keys on the *instance* alpha only: a batch whose own animated colour alpha dips is /// a per-batch quantity the reference combines separately, and forcing those onto a blend pass would /// change every existing unit's look. +#[allow(clippy::too_many_arguments)] // one Bevy system's full input set pub(crate) fn apply_aura_alpha( time: Res